gitlogstats 1.3.0__tar.gz → 1.4.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.
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/PKG-INFO +11 -5
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/README.md +10 -4
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/docs/SPEC.md +17 -5
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/pyproject.toml +1 -1
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/scripts/board/manifest.yaml +48 -15
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/src/gitlogstats/__main__.py +14 -4
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/src/gitlogstats/git_logs_parser.py +48 -3
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/src/gitlogstats/github.py +131 -9
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/src/gitlogstats/github_stats.py +26 -4
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/src/gitlogstats/identity.py +20 -9
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/tests/fixtures/github/pulls_page1.json +4 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/tests/test_github.py +72 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/tests/test_identity.py +22 -6
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/tests/test_integration.py +432 -16
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/.claude/CLAUDE.md +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/.claude/agents/developer-agent.md +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/.claude/agents/spec-reviewer.md +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/.claude/hooks/format-edited.sh +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/.claude/hooks/guard-paths.sh +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/.claude/hooks/guard-paths.test.mjs +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/.claude/hooks/verify.sh +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/.claude/settings.json +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/.claude/skills/phase-handoff/SKILL.md +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/.claude/skills/stale-check/SKILL.md +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/.github/workflows/build.yaml +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/.gitignore +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/.prettierrc.json +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/LICENSE +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/Pipfile +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/Pipfile.lock +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/package-lock.json +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/package.json +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/repos.txt +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/requirements.txt +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/scripts/board/config.mjs +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/scripts/board/derive.mjs +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/scripts/board/derive.test.mjs +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/scripts/board/sync.mjs +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/src/gitlogstats/__init__.py +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/tests/fake_gh.py +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/tests/fixtures/github/commit_linked.json +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/tests/fixtures/github/commit_unlinked.json +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/tests/fixtures/github/contributors.json +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/tests/fixtures/github/contributors_page2.json +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/tests/fixtures/github/pulls_page2.json +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/tests/fixtures/github/user_full.json +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.0}/tests/fixtures/github/user_private.json +0 -0
- {gitlogstats-1.3.0 → gitlogstats-1.4.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.
|
|
3
|
+
Version: 1.4.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, closed without merging and merged by themselves (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, their **pushes** to the analyzed branch and their commits made on GitHub's **website** (see [GitHub pull requests](#github-pull-requests)).
|
|
713
713
|
|
|
714
714
|
The results can be formatted as `csv`, `json`, or `markdown`.
|
|
715
715
|
|
|
@@ -753,8 +753,10 @@ options:
|
|
|
753
753
|
-c, --clean Remove contributors without any contribuition
|
|
754
754
|
--github Also report GitHub pull requests opened, merged,
|
|
755
755
|
closed and self-merged per contributor, with their git
|
|
756
|
-
and GitHub emails,
|
|
757
|
-
|
|
756
|
+
and GitHub emails, their pushes to the analyzed branch
|
|
757
|
+
and their commits made on the GitHub website, and
|
|
758
|
+
match GitHub users to git authors. Needs the GitHub
|
|
759
|
+
CLI (gh), signed in
|
|
758
760
|
```
|
|
759
761
|
|
|
760
762
|
In case `gitlogstats` is not found as a command, even after installing, try `python -m gitlogstats` or `python3 -m gitlogstats` instead.
|
|
@@ -837,7 +839,7 @@ gitlogstats -rf repos.txt -c
|
|
|
837
839
|
|
|
838
840
|
### GitHub pull requests
|
|
839
841
|
|
|
840
|
-
Add `--github` to also count each contributor's pull requests.
|
|
842
|
+
Add `--github` to also count each contributor's pull requests, pushes and website commits. Nine columns are appended to every output format:
|
|
841
843
|
|
|
842
844
|
| Column | Meaning |
|
|
843
845
|
| --------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
@@ -848,6 +850,8 @@ Add `--github` to also count each contributor's pull requests. Seven columns are
|
|
|
848
850
|
| `prs_self_merged` | pull requests they authored and also merged themselves, merged in the date range (also in `prs_merged`) |
|
|
849
851
|
| `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
852
|
| `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 |
|
|
853
|
+
| `pushes_to_default` | times they pushed directly to the analyzed branch (the default branch, or the `-b` branch) in the date range; each push counts once however many commits it carries. Pull request merges, merge-queue merges and creating or deleting branches are not pushes |
|
|
854
|
+
| `web_commits` | their commits counted in `commits` that were made on GitHub's website (committer `GitHub <noreply@github.com>`), such as editing, creating, deleting or uploading a file in the browser; never more than `commits`. The commit GitHub makes when it squash- or rebase-merges a pull request is not counted |
|
|
851
855
|
|
|
852
856
|
```
|
|
853
857
|
gitlogstats -r https://github.com/bloombar/gitlogstats.git --github
|
|
@@ -862,6 +866,8 @@ gitlogstats: --github needs the GitHub CLI (gh) to be signed in. Run 'gh auth lo
|
|
|
862
866
|
|
|
863
867
|
**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.
|
|
864
868
|
|
|
869
|
+
**Limits of pushes and website commits.** Pushes come from GitHub's repository activity record, which starts in September 2023: earlier pushes are not counted, however old the date range. A file edited on the website directly on the analyzed branch is recorded by GitHub as a push too, so it counts in both `pushes_to_default` and `web_commits`. Reading it works for any public repository you can see; for a private one you need access to it, and if GitHub refuses, the repository's GitHub columns are empty with the reason on standard error, as above. A push is credited to the GitHub user who pushed it, so a person who only pushes (and has no commits or pull requests in the range) still gets a row of their own. Pushes made while a tag or commit (not a branch) is checked out with `-b` are not counted. Website commits are recognised by GitHub being the committer, so commits made through GitHub's API, for example in the `github.dev` editor, are counted as website commits too, and so is the first commit GitHub creates when a repository is created on the website. Website commits follow `commits`: the same date range, exclusions (`-x`) and branch, credited to the commit's author and added up when names are combined into one row.
|
|
870
|
+
|
|
865
871
|
**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:
|
|
866
872
|
|
|
867
873
|
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.
|
|
@@ -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, closed without merging and merged by themselves (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, their **pushes** to the analyzed branch and their commits made on GitHub's **website** (see [GitHub pull requests](#github-pull-requests)).
|
|
16
16
|
|
|
17
17
|
The results can be formatted as `csv`, `json`, or `markdown`.
|
|
18
18
|
|
|
@@ -56,8 +56,10 @@ options:
|
|
|
56
56
|
-c, --clean Remove contributors without any contribuition
|
|
57
57
|
--github Also report GitHub pull requests opened, merged,
|
|
58
58
|
closed and self-merged per contributor, with their git
|
|
59
|
-
and GitHub emails,
|
|
60
|
-
|
|
59
|
+
and GitHub emails, their pushes to the analyzed branch
|
|
60
|
+
and their commits made on the GitHub website, and
|
|
61
|
+
match GitHub users to git authors. Needs the GitHub
|
|
62
|
+
CLI (gh), signed in
|
|
61
63
|
```
|
|
62
64
|
|
|
63
65
|
In case `gitlogstats` is not found as a command, even after installing, try `python -m gitlogstats` or `python3 -m gitlogstats` instead.
|
|
@@ -140,7 +142,7 @@ gitlogstats -rf repos.txt -c
|
|
|
140
142
|
|
|
141
143
|
### GitHub pull requests
|
|
142
144
|
|
|
143
|
-
Add `--github` to also count each contributor's pull requests.
|
|
145
|
+
Add `--github` to also count each contributor's pull requests, pushes and website commits. Nine columns are appended to every output format:
|
|
144
146
|
|
|
145
147
|
| Column | Meaning |
|
|
146
148
|
| --------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
@@ -151,6 +153,8 @@ Add `--github` to also count each contributor's pull requests. Seven columns are
|
|
|
151
153
|
| `prs_self_merged` | pull requests they authored and also merged themselves, merged in the date range (also in `prs_merged`) |
|
|
152
154
|
| `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
155
|
| `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 |
|
|
156
|
+
| `pushes_to_default` | times they pushed directly to the analyzed branch (the default branch, or the `-b` branch) in the date range; each push counts once however many commits it carries. Pull request merges, merge-queue merges and creating or deleting branches are not pushes |
|
|
157
|
+
| `web_commits` | their commits counted in `commits` that were made on GitHub's website (committer `GitHub <noreply@github.com>`), such as editing, creating, deleting or uploading a file in the browser; never more than `commits`. The commit GitHub makes when it squash- or rebase-merges a pull request is not counted |
|
|
154
158
|
|
|
155
159
|
```
|
|
156
160
|
gitlogstats -r https://github.com/bloombar/gitlogstats.git --github
|
|
@@ -165,6 +169,8 @@ gitlogstats: --github needs the GitHub CLI (gh) to be signed in. Run 'gh auth lo
|
|
|
165
169
|
|
|
166
170
|
**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.
|
|
167
171
|
|
|
172
|
+
**Limits of pushes and website commits.** Pushes come from GitHub's repository activity record, which starts in September 2023: earlier pushes are not counted, however old the date range. A file edited on the website directly on the analyzed branch is recorded by GitHub as a push too, so it counts in both `pushes_to_default` and `web_commits`. Reading it works for any public repository you can see; for a private one you need access to it, and if GitHub refuses, the repository's GitHub columns are empty with the reason on standard error, as above. A push is credited to the GitHub user who pushed it, so a person who only pushes (and has no commits or pull requests in the range) still gets a row of their own. Pushes made while a tag or commit (not a branch) is checked out with `-b` are not counted. Website commits are recognised by GitHub being the committer, so commits made through GitHub's API, for example in the `github.dev` editor, are counted as website commits too, and so is the first commit GitHub creates when a repository is created on the website. Website commits follow `commits`: the same date range, exclusions (`-x`) and branch, credited to the commit's author and added up when names are combined into one row.
|
|
173
|
+
|
|
168
174
|
**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:
|
|
169
175
|
|
|
170
176
|
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.
|
|
@@ -126,11 +126,23 @@ With `--github`, every row also reports the contributor's email addresses in two
|
|
|
126
126
|
|
|
127
127
|
**Status:** Done
|
|
128
128
|
|
|
129
|
+
#### STAT-14 Pushes to the analyzed branch
|
|
130
|
+
|
|
131
|
+
With `--github`, for each contributor, the number of times they pushed directly to the analyzed branch (the default branch, or the `-b` branch) in the date range, reported in a `pushes_to_default` column appended after `github_email`. Pushes come from GitHub's repository activity record (`repos/{owner}/{repo}/activity`), which names the GitHub user who pushed: activity types `push` and `force_push` count; pull-request merges, merge-queue merges and branch creation or deletion do not. One push counts once however many commits it carries. Each push is filtered by its own timestamp, using the same inclusive local-time range as FILT-1, and credited to the row of the GitHub user who pushed (STAT-11); a GitHub user with pushes but no other row gets a GitHub-only row. GitHub's activity record does not go back before September 2023, so older pushes are not counted. Without `-b`, the branch is the default branch; if the clone is on no branch (a tag or commit was checked out), no pushes are counted. GitHub lets anyone who can see a public repository read its activity record; a private repository needs access to it.
|
|
132
|
+
|
|
133
|
+
**Status:** Done
|
|
134
|
+
|
|
135
|
+
#### STAT-15 Commits made on the GitHub website
|
|
136
|
+
|
|
137
|
+
With `--github`, for each contributor, how many of their commits counted in `commits` (STAT-1: same date range, exclusions and branch, merges excluded) were made on GitHub's website rather than in a local clone, reported in a `web_commits` column appended after `pushes_to_default`, so `web_commits` is never larger than `commits`. A commit counts when GitHub created it — its committer is `GitHub <noreply@github.com>` — and it is not the result of merging a pull request: a commit GitHub created when squash- or rebase-merging a pull request is not a website commit. Such a commit is the merge commit GitHub names for a merged pull request, or was committed within 2 seconds of one (the other commits of a rebase merge); GitHub commits the result a few seconds before it marks the pull request merged, so the pull request's merge time is used only when GitHub names no merge commit. A named merge commit committed more than a minute before or after the merge time is the pull request's own head commit, which reached the branch another way, so that pull request excludes nothing. A commit GitHub created when a repository was created on the website (a root commit) counts like any other. Such commits are typically file edits, creations, deletions and uploads made in the browser; commits made through GitHub's API (e.g. `github.dev`) look the same and are counted too. Website commits are credited to the commit's author, like `commits`, and summed when identities are combined (STAT-11). When GitHub statistics are unavailable for a repository (CLI-3), `pushes_to_default` and `web_commits` are empty (`null` in JSON).
|
|
138
|
+
|
|
139
|
+
**Status:** Done
|
|
140
|
+
|
|
129
141
|
### 3. Filtering
|
|
130
142
|
|
|
131
143
|
#### FILT-1 Date range
|
|
132
144
|
|
|
133
|
-
`-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).
|
|
145
|
+
`-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 and push count is filtered by its own timestamp in the same range (CLI-3).
|
|
134
146
|
|
|
135
147
|
**Status:** Done
|
|
136
148
|
|
|
@@ -148,7 +160,7 @@ With `--github`, every row also reports the contributor's email addresses in two
|
|
|
148
160
|
|
|
149
161
|
#### FILT-4 Remove inactive contributors
|
|
150
162
|
|
|
151
|
-
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
|
|
163
|
+
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, pull-request nor push activity are removed (CLI-3).
|
|
152
164
|
|
|
153
165
|
**Status:** Done
|
|
154
166
|
|
|
@@ -156,7 +168,7 @@ By default, contributors with no activity in the date range are listed with zero
|
|
|
156
168
|
|
|
157
169
|
#### OUT-1 CSV output
|
|
158
170
|
|
|
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).
|
|
171
|
+
`-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, pushes_to_default, web_commits` are appended, and are in the header even when there are no rows (CLI-3).
|
|
160
172
|
|
|
161
173
|
**Status:** Done
|
|
162
174
|
|
|
@@ -200,12 +212,12 @@ Invalid dates, and a start date after the end date, are rejected with a usage er
|
|
|
200
212
|
|
|
201
213
|
#### CLI-3 GitHub pull-request statistics
|
|
202
214
|
|
|
203
|
-
`--github` adds GitHub pull-request statistics (STAT-8 to STAT-12)
|
|
215
|
+
`--github` adds GitHub pull-request statistics (STAT-8 to STAT-12), contributor emails (STAT-13), pushes to the analyzed branch (STAT-14) and website commits (STAT-15) 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, pushes_to_default, web_commits` 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:
|
|
204
216
|
|
|
205
217
|
- `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.`
|
|
206
218
|
- `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.`
|
|
207
219
|
|
|
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
|
|
220
|
+
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, pull-request nor push activity; `-x` does not apply to pull requests or pushes (it limits which commits count as website commits, as it does for `commits`); `-b` also selects the branch whose pushes are counted. 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`, `pushes_to_default` and `web_commits`) 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.
|
|
209
221
|
|
|
210
222
|
**Status:** Done
|
|
211
223
|
|
|
@@ -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.
|
|
8
|
+
version = "1.4.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-12)
|
|
23
|
-
contributor emails (STAT-13)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
22
|
+
full: --github adds GitHub pull-request statistics (STAT-8 to STAT-12),
|
|
23
|
+
contributor emails (STAT-13), pushes to the analyzed branch (STAT-14) and
|
|
24
|
+
website commits (STAT-15) for repositories hosted on github.com, appending
|
|
25
|
+
the columns github_username, prs_opened, prs_merged, prs_closed_unmerged,
|
|
26
|
+
prs_self_merged, git_email, github_email, pushes_to_default, web_commits to
|
|
27
|
+
every output format. Without --github, the output and behaviour are
|
|
28
|
+
unchanged, GitHub is never contacted and the GitHub CLI (gh) is not needed.
|
|
29
|
+
With --github, the GitHub API is called through gh api, so gh must be
|
|
30
|
+
installed and signed in. This is checked before any repository is cloned; if
|
|
31
|
+
it fails, nothing is printed to standard…
|
|
32
32
|
section: §5 Command-Line Interface
|
|
33
33
|
family: CLI
|
|
34
34
|
phase: 1
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
whose commit date falls between the start of the start date and the end of
|
|
40
40
|
the end date, inclusive, in local time. Defaults: one year ago until today.
|
|
41
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
|
|
43
|
-
same range (CLI-3)."
|
|
42
|
+
--github, each pull request and push count is filtered by its own timestamp
|
|
43
|
+
in the same range (CLI-3)."
|
|
44
44
|
section: §3 Filtering
|
|
45
45
|
family: FILT
|
|
46
46
|
phase: 1
|
|
@@ -75,7 +75,7 @@
|
|
|
75
75
|
title: Remove inactive contributors
|
|
76
76
|
full: By default, contributors with no activity in the date range are listed
|
|
77
77
|
with zeros. -c/--clean removes them. With --github, only rows with neither
|
|
78
|
-
git
|
|
78
|
+
git, pull-request nor push activity are removed (CLI-3).
|
|
79
79
|
section: §3 Filtering
|
|
80
80
|
family: FILT
|
|
81
81
|
phase: 1
|
|
@@ -89,8 +89,9 @@
|
|
|
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, prs_self_merged, git_email, github_email
|
|
93
|
-
and are in the header even when
|
|
92
|
+
prs_closed_unmerged, prs_self_merged, git_email, github_email,
|
|
93
|
+
pushes_to_default, web_commits are appended, and are in the header even when
|
|
94
|
+
there are no rows (CLI-3).
|
|
94
95
|
section: §4 Output
|
|
95
96
|
family: OUT
|
|
96
97
|
phase: 1
|
|
@@ -364,6 +365,38 @@
|
|
|
364
365
|
family: STAT
|
|
365
366
|
phase: 1
|
|
366
367
|
status: Done
|
|
368
|
+
- id: STAT-14
|
|
369
|
+
title: Pushes to the analyzed branch
|
|
370
|
+
full: "With --github, for each contributor, the number of times they pushed
|
|
371
|
+
directly to the analyzed branch (the default branch, or the -b branch) in
|
|
372
|
+
the date range, reported in a pushes_to_default column appended after
|
|
373
|
+
github_email. Pushes come from GitHub's repository activity record
|
|
374
|
+
(repos/{owner}/{repo}/activity), which names the GitHub user who pushed:
|
|
375
|
+
activity types push and force_push count; pull-request merges, merge-queue
|
|
376
|
+
merges and branch creation or deletion do not. One push counts once however
|
|
377
|
+
many commits it carries. Each push is filtered by its own timestamp, using
|
|
378
|
+
the same inclusive local-time range as FILT-1, and credited to the row of
|
|
379
|
+
the GitHub user who pushed (STAT-11); a GitHub…"
|
|
380
|
+
section: §2 Contribution Statistics
|
|
381
|
+
family: STAT
|
|
382
|
+
phase: 1
|
|
383
|
+
status: Done
|
|
384
|
+
- id: STAT-15
|
|
385
|
+
title: Commits made on the GitHub website
|
|
386
|
+
full: "With --github, for each contributor, how many of their commits counted in
|
|
387
|
+
commits (STAT-1: same date range, exclusions and branch, merges excluded)
|
|
388
|
+
were made on GitHub's website rather than in a local clone, reported in a
|
|
389
|
+
web_commits column appended after pushes_to_default, so web_commits is never
|
|
390
|
+
larger than commits. A commit counts when GitHub created it — its committer
|
|
391
|
+
is GitHub <noreply@github.com> — and it is not the result of merging a pull
|
|
392
|
+
request: a commit GitHub created when squash- or rebase-merging a pull
|
|
393
|
+
request is not a website commit. Such a commit is the merge commit GitHub
|
|
394
|
+
names for a merged pull request, or was committed within 2 seconds of one
|
|
395
|
+
(the other commits of a…"
|
|
396
|
+
section: §2 Contribution Statistics
|
|
397
|
+
family: STAT
|
|
398
|
+
phase: 1
|
|
399
|
+
status: Done
|
|
367
400
|
- id: FUTURE-author-date-filtering
|
|
368
401
|
title: Author-date filtering
|
|
369
402
|
full: Author-date filtering — offer filtering by author date, so rebased or
|
|
@@ -6,7 +6,13 @@ import re
|
|
|
6
6
|
import sys
|
|
7
7
|
from . import GitLogsParser
|
|
8
8
|
from .git_logs_parser import FIELDNAMES
|
|
9
|
-
from .github import
|
|
9
|
+
from .github import (
|
|
10
|
+
ACTIVITY_FIELDNAMES,
|
|
11
|
+
EMAIL_FIELDNAMES,
|
|
12
|
+
GITHUB_FIELDNAMES,
|
|
13
|
+
GitHubUnavailable,
|
|
14
|
+
check_gh,
|
|
15
|
+
)
|
|
10
16
|
from .github_stats import github_rows
|
|
11
17
|
|
|
12
18
|
|
|
@@ -142,8 +148,10 @@ def main():
|
|
|
142
148
|
parser.add_argument(
|
|
143
149
|
"--github",
|
|
144
150
|
help="Also report GitHub pull requests opened, merged, closed and "
|
|
145
|
-
"self-merged per contributor, with their git and GitHub emails,
|
|
146
|
-
"
|
|
151
|
+
"self-merged per contributor, with their git and GitHub emails, their "
|
|
152
|
+
"pushes to the analyzed branch and their commits made on the GitHub "
|
|
153
|
+
"website, and match GitHub users to git authors. "
|
|
154
|
+
"Needs the GitHub CLI (gh), signed in",
|
|
147
155
|
default=False,
|
|
148
156
|
action="store_true",
|
|
149
157
|
)
|
|
@@ -234,7 +242,9 @@ def main():
|
|
|
234
242
|
if last_parser is not None:
|
|
235
243
|
# an empty CSV table still gets the GitHub columns with --github
|
|
236
244
|
fieldnames = (
|
|
237
|
-
FIELDNAMES + GITHUB_FIELDNAMES + EMAIL_FIELDNAMES
|
|
245
|
+
FIELDNAMES + GITHUB_FIELDNAMES + EMAIL_FIELDNAMES + ACTIVITY_FIELDNAMES
|
|
246
|
+
if args.github
|
|
247
|
+
else None
|
|
238
248
|
)
|
|
239
249
|
output = last_parser.format_results(all_results, args.format, fieldnames)
|
|
240
250
|
# end with exactly one newline: CSV and Markdown output already has one
|
|
@@ -21,6 +21,11 @@ FILES_RE = re.compile(r"(\d+) files? changed")
|
|
|
21
21
|
INSERTIONS_RE = re.compile(r"(\d+) insertions?\(\+\)")
|
|
22
22
|
DELETIONS_RE = re.compile(r"(\d+) deletions?\(-\)")
|
|
23
23
|
|
|
24
|
+
# STAT-15: the header of each commit when listing who committed it and when
|
|
25
|
+
COMMITTER_LOG_FORMAT = (
|
|
26
|
+
f"--format={RECORD_SEP}%H{FIELD_SEP}%aN{FIELD_SEP}%aE{FIELD_SEP}%cE{FIELD_SEP}%ct"
|
|
27
|
+
)
|
|
28
|
+
|
|
24
29
|
# the fields reported for each contributor, in output order
|
|
25
30
|
FIELDNAMES = [
|
|
26
31
|
"username",
|
|
@@ -128,11 +133,13 @@ class GitLogsParser:
|
|
|
128
133
|
f"--until={until:%Y-%m-%d %H:%M:%S}",
|
|
129
134
|
]
|
|
130
135
|
|
|
131
|
-
def git_log_command(self):
|
|
136
|
+
def git_log_command(self, log_format=LOG_FORMAT, shortstat=True):
|
|
132
137
|
"""
|
|
133
138
|
Build the git log command that lists every non-merge commit in the date
|
|
134
139
|
range with its author and a one-line summary of its changes. Merge
|
|
135
140
|
commits come from merge_log_command instead.
|
|
141
|
+
@param log_format: the --format argument
|
|
142
|
+
@param shortstat: whether to add the summary of changes
|
|
136
143
|
@returns: the command as a list of arguments, ready for subprocess
|
|
137
144
|
"""
|
|
138
145
|
# --full-history stops git from hiding commits while simplifying the
|
|
@@ -142,8 +149,8 @@ class GitLogsParser:
|
|
|
142
149
|
"log",
|
|
143
150
|
"--full-history",
|
|
144
151
|
"--no-merges",
|
|
145
|
-
"--shortstat",
|
|
146
|
-
|
|
152
|
+
*(["--shortstat"] if shortstat else []),
|
|
153
|
+
log_format,
|
|
147
154
|
*self.date_range_args(),
|
|
148
155
|
"--",
|
|
149
156
|
".",
|
|
@@ -185,6 +192,44 @@ class GitLogsParser:
|
|
|
185
192
|
*self.date_range_args(),
|
|
186
193
|
]
|
|
187
194
|
|
|
195
|
+
def current_branch(self):
|
|
196
|
+
"""
|
|
197
|
+
Name the branch checked out in the repository.
|
|
198
|
+
@returns: the branch name, or None if HEAD is detached
|
|
199
|
+
"""
|
|
200
|
+
result = subprocess.run(
|
|
201
|
+
["git", "symbolic-ref", "--short", "-q", "HEAD"], capture_output=True
|
|
202
|
+
)
|
|
203
|
+
if result.returncode != 0:
|
|
204
|
+
return None
|
|
205
|
+
return result.stdout.decode("utf-8", errors="replace").strip()
|
|
206
|
+
|
|
207
|
+
def committed_commits(self):
|
|
208
|
+
"""
|
|
209
|
+
STAT-15: list the commits counted in `commits` (STAT-1: the same date
|
|
210
|
+
range, exclusions and branch, merges left out) with their committers.
|
|
211
|
+
@returns: a list of dicts with hash, name (author), email (author),
|
|
212
|
+
committer_email and committed (committer time, epoch seconds)
|
|
213
|
+
"""
|
|
214
|
+
cmd = self.git_log_command(COMMITTER_LOG_FORMAT, shortstat=False)
|
|
215
|
+
self.verboseprint(f"Running command: {' '.join(cmd)}")
|
|
216
|
+
result = subprocess.run(cmd, capture_output=True, check=True)
|
|
217
|
+
commits = []
|
|
218
|
+
for record in result.stdout.decode("utf-8", errors="replace").split(RECORD_SEP)[
|
|
219
|
+
1:
|
|
220
|
+
]:
|
|
221
|
+
fields = record.strip("\n").split(FIELD_SEP)
|
|
222
|
+
commits.append(
|
|
223
|
+
{
|
|
224
|
+
"hash": fields[0],
|
|
225
|
+
"name": fields[1],
|
|
226
|
+
"email": fields[2],
|
|
227
|
+
"committer_email": fields[3],
|
|
228
|
+
"committed": int(fields[4]),
|
|
229
|
+
}
|
|
230
|
+
)
|
|
231
|
+
return commits
|
|
232
|
+
|
|
188
233
|
def run_git_log(self, cmd):
|
|
189
234
|
"""
|
|
190
235
|
Run a git log command and parse its output into per-commit records.
|
|
@@ -11,6 +11,7 @@ import json
|
|
|
11
11
|
import re
|
|
12
12
|
import shutil
|
|
13
13
|
import subprocess
|
|
14
|
+
import urllib.parse
|
|
14
15
|
|
|
15
16
|
# CLI-3: the extra output columns
|
|
16
17
|
GITHUB_FIELDNAMES = [
|
|
@@ -24,6 +25,25 @@ GITHUB_FIELDNAMES = [
|
|
|
24
25
|
# STAT-13: the email columns that follow the pull-request columns
|
|
25
26
|
EMAIL_FIELDNAMES = ["git_email", "github_email"]
|
|
26
27
|
|
|
28
|
+
# STAT-14, STAT-15: the activity columns that follow the email columns
|
|
29
|
+
ACTIVITY_FIELDNAMES = ["pushes_to_default", "web_commits"]
|
|
30
|
+
|
|
31
|
+
# STAT-14: the activity types that are a person pushing to a branch
|
|
32
|
+
PUSH_TYPES = ("push", "force_push")
|
|
33
|
+
|
|
34
|
+
# STAT-15: the committer GitHub records for commits it creates itself
|
|
35
|
+
GITHUB_COMMITTER_EMAIL = "noreply@github.com"
|
|
36
|
+
|
|
37
|
+
# STAT-15: how many seconds a commit's committer time may differ from a pull
|
|
38
|
+
# request's merge commit and still be part of that pull request's merge
|
|
39
|
+
MERGE_TIME_TOLERANCE = 2
|
|
40
|
+
|
|
41
|
+
# STAT-15: GitHub commits a merge result seconds before it stamps the pull
|
|
42
|
+
# request merged; a "merge commit" much older than the merge is the pull
|
|
43
|
+
# request's own head commit, which reached the branch another way, so that
|
|
44
|
+
# merge created no commit
|
|
45
|
+
MERGE_COMMIT_LAG = 60
|
|
46
|
+
|
|
27
47
|
# CLI-3: the two user-facing messages, verbatim from the SPEC
|
|
28
48
|
GH_NOT_INSTALLED = (
|
|
29
49
|
"gitlogstats: --github needs the GitHub CLI (gh), which is not installed. "
|
|
@@ -43,7 +63,8 @@ PULLS_QUERY = (
|
|
|
43
63
|
"pullRequests(first:100,after:$endCursor){"
|
|
44
64
|
"pageInfo{hasNextPage endCursor} "
|
|
45
65
|
"nodes{author{login __typename} mergedBy{login __typename} "
|
|
46
|
-
"state createdAt mergedAt closedAt baseRefName
|
|
66
|
+
"state createdAt mergedAt closedAt baseRefName "
|
|
67
|
+
"mergeCommit{oid committedDate}}}}}"
|
|
47
68
|
)
|
|
48
69
|
|
|
49
70
|
# Login given to pull requests whose author's account was deleted
|
|
@@ -217,7 +238,9 @@ def fetch_pull_requests(repo):
|
|
|
217
238
|
@param repo: "owner/repo"
|
|
218
239
|
@returns: a list of dicts with login, user_type, state ("open" or
|
|
219
240
|
"closed"), created_at, merged_at, closed_at, base (the base branch
|
|
220
|
-
name)
|
|
241
|
+
name), merged_by (a login, or None if not merged or unknown),
|
|
242
|
+
merge_commit (the hash of the commit that merged it, or None) and
|
|
243
|
+
merge_committed_at (that commit's committer time, or None)
|
|
221
244
|
@raises GitHubError: if the request fails
|
|
222
245
|
"""
|
|
223
246
|
owner, name = repo.split("/", 1)
|
|
@@ -229,6 +252,7 @@ def fetch_pull_requests(repo):
|
|
|
229
252
|
raise GitHubError(f"could not read pull requests of {repo}")
|
|
230
253
|
for pull in repository["pullRequests"]["nodes"]:
|
|
231
254
|
author = pull.get("author")
|
|
255
|
+
merge_commit = pull.get("mergeCommit") or {}
|
|
232
256
|
result.append(
|
|
233
257
|
{
|
|
234
258
|
"login": _graphql_login(author) or GHOST_LOGIN,
|
|
@@ -239,6 +263,8 @@ def fetch_pull_requests(repo):
|
|
|
239
263
|
"closed_at": pull.get("closedAt"),
|
|
240
264
|
"base": pull.get("baseRefName"),
|
|
241
265
|
"merged_by": _graphql_login(pull.get("mergedBy")),
|
|
266
|
+
"merge_commit": merge_commit.get("oid"),
|
|
267
|
+
"merge_committed_at": merge_commit.get("committedDate"),
|
|
242
268
|
}
|
|
243
269
|
)
|
|
244
270
|
return result
|
|
@@ -256,16 +282,13 @@ def _local_time(timestamp):
|
|
|
256
282
|
return datetime.datetime.fromtimestamp(calendar.timegm(utc.timetuple()))
|
|
257
283
|
|
|
258
284
|
|
|
259
|
-
def
|
|
285
|
+
def _range_test(start, end):
|
|
260
286
|
"""
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
@param prs: the list from fetch_pull_requests
|
|
287
|
+
Build a test for whether a GitHub timestamp falls in the date range. The
|
|
288
|
+
range is inclusive local time, as in FILT-1.
|
|
264
289
|
@param start: first day, mm/dd/yyyy
|
|
265
290
|
@param end: last day, mm/dd/yyyy
|
|
266
|
-
@
|
|
267
|
-
@returns: {login: {"prs_opened", "prs_merged", "prs_closed_unmerged",
|
|
268
|
-
"prs_self_merged"}}
|
|
291
|
+
@returns: a function taking a timestamp (or None) and returning a bool
|
|
269
292
|
"""
|
|
270
293
|
since = datetime.datetime.strptime(start, "%m/%d/%Y")
|
|
271
294
|
until = datetime.datetime.strptime(end, "%m/%d/%Y").replace(
|
|
@@ -276,6 +299,22 @@ def count_pull_requests(prs, start, end, branch=None):
|
|
|
276
299
|
moment = _local_time(timestamp)
|
|
277
300
|
return moment is not None and since <= moment <= until
|
|
278
301
|
|
|
302
|
+
return in_range
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def count_pull_requests(prs, start, end, branch=None):
|
|
306
|
+
"""
|
|
307
|
+
STAT-8, STAT-9, STAT-10, STAT-12: count each user's pull requests in a date range.
|
|
308
|
+
The range is inclusive local time, as in FILT-1.
|
|
309
|
+
@param prs: the list from fetch_pull_requests
|
|
310
|
+
@param start: first day, mm/dd/yyyy
|
|
311
|
+
@param end: last day, mm/dd/yyyy
|
|
312
|
+
@param branch: count only pull requests with this base branch, if given
|
|
313
|
+
@returns: {login: {"prs_opened", "prs_merged", "prs_closed_unmerged",
|
|
314
|
+
"prs_self_merged"}}
|
|
315
|
+
"""
|
|
316
|
+
in_range = _range_test(start, end)
|
|
317
|
+
|
|
279
318
|
counts = {}
|
|
280
319
|
for pr in prs:
|
|
281
320
|
if branch is not None and pr["base"] != branch:
|
|
@@ -306,6 +345,89 @@ def count_pull_requests(prs, start, end, branch=None):
|
|
|
306
345
|
return counts
|
|
307
346
|
|
|
308
347
|
|
|
348
|
+
def fetch_pushes(repo, branch):
|
|
349
|
+
"""
|
|
350
|
+
STAT-14: list the pushes to one branch from GitHub's activity record.
|
|
351
|
+
@param repo: "owner/repo"
|
|
352
|
+
@param branch: the branch name, e.g. "main"
|
|
353
|
+
@returns: a list of {"login", "timestamp"}, one per push or force push;
|
|
354
|
+
the login of a deleted account is GHOST_LOGIN
|
|
355
|
+
@raises GitHubError: if the request fails
|
|
356
|
+
"""
|
|
357
|
+
ref = urllib.parse.quote(f"refs/heads/{branch}", safe="/")
|
|
358
|
+
activity = gh_api(f"repos/{repo}/activity?per_page=100&ref={ref}", paginate=True)
|
|
359
|
+
return [
|
|
360
|
+
{
|
|
361
|
+
"login": (item.get("actor") or {}).get("login") or GHOST_LOGIN,
|
|
362
|
+
"timestamp": item.get("timestamp"),
|
|
363
|
+
}
|
|
364
|
+
for item in activity
|
|
365
|
+
if item.get("activity_type") in PUSH_TYPES
|
|
366
|
+
]
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def count_pushes(pushes, start, end):
|
|
370
|
+
"""
|
|
371
|
+
STAT-14: count each user's pushes in a date range (inclusive local time).
|
|
372
|
+
@param pushes: the list from fetch_pushes
|
|
373
|
+
@param start: first day, mm/dd/yyyy
|
|
374
|
+
@param end: last day, mm/dd/yyyy
|
|
375
|
+
@returns: {login: number of pushes}
|
|
376
|
+
"""
|
|
377
|
+
in_range = _range_test(start, end)
|
|
378
|
+
counts = {}
|
|
379
|
+
for push in pushes:
|
|
380
|
+
if in_range(push["timestamp"]):
|
|
381
|
+
counts[push["login"]] = counts.get(push["login"], 0) + 1
|
|
382
|
+
return counts
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def _utc_seconds(timestamp):
|
|
386
|
+
"""Convert a GitHub UTC timestamp to seconds since the epoch."""
|
|
387
|
+
utc = datetime.datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%SZ")
|
|
388
|
+
return calendar.timegm(utc.timetuple())
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def count_web_commits(commits, prs):
|
|
392
|
+
"""
|
|
393
|
+
STAT-15: count each author's commits that GitHub created itself, other
|
|
394
|
+
than the result of merging a pull request. A merge result is the commit
|
|
395
|
+
GitHub names as a pull request's merge commit, or one committed at the
|
|
396
|
+
same moment (within MERGE_TIME_TOLERANCE seconds) as that commit, which
|
|
397
|
+
covers the other commits of a rebase merge. GitHub commits the merge
|
|
398
|
+
result a few seconds before it stamps the pull request merged, so a pull
|
|
399
|
+
request without a merge commit is compared by its merge time instead.
|
|
400
|
+
@param commits: dicts with "hash", "name" (author), "committer_email" and
|
|
401
|
+
"committed" (committer time, seconds since the epoch), for the
|
|
402
|
+
commits counted in STAT-1
|
|
403
|
+
@param prs: the list from fetch_pull_requests
|
|
404
|
+
@returns: {author name: number of website commits}
|
|
405
|
+
"""
|
|
406
|
+
merge_hashes = set()
|
|
407
|
+
merge_times = []
|
|
408
|
+
for pr in prs:
|
|
409
|
+
if not pr["merged_at"]:
|
|
410
|
+
continue
|
|
411
|
+
merged = _utc_seconds(pr["merged_at"])
|
|
412
|
+
committed = _utc_seconds(pr.get("merge_committed_at") or pr["merged_at"])
|
|
413
|
+
if abs(merged - committed) > MERGE_COMMIT_LAG:
|
|
414
|
+
continue # the pull request's own head commit, made earlier
|
|
415
|
+
if pr.get("merge_commit"):
|
|
416
|
+
merge_hashes.add(pr["merge_commit"])
|
|
417
|
+
merge_times.append(committed)
|
|
418
|
+
counts = {}
|
|
419
|
+
for commit in commits:
|
|
420
|
+
if commit["committer_email"].strip().casefold() != GITHUB_COMMITTER_EMAIL:
|
|
421
|
+
continue
|
|
422
|
+
if commit["hash"] in merge_hashes or any(
|
|
423
|
+
abs(commit["committed"] - merged) <= MERGE_TIME_TOLERANCE
|
|
424
|
+
for merged in merge_times
|
|
425
|
+
):
|
|
426
|
+
continue
|
|
427
|
+
counts[commit["name"]] = counts.get(commit["name"], 0) + 1
|
|
428
|
+
return counts
|
|
429
|
+
|
|
430
|
+
|
|
309
431
|
def commit_login(repo, sha):
|
|
310
432
|
"""
|
|
311
433
|
STAT-11: find the GitHub account GitHub links to a commit.
|