gitlogstats 1.1.0__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.
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/.claude/CLAUDE.md +2 -1
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/.claude/settings.json +3 -0
- gitlogstats-1.2.0/.github/workflows/build.yaml +65 -0
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/PKG-INFO +14 -12
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/Pipfile +0 -2
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/Pipfile.lock +1 -59
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/README.md +13 -11
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/docs/SPEC.md +12 -6
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/pyproject.toml +1 -1
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/scripts/board/manifest.yaml +35 -16
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/src/gitlogstats/__main__.py +3 -3
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/src/gitlogstats/github.py +96 -19
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/src/gitlogstats/identity.py +2 -2
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/tests/fake_gh.py +16 -2
- gitlogstats-1.2.0/tests/fixtures/github/pulls_page1.json +29 -0
- gitlogstats-1.2.0/tests/fixtures/github/pulls_page2.json +53 -0
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/tests/test_github.py +209 -25
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/tests/test_identity.py +12 -4
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/tests/test_integration.py +107 -40
- gitlogstats-1.1.0/.github/workflows/build.yaml +0 -46
- gitlogstats-1.1.0/tests/fixtures/github/pulls_page1.json +0 -20
- gitlogstats-1.1.0/tests/fixtures/github/pulls_page2.json +0 -29
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/.claude/agents/developer-agent.md +0 -0
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/.claude/agents/spec-reviewer.md +0 -0
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/.claude/hooks/format-edited.sh +0 -0
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/.claude/hooks/guard-paths.sh +0 -0
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/.claude/hooks/guard-paths.test.mjs +0 -0
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/.claude/hooks/verify.sh +0 -0
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/.claude/skills/phase-handoff/SKILL.md +0 -0
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/.claude/skills/stale-check/SKILL.md +0 -0
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/.gitignore +0 -0
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/.prettierrc.json +0 -0
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/LICENSE +0 -0
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/package-lock.json +0 -0
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/package.json +0 -0
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/repos.txt +0 -0
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/requirements.txt +0 -0
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/scripts/board/config.mjs +0 -0
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/scripts/board/derive.mjs +0 -0
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/scripts/board/derive.test.mjs +0 -0
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/scripts/board/sync.mjs +0 -0
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/src/gitlogstats/__init__.py +0 -0
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/src/gitlogstats/git_logs_parser.py +0 -0
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/src/gitlogstats/github_stats.py +0 -0
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/tests/fixtures/github/commit_linked.json +0 -0
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/tests/fixtures/github/commit_unlinked.json +0 -0
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/tests/fixtures/github/contributors.json +0 -0
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/tests/fixtures/github/contributors_page2.json +0 -0
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/tests/fixtures/github/user_full.json +0 -0
- {gitlogstats-1.1.0 → gitlogstats-1.2.0}/tests/fixtures/github/user_private.json +0 -0
- {gitlogstats-1.1.0 → 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
|
-
-
|
|
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
|
|
|
@@ -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.
|
|
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,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
|
|
712
|
+
With `--github`, it also reports each developer's GitHub **pull requests** opened, merged, closed without merging and merged by themselves (see [GitHub pull requests](#github-pull-requests)).
|
|
713
713
|
|
|
714
714
|
The results can be formatted as `csv`, `json`, or `markdown`.
|
|
715
715
|
|
|
@@ -751,9 +751,10 @@ options:
|
|
|
751
751
|
The branch to parse, if not the default branch
|
|
752
752
|
-v, --verbose Whether to output debugging info
|
|
753
753
|
-c, --clean Remove contributors without any contribuition
|
|
754
|
-
--github Also report GitHub pull requests opened, merged
|
|
755
|
-
closed per contributor, and match
|
|
756
|
-
authors. Needs the GitHub CLI
|
|
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
|
|
757
758
|
```
|
|
758
759
|
|
|
759
760
|
In case `gitlogstats` is not found as a command, even after installing, try `python -m gitlogstats` or `python3 -m gitlogstats` instead.
|
|
@@ -836,14 +837,15 @@ gitlogstats -rf repos.txt -c
|
|
|
836
837
|
|
|
837
838
|
### GitHub pull requests
|
|
838
839
|
|
|
839
|
-
Add `--github` to also count each contributor's pull requests.
|
|
840
|
+
Add `--github` to also count each contributor's pull requests. Five columns are appended to every output format:
|
|
840
841
|
|
|
841
|
-
| Column | Meaning
|
|
842
|
-
| --------------------- |
|
|
843
|
-
| `github_username` | the GitHub account matched to the contributor (never the display name), so you can check the matching
|
|
844
|
-
| `prs_opened` | pull requests they opened in the date range, whatever their state now (drafts included)
|
|
845
|
-
| `prs_merged` | pull requests they authored that were merged in the date range, by anyone and in any way (squash too)
|
|
846
|
-
| `prs_closed_unmerged` | pull requests they authored that were closed without merging in the date range
|
|
842
|
+
| Column | Meaning |
|
|
843
|
+
| --------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
844
|
+
| `github_username` | the GitHub account matched to the contributor (never the display name), so you can check the matching |
|
|
845
|
+
| `prs_opened` | pull requests they opened in the date range, whatever their state now (drafts included) |
|
|
846
|
+
| `prs_merged` | pull requests they authored that were merged in the date range, by anyone and in any way (squash too) |
|
|
847
|
+
| `prs_closed_unmerged` | pull requests they authored that were closed without merging in the date range |
|
|
848
|
+
| `prs_self_merged` | pull requests they authored and also merged themselves, merged in the date range (also in `prs_merged`) |
|
|
847
849
|
|
|
848
850
|
```
|
|
849
851
|
gitlogstats -r https://github.com/bloombar/gitlogstats.git --github
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_meta": {
|
|
3
3
|
"hash": {
|
|
4
|
-
"sha256": "
|
|
4
|
+
"sha256": "d1fee7a304fa34eaf74ad0dd93b88859d18b03c32476298928a490a03ec616e8"
|
|
5
5
|
},
|
|
6
6
|
"pipfile-spec": 6,
|
|
7
7
|
"requires": {
|
|
@@ -16,67 +16,9 @@
|
|
|
16
16
|
]
|
|
17
17
|
},
|
|
18
18
|
"default": {
|
|
19
|
-
"build": {
|
|
20
|
-
"hashes": [
|
|
21
|
-
"sha256:51cc11666391ab6f092070437ac747002ff46f3e4113a3622177ee6b488bfc53",
|
|
22
|
-
"sha256:ecd351a4be9d35a9eaaba244a7687143c9c7d4aea6ac964e7e7ddab20cbcf4e7"
|
|
23
|
-
],
|
|
24
|
-
"index": "pypi",
|
|
25
|
-
"markers": "python_version >= '3.10'",
|
|
26
|
-
"version": "==1.6.1"
|
|
27
|
-
},
|
|
28
19
|
"gitlogstats": {
|
|
29
20
|
"editable": true,
|
|
30
21
|
"path": "."
|
|
31
|
-
},
|
|
32
|
-
"iniconfig": {
|
|
33
|
-
"hashes": [
|
|
34
|
-
"sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960",
|
|
35
|
-
"sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7"
|
|
36
|
-
],
|
|
37
|
-
"markers": "python_version >= '3.10'",
|
|
38
|
-
"version": "==2.3.1"
|
|
39
|
-
},
|
|
40
|
-
"packaging": {
|
|
41
|
-
"hashes": [
|
|
42
|
-
"sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79",
|
|
43
|
-
"sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c"
|
|
44
|
-
],
|
|
45
|
-
"markers": "python_version >= '3.9'",
|
|
46
|
-
"version": "==26.3"
|
|
47
|
-
},
|
|
48
|
-
"pluggy": {
|
|
49
|
-
"hashes": [
|
|
50
|
-
"sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec",
|
|
51
|
-
"sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8"
|
|
52
|
-
],
|
|
53
|
-
"markers": "python_version >= '3.10'",
|
|
54
|
-
"version": "==1.7.0"
|
|
55
|
-
},
|
|
56
|
-
"pygments": {
|
|
57
|
-
"hashes": [
|
|
58
|
-
"sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9",
|
|
59
|
-
"sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c"
|
|
60
|
-
],
|
|
61
|
-
"markers": "python_version >= '3.9'",
|
|
62
|
-
"version": "==2.21.0"
|
|
63
|
-
},
|
|
64
|
-
"pyproject-hooks": {
|
|
65
|
-
"hashes": [
|
|
66
|
-
"sha256:5fc53fdac9f7bd63fbcdc868fb5f90b4784d78a53a3d3388cd738b807441a20b",
|
|
67
|
-
"sha256:defda19b854fa0d3bd4f76ea4ddcba8abd7dcfcdd585a6690ade050744fc5f43"
|
|
68
|
-
],
|
|
69
|
-
"markers": "python_version >= '3.8'",
|
|
70
|
-
"version": "==1.3.3"
|
|
71
|
-
},
|
|
72
|
-
"pytest": {
|
|
73
|
-
"hashes": [
|
|
74
|
-
"sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313",
|
|
75
|
-
"sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c"
|
|
76
|
-
],
|
|
77
|
-
"index": "pypi",
|
|
78
|
-
"markers": "python_version >= '3.10'",
|
|
79
|
-
"version": "==9.1.1"
|
|
80
22
|
}
|
|
81
23
|
},
|
|
82
24
|
"develop": {
|
|
@@ -12,7 +12,7 @@ This script calculates the following, for each developer in each repository:
|
|
|
12
12
|
- number of **files changed**
|
|
13
13
|
- number of **merges** into the analyzed branch
|
|
14
14
|
|
|
15
|
-
With `--github`, it also reports each developer's GitHub **pull requests** opened, merged
|
|
15
|
+
With `--github`, it also reports each developer's GitHub **pull requests** opened, merged, closed without merging and merged by themselves (see [GitHub pull requests](#github-pull-requests)).
|
|
16
16
|
|
|
17
17
|
The results can be formatted as `csv`, `json`, or `markdown`.
|
|
18
18
|
|
|
@@ -54,9 +54,10 @@ options:
|
|
|
54
54
|
The branch to parse, if not the default branch
|
|
55
55
|
-v, --verbose Whether to output debugging info
|
|
56
56
|
-c, --clean Remove contributors without any contribuition
|
|
57
|
-
--github Also report GitHub pull requests opened, merged
|
|
58
|
-
closed per contributor, and match
|
|
59
|
-
authors. Needs the GitHub CLI
|
|
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
|
|
60
61
|
```
|
|
61
62
|
|
|
62
63
|
In case `gitlogstats` is not found as a command, even after installing, try `python -m gitlogstats` or `python3 -m gitlogstats` instead.
|
|
@@ -139,14 +140,15 @@ gitlogstats -rf repos.txt -c
|
|
|
139
140
|
|
|
140
141
|
### GitHub pull requests
|
|
141
142
|
|
|
142
|
-
Add `--github` to also count each contributor's pull requests.
|
|
143
|
+
Add `--github` to also count each contributor's pull requests. Five columns are appended to every output format:
|
|
143
144
|
|
|
144
|
-
| Column | Meaning
|
|
145
|
-
| --------------------- |
|
|
146
|
-
| `github_username` | the GitHub account matched to the contributor (never the display name), so you can check the matching
|
|
147
|
-
| `prs_opened` | pull requests they opened in the date range, whatever their state now (drafts included)
|
|
148
|
-
| `prs_merged` | pull requests they authored that were merged in the date range, by anyone and in any way (squash too)
|
|
149
|
-
| `prs_closed_unmerged` | pull requests they authored that were closed without merging in the date range
|
|
145
|
+
| Column | Meaning |
|
|
146
|
+
| --------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
147
|
+
| `github_username` | the GitHub account matched to the contributor (never the display name), so you can check the matching |
|
|
148
|
+
| `prs_opened` | pull requests they opened in the date range, whatever their state now (drafts included) |
|
|
149
|
+
| `prs_merged` | pull requests they authored that were merged in the date range, by anyone and in any way (squash too) |
|
|
150
|
+
| `prs_closed_unmerged` | pull requests they authored that were closed without merging in the date range |
|
|
151
|
+
| `prs_self_merged` | pull requests they authored and also merged themselves, merged in the date range (also in `prs_merged`) |
|
|
150
152
|
|
|
151
153
|
```
|
|
152
154
|
gitlogstats -r https://github.com/bloombar/gitlogstats.git --github
|
|
@@ -114,6 +114,12 @@ If a tier finds several GitHub users for one identity, that tier is skipped as a
|
|
|
114
114
|
|
|
115
115
|
**Status:** Done
|
|
116
116
|
|
|
117
|
+
#### STAT-12 Self-merged pull requests
|
|
118
|
+
|
|
119
|
+
With `--github`, for each contributor, the number of pull requests they authored and also merged themselves (the GitHub user who merged the pull request is its author, ignoring case), with a merge time (`merged_at`) in the date range. Reported in a `prs_self_merged` column, appended after `prs_closed_unmerged`. Every self-merged pull request is also counted in `prs_merged` (STAT-9), so `prs_self_merged` is never larger than `prs_merged`. A merged pull request whose merger GitHub does not report (e.g. a deleted account) is not counted as self-merged. `-b` and `-s/-e` apply as for `prs_merged`.
|
|
120
|
+
|
|
121
|
+
**Status:** Done
|
|
122
|
+
|
|
117
123
|
### 3. Filtering
|
|
118
124
|
|
|
119
125
|
#### FILT-1 Date range
|
|
@@ -144,7 +150,7 @@ By default, contributors with no activity in the date range are listed with zero
|
|
|
144
150
|
|
|
145
151
|
#### OUT-1 CSV output
|
|
146
152
|
|
|
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).
|
|
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).
|
|
148
154
|
|
|
149
155
|
**Status:** Done
|
|
150
156
|
|
|
@@ -188,12 +194,12 @@ Invalid dates, and a start date after the end date, are rejected with a usage er
|
|
|
188
194
|
|
|
189
195
|
#### CLI-3 GitHub pull-request statistics
|
|
190
196
|
|
|
191
|
-
`--github` adds GitHub pull-request statistics (STAT-8 to STAT-
|
|
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:
|
|
192
198
|
|
|
193
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.`
|
|
194
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.`
|
|
195
201
|
|
|
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
|
|
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.
|
|
197
203
|
|
|
198
204
|
**Status:** Done
|
|
199
205
|
|
|
@@ -207,7 +213,7 @@ The tool installs with `pip install gitlogstats`, which provides a `gitlogstats`
|
|
|
207
213
|
|
|
208
214
|
#### PKG-2 Continuous integration and release
|
|
209
215
|
|
|
210
|
-
GitHub Actions builds the package on
|
|
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.
|
|
211
217
|
|
|
212
218
|
**Status:** Done
|
|
213
219
|
|
|
@@ -219,9 +225,9 @@ Unit tests cover parsing and formatting, and integration tests build real git re
|
|
|
219
225
|
|
|
220
226
|
#### PKG-4 Run tests in CI
|
|
221
227
|
|
|
222
|
-
The CI workflow runs the test suite on every push and
|
|
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.
|
|
223
229
|
|
|
224
|
-
**Status:**
|
|
230
|
+
**Status:** Done
|
|
225
231
|
|
|
226
232
|
### 7. Future Work
|
|
227
233
|
|
|
@@ -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.2.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-
|
|
22
|
+
full: "--github adds GitHub pull-request statistics (STAT-8 to STAT-12) for
|
|
23
23
|
repositories hosted on github.com, appending the columns github_username,
|
|
24
|
-
prs_opened, prs_merged, prs_closed_unmerged to every output
|
|
25
|
-
--github, the output and behaviour are unchanged, GitHub is
|
|
26
|
-
and the GitHub CLI (gh) is not needed. With --github, the
|
|
27
|
-
called through gh api, so gh must be installed and signed in.
|
|
28
|
-
checked before any repository is cloned; if it fails, nothing is
|
|
29
|
-
standard output, the tool exits with status 1, and one plain
|
|
30
|
-
standard error says why and what to do — no stack trace: gh not
|
|
31
|
-
gitlogstats: --github
|
|
24
|
+
prs_opened, prs_merged, prs_closed_unmerged, prs_self_merged to every output
|
|
25
|
+
format. Without --github, the output and behaviour are unchanged, GitHub is
|
|
26
|
+
never contacted and the GitHub CLI (gh) is not needed. With --github, the
|
|
27
|
+
GitHub API is called through gh api, so gh must be installed and signed in.
|
|
28
|
+
This is checked before any repository is cloned; if it fails, nothing is
|
|
29
|
+
printed to standard output, the tool exits with status 1, and one plain
|
|
30
|
+
message on standard error says why and what to do — no stack trace: gh not
|
|
31
|
+
found: gitlogstats: --github…"
|
|
32
32
|
section: §5 Command-Line Interface
|
|
33
33
|
family: CLI
|
|
34
34
|
phase: 1
|
|
@@ -89,8 +89,8 @@
|
|
|
89
89
|
containing commas, quotes or line breaks are quoted. The header is printed
|
|
90
90
|
even when there are no rows, and the output ends with a single newline. With
|
|
91
91
|
--github, the columns github_username, prs_opened, prs_merged,
|
|
92
|
-
prs_closed_unmerged are appended, and are in the header
|
|
93
|
-
no rows (CLI-3).
|
|
92
|
+
prs_closed_unmerged, prs_self_merged are appended, and are in the header
|
|
93
|
+
even when there are no rows (CLI-3).
|
|
94
94
|
section: §4 Output
|
|
95
95
|
family: OUT
|
|
96
96
|
phase: 1
|
|
@@ -141,8 +141,11 @@
|
|
|
141
141
|
status: Done
|
|
142
142
|
- id: PKG-2
|
|
143
143
|
title: Continuous integration and release
|
|
144
|
-
full: GitHub Actions builds the package on
|
|
145
|
-
and
|
|
144
|
+
full: GitHub Actions builds the package on Python 3.9 and 3.11 for every pull
|
|
145
|
+
request into master and every push to master. It publishes to PyPI only when
|
|
146
|
+
a pull request is merged into master and the build of the merged code
|
|
147
|
+
passes; pushes to other branches publish nothing. A merge whose version is
|
|
148
|
+
already on PyPI (no version bump) skips the upload instead of failing.
|
|
146
149
|
section: §6 Packaging & Quality
|
|
147
150
|
family: PKG
|
|
148
151
|
phase: 1
|
|
@@ -159,12 +162,13 @@
|
|
|
159
162
|
status: Done
|
|
160
163
|
- id: PKG-4
|
|
161
164
|
title: Run tests in CI
|
|
162
|
-
full: The CI workflow runs the test suite on every
|
|
163
|
-
|
|
165
|
+
full: The CI workflow runs the Python test suite (pytest) on every pull request
|
|
166
|
+
into master and every push to master, and on the merged code before a
|
|
167
|
+
release; a failing test stops the release.
|
|
164
168
|
section: §6 Packaging & Quality
|
|
165
169
|
family: PKG
|
|
166
170
|
phase: 1
|
|
167
|
-
status:
|
|
171
|
+
status: Done
|
|
168
172
|
- id: REPO-1
|
|
169
173
|
title: Single repository
|
|
170
174
|
full: -r/--repository <url> analyzes one repository. Any URL or path that git
|
|
@@ -329,6 +333,21 @@
|
|
|
329
333
|
family: STAT
|
|
330
334
|
phase: 1
|
|
331
335
|
status: Done
|
|
336
|
+
- id: STAT-12
|
|
337
|
+
title: Self-merged pull requests
|
|
338
|
+
full: With --github, for each contributor, the number of pull requests they
|
|
339
|
+
authored and also merged themselves (the GitHub user who merged the pull
|
|
340
|
+
request is its author, ignoring case), with a merge time (merged_at) in the
|
|
341
|
+
date range. Reported in a prs_self_merged column, appended after
|
|
342
|
+
prs_closed_unmerged. Every self-merged pull request is also counted in
|
|
343
|
+
prs_merged (STAT-9), so prs_self_merged is never larger than prs_merged. A
|
|
344
|
+
merged pull request whose merger GitHub does not report (e.g. a deleted
|
|
345
|
+
account) is not counted as self-merged. -b and -s/-e apply as for
|
|
346
|
+
prs_merged.
|
|
347
|
+
section: §2 Contribution Statistics
|
|
348
|
+
family: STAT
|
|
349
|
+
phase: 1
|
|
350
|
+
status: Done
|
|
332
351
|
- id: FUTURE-author-date-filtering
|
|
333
352
|
title: Author-date filtering
|
|
334
353
|
full: Author-date filtering — offer filtering by author date, so rebased or
|
|
@@ -141,9 +141,9 @@ def main():
|
|
|
141
141
|
)
|
|
142
142
|
parser.add_argument(
|
|
143
143
|
"--github",
|
|
144
|
-
help="Also report GitHub pull requests opened, merged
|
|
145
|
-
"contributor, and match GitHub users to git authors.
|
|
146
|
-
"(gh), signed in",
|
|
144
|
+
help="Also report GitHub pull requests opened, merged, closed and "
|
|
145
|
+
"self-merged per contributor, and match GitHub users to git authors. "
|
|
146
|
+
"Needs the GitHub CLI (gh), signed in",
|
|
147
147
|
default=False,
|
|
148
148
|
action="store_true",
|
|
149
149
|
)
|
|
@@ -18,6 +18,7 @@ GITHUB_FIELDNAMES = [
|
|
|
18
18
|
"prs_opened",
|
|
19
19
|
"prs_merged",
|
|
20
20
|
"prs_closed_unmerged",
|
|
21
|
+
"prs_self_merged",
|
|
21
22
|
]
|
|
22
23
|
|
|
23
24
|
# CLI-3: the two user-facing messages, verbatim from the SPEC
|
|
@@ -31,6 +32,17 @@ GH_NOT_SIGNED_IN = (
|
|
|
31
32
|
"Run 'gh auth login', or run without --github."
|
|
32
33
|
)
|
|
33
34
|
|
|
35
|
+
# STAT-12: the list endpoint of the REST API does not say who merged a pull
|
|
36
|
+
# request, but GraphQL does, in one paginated query (gh fills in $endCursor)
|
|
37
|
+
PULLS_QUERY = (
|
|
38
|
+
"query($owner:String!,$name:String!,$endCursor:String){"
|
|
39
|
+
"repository(owner:$owner,name:$name){"
|
|
40
|
+
"pullRequests(first:100,after:$endCursor){"
|
|
41
|
+
"pageInfo{hasNextPage endCursor} "
|
|
42
|
+
"nodes{author{login __typename} mergedBy{login __typename} "
|
|
43
|
+
"state createdAt mergedAt closedAt baseRefName}}}}"
|
|
44
|
+
)
|
|
45
|
+
|
|
34
46
|
# Login given to pull requests whose author's account was deleted
|
|
35
47
|
GHOST_LOGIN = "ghost"
|
|
36
48
|
|
|
@@ -148,29 +160,84 @@ def gh_api(path, paginate=False):
|
|
|
148
160
|
raise GitHubError(f"gh api {path} returned invalid JSON: {exc}")
|
|
149
161
|
|
|
150
162
|
|
|
163
|
+
def gh_graphql(query, variables):
|
|
164
|
+
"""
|
|
165
|
+
Run a paginated GraphQL query through gh. gh prints one JSON document per
|
|
166
|
+
page, back to back.
|
|
167
|
+
@param query: the query text; it must use $endCursor for pagination
|
|
168
|
+
@param variables: {name: string value}, passed as separate arguments
|
|
169
|
+
@returns: a list with one parsed response per page
|
|
170
|
+
@raises GitHubError: if gh exits with an error or the response has errors
|
|
171
|
+
"""
|
|
172
|
+
cmd = ["gh", "api", "graphql", "--paginate", "-f", f"query={query}"]
|
|
173
|
+
for name, value in variables.items():
|
|
174
|
+
# -f sends a raw string; -F would turn "2048" or "true" into a number
|
|
175
|
+
# or boolean, which GitHub rejects for String! variables
|
|
176
|
+
cmd += ["-f", f"{name}={value}"]
|
|
177
|
+
result = subprocess.run(
|
|
178
|
+
cmd,
|
|
179
|
+
stdout=subprocess.PIPE,
|
|
180
|
+
stderr=subprocess.PIPE,
|
|
181
|
+
universal_newlines=True,
|
|
182
|
+
)
|
|
183
|
+
if result.returncode != 0:
|
|
184
|
+
raise GitHubError(result.stderr.strip() or "gh api graphql failed")
|
|
185
|
+
try:
|
|
186
|
+
pages = _parse_json_stream(result.stdout)
|
|
187
|
+
except ValueError as exc: # json.JSONDecodeError is a ValueError
|
|
188
|
+
raise GitHubError(f"gh api graphql returned invalid JSON: {exc}")
|
|
189
|
+
if isinstance(pages, dict):
|
|
190
|
+
pages = [pages]
|
|
191
|
+
for page in pages:
|
|
192
|
+
if page.get("errors"):
|
|
193
|
+
messages = [e.get("message", "unknown error") for e in page["errors"]]
|
|
194
|
+
raise GitHubError("; ".join(messages))
|
|
195
|
+
return pages
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _graphql_login(account):
|
|
199
|
+
"""
|
|
200
|
+
Read a login from a GraphQL author or merger. GraphQL leaves "[bot]" off
|
|
201
|
+
bot logins that the REST API includes, so it is added back.
|
|
202
|
+
@param account: {"login", "__typename"}, or None for a deleted account
|
|
203
|
+
@returns: the login, or None if there is no account
|
|
204
|
+
"""
|
|
205
|
+
if not account:
|
|
206
|
+
return None
|
|
207
|
+
login = account["login"]
|
|
208
|
+
return login + "[bot]" if account.get("__typename") == "Bot" else login
|
|
209
|
+
|
|
210
|
+
|
|
151
211
|
def fetch_pull_requests(repo):
|
|
152
212
|
"""
|
|
153
213
|
List every pull request of a repository, in any state.
|
|
154
214
|
@param repo: "owner/repo"
|
|
155
|
-
@returns: a list of dicts with login, user_type, state
|
|
156
|
-
merged_at, closed_at
|
|
215
|
+
@returns: a list of dicts with login, user_type, state ("open" or
|
|
216
|
+
"closed"), created_at, merged_at, closed_at, base (the base branch
|
|
217
|
+
name) and merged_by (a login, or None if not merged or unknown)
|
|
157
218
|
@raises GitHubError: if the request fails
|
|
158
219
|
"""
|
|
159
|
-
|
|
220
|
+
owner, name = repo.split("/", 1)
|
|
221
|
+
pages = gh_graphql(PULLS_QUERY, {"owner": owner, "name": name})
|
|
160
222
|
result = []
|
|
161
|
-
for
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
{
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
223
|
+
for page in pages:
|
|
224
|
+
repository = (page.get("data") or {}).get("repository")
|
|
225
|
+
if repository is None:
|
|
226
|
+
raise GitHubError(f"could not read pull requests of {repo}")
|
|
227
|
+
for pull in repository["pullRequests"]["nodes"]:
|
|
228
|
+
author = pull.get("author")
|
|
229
|
+
result.append(
|
|
230
|
+
{
|
|
231
|
+
"login": _graphql_login(author) or GHOST_LOGIN,
|
|
232
|
+
"user_type": author.get("__typename") if author else None,
|
|
233
|
+
"state": "open" if pull.get("state") == "OPEN" else "closed",
|
|
234
|
+
"created_at": pull.get("createdAt"),
|
|
235
|
+
"merged_at": pull.get("mergedAt"),
|
|
236
|
+
"closed_at": pull.get("closedAt"),
|
|
237
|
+
"base": pull.get("baseRefName"),
|
|
238
|
+
"merged_by": _graphql_login(pull.get("mergedBy")),
|
|
239
|
+
}
|
|
240
|
+
)
|
|
174
241
|
return result
|
|
175
242
|
|
|
176
243
|
|
|
@@ -188,13 +255,14 @@ def _local_time(timestamp):
|
|
|
188
255
|
|
|
189
256
|
def count_pull_requests(prs, start, end, branch=None):
|
|
190
257
|
"""
|
|
191
|
-
STAT-8, STAT-9, STAT-10: count each user's pull requests in a date range.
|
|
258
|
+
STAT-8, STAT-9, STAT-10, STAT-12: count each user's pull requests in a date range.
|
|
192
259
|
The range is inclusive local time, as in FILT-1.
|
|
193
260
|
@param prs: the list from fetch_pull_requests
|
|
194
261
|
@param start: first day, mm/dd/yyyy
|
|
195
262
|
@param end: last day, mm/dd/yyyy
|
|
196
263
|
@param branch: count only pull requests with this base branch, if given
|
|
197
|
-
@returns: {login: {"prs_opened", "prs_merged", "prs_closed_unmerged"
|
|
264
|
+
@returns: {login: {"prs_opened", "prs_merged", "prs_closed_unmerged",
|
|
265
|
+
"prs_self_merged"}}
|
|
198
266
|
"""
|
|
199
267
|
since = datetime.datetime.strptime(start, "%m/%d/%Y")
|
|
200
268
|
until = datetime.datetime.strptime(end, "%m/%d/%Y").replace(
|
|
@@ -211,12 +279,21 @@ def count_pull_requests(prs, start, end, branch=None):
|
|
|
211
279
|
continue
|
|
212
280
|
row = counts.setdefault(
|
|
213
281
|
pr["login"],
|
|
214
|
-
{
|
|
282
|
+
{
|
|
283
|
+
"prs_opened": 0,
|
|
284
|
+
"prs_merged": 0,
|
|
285
|
+
"prs_closed_unmerged": 0,
|
|
286
|
+
"prs_self_merged": 0,
|
|
287
|
+
},
|
|
215
288
|
)
|
|
216
289
|
if in_range(pr["created_at"]):
|
|
217
290
|
row["prs_opened"] += 1
|
|
218
291
|
if in_range(pr["merged_at"]):
|
|
219
292
|
row["prs_merged"] += 1
|
|
293
|
+
# STAT-12: the merger is the author; an unknown merger never is
|
|
294
|
+
merger = pr.get("merged_by")
|
|
295
|
+
if merger and merger.casefold() == pr["login"].casefold():
|
|
296
|
+
row["prs_self_merged"] += 1
|
|
220
297
|
if (
|
|
221
298
|
pr["state"] == "closed"
|
|
222
299
|
and not pr["merged_at"]
|
|
@@ -184,7 +184,7 @@ def consolidate(rows, matches, pr_counts, repository, start, end):
|
|
|
184
184
|
appended after the git rows in login order.
|
|
185
185
|
@param rows: git rows for one repository, from GitLogsParser.parse().
|
|
186
186
|
@param matches: {author_name: (login, tier)} from match_identities.
|
|
187
|
-
@param pr_counts: {login: {
|
|
187
|
+
@param pr_counts: {login: {PR column: count}} from count_pull_requests.
|
|
188
188
|
@param repository: kept for GitHub-only rows.
|
|
189
189
|
@param start: start date kept for GitHub-only rows.
|
|
190
190
|
@param end: end date kept for GitHub-only rows.
|
|
@@ -243,7 +243,7 @@ def unavailable_row(row):
|
|
|
243
243
|
"""
|
|
244
244
|
Copy a git row with the GitHub columns marked unavailable (CLI-3).
|
|
245
245
|
@param row: a git row.
|
|
246
|
-
@returns: a new row whose
|
|
246
|
+
@returns: a new row whose GitHub columns are all None.
|
|
247
247
|
"""
|
|
248
248
|
out = {field: row[field] for field in FIELDNAMES}
|
|
249
249
|
for field in GITHUB_FIELDNAMES:
|