gitlogstats 1.1.0__tar.gz → 1.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/.claude/CLAUDE.md +2 -1
  2. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/.claude/settings.json +3 -0
  3. gitlogstats-1.3.0/.github/workflows/build.yaml +65 -0
  4. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/PKG-INFO +16 -12
  5. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/Pipfile +0 -2
  6. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/Pipfile.lock +1 -59
  7. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/README.md +15 -11
  8. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/docs/SPEC.md +18 -6
  9. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/pyproject.toml +1 -1
  10. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/scripts/board/manifest.yaml +52 -17
  11. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/src/gitlogstats/__main__.py +7 -5
  12. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/src/gitlogstats/github.py +99 -19
  13. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/src/gitlogstats/github_stats.py +30 -3
  14. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/src/gitlogstats/identity.py +43 -10
  15. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/tests/fake_gh.py +16 -2
  16. gitlogstats-1.3.0/tests/fixtures/github/pulls_page1.json +29 -0
  17. gitlogstats-1.3.0/tests/fixtures/github/pulls_page2.json +53 -0
  18. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/tests/test_github.py +213 -25
  19. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/tests/test_identity.py +76 -13
  20. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/tests/test_integration.py +313 -40
  21. gitlogstats-1.1.0/.github/workflows/build.yaml +0 -46
  22. gitlogstats-1.1.0/tests/fixtures/github/pulls_page1.json +0 -20
  23. gitlogstats-1.1.0/tests/fixtures/github/pulls_page2.json +0 -29
  24. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/.claude/agents/developer-agent.md +0 -0
  25. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/.claude/agents/spec-reviewer.md +0 -0
  26. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/.claude/hooks/format-edited.sh +0 -0
  27. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/.claude/hooks/guard-paths.sh +0 -0
  28. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/.claude/hooks/guard-paths.test.mjs +0 -0
  29. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/.claude/hooks/verify.sh +0 -0
  30. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/.claude/skills/phase-handoff/SKILL.md +0 -0
  31. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/.claude/skills/stale-check/SKILL.md +0 -0
  32. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/.gitignore +0 -0
  33. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/.prettierrc.json +0 -0
  34. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/LICENSE +0 -0
  35. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/package-lock.json +0 -0
  36. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/package.json +0 -0
  37. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/repos.txt +0 -0
  38. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/requirements.txt +0 -0
  39. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/scripts/board/config.mjs +0 -0
  40. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/scripts/board/derive.mjs +0 -0
  41. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/scripts/board/derive.test.mjs +0 -0
  42. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/scripts/board/sync.mjs +0 -0
  43. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/src/gitlogstats/__init__.py +0 -0
  44. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/src/gitlogstats/git_logs_parser.py +0 -0
  45. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/tests/fixtures/github/commit_linked.json +0 -0
  46. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/tests/fixtures/github/commit_unlinked.json +0 -0
  47. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/tests/fixtures/github/contributors.json +0 -0
  48. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/tests/fixtures/github/contributors_page2.json +0 -0
  49. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/tests/fixtures/github/user_full.json +0 -0
  50. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/tests/fixtures/github/user_private.json +0 -0
  51. {gitlogstats-1.1.0 → gitlogstats-1.3.0}/tests/test_git_logs_parser.py +0 -0
@@ -70,7 +70,8 @@ Changes are tracked on the GitHub project board (https://github.com/users/bloomb
70
70
  - **Do not commit code changes directly to the default branch (`master`).** Create a feature branch named `feat/<REQ-ID>-<slug>` (e.g. `feat/STAT-6-mailmap`; use a short descriptive slug when no requirement id applies). **Exception:** documentation (Markdown and anything under `docs/`) and project tooling under `scripts/` and `.claude/` may be committed directly to the default branch without a PR.
71
71
  - **Open a pull request** whose description includes `Closes #N` — `N` is the board issue's number — so the issue closes on merge. Commit and push only when asked, and run the checks above first.
72
72
  - **Do not hand-create board issues.** The board is generated from the SPEC: `npm run board:derive` regenerates `scripts/board/manifest.yaml`, then `npm run board:sync` creates or updates the issues (`-- --dry-run` previews; `-- --reconcile` forces SPEC statuses back onto the board). A requirement marked `Done` in the SPEC is always closed and moved to Done; other status moves are left to the board unless `--reconcile` is passed.
73
- - Pushing to `master` publishes a release to PyPI (see `.github/workflows/build.yaml`), so bump `version` in `pyproject.toml` when a change to the package should be released.
73
+ - **Claude runs both board commands itself** (they are pre-approved in `.claude/settings.json`): `board:derive` whenever `docs/SPEC.md` changes, committing the manifest with the change; `board:sync` **before** opening a PR when the SPEC adds requirements (so `Closes #N` has an issue to point at), and again **after** the PR merges. Sync reads the working tree, so before the merge run it from a tree where the new requirements are still `Todo`/`In Progress` — syncing a branch that already marks them `Done` closes their issues before the code lands.
74
+ - Merging a pull request into `master` publishes a release to PyPI once the tests pass on the merged code (see `.github/workflows/build.yaml`); pushes to other branches, and direct pushes to `master`, publish nothing. Bump `version` in `pyproject.toml` in the PR when a change to the package should be released; a merge without a bump skips the upload.
74
75
 
75
76
  If asked to use a different workflow (e.g. commit code straight to the default branch), **remind the user that it diverges from this flow and confirm before proceeding** — then honor the confirmed request.
76
77
 
@@ -1,4 +1,7 @@
1
1
  {
2
+ "permissions": {
3
+ "allow": ["Bash(npm run board:derive*)", "Bash(npm run board:sync*)"]
4
+ },
2
5
  "hooks": {
3
6
  "PreToolUse": [
4
7
  {
@@ -0,0 +1,65 @@
1
+ name: CI / CD
2
+ # CI runs on every pull request into master and on every push to master.
3
+ # Delivery to PyPI happens only when a pull request is merged into master,
4
+ # and only if the tests pass on the merged code (PKG-2, PKG-4).
5
+ on:
6
+ push:
7
+ branches: [master]
8
+ pull_request:
9
+ branches: [master]
10
+ types: [opened, synchronize, reopened, closed]
11
+ permissions:
12
+ contents: read
13
+ jobs:
14
+ build:
15
+ # a pull request closed without merging needs no checks
16
+ if: github.event.action != 'closed' || github.event.pull_request.merged
17
+ runs-on: ubuntu-latest
18
+ timeout-minutes: 10
19
+ strategy:
20
+ matrix:
21
+ python-version: ['3.9', '3.11']
22
+ steps:
23
+ - uses: actions/checkout@v4
24
+ with:
25
+ # after a merge, test the merge commit that landed on master
26
+ ref: ${{ github.event.pull_request.merged && github.event.pull_request.merge_commit_sha || github.sha }}
27
+ - name: Set up Python ${{ matrix.python-version }}
28
+ uses: actions/setup-python@v5
29
+ with:
30
+ python-version: ${{ matrix.python-version }}
31
+ - name: Install dependencies
32
+ run: |
33
+ python -m pip install --upgrade pip
34
+ pip install --user pipenv
35
+ pipenv --python python install --dev --skip-lock
36
+ - name: Run tests
37
+ run: |
38
+ pipenv run python -m pytest -q
39
+ - name: Build package
40
+ run: |
41
+ pipenv run python -m build .
42
+ deliver:
43
+ # only a merged pull request is released, and only after build passed
44
+ if: github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged
45
+ needs: [build]
46
+ runs-on: ubuntu-latest
47
+ timeout-minutes: 5
48
+ steps:
49
+ - uses: actions/checkout@v4
50
+ with:
51
+ ref: ${{ github.event.pull_request.merge_commit_sha }}
52
+ - name: Set up Python
53
+ uses: actions/setup-python@v5
54
+ with:
55
+ python-version: '3.11'
56
+ - name: Build package
57
+ run: |
58
+ python -m pip install --upgrade pip build
59
+ python -m build .
60
+ - name: Publish to PyPI
61
+ uses: pypa/gh-action-pypi-publish@release/v1
62
+ with:
63
+ password: ${{ secrets.PYPI_API_TOKEN }}
64
+ # a merge without a version bump (e.g. tooling only) skips the upload
65
+ skip-existing: true
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: gitlogstats
3
- Version: 1.1.0
3
+ Version: 1.3.0
4
4
  Summary: Track developers' contributions to one or more git repositories within a particular time range.
5
5
  Project-URL: Homepage, https://github.com/bloombar/gitlogstats
6
6
  Project-URL: Repository, https://github.com/bloombar/gitlogstats.git
@@ -709,7 +709,7 @@ This script calculates the following, for each developer in each repository:
709
709
  - number of **files changed**
710
710
  - number of **merges** into the analyzed branch
711
711
 
712
- With `--github`, it also reports each developer's GitHub **pull requests** opened, merged and closed without merging (see [GitHub pull requests](#github-pull-requests)).
712
+ With `--github`, it also reports each developer's GitHub **pull requests** opened, merged, closed without merging and merged by themselves (see [GitHub pull requests](#github-pull-requests)).
713
713
 
714
714
  The results can be formatted as `csv`, `json`, or `markdown`.
715
715
 
@@ -751,8 +751,9 @@ options:
751
751
  The branch to parse, if not the default branch
752
752
  -v, --verbose Whether to output debugging info
753
753
  -c, --clean Remove contributors without any contribuition
754
- --github Also report GitHub pull requests opened, merged and
755
- closed per contributor, and match GitHub users to git
754
+ --github Also report GitHub pull requests opened, merged,
755
+ closed and self-merged per contributor, with their git
756
+ and GitHub emails, and match GitHub users to git
756
757
  authors. Needs the GitHub CLI (gh), signed in
757
758
  ```
758
759
 
@@ -836,14 +837,17 @@ gitlogstats -rf repos.txt -c
836
837
 
837
838
  ### GitHub pull requests
838
839
 
839
- Add `--github` to also count each contributor's pull requests. Four columns are appended to every output format:
840
+ Add `--github` to also count each contributor's pull requests. Seven columns are appended to every output format:
840
841
 
841
- | Column | Meaning |
842
- | --------------------- | ----------------------------------------------------------------------------------------------------- |
843
- | `github_username` | the GitHub account matched to the contributor (never the display name), so you can check the matching |
844
- | `prs_opened` | pull requests they opened in the date range, whatever their state now (drafts included) |
845
- | `prs_merged` | pull requests they authored that were merged in the date range, by anyone and in any way (squash too) |
846
- | `prs_closed_unmerged` | pull requests they authored that were closed without merging in the date range |
842
+ | Column | Meaning |
843
+ | --------------------- | ------------------------------------------------------------------------------------------------------- |
844
+ | `github_username` | the GitHub account matched to the contributor (never the display name), so you can check the matching |
845
+ | `prs_opened` | pull requests they opened in the date range, whatever their state now (drafts included) |
846
+ | `prs_merged` | pull requests they authored that were merged in the date range, by anyone and in any way (squash too) |
847
+ | `prs_closed_unmerged` | pull requests they authored that were closed without merging in the date range |
848
+ | `prs_self_merged` | pull requests they authored and also merged themselves, merged in the date range (also in `prs_merged`) |
849
+ | `git_email` | every distinct author email of the git names combined into the row, sorted alphabetically (ignoring case) and separated by `; `; empty for a GitHub-only row |
850
+ | `github_email` | the public email on the matched GitHub profile; empty when the profile has none (GitHub does not reveal private emails) or no account matched |
847
851
 
848
852
  ```
849
853
  gitlogstats -r https://github.com/bloombar/gitlogstats.git --github
@@ -856,7 +860,7 @@ gitlogstats: --github needs the GitHub CLI (gh), which is not installed. Install
856
860
  gitlogstats: --github needs the GitHub CLI (gh) to be signed in. Run 'gh auth login', or run without --github.
857
861
  ```
858
862
 
859
- **Empty versus zero.** A zero means GitHub was asked and the person has none. An empty value (`null` in JSON) means GitHub could not be asked for that repository: it is not on github.com, or the request failed (no network, rate limit, a private repository you cannot see). The git statistics are still reported, the reason is printed to standard error, and the other repositories carry on. A contributor with no GitHub account matched has an empty `github_username` and zero pull requests. A GitHub user who opened pull requests but has no commits is listed in a row of their own with an empty `username` and zero git statistics.
863
+ **Empty versus zero.** A zero means GitHub was asked and the person has none. An empty value (`null` in JSON) means GitHub could not be asked for that repository: it is not on github.com, or the request failed (no network, rate limit, a private repository you cannot see). The git statistics are still reported, the reason is printed to standard error, and the other repositories carry on. A contributor with no GitHub account matched has an empty `github_username` and zero pull requests. A GitHub user who opened pull requests but has no commits is listed in a row of their own with an empty `username` and zero git statistics. When GitHub could not be asked, `git_email` is still filled in and `github_email` is empty. The emails come from the whole history of the branch, not only from commits in the date range.
860
864
 
861
865
  **Matching GitHub users to git authors.** Each git author name is compared with the GitHub users active in the repository, trying these in order and stopping at the first that finds exactly one user:
862
866
 
@@ -5,8 +5,6 @@ name = "pypi"
5
5
 
6
6
  [packages]
7
7
  gitlogstats = {editable = true, path = "."}
8
- pytest = "*"
9
- build = "*"
10
8
 
11
9
  [dev-packages]
12
10
  build = "*"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "_meta": {
3
3
  "hash": {
4
- "sha256": "70462452caf90504e4b0e1fa21f9c94c9ab312ca80771fa5d2d656e939a719aa"
4
+ "sha256": "d1fee7a304fa34eaf74ad0dd93b88859d18b03c32476298928a490a03ec616e8"
5
5
  },
6
6
  "pipfile-spec": 6,
7
7
  "requires": {
@@ -16,67 +16,9 @@
16
16
  ]
17
17
  },
18
18
  "default": {
19
- "build": {
20
- "hashes": [
21
- "sha256:51cc11666391ab6f092070437ac747002ff46f3e4113a3622177ee6b488bfc53",
22
- "sha256:ecd351a4be9d35a9eaaba244a7687143c9c7d4aea6ac964e7e7ddab20cbcf4e7"
23
- ],
24
- "index": "pypi",
25
- "markers": "python_version >= '3.10'",
26
- "version": "==1.6.1"
27
- },
28
19
  "gitlogstats": {
29
20
  "editable": true,
30
21
  "path": "."
31
- },
32
- "iniconfig": {
33
- "hashes": [
34
- "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960",
35
- "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7"
36
- ],
37
- "markers": "python_version >= '3.10'",
38
- "version": "==2.3.1"
39
- },
40
- "packaging": {
41
- "hashes": [
42
- "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79",
43
- "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c"
44
- ],
45
- "markers": "python_version >= '3.9'",
46
- "version": "==26.3"
47
- },
48
- "pluggy": {
49
- "hashes": [
50
- "sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec",
51
- "sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8"
52
- ],
53
- "markers": "python_version >= '3.10'",
54
- "version": "==1.7.0"
55
- },
56
- "pygments": {
57
- "hashes": [
58
- "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9",
59
- "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c"
60
- ],
61
- "markers": "python_version >= '3.9'",
62
- "version": "==2.21.0"
63
- },
64
- "pyproject-hooks": {
65
- "hashes": [
66
- "sha256:5fc53fdac9f7bd63fbcdc868fb5f90b4784d78a53a3d3388cd738b807441a20b",
67
- "sha256:defda19b854fa0d3bd4f76ea4ddcba8abd7dcfcdd585a6690ade050744fc5f43"
68
- ],
69
- "markers": "python_version >= '3.8'",
70
- "version": "==1.3.3"
71
- },
72
- "pytest": {
73
- "hashes": [
74
- "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313",
75
- "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c"
76
- ],
77
- "index": "pypi",
78
- "markers": "python_version >= '3.10'",
79
- "version": "==9.1.1"
80
22
  }
81
23
  },
82
24
  "develop": {
@@ -12,7 +12,7 @@ This script calculates the following, for each developer in each repository:
12
12
  - number of **files changed**
13
13
  - number of **merges** into the analyzed branch
14
14
 
15
- With `--github`, it also reports each developer's GitHub **pull requests** opened, merged and closed without merging (see [GitHub pull requests](#github-pull-requests)).
15
+ With `--github`, it also reports each developer's GitHub **pull requests** opened, merged, closed without merging and merged by themselves (see [GitHub pull requests](#github-pull-requests)).
16
16
 
17
17
  The results can be formatted as `csv`, `json`, or `markdown`.
18
18
 
@@ -54,8 +54,9 @@ options:
54
54
  The branch to parse, if not the default branch
55
55
  -v, --verbose Whether to output debugging info
56
56
  -c, --clean Remove contributors without any contribuition
57
- --github Also report GitHub pull requests opened, merged and
58
- closed per contributor, and match GitHub users to git
57
+ --github Also report GitHub pull requests opened, merged,
58
+ closed and self-merged per contributor, with their git
59
+ and GitHub emails, and match GitHub users to git
59
60
  authors. Needs the GitHub CLI (gh), signed in
60
61
  ```
61
62
 
@@ -139,14 +140,17 @@ gitlogstats -rf repos.txt -c
139
140
 
140
141
  ### GitHub pull requests
141
142
 
142
- Add `--github` to also count each contributor's pull requests. Four columns are appended to every output format:
143
+ Add `--github` to also count each contributor's pull requests. Seven columns are appended to every output format:
143
144
 
144
- | Column | Meaning |
145
- | --------------------- | ----------------------------------------------------------------------------------------------------- |
146
- | `github_username` | the GitHub account matched to the contributor (never the display name), so you can check the matching |
147
- | `prs_opened` | pull requests they opened in the date range, whatever their state now (drafts included) |
148
- | `prs_merged` | pull requests they authored that were merged in the date range, by anyone and in any way (squash too) |
149
- | `prs_closed_unmerged` | pull requests they authored that were closed without merging in the date range |
145
+ | Column | Meaning |
146
+ | --------------------- | ------------------------------------------------------------------------------------------------------- |
147
+ | `github_username` | the GitHub account matched to the contributor (never the display name), so you can check the matching |
148
+ | `prs_opened` | pull requests they opened in the date range, whatever their state now (drafts included) |
149
+ | `prs_merged` | pull requests they authored that were merged in the date range, by anyone and in any way (squash too) |
150
+ | `prs_closed_unmerged` | pull requests they authored that were closed without merging in the date range |
151
+ | `prs_self_merged` | pull requests they authored and also merged themselves, merged in the date range (also in `prs_merged`) |
152
+ | `git_email` | every distinct author email of the git names combined into the row, sorted alphabetically (ignoring case) and separated by `; `; empty for a GitHub-only row |
153
+ | `github_email` | the public email on the matched GitHub profile; empty when the profile has none (GitHub does not reveal private emails) or no account matched |
150
154
 
151
155
  ```
152
156
  gitlogstats -r https://github.com/bloombar/gitlogstats.git --github
@@ -159,7 +163,7 @@ gitlogstats: --github needs the GitHub CLI (gh), which is not installed. Install
159
163
  gitlogstats: --github needs the GitHub CLI (gh) to be signed in. Run 'gh auth login', or run without --github.
160
164
  ```
161
165
 
162
- **Empty versus zero.** A zero means GitHub was asked and the person has none. An empty value (`null` in JSON) means GitHub could not be asked for that repository: it is not on github.com, or the request failed (no network, rate limit, a private repository you cannot see). The git statistics are still reported, the reason is printed to standard error, and the other repositories carry on. A contributor with no GitHub account matched has an empty `github_username` and zero pull requests. A GitHub user who opened pull requests but has no commits is listed in a row of their own with an empty `username` and zero git statistics.
166
+ **Empty versus zero.** A zero means GitHub was asked and the person has none. An empty value (`null` in JSON) means GitHub could not be asked for that repository: it is not on github.com, or the request failed (no network, rate limit, a private repository you cannot see). The git statistics are still reported, the reason is printed to standard error, and the other repositories carry on. A contributor with no GitHub account matched has an empty `github_username` and zero pull requests. A GitHub user who opened pull requests but has no commits is listed in a row of their own with an empty `username` and zero git statistics. When GitHub could not be asked, `git_email` is still filled in and `github_email` is empty. The emails come from the whole history of the branch, not only from commits in the date range.
163
167
 
164
168
  **Matching GitHub users to git authors.** Each git author name is compared with the GitHub users active in the repository, trying these in order and stopping at the first that finds exactly one user:
165
169
 
@@ -114,6 +114,18 @@ If a tier finds several GitHub users for one identity, that tier is skipped as a
114
114
 
115
115
  **Status:** Done
116
116
 
117
+ #### STAT-12 Self-merged pull requests
118
+
119
+ With `--github`, for each contributor, the number of pull requests they authored and also merged themselves (the GitHub user who merged the pull request is its author, ignoring case), with a merge time (`merged_at`) in the date range. Reported in a `prs_self_merged` column, appended after `prs_closed_unmerged`. Every self-merged pull request is also counted in `prs_merged` (STAT-9), so `prs_self_merged` is never larger than `prs_merged`. A merged pull request whose merger GitHub does not report (e.g. a deleted account) is not counted as self-merged. `-b` and `-s/-e` apply as for `prs_merged`.
120
+
121
+ **Status:** Done
122
+
123
+ #### STAT-13 Contributor email addresses
124
+
125
+ With `--github`, every row also reports the contributor's email addresses in two columns appended after `prs_self_merged`: `git_email` lists every distinct author email of the git identities combined into the row (STAT-11), sorted alphabetically (ignoring case) and separated by `; `, and is empty for a GitHub-only row; `github_email` is the public email on the matched GitHub user's profile, and is empty when the profile has none (GitHub does not reveal private emails) or the row has no GitHub user. Emails come from the whole history of the analyzed branch, like the contributor list, not only from commits in the date range. When GitHub statistics are unavailable for a repository (CLI-3), `git_email` is still filled in and `github_email` is empty (`null` in JSON). Without `--github`, neither column appears.
126
+
127
+ **Status:** Done
128
+
117
129
  ### 3. Filtering
118
130
 
119
131
  #### FILT-1 Date range
@@ -144,7 +156,7 @@ By default, contributors with no activity in the date range are listed with zero
144
156
 
145
157
  #### OUT-1 CSV output
146
158
 
147
- `-f csv` (the default) prints a header row followed by one row per contributor per repository, with fields `username, repository, start_date, end_date, commits, insertions, deletions, files, merges`. New fields are added at the end so existing column positions do not change. Values containing commas, quotes or line breaks are quoted. The header is printed even when there are no rows, and the output ends with a single newline. With `--github`, the columns `github_username, prs_opened, prs_merged, prs_closed_unmerged` are appended, and are in the header even when there are no rows (CLI-3).
159
+ `-f csv` (the default) prints a header row followed by one row per contributor per repository, with fields `username, repository, start_date, end_date, commits, insertions, deletions, files, merges`. New fields are added at the end so existing column positions do not change. Values containing commas, quotes or line breaks are quoted. The header is printed even when there are no rows, and the output ends with a single newline. With `--github`, the columns `github_username, prs_opened, prs_merged, prs_closed_unmerged, prs_self_merged, git_email, github_email` are appended, and are in the header even when there are no rows (CLI-3).
148
160
 
149
161
  **Status:** Done
150
162
 
@@ -188,12 +200,12 @@ Invalid dates, and a start date after the end date, are rejected with a usage er
188
200
 
189
201
  #### CLI-3 GitHub pull-request statistics
190
202
 
191
- `--github` adds GitHub pull-request statistics (STAT-8 to STAT-11) for repositories hosted on github.com, appending the columns `github_username, prs_opened, prs_merged, prs_closed_unmerged` to every output format. Without `--github`, the output and behaviour are unchanged, GitHub is never contacted and the GitHub CLI (`gh`) is not needed. With `--github`, the GitHub API is called through `gh api`, so `gh` must be installed and signed in. This is checked before any repository is cloned; if it fails, nothing is printed to standard output, the tool exits with status 1, and one plain message on standard error says why and what to do — no stack trace:
203
+ `--github` adds GitHub pull-request statistics (STAT-8 to STAT-12) and contributor emails (STAT-13) for repositories hosted on github.com, appending the columns `github_username, prs_opened, prs_merged, prs_closed_unmerged, prs_self_merged, git_email, github_email` to every output format. Without `--github`, the output and behaviour are unchanged, GitHub is never contacted and the GitHub CLI (`gh`) is not needed. With `--github`, the GitHub API is called through `gh api`, so `gh` must be installed and signed in. This is checked before any repository is cloned; if it fails, nothing is printed to standard output, the tool exits with status 1, and one plain message on standard error says why and what to do — no stack trace:
192
204
 
193
205
  - `gh` not found: `gitlogstats: --github needs the GitHub CLI (gh), which is not installed. Install it from https://cli.github.com and run 'gh auth login', or run without --github.`
194
206
  - `gh` not signed in (`gh auth status` fails): `gitlogstats: --github needs the GitHub CLI (gh) to be signed in. Run 'gh auth login', or run without --github.`
195
207
 
196
- The other options apply to pull requests as follows: `-s/-e` filter each count by its own timestamp, using the same inclusive local-time range as FILT-1; `-u` selects the combined row whose author names, emails or GitHub username match exactly (the username ignoring case); `-b` counts only pull requests whose base branch is that branch, and without `-b` every pull request counts; `-c` removes only rows with neither git nor pull-request activity; `-x` does not apply. For a repository not on github.com, or when GitHub cannot be reached for one repository (no network, rate limit, private repository without access), the git statistics are still reported, the four GitHub columns are empty (`null` in JSON) rather than zero, and the reason is printed to standard error.
208
+ The other options apply to pull requests as follows: `-s/-e` filter each count by its own timestamp, using the same inclusive local-time range as FILT-1; `-u` selects the combined row whose author names, emails or GitHub username match exactly (the username ignoring case); `-b` counts only pull requests whose base branch is that branch, and without `-b` every pull request counts; `-c` removes only rows with neither git nor pull-request activity; `-x` does not apply. For a repository not on github.com, or when GitHub cannot be reached for one repository (no network, rate limit, private repository without access), the git statistics are still reported, the GitHub columns (including `github_email`) are empty (`null` in JSON) rather than zero while `git_email` is still filled in (STAT-13), and the reason is printed to standard error.
197
209
 
198
210
  **Status:** Done
199
211
 
@@ -207,7 +219,7 @@ The tool installs with `pip install gitlogstats`, which provides a `gitlogstats`
207
219
 
208
220
  #### PKG-2 Continuous integration and release
209
221
 
210
- GitHub Actions builds the package on every push, on Python 3.9 and 3.11, and publishes it to PyPI.
222
+ GitHub Actions builds the package on Python 3.9 and 3.11 for every pull request into `master` and every push to `master`. It publishes to PyPI only when a pull request is merged into `master` and the build of the merged code passes; pushes to other branches publish nothing. A merge whose version is already on PyPI (no version bump) skips the upload instead of failing.
211
223
 
212
224
  **Status:** Done
213
225
 
@@ -219,9 +231,9 @@ Unit tests cover parsing and formatting, and integration tests build real git re
219
231
 
220
232
  #### PKG-4 Run tests in CI
221
233
 
222
- The CI workflow runs the test suite on every push and stops the release if any test fails. (Today it builds and publishes without running tests.)
234
+ The CI workflow runs the Python test suite (`pytest`) on every pull request into `master` and every push to `master`, and on the merged code before a release; a failing test stops the release.
223
235
 
224
- **Status:** Todo
236
+ **Status:** Done
225
237
 
226
238
  ### 7. Future Work
227
239
 
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
5
5
  [project]
6
6
  name = "gitlogstats"
7
7
  description = "Track developers' contributions to one or more git repositories within a particular time range."
8
- version = "1.1.0"
8
+ version = "1.3.0"
9
9
  authors = [
10
10
  { name="Tobody Tothing", email="nobody.nothing@nowhere.com" },
11
11
  ]
@@ -19,16 +19,16 @@
19
19
  status: Done
20
20
  - id: CLI-3
21
21
  title: GitHub pull-request statistics
22
- full: "--github adds GitHub pull-request statistics (STAT-8 to STAT-11) for
23
- repositories hosted on github.com, appending the columns github_username,
24
- prs_opened, prs_merged, prs_closed_unmerged to every output format. Without
25
- --github, the output and behaviour are unchanged, GitHub is never contacted
26
- and the GitHub CLI (gh) is not needed. With --github, the GitHub API is
27
- called through gh api, so gh must be installed and signed in. This is
28
- checked before any repository is cloned; if it fails, nothing is printed to
29
- standard output, the tool exits with status 1, and one plain message on
30
- standard error says why and what to do — no stack trace: gh not found:
31
- gitlogstats: --github needs the GitHub CLI…"
22
+ full: --github adds GitHub pull-request statistics (STAT-8 to STAT-12) and
23
+ contributor emails (STAT-13) for repositories hosted on github.com,
24
+ appending the columns github_username, prs_opened, prs_merged,
25
+ prs_closed_unmerged, prs_self_merged, git_email, github_email to every
26
+ output format. Without --github, the output and behaviour are unchanged,
27
+ GitHub is never contacted and the GitHub CLI (gh) is not needed. With
28
+ --github, the GitHub API is called through gh api, so gh must be installed
29
+ and signed in. This is checked before any repository is cloned; if it fails,
30
+ nothing is printed to standard output, the tool exits with status 1, and one
31
+ plain message on standard error says why and what to do…
32
32
  section: §5 Command-Line Interface
33
33
  family: CLI
34
34
  phase: 1
@@ -89,8 +89,8 @@
89
89
  containing commas, quotes or line breaks are quoted. The header is printed
90
90
  even when there are no rows, and the output ends with a single newline. With
91
91
  --github, the columns github_username, prs_opened, prs_merged,
92
- prs_closed_unmerged are appended, and are in the header even when there are
93
- no rows (CLI-3).
92
+ prs_closed_unmerged, prs_self_merged, git_email, github_email are appended,
93
+ and are in the header even when there are no rows (CLI-3).
94
94
  section: §4 Output
95
95
  family: OUT
96
96
  phase: 1
@@ -141,8 +141,11 @@
141
141
  status: Done
142
142
  - id: PKG-2
143
143
  title: Continuous integration and release
144
- full: GitHub Actions builds the package on every push, on Python 3.9 and 3.11,
145
- and publishes it to PyPI.
144
+ full: GitHub Actions builds the package on Python 3.9 and 3.11 for every pull
145
+ request into master and every push to master. It publishes to PyPI only when
146
+ a pull request is merged into master and the build of the merged code
147
+ passes; pushes to other branches publish nothing. A merge whose version is
148
+ already on PyPI (no version bump) skips the upload instead of failing.
146
149
  section: §6 Packaging & Quality
147
150
  family: PKG
148
151
  phase: 1
@@ -159,12 +162,13 @@
159
162
  status: Done
160
163
  - id: PKG-4
161
164
  title: Run tests in CI
162
- full: The CI workflow runs the test suite on every push and stops the release if
163
- any test fails. (Today it builds and publishes without running tests.)
165
+ full: The CI workflow runs the Python test suite (pytest) on every pull request
166
+ into master and every push to master, and on the merged code before a
167
+ release; a failing test stops the release.
164
168
  section: §6 Packaging & Quality
165
169
  family: PKG
166
170
  phase: 1
167
- status: Todo
171
+ status: Done
168
172
  - id: REPO-1
169
173
  title: Single repository
170
174
  full: -r/--repository <url> analyzes one repository. Any URL or path that git
@@ -329,6 +333,37 @@
329
333
  family: STAT
330
334
  phase: 1
331
335
  status: Done
336
+ - id: STAT-12
337
+ title: Self-merged pull requests
338
+ full: With --github, for each contributor, the number of pull requests they
339
+ authored and also merged themselves (the GitHub user who merged the pull
340
+ request is its author, ignoring case), with a merge time (merged_at) in the
341
+ date range. Reported in a prs_self_merged column, appended after
342
+ prs_closed_unmerged. Every self-merged pull request is also counted in
343
+ prs_merged (STAT-9), so prs_self_merged is never larger than prs_merged. A
344
+ merged pull request whose merger GitHub does not report (e.g. a deleted
345
+ account) is not counted as self-merged. -b and -s/-e apply as for
346
+ prs_merged.
347
+ section: §2 Contribution Statistics
348
+ family: STAT
349
+ phase: 1
350
+ status: Done
351
+ - id: STAT-13
352
+ title: Contributor email addresses
353
+ full: "With --github, every row also reports the contributor's email addresses
354
+ in two columns appended after prs_self_merged: git_email lists every
355
+ distinct author email of the git identities combined into the row (STAT-11),
356
+ sorted alphabetically (ignoring case) and separated by ; , and is empty for
357
+ a GitHub-only row; github_email is the public email on the matched GitHub
358
+ user's profile, and is empty when the profile has none (GitHub does not
359
+ reveal private emails) or the row has no GitHub user. Emails come from the
360
+ whole history of the analyzed branch, like the contributor list, not only
361
+ from commits in the date range. When GitHub statistics are unavailable for a
362
+ repository (CLI-3), git_email is…"
363
+ section: §2 Contribution Statistics
364
+ family: STAT
365
+ phase: 1
366
+ status: Done
332
367
  - id: FUTURE-author-date-filtering
333
368
  title: Author-date filtering
334
369
  full: Author-date filtering — offer filtering by author date, so rebased or
@@ -6,7 +6,7 @@ import re
6
6
  import sys
7
7
  from . import GitLogsParser
8
8
  from .git_logs_parser import FIELDNAMES
9
- from .github import GITHUB_FIELDNAMES, GitHubUnavailable, check_gh
9
+ from .github import EMAIL_FIELDNAMES, GITHUB_FIELDNAMES, GitHubUnavailable, check_gh
10
10
  from .github_stats import github_rows
11
11
 
12
12
 
@@ -141,9 +141,9 @@ def main():
141
141
  )
142
142
  parser.add_argument(
143
143
  "--github",
144
- help="Also report GitHub pull requests opened, merged and closed per "
145
- "contributor, and match GitHub users to git authors. Needs the GitHub CLI "
146
- "(gh), signed in",
144
+ help="Also report GitHub pull requests opened, merged, closed and "
145
+ "self-merged per contributor, with their git and GitHub emails, and "
146
+ "match GitHub users to git authors. Needs the GitHub CLI (gh), signed in",
147
147
  default=False,
148
148
  action="store_true",
149
149
  )
@@ -233,7 +233,9 @@ def main():
233
233
  # emit a single table (or JSON array) covering all repositories
234
234
  if last_parser is not None:
235
235
  # an empty CSV table still gets the GitHub columns with --github
236
- fieldnames = FIELDNAMES + GITHUB_FIELDNAMES if args.github else None
236
+ fieldnames = (
237
+ FIELDNAMES + GITHUB_FIELDNAMES + EMAIL_FIELDNAMES if args.github else None
238
+ )
237
239
  output = last_parser.format_results(all_results, args.format, fieldnames)
238
240
  # end with exactly one newline: CSV and Markdown output already has one
239
241
  print(output, end="" if output.endswith("\n") else "\n")