gitlogstats 1.0.9__tar.gz → 1.2.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 (49) hide show
  1. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/.claude/CLAUDE.md +2 -1
  2. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/.claude/settings.json +3 -0
  3. gitlogstats-1.2.0/.github/workflows/build.yaml +65 -0
  4. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/PKG-INFO +61 -7
  5. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/Pipfile +0 -2
  6. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/Pipfile.lock +1 -59
  7. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/README.md +60 -6
  8. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/docs/SPEC.md +61 -9
  9. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/pyproject.toml +1 -1
  10. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/scripts/board/manifest.yaml +136 -11
  11. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/src/gitlogstats/__main__.py +32 -4
  12. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/src/gitlogstats/git_logs_parser.py +30 -3
  13. gitlogstats-1.2.0/src/gitlogstats/github.py +348 -0
  14. gitlogstats-1.2.0/src/gitlogstats/github_stats.py +152 -0
  15. gitlogstats-1.2.0/src/gitlogstats/identity.py +251 -0
  16. gitlogstats-1.2.0/tests/fake_gh.py +106 -0
  17. gitlogstats-1.2.0/tests/fixtures/github/commit_linked.json +5 -0
  18. gitlogstats-1.2.0/tests/fixtures/github/commit_unlinked.json +5 -0
  19. gitlogstats-1.2.0/tests/fixtures/github/contributors.json +4 -0
  20. gitlogstats-1.2.0/tests/fixtures/github/contributors_page2.json +3 -0
  21. gitlogstats-1.2.0/tests/fixtures/github/pulls_page1.json +29 -0
  22. gitlogstats-1.2.0/tests/fixtures/github/pulls_page2.json +53 -0
  23. gitlogstats-1.2.0/tests/fixtures/github/user_full.json +1 -0
  24. gitlogstats-1.2.0/tests/fixtures/github/user_private.json +1 -0
  25. gitlogstats-1.2.0/tests/test_github.py +726 -0
  26. gitlogstats-1.2.0/tests/test_identity.py +378 -0
  27. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/tests/test_integration.py +649 -0
  28. gitlogstats-1.0.9/.github/workflows/build.yaml +0 -46
  29. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/.claude/agents/developer-agent.md +0 -0
  30. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/.claude/agents/spec-reviewer.md +0 -0
  31. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/.claude/hooks/format-edited.sh +0 -0
  32. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/.claude/hooks/guard-paths.sh +0 -0
  33. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/.claude/hooks/guard-paths.test.mjs +0 -0
  34. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/.claude/hooks/verify.sh +0 -0
  35. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/.claude/skills/phase-handoff/SKILL.md +0 -0
  36. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/.claude/skills/stale-check/SKILL.md +0 -0
  37. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/.gitignore +0 -0
  38. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/.prettierrc.json +0 -0
  39. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/LICENSE +0 -0
  40. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/package-lock.json +0 -0
  41. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/package.json +0 -0
  42. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/repos.txt +0 -0
  43. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/requirements.txt +0 -0
  44. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/scripts/board/config.mjs +0 -0
  45. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/scripts/board/derive.mjs +0 -0
  46. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/scripts/board/derive.test.mjs +0 -0
  47. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/scripts/board/sync.mjs +0 -0
  48. {gitlogstats-1.0.9 → gitlogstats-1.2.0}/src/gitlogstats/__init__.py +0 -0
  49. {gitlogstats-1.0.9 → gitlogstats-1.2.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.0.9
3
+ Version: 1.2.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,6 +709,8 @@ 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, closed without merging and merged by themselves (see [GitHub pull requests](#github-pull-requests)).
713
+
712
714
  The results can be formatted as `csv`, `json`, or `markdown`.
713
715
 
714
716
  ## Install
@@ -722,26 +724,37 @@ e.g. `pip install gitlogstats`, `pip3 install gitlogstats`, depending on your en
722
724
  The command `gitlogstats --help` shows the usage instructions:
723
725
 
724
726
  ```
725
- usage: gitlogstats [-h] (-r REPOSITORY | -rf REPOFILE) [-u USER] [-s START] [-e END] [-x EXCLUSIONS] [-f {csv,json,markdown}] [-b BRANCH] [-v] [-c]
727
+ usage: gitlogstats [-h] (-r REPOSITORY | -rf REPOFILE) [-u USER] [-s START]
728
+ [-e END] [-x EXCLUSIONS] [-f {csv,json,markdown}]
729
+ [-b BRANCH] [-v] [-c] [--github]
726
730
 
727
- optional arguments:
731
+ options:
728
732
  -h, --help show this help message and exit
729
733
  -r REPOSITORY, --repository REPOSITORY
730
734
  the public URL of the repository whose logs to parse
731
735
  -rf REPOFILE, --repofile REPOFILE
732
- the path to simple text file with a list of repository URLs to parse
733
- -u USER, --user USER The git username (or email address) to report. Default is all contributing users
736
+ the path to simple text file with a list of repository
737
+ URLs to parse
738
+ -u USER, --user USER The git username (or email address) to report. Default
739
+ is all contributing users
734
740
  -s START, --start START
735
741
  Start date in mm/dd/yyyy format
736
742
  -e END, --end END End date in mm/dd/yyyy format
737
743
  -x EXCLUSIONS, --exclusions EXCLUSIONS
738
- A comma-separated list of files, patterns or directories to exclude, e.g. --exclusions "foo.zip, *.jpg, vendor". Replaces the default list; use "" to exclude nothing
744
+ A comma-separated list of files, patterns or
745
+ directories to exclude, e.g. --exclusions "foo.zip,
746
+ *.jpg, vendor". Replaces the default list; use "" to
747
+ exclude nothing
739
748
  -f {csv,json,markdown}, --format {csv,json,markdown}
740
749
  The format in which to output the results
741
750
  -b BRANCH, --branch BRANCH
742
- The branch to checkout before compiling statistics. Defaults to the repository's default branch.
751
+ The branch to parse, if not the default branch
743
752
  -v, --verbose Whether to output debugging info
744
753
  -c, --clean Remove contributors without any contribuition
754
+ --github Also report GitHub pull requests opened, merged,
755
+ closed and self-merged per contributor, and match
756
+ GitHub users to git authors. Needs the GitHub CLI
757
+ (gh), signed in
745
758
  ```
746
759
 
747
760
  In case `gitlogstats` is not found as a command, even after installing, try `python -m gitlogstats` or `python3 -m gitlogstats` instead.
@@ -822,6 +835,47 @@ By default, every contributor in a repository's history is listed, with zeros fo
822
835
  gitlogstats -rf repos.txt -c
823
836
  ```
824
837
 
838
+ ### GitHub pull requests
839
+
840
+ Add `--github` to also count each contributor's pull requests. Five columns are appended to every output format:
841
+
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
+
850
+ ```
851
+ gitlogstats -r https://github.com/bloombar/gitlogstats.git --github
852
+ ```
853
+
854
+ **Requirements.** Only `--github` needs the [GitHub CLI](https://cli.github.com) (`gh`); without the flag nothing changes and GitHub is never contacted. Install `gh`, then run `gh auth login`. The tool checks this before cloning anything. If it fails, nothing is printed to standard output, the exit status is 1 and one of these messages is printed to standard error:
855
+
856
+ ```
857
+ 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.
858
+ gitlogstats: --github needs the GitHub CLI (gh) to be signed in. Run 'gh auth login', or run without --github.
859
+ ```
860
+
861
+ **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.
862
+
863
+ **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:
864
+
865
+ 1. **Email.** The email is a GitHub `noreply` address containing the username, GitHub itself links one of the author's commits to an account, or the email is the public email of a GitHub profile.
866
+ 2. **Username.** The author name, or the part of the email before the `@`, is the GitHub username (ignoring case, and treating spaces, hyphens, underscores and dots as alike). The part before the `@` is skipped when it names a role rather than a person (such as `noreply`, `admin` or `info`), or when several author names in the repository share it (`alex@gmail…` and `alex@school…`), since it could belong to either.
867
+ 3. **Display name.** The author name is the GitHub profile name (ignoring case, accents, punctuation and word order, so `Grace Hopper` matches `Hopper, Grace`).
868
+
869
+ If a step finds several users it is skipped as ambiguous and the next is tried. All git names matched to one GitHub user become one row: the `username` is the name with the most commits and the git statistics are added up. Use `-v` to see which step matched each name, the ambiguous and unmatched names, and how many `gh` calls were made. A wrong match can be fixed with a `.mailmap` file.
870
+
871
+ **How the other options apply.**
872
+
873
+ - `-s` / `-e`: each pull request count uses its own timestamp (opened, merged or closed) against the same inclusive local-time range as commits.
874
+ - `-b`: only pull requests whose base branch is that branch are counted; without `-b`, all pull requests count.
875
+ - `-u`: selects the combined row whose author names or emails equal the value exactly, or whose GitHub username equals it ignoring case. The whole combined row is reported, so `-u` with one email address also includes the person's commits under their other names and emails, and a value that matches nobody prints no row.
876
+ - `-c`: removes only rows with no git activity and no pull requests.
877
+ - `-x`: has no effect on pull requests.
878
+
825
879
  ### Formatting the results
826
880
 
827
881
  The results can be formatted as `csv`, `json`, or a `markdown` table. The default is `csv`. Use the `-f` flag to control the output format.
@@ -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,6 +12,8 @@ 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, closed without merging and merged by themselves (see [GitHub pull requests](#github-pull-requests)).
16
+
15
17
  The results can be formatted as `csv`, `json`, or `markdown`.
16
18
 
17
19
  ## Install
@@ -25,26 +27,37 @@ e.g. `pip install gitlogstats`, `pip3 install gitlogstats`, depending on your en
25
27
  The command `gitlogstats --help` shows the usage instructions:
26
28
 
27
29
  ```
28
- usage: gitlogstats [-h] (-r REPOSITORY | -rf REPOFILE) [-u USER] [-s START] [-e END] [-x EXCLUSIONS] [-f {csv,json,markdown}] [-b BRANCH] [-v] [-c]
30
+ usage: gitlogstats [-h] (-r REPOSITORY | -rf REPOFILE) [-u USER] [-s START]
31
+ [-e END] [-x EXCLUSIONS] [-f {csv,json,markdown}]
32
+ [-b BRANCH] [-v] [-c] [--github]
29
33
 
30
- optional arguments:
34
+ options:
31
35
  -h, --help show this help message and exit
32
36
  -r REPOSITORY, --repository REPOSITORY
33
37
  the public URL of the repository whose logs to parse
34
38
  -rf REPOFILE, --repofile REPOFILE
35
- the path to simple text file with a list of repository URLs to parse
36
- -u USER, --user USER The git username (or email address) to report. Default is all contributing users
39
+ the path to simple text file with a list of repository
40
+ URLs to parse
41
+ -u USER, --user USER The git username (or email address) to report. Default
42
+ is all contributing users
37
43
  -s START, --start START
38
44
  Start date in mm/dd/yyyy format
39
45
  -e END, --end END End date in mm/dd/yyyy format
40
46
  -x EXCLUSIONS, --exclusions EXCLUSIONS
41
- A comma-separated list of files, patterns or directories to exclude, e.g. --exclusions "foo.zip, *.jpg, vendor". Replaces the default list; use "" to exclude nothing
47
+ A comma-separated list of files, patterns or
48
+ directories to exclude, e.g. --exclusions "foo.zip,
49
+ *.jpg, vendor". Replaces the default list; use "" to
50
+ exclude nothing
42
51
  -f {csv,json,markdown}, --format {csv,json,markdown}
43
52
  The format in which to output the results
44
53
  -b BRANCH, --branch BRANCH
45
- The branch to checkout before compiling statistics. Defaults to the repository's default branch.
54
+ The branch to parse, if not the default branch
46
55
  -v, --verbose Whether to output debugging info
47
56
  -c, --clean Remove contributors without any contribuition
57
+ --github Also report GitHub pull requests opened, merged,
58
+ closed and self-merged per contributor, and match
59
+ GitHub users to git authors. Needs the GitHub CLI
60
+ (gh), signed in
48
61
  ```
49
62
 
50
63
  In case `gitlogstats` is not found as a command, even after installing, try `python -m gitlogstats` or `python3 -m gitlogstats` instead.
@@ -125,6 +138,47 @@ By default, every contributor in a repository's history is listed, with zeros fo
125
138
  gitlogstats -rf repos.txt -c
126
139
  ```
127
140
 
141
+ ### GitHub pull requests
142
+
143
+ Add `--github` to also count each contributor's pull requests. Five columns are appended to every output format:
144
+
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
+
153
+ ```
154
+ gitlogstats -r https://github.com/bloombar/gitlogstats.git --github
155
+ ```
156
+
157
+ **Requirements.** Only `--github` needs the [GitHub CLI](https://cli.github.com) (`gh`); without the flag nothing changes and GitHub is never contacted. Install `gh`, then run `gh auth login`. The tool checks this before cloning anything. If it fails, nothing is printed to standard output, the exit status is 1 and one of these messages is printed to standard error:
158
+
159
+ ```
160
+ 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.
161
+ gitlogstats: --github needs the GitHub CLI (gh) to be signed in. Run 'gh auth login', or run without --github.
162
+ ```
163
+
164
+ **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.
165
+
166
+ **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:
167
+
168
+ 1. **Email.** The email is a GitHub `noreply` address containing the username, GitHub itself links one of the author's commits to an account, or the email is the public email of a GitHub profile.
169
+ 2. **Username.** The author name, or the part of the email before the `@`, is the GitHub username (ignoring case, and treating spaces, hyphens, underscores and dots as alike). The part before the `@` is skipped when it names a role rather than a person (such as `noreply`, `admin` or `info`), or when several author names in the repository share it (`alex@gmail…` and `alex@school…`), since it could belong to either.
170
+ 3. **Display name.** The author name is the GitHub profile name (ignoring case, accents, punctuation and word order, so `Grace Hopper` matches `Hopper, Grace`).
171
+
172
+ If a step finds several users it is skipped as ambiguous and the next is tried. All git names matched to one GitHub user become one row: the `username` is the name with the most commits and the git statistics are added up. Use `-v` to see which step matched each name, the ambiguous and unmatched names, and how many `gh` calls were made. A wrong match can be fixed with a `.mailmap` file.
173
+
174
+ **How the other options apply.**
175
+
176
+ - `-s` / `-e`: each pull request count uses its own timestamp (opened, merged or closed) against the same inclusive local-time range as commits.
177
+ - `-b`: only pull requests whose base branch is that branch are counted; without `-b`, all pull requests count.
178
+ - `-u`: selects the combined row whose author names or emails equal the value exactly, or whose GitHub username equals it ignoring case. The whole combined row is reported, so `-u` with one email address also includes the person's commits under their other names and emails, and a value that matches nobody prints no row.
179
+ - `-c`: removes only rows with no git activity and no pull requests.
180
+ - `-x`: has no effect on pull requests.
181
+
128
182
  ### Formatting the results
129
183
 
130
184
  The results can be formatted as `csv`, `json`, or a `markdown` table. The default is `csv`. Use the `-f` flag to control the output format.
@@ -35,7 +35,7 @@ Repositories are cloned into a `repos/` directory under the current working dire
35
35
 
36
36
  #### REPO-4 Branch selection
37
37
 
38
- `-b/--branch <name>` checks out the given branch before statistics are computed. Without `-b`, the repository's default branch is used — including when an earlier run left the cached clone on another branch.
38
+ `-b/--branch <name>` checks out the given branch before statistics are computed. Without `-b`, the repository's default branch is used — including when an earlier run left the cached clone on another branch. With `--github`, it also limits which pull requests are counted to those whose base branch is that branch (CLI-3).
39
39
 
40
40
  **Status:** Done
41
41
 
@@ -83,17 +83,54 @@ For each contributor, the number of merge commits they authored directly on the
83
83
 
84
84
  **Status:** Done
85
85
 
86
+ #### STAT-8 Pull requests opened
87
+
88
+ With `--github` (CLI-3), for each contributor, the number of pull requests they authored on GitHub whose creation time (`created_at`) falls in the date range, whatever their state now (open, merged or closed; drafts included). Reported in a `prs_opened` column.
89
+
90
+ **Status:** Done
91
+
92
+ #### STAT-9 Pull requests merged
93
+
94
+ With `--github`, for each contributor, the number of pull requests they authored that were merged (`merged_at` set) in the date range, whoever merged them. Reported in a `prs_merged` column. Unlike merges (STAT-7), this counts squash and rebase merges too, since GitHub records them as merged pull requests.
95
+
96
+ **Status:** Done
97
+
98
+ #### STAT-10 Pull requests closed without merging
99
+
100
+ With `--github`, for each contributor, the number of pull requests they authored that were closed without being merged (`state` closed and `merged_at` not set), with a closing time (`closed_at`) in the date range. Reported in a `prs_closed_unmerged` column.
101
+
102
+ **Status:** Done
103
+
104
+ #### STAT-11 Matching GitHub users to git authors
105
+
106
+ With `--github`, each pull-request author (a GitHub username) is matched to the git identities (author name and email) in the repository's history. Each git identity is tried against the GitHub users active in that repository (pull-request authors and the accounts GitHub links to its commits), tier by tier, stopping at the first tier that finds exactly one GitHub user:
107
+
108
+ 1. **Email.** The email is a GitHub `noreply` address (`12345+login@users.noreply.github.com` or `login@users.noreply.github.com`); or GitHub links one of the identity's commits to an account (the commit's `author.login`, which covers private emails GitHub knows but does not publish); or the email equals a candidate's public profile email (case-insensitive).
109
+ 2. **Username.** The author name, or the email's local part (before `@`), equals a candidate's GitHub username, ignoring case and treating spaces, hyphens, underscores and dots as the same (`Ada Lovelace` matches `Ada-Lovelace`). An email local part is not used when it names a role rather than a person (such as `noreply`, `admin` or `info`), or when it is shared by several author names in the repository (`alex@gmail…` and `alex@school…`), since it could belong to either.
110
+ 3. **Display name.** The author name equals a candidate's GitHub display name, ignoring case, accents, repeated spaces and punctuation, and word order (`Grace Hopper` matches `Hopper, Grace`).
111
+ 4. **No match.** A GitHub user matched by no git identity is reported in a row of its own, with an empty `username`, its pull-request statistics and zero git statistics.
112
+
113
+ If a tier finds several GitHub users for one identity, that tier is skipped as ambiguous and the next is tried; ambiguous and unmatched identities are listed with `-v`. All git identities matched to the same GitHub user are combined into one row: the `username` is the matched author name with the most commits (ties broken alphabetically), and the git statistics are summed. A git identity matched to no GitHub user keeps its own row with an empty `github_username` and zero pull-request statistics. Every row carries the matched GitHub username (never the display name) in a `github_username` column, so matches can be checked and corrected by hand, e.g. with a `.mailmap` (STAT-6).
114
+
115
+ **Status:** Done
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
+
86
123
  ### 3. Filtering
87
124
 
88
125
  #### FILT-1 Date range
89
126
 
90
- `-s/--start` and `-e/--end` (format `mm/dd/yyyy`) limit the analysis to commits whose commit date falls between the start of the start date and the end of the end date, inclusive, in local time. Defaults: one year ago until today. The output records the requested start and end dates on every row.
127
+ `-s/--start` and `-e/--end` (format `mm/dd/yyyy`) limit the analysis to commits whose commit date falls between the start of the start date and the end of the end date, inclusive, in local time. Defaults: one year ago until today. The output records the requested start and end dates on every row. With `--github`, each pull request count is filtered by its own timestamp in the same range (CLI-3).
91
128
 
92
129
  **Status:** Done
93
130
 
94
131
  #### FILT-2 Single contributor
95
132
 
96
- `-u/--user <name>` reports only the contributor whose author name or email address matches exactly. Without `-u`, every contributor in the repository's history is reported.
133
+ `-u/--user <name>` reports only the contributor whose author name or email address matches exactly. Without `-u`, every contributor in the repository's history is reported. With `--github`, it selects the combined row, and a value may also be the GitHub username (ignoring case); see CLI-3.
97
134
 
98
135
  **Status:** Done
99
136
 
@@ -105,7 +142,7 @@ For each contributor, the number of merge commits they authored directly on the
105
142
 
106
143
  #### FILT-4 Remove inactive contributors
107
144
 
108
- By default, contributors with no activity in the date range are listed with zeros. `-c/--clean` removes them.
145
+ By default, contributors with no activity in the date range are listed with zeros. `-c/--clean` removes them. With `--github`, only rows with neither git nor pull-request activity are removed (CLI-3).
109
146
 
110
147
  **Status:** Done
111
148
 
@@ -113,7 +150,7 @@ By default, contributors with no activity in the date range are listed with zero
113
150
 
114
151
  #### OUT-1 CSV output
115
152
 
116
- `-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.
153
+ `-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` are appended, and are in the header even when there are no rows (CLI-3).
117
154
 
118
155
  **Status:** Done
119
156
 
@@ -137,7 +174,7 @@ When several repositories are analyzed, the results are printed once, after all
137
174
 
138
175
  #### OUT-5 Verbose diagnostics
139
176
 
140
- `-v/--verbose` prints debugging information (directories, the git commands run, contributors found) to standard error, so standard output stays machine-readable.
177
+ `-v/--verbose` prints debugging information (directories, the git commands run, contributors found) to standard error, so standard output stays machine-readable. With `--github`, it also prints how each author name was matched to a GitHub user, the ambiguous and unmatched names, and the number of `gh` API calls made for each repository (CLI-3).
141
178
 
142
179
  **Status:** Done
143
180
 
@@ -155,6 +192,17 @@ Invalid dates, and a start date after the end date, are rejected with a usage er
155
192
 
156
193
  **Status:** Done
157
194
 
195
+ #### CLI-3 GitHub pull-request statistics
196
+
197
+ `--github` adds GitHub pull-request statistics (STAT-8 to STAT-12) for repositories hosted on github.com, appending the columns `github_username, prs_opened, prs_merged, prs_closed_unmerged, prs_self_merged` 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:
198
+
199
+ - `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.`
200
+ - `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.`
201
+
202
+ 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 five GitHub columns are empty (`null` in JSON) rather than zero, and the reason is printed to standard error.
203
+
204
+ **Status:** Done
205
+
158
206
  ### 6. Packaging & Quality
159
207
 
160
208
  #### PKG-1 Installable package
@@ -165,7 +213,7 @@ The tool installs with `pip install gitlogstats`, which provides a `gitlogstats`
165
213
 
166
214
  #### PKG-2 Continuous integration and release
167
215
 
168
- GitHub Actions builds the package on every push, on Python 3.9 and 3.11, and publishes it to PyPI.
216
+ 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.
169
217
 
170
218
  **Status:** Done
171
219
 
@@ -177,9 +225,9 @@ Unit tests cover parsing and formatting, and integration tests build real git re
177
225
 
178
226
  #### PKG-4 Run tests in CI
179
227
 
180
- 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.)
228
+ 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.
181
229
 
182
- **Status:** Todo
230
+ **Status:** Done
183
231
 
184
232
  ### 7. Future Work
185
233
 
@@ -189,3 +237,7 @@ The CI workflow runs the test suite on every push and stops the release if any t
189
237
  - **Parallel processing** — clone and analyze many repositories concurrently to speed up large batches.
190
238
  - **Per-file breakdown** — optionally report each contributor's changes per file or directory.
191
239
  - **Output to file** — write results to a file (e.g. `-o report.csv`) instead of only standard output.
240
+ - **GitHub without the gh CLI** — let `--github` call the GitHub API directly with a `GITHUB_TOKEN`, for users who cannot install `gh`.
241
+ - **GitHub Enterprise hosts** — extend `--github` to repositories on GitHub Enterprise Server, via `gh api --hostname` or a configurable API URL.
242
+ - **Pull-request reviews and mergers** — report who reviewed and who merged each pull request, in addition to who opened it.
243
+ - **GitHub-only mode** — report pull-request statistics without computing git statistics, for faster runs over large batches.
@@ -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.0.9"
8
+ version = "1.2.0"
9
9
  authors = [
10
10
  { name="Tobody Tothing", email="nobody.nothing@nowhere.com" },
11
11
  ]