gitlogstats 1.0.9__tar.gz → 1.1.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 (48) hide show
  1. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/PKG-INFO +59 -7
  2. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/README.md +58 -6
  3. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/docs/SPEC.md +52 -6
  4. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/pyproject.toml +1 -1
  5. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/scripts/board/manifest.yaml +112 -6
  6. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/src/gitlogstats/__main__.py +32 -4
  7. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/src/gitlogstats/git_logs_parser.py +30 -3
  8. gitlogstats-1.1.0/src/gitlogstats/github.py +271 -0
  9. gitlogstats-1.1.0/src/gitlogstats/github_stats.py +152 -0
  10. gitlogstats-1.1.0/src/gitlogstats/identity.py +251 -0
  11. gitlogstats-1.1.0/tests/fake_gh.py +92 -0
  12. gitlogstats-1.1.0/tests/fixtures/github/commit_linked.json +5 -0
  13. gitlogstats-1.1.0/tests/fixtures/github/commit_unlinked.json +5 -0
  14. gitlogstats-1.1.0/tests/fixtures/github/contributors.json +4 -0
  15. gitlogstats-1.1.0/tests/fixtures/github/contributors_page2.json +3 -0
  16. gitlogstats-1.1.0/tests/fixtures/github/pulls_page1.json +20 -0
  17. gitlogstats-1.1.0/tests/fixtures/github/pulls_page2.json +29 -0
  18. gitlogstats-1.1.0/tests/fixtures/github/user_full.json +1 -0
  19. gitlogstats-1.1.0/tests/fixtures/github/user_private.json +1 -0
  20. gitlogstats-1.1.0/tests/test_github.py +542 -0
  21. gitlogstats-1.1.0/tests/test_identity.py +370 -0
  22. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/tests/test_integration.py +582 -0
  23. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/.claude/CLAUDE.md +0 -0
  24. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/.claude/agents/developer-agent.md +0 -0
  25. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/.claude/agents/spec-reviewer.md +0 -0
  26. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/.claude/hooks/format-edited.sh +0 -0
  27. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/.claude/hooks/guard-paths.sh +0 -0
  28. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/.claude/hooks/guard-paths.test.mjs +0 -0
  29. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/.claude/hooks/verify.sh +0 -0
  30. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/.claude/settings.json +0 -0
  31. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/.claude/skills/phase-handoff/SKILL.md +0 -0
  32. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/.claude/skills/stale-check/SKILL.md +0 -0
  33. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/.github/workflows/build.yaml +0 -0
  34. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/.gitignore +0 -0
  35. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/.prettierrc.json +0 -0
  36. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/LICENSE +0 -0
  37. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/Pipfile +0 -0
  38. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/Pipfile.lock +0 -0
  39. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/package-lock.json +0 -0
  40. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/package.json +0 -0
  41. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/repos.txt +0 -0
  42. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/requirements.txt +0 -0
  43. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/scripts/board/config.mjs +0 -0
  44. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/scripts/board/derive.mjs +0 -0
  45. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/scripts/board/derive.test.mjs +0 -0
  46. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/scripts/board/sync.mjs +0 -0
  47. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/src/gitlogstats/__init__.py +0 -0
  48. {gitlogstats-1.0.9 → gitlogstats-1.1.0}/tests/test_git_logs_parser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: gitlogstats
3
- Version: 1.0.9
3
+ Version: 1.1.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 and closed without merging (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,36 @@ 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 and
755
+ closed per contributor, and match GitHub users to git
756
+ authors. Needs the GitHub CLI (gh), signed in
745
757
  ```
746
758
 
747
759
  In case `gitlogstats` is not found as a command, even after installing, try `python -m gitlogstats` or `python3 -m gitlogstats` instead.
@@ -822,6 +834,46 @@ By default, every contributor in a repository's history is listed, with zeros fo
822
834
  gitlogstats -rf repos.txt -c
823
835
  ```
824
836
 
837
+ ### GitHub pull requests
838
+
839
+ Add `--github` to also count each contributor's pull requests. Four columns are appended to every output format:
840
+
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 |
847
+
848
+ ```
849
+ gitlogstats -r https://github.com/bloombar/gitlogstats.git --github
850
+ ```
851
+
852
+ **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:
853
+
854
+ ```
855
+ 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.
856
+ gitlogstats: --github needs the GitHub CLI (gh) to be signed in. Run 'gh auth login', or run without --github.
857
+ ```
858
+
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.
860
+
861
+ **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
+
863
+ 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.
864
+ 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.
865
+ 3. **Display name.** The author name is the GitHub profile name (ignoring case, accents, punctuation and word order, so `Grace Hopper` matches `Hopper, Grace`).
866
+
867
+ 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.
868
+
869
+ **How the other options apply.**
870
+
871
+ - `-s` / `-e`: each pull request count uses its own timestamp (opened, merged or closed) against the same inclusive local-time range as commits.
872
+ - `-b`: only pull requests whose base branch is that branch are counted; without `-b`, all pull requests count.
873
+ - `-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.
874
+ - `-c`: removes only rows with no git activity and no pull requests.
875
+ - `-x`: has no effect on pull requests.
876
+
825
877
  ### Formatting the results
826
878
 
827
879
  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.
@@ -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 and closed without merging (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,36 @@ 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 and
58
+ closed per contributor, and match GitHub users to git
59
+ authors. Needs the GitHub CLI (gh), signed in
48
60
  ```
49
61
 
50
62
  In case `gitlogstats` is not found as a command, even after installing, try `python -m gitlogstats` or `python3 -m gitlogstats` instead.
@@ -125,6 +137,46 @@ By default, every contributor in a repository's history is listed, with zeros fo
125
137
  gitlogstats -rf repos.txt -c
126
138
  ```
127
139
 
140
+ ### GitHub pull requests
141
+
142
+ Add `--github` to also count each contributor's pull requests. Four columns are appended to every output format:
143
+
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 |
150
+
151
+ ```
152
+ gitlogstats -r https://github.com/bloombar/gitlogstats.git --github
153
+ ```
154
+
155
+ **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:
156
+
157
+ ```
158
+ 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.
159
+ gitlogstats: --github needs the GitHub CLI (gh) to be signed in. Run 'gh auth login', or run without --github.
160
+ ```
161
+
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.
163
+
164
+ **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
+
166
+ 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.
167
+ 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.
168
+ 3. **Display name.** The author name is the GitHub profile name (ignoring case, accents, punctuation and word order, so `Grace Hopper` matches `Hopper, Grace`).
169
+
170
+ 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.
171
+
172
+ **How the other options apply.**
173
+
174
+ - `-s` / `-e`: each pull request count uses its own timestamp (opened, merged or closed) against the same inclusive local-time range as commits.
175
+ - `-b`: only pull requests whose base branch is that branch are counted; without `-b`, all pull requests count.
176
+ - `-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.
177
+ - `-c`: removes only rows with no git activity and no pull requests.
178
+ - `-x`: has no effect on pull requests.
179
+
128
180
  ### Formatting the results
129
181
 
130
182
  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,48 @@ 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
+
86
117
  ### 3. Filtering
87
118
 
88
119
  #### FILT-1 Date range
89
120
 
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.
121
+ `-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
122
 
92
123
  **Status:** Done
93
124
 
94
125
  #### FILT-2 Single contributor
95
126
 
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.
127
+ `-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
128
 
98
129
  **Status:** Done
99
130
 
@@ -105,7 +136,7 @@ For each contributor, the number of merge commits they authored directly on the
105
136
 
106
137
  #### FILT-4 Remove inactive contributors
107
138
 
108
- By default, contributors with no activity in the date range are listed with zeros. `-c/--clean` removes them.
139
+ 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
140
 
110
141
  **Status:** Done
111
142
 
@@ -113,7 +144,7 @@ By default, contributors with no activity in the date range are listed with zero
113
144
 
114
145
  #### OUT-1 CSV output
115
146
 
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.
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).
117
148
 
118
149
  **Status:** Done
119
150
 
@@ -137,7 +168,7 @@ When several repositories are analyzed, the results are printed once, after all
137
168
 
138
169
  #### OUT-5 Verbose diagnostics
139
170
 
140
- `-v/--verbose` prints debugging information (directories, the git commands run, contributors found) to standard error, so standard output stays machine-readable.
171
+ `-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
172
 
142
173
  **Status:** Done
143
174
 
@@ -155,6 +186,17 @@ Invalid dates, and a start date after the end date, are rejected with a usage er
155
186
 
156
187
  **Status:** Done
157
188
 
189
+ #### CLI-3 GitHub pull-request statistics
190
+
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:
192
+
193
+ - `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
+ - `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
+
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.
197
+
198
+ **Status:** Done
199
+
158
200
  ### 6. Packaging & Quality
159
201
 
160
202
  #### PKG-1 Installable package
@@ -189,3 +231,7 @@ The CI workflow runs the test suite on every push and stops the release if any t
189
231
  - **Parallel processing** — clone and analyze many repositories concurrently to speed up large batches.
190
232
  - **Per-file breakdown** — optionally report each contributor's changes per file or directory.
191
233
  - **Output to file** — write results to a file (e.g. `-o report.csv`) instead of only standard output.
234
+ - **GitHub without the gh CLI** — let `--github` call the GitHub API directly with a `GITHUB_TOKEN`, for users who cannot install `gh`.
235
+ - **GitHub Enterprise hosts** — extend `--github` to repositories on GitHub Enterprise Server, via `gh api --hostname` or a configurable API URL.
236
+ - **Pull-request reviews and mergers** — report who reviewed and who merged each pull request, in addition to who opened it.
237
+ - **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.1.0"
9
9
  authors = [
10
10
  { name="Tobody Tothing", email="nobody.nothing@nowhere.com" },
11
11
  ]
@@ -17,12 +17,30 @@
17
17
  family: CLI
18
18
  phase: 1
19
19
  status: Done
20
+ - id: CLI-3
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…"
32
+ section: §5 Command-Line Interface
33
+ family: CLI
34
+ phase: 1
35
+ status: Done
20
36
  - id: FILT-1
21
37
  title: Date range
22
38
  full: "-s/--start and -e/--end (format mm/dd/yyyy) limit the analysis to commits
23
39
  whose commit date falls between the start of the start date and the end of
24
40
  the end date, inclusive, in local time. Defaults: one year ago until today.
25
- The output records the requested start and end dates on every row."
41
+ The output records the requested start and end dates on every row. With
42
+ --github, each pull request count is filtered by its own timestamp in the
43
+ same range (CLI-3)."
26
44
  section: §3 Filtering
27
45
  family: FILT
28
46
  phase: 1
@@ -31,7 +49,8 @@
31
49
  title: Single contributor
32
50
  full: -u/--user <name> reports only the contributor whose author name or email
33
51
  address matches exactly. Without -u, every contributor in the repository's
34
- history is reported.
52
+ history is reported. With --github, it selects the combined row, and a value
53
+ may also be the GitHub username (ignoring case); see CLI-3.
35
54
  section: §3 Filtering
36
55
  family: FILT
37
56
  phase: 1
@@ -55,7 +74,8 @@
55
74
  - id: FILT-4
56
75
  title: Remove inactive contributors
57
76
  full: By default, contributors with no activity in the date range are listed
58
- with zeros. -c/--clean removes them.
77
+ with zeros. -c/--clean removes them. With --github, only rows with neither
78
+ git nor pull-request activity are removed (CLI-3).
59
79
  section: §3 Filtering
60
80
  family: FILT
61
81
  phase: 1
@@ -67,7 +87,10 @@
67
87
  end_date, commits, insertions, deletions, files, merges. New fields are
68
88
  added at the end so existing column positions do not change. Values
69
89
  containing commas, quotes or line breaks are quoted. The header is printed
70
- even when there are no rows, and the output ends with a single newline.
90
+ even when there are no rows, and the output ends with a single newline. With
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).
71
94
  section: §4 Output
72
95
  family: OUT
73
96
  phase: 1
@@ -101,7 +124,9 @@
101
124
  title: Verbose diagnostics
102
125
  full: -v/--verbose prints debugging information (directories, the git commands
103
126
  run, contributors found) to standard error, so standard output stays
104
- machine-readable.
127
+ machine-readable. With --github, it also prints how each author name was
128
+ matched to a GitHub user, the ambiguous and unmatched names, and the number
129
+ of gh API calls made for each repository (CLI-3).
105
130
  section: §4 Output
106
131
  family: OUT
107
132
  phase: 1
@@ -172,7 +197,9 @@
172
197
  title: Branch selection
173
198
  full: -b/--branch <name> checks out the given branch before statistics are
174
199
  computed. Without -b, the repository's default branch is used — including
175
- when an earlier run left the cached clone on another branch.
200
+ when an earlier run left the cached clone on another branch. With --github,
201
+ it also limits which pull requests are counted to those whose base branch is
202
+ that branch (CLI-3).
176
203
  section: §1 Repository Selection
177
204
  family: REPO
178
205
  phase: 1
@@ -255,6 +282,53 @@
255
282
  family: STAT
256
283
  phase: 1
257
284
  status: Done
285
+ - id: STAT-8
286
+ title: Pull requests opened
287
+ full: With --github (CLI-3), for each contributor, the number of pull requests
288
+ they authored on GitHub whose creation time (created_at) falls in the date
289
+ range, whatever their state now (open, merged or closed; drafts included).
290
+ Reported in a prs_opened column.
291
+ section: §2 Contribution Statistics
292
+ family: STAT
293
+ phase: 1
294
+ status: Done
295
+ - id: STAT-9
296
+ title: Pull requests merged
297
+ full: With --github, for each contributor, the number of pull requests they
298
+ authored that were merged (merged_at set) in the date range, whoever merged
299
+ them. Reported in a prs_merged column. Unlike merges (STAT-7), this counts
300
+ squash and rebase merges too, since GitHub records them as merged pull
301
+ requests.
302
+ section: §2 Contribution Statistics
303
+ family: STAT
304
+ phase: 1
305
+ status: Done
306
+ - id: STAT-10
307
+ title: Pull requests closed without merging
308
+ full: With --github, for each contributor, the number of pull requests they
309
+ authored that were closed without being merged (state closed and merged_at
310
+ not set), with a closing time (closed_at) in the date range. Reported in a
311
+ prs_closed_unmerged column.
312
+ section: §2 Contribution Statistics
313
+ family: STAT
314
+ phase: 1
315
+ status: Done
316
+ - id: STAT-11
317
+ title: Matching GitHub users to git authors
318
+ full: "With --github, each pull-request author (a GitHub username) is matched to
319
+ the git identities (author name and email) in the repository's history. Each
320
+ git identity is tried against the GitHub users active in that repository
321
+ (pull-request authors and the accounts GitHub links to its commits), tier by
322
+ tier, stopping at the first tier that finds exactly one GitHub user: 1.
323
+ Email. The email is a GitHub noreply address
324
+ (12345+login@users.noreply.github.com or login@users.noreply.github.com); or
325
+ GitHub links one of the identity's commits to an account (the commit's
326
+ author.login, which covers private emails GitHub knows but does not
327
+ publish); or the email equals a candidate's public profile email…"
328
+ section: §2 Contribution Statistics
329
+ family: STAT
330
+ phase: 1
331
+ status: Done
258
332
  - id: FUTURE-author-date-filtering
259
333
  title: Author-date filtering
260
334
  full: Author-date filtering — offer filtering by author date, so rebased or
@@ -272,6 +346,30 @@
272
346
  family: FUTURE
273
347
  phase: 2
274
348
  status: Todo
349
+ - id: FUTURE-github-enterprise-hosts
350
+ title: GitHub Enterprise hosts
351
+ full: GitHub Enterprise hosts — extend --github to repositories on GitHub
352
+ Enterprise Server, via gh api --hostname or a configurable API URL.
353
+ section: §7 Future Work
354
+ family: FUTURE
355
+ phase: 2
356
+ status: Todo
357
+ - id: FUTURE-github-only-mode
358
+ title: GitHub-only mode
359
+ full: GitHub-only mode — report pull-request statistics without computing git
360
+ statistics, for faster runs over large batches.
361
+ section: §7 Future Work
362
+ family: FUTURE
363
+ phase: 2
364
+ status: Todo
365
+ - id: FUTURE-github-without-the-gh-cli
366
+ title: GitHub without the gh CLI
367
+ full: GitHub without the gh CLI — let --github call the GitHub API directly with
368
+ a GITHUB_TOKEN, for users who cannot install gh.
369
+ section: §7 Future Work
370
+ family: FUTURE
371
+ phase: 2
372
+ status: Todo
275
373
  - id: FUTURE-output-to-file
276
374
  title: Output to file
277
375
  full: Output to file — write results to a file (e.g. -o report.csv) instead of
@@ -304,3 +402,11 @@
304
402
  family: FUTURE
305
403
  phase: 2
306
404
  status: Todo
405
+ - id: FUTURE-pull-request-reviews-and-mergers
406
+ title: Pull-request reviews and mergers
407
+ full: Pull-request reviews and mergers — report who reviewed and who merged each
408
+ pull request, in addition to who opened it.
409
+ section: §7 Future Work
410
+ family: FUTURE
411
+ phase: 2
412
+ status: Todo
@@ -3,7 +3,11 @@ import subprocess
3
3
  import argparse
4
4
  import datetime
5
5
  import re
6
+ import sys
6
7
  from . import GitLogsParser
8
+ from .git_logs_parser import FIELDNAMES
9
+ from .github import GITHUB_FIELDNAMES, GitHubUnavailable, check_gh
10
+ from .github_stats import github_rows
7
11
 
8
12
 
9
13
  def valid_date(value):
@@ -135,6 +139,14 @@ def main():
135
139
  default=False,
136
140
  action="store_true",
137
141
  )
142
+ parser.add_argument(
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",
147
+ default=False,
148
+ action="store_true",
149
+ )
138
150
  args = parser.parse_args()
139
151
 
140
152
  if datetime.datetime.strptime(args.start, "%m/%d/%Y") > datetime.datetime.strptime(
@@ -145,6 +157,14 @@ def main():
145
157
  # split up comma-separated string into list, ignoring empty items
146
158
  args.exclusions = [x.strip() for x in re.split(r",", args.exclusions) if x.strip()]
147
159
 
160
+ # CLI-3: check the GitHub CLI before anything is created or cloned
161
+ if args.github:
162
+ try:
163
+ check_gh()
164
+ except GitHubUnavailable as exc:
165
+ print(exc, file=sys.stderr)
166
+ sys.exit(1)
167
+
148
168
  # deal with repofile, if specified
149
169
  repository_urls = [args.repository]
150
170
  if args.repofile:
@@ -193,20 +213,28 @@ def main():
193
213
  ["git", "pull"], capture_output=True, check=True
194
214
  ) # get the latest code from github
195
215
 
216
+ # with --github, -u and -c are applied after GitHub users are combined
196
217
  last_parser = GitLogsParser(
197
218
  repo=repo_dir,
198
219
  start=args.start,
199
220
  end=args.end,
200
- username=args.user,
221
+ username=None if args.github else args.user,
201
222
  exclusions=args.exclusions,
202
223
  verbose=args.verbose,
203
- clean=args.clean,
224
+ clean=False if args.github else args.clean,
204
225
  )
205
- all_results.extend(last_parser.parse()) # collect across repos
226
+ rows = last_parser.parse()
227
+ if args.github:
228
+ rows = github_rows(
229
+ last_parser, rows, repo_url, last_parser.get_identities(), args
230
+ )
231
+ all_results.extend(rows) # collect across repos
206
232
 
207
233
  # emit a single table (or JSON array) covering all repositories
208
234
  if last_parser is not None:
209
- output = last_parser.format_results(all_results, args.format)
235
+ # an empty CSV table still gets the GitHub columns with --github
236
+ fieldnames = FIELDNAMES + GITHUB_FIELDNAMES if args.github else None
237
+ output = last_parser.format_results(all_results, args.format, fieldnames)
210
238
  # end with exactly one newline: CSV and Markdown output already has one
211
239
  print(output, end="" if output.endswith("\n") else "\n")
212
240
 
@@ -93,6 +93,25 @@ class GitLogsParser:
93
93
  self.verboseprint(f"Contributors: {', '.join(contributors)}...")
94
94
  return contributors
95
95
 
96
+ def get_identities(self):
97
+ """
98
+ Return every identity (author name and email) in this repository's
99
+ history, the same population as get_contributors (STAT-11).
100
+ @returns: {author_name: {email: hash of one commit with that identity}}
101
+ """
102
+ cmd = ["git", "log", LOG_FORMAT]
103
+ self.verboseprint(f"Running command: {' '.join(cmd)}")
104
+ result = subprocess.run(cmd, capture_output=True, check=True)
105
+ identities = {}
106
+ for record in result.stdout.decode("utf-8", errors="replace").split(RECORD_SEP)[
107
+ 1:
108
+ ]:
109
+ fields = record.strip("\n").split(FIELD_SEP)
110
+ name = fields[1].strip().strip("'") # as get_contributors does
111
+ if name:
112
+ identities.setdefault(name, {}).setdefault(fields[2], fields[0])
113
+ return identities
114
+
96
115
  def date_range_args(self):
97
116
  """
98
117
  Build the git log options that limit commits to the date range.
@@ -275,11 +294,12 @@ class GitLogsParser:
275
294
  stats.append(entry)
276
295
  return stats
277
296
 
278
- def format_results(self, results, output_format):
297
+ def format_results(self, results, output_format, fieldnames=None):
279
298
  """
280
299
  Format the parsed data in the selected format.
281
300
  @param results: A list of dictionaries, with each representing a contributor to the repository.
282
301
  @param output_format: The desired output format, e.g. 'csv', 'json', or 'markdown'
302
+ @param fieldnames: the columns of an empty CSV table; defaults to FIELDNAMES
283
303
  """
284
304
  self.verboseprint(f"Outputting results in {output_format.upper()} format:...")
285
305
  output = ""
@@ -288,7 +308,10 @@ class GitLogsParser:
288
308
  # always include the header, so even an empty result is a valid
289
309
  # CSV table; quote values that contain commas, quotes, or line breaks
290
310
  buffer = io.StringIO()
291
- fieldnames = list(results[0].keys()) if results else FIELDNAMES
311
+ if results:
312
+ fieldnames = list(results[0].keys())
313
+ elif fieldnames is None:
314
+ fieldnames = FIELDNAMES
292
315
  writer = csv.DictWriter(buffer, fieldnames=fieldnames, lineterminator="\n")
293
316
  writer.writeheader()
294
317
  writer.writerows(results)
@@ -308,7 +331,11 @@ class GitLogsParser:
308
331
  ) # heading separator lines
309
332
  for entry in results:
310
333
  # each row of values, with any pipes escaped so they do not split cells
311
- str_values = [str(val).replace("|", "\\|") for val in entry.values()]
334
+ # (an unavailable value, None, is left blank)
335
+ str_values = [
336
+ ("" if val is None else str(val)).replace("|", "\\|")
337
+ for val in entry.values()
338
+ ]
312
339
  output += "| " + " | ".join(str_values) + " |\n"
313
340
 
314
341
  return output