gitlogstats 1.5.1__tar.gz → 1.6.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 (55) hide show
  1. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/PKG-INFO +18 -2
  2. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/README.md +17 -1
  3. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/docs/SPEC.md +10 -0
  4. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/docs/examples/README.md +4 -4
  5. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/docs/examples/assignment-stats.sh +9 -5
  6. gitlogstats-1.6.0/docs/examples/project-1-stats.csv +126 -0
  7. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/pyproject.toml +1 -1
  8. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/scripts/board/manifest.yaml +16 -0
  9. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/src/gitlogstats/__main__.py +20 -6
  10. gitlogstats-1.6.0/src/gitlogstats/aggregate.py +177 -0
  11. gitlogstats-1.6.0/tests/test_aggregate.py +368 -0
  12. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/tests/test_integration.py +260 -0
  13. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/.claude/CLAUDE.md +0 -0
  14. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/.claude/agents/developer-agent.md +0 -0
  15. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/.claude/agents/spec-reviewer.md +0 -0
  16. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/.claude/hooks/format-edited.sh +0 -0
  17. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/.claude/hooks/guard-paths.sh +0 -0
  18. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/.claude/hooks/guard-paths.test.mjs +0 -0
  19. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/.claude/hooks/verify.sh +0 -0
  20. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/.claude/settings.json +0 -0
  21. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/.claude/skills/phase-handoff/SKILL.md +0 -0
  22. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/.claude/skills/stale-check/SKILL.md +0 -0
  23. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/.github/workflows/build.yaml +0 -0
  24. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/.gitignore +0 -0
  25. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/.prettierrc.json +0 -0
  26. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/LICENSE +0 -0
  27. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/Pipfile +0 -0
  28. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/Pipfile.lock +0 -0
  29. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/package-lock.json +0 -0
  30. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/package.json +0 -0
  31. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/repos.txt +0 -0
  32. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/requirements.txt +0 -0
  33. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/scripts/board/config.mjs +0 -0
  34. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/scripts/board/derive.mjs +0 -0
  35. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/scripts/board/derive.test.mjs +0 -0
  36. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/scripts/board/sync.mjs +0 -0
  37. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/src/gitlogstats/__init__.py +0 -0
  38. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/src/gitlogstats/clones.py +0 -0
  39. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/src/gitlogstats/git_logs_parser.py +0 -0
  40. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/src/gitlogstats/github.py +0 -0
  41. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/src/gitlogstats/github_stats.py +0 -0
  42. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/src/gitlogstats/identity.py +0 -0
  43. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/tests/fake_gh.py +0 -0
  44. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/tests/fixtures/github/commit_linked.json +0 -0
  45. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/tests/fixtures/github/commit_unlinked.json +0 -0
  46. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/tests/fixtures/github/contributors.json +0 -0
  47. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/tests/fixtures/github/contributors_page2.json +0 -0
  48. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/tests/fixtures/github/pulls_page1.json +0 -0
  49. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/tests/fixtures/github/pulls_page2.json +0 -0
  50. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/tests/fixtures/github/user_full.json +0 -0
  51. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/tests/fixtures/github/user_private.json +0 -0
  52. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/tests/test_clobber.py +0 -0
  53. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/tests/test_git_logs_parser.py +0 -0
  54. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/tests/test_github.py +0 -0
  55. {gitlogstats-1.5.1 → gitlogstats-1.6.0}/tests/test_identity.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: gitlogstats
3
- Version: 1.5.1
3
+ Version: 1.6.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
@@ -726,7 +726,8 @@ The command `gitlogstats --help` shows the usage instructions:
726
726
  ```
727
727
  usage: gitlogstats [-h] (-r REPOSITORY | -rf REPOFILE) [-u USER] [-s START]
728
728
  [-e END] [-x EXCLUSIONS] [-f {csv,json,markdown}]
729
- [-b BRANCH] [-v] [-c] [--github] [--clobber]
729
+ [-b BRANCH] [-v] [-c] [-a {developer,repository}]
730
+ [--github] [--clobber]
730
731
 
731
732
  options:
732
733
  -h, --help show this help message and exit
@@ -751,6 +752,10 @@ options:
751
752
  The branch to parse, if not the default branch
752
753
  -v, --verbose Whether to output debugging info
753
754
  -c, --clean Remove contributors without any contribuition
755
+ -a {developer,repository}, --aggregate {developer,repository}
756
+ Combine the results into one row per developer, summed
757
+ over all repositories, or one row per repository,
758
+ summed over its contributors
754
759
  --github Also report GitHub pull requests opened, merged,
755
760
  closed and self-merged per contributor, with their git
756
761
  and GitHub emails, their pushes to the analyzed branch
@@ -840,6 +845,17 @@ By default, every contributor in a repository's history is listed, with zeros fo
840
845
  gitlogstats -rf repos.txt -c
841
846
  ```
842
847
 
848
+ ### Totals per developer or per repository
849
+
850
+ By default there is one row per contributor per repository. Add `-a developer` to get one row per developer with their statistics summed over every repository, or `-a repository` to get one row per repository summed over its contributors.
851
+
852
+ ```
853
+ gitlogstats -rf repos.txt -a developer
854
+ gitlogstats -rf repos.txt -a repository
855
+ ```
856
+
857
+ Developer rows have no `repository` column; instead a `repositories` column counts the repositories the developer was active in during the date range. Repository rows have no username or email columns; a `contributors` column counts the contributors who were active in the repository during the date range. `-u` and `-c` are applied first, so `-u alice -a repository` shows Alice's share of each repository. With `--github`, a developer's rows are combined by GitHub username, so different git names linked to one GitHub account become one row, and their email addresses are listed together. A developer matched to a GitHub account in one repository but not in another (for example a repository not on GitHub) keeps a separate row for each.
858
+
843
859
  ### Cloned repositories
844
860
 
845
861
  Each repository is cloned into a `repos/` directory under the directory you run the command from, and later runs update (`git pull`) the existing clone instead of cloning again, which makes them much faster. Add `--clobber` to delete the clones the run used, once the results have been printed; `repos/` itself is removed too if nothing else is left in it. Clones from other runs are kept, and nothing is deleted if the run fails before printing, so you can inspect it. With `-v`, each deleted path is listed on standard error.
@@ -29,7 +29,8 @@ The command `gitlogstats --help` shows the usage instructions:
29
29
  ```
30
30
  usage: gitlogstats [-h] (-r REPOSITORY | -rf REPOFILE) [-u USER] [-s START]
31
31
  [-e END] [-x EXCLUSIONS] [-f {csv,json,markdown}]
32
- [-b BRANCH] [-v] [-c] [--github] [--clobber]
32
+ [-b BRANCH] [-v] [-c] [-a {developer,repository}]
33
+ [--github] [--clobber]
33
34
 
34
35
  options:
35
36
  -h, --help show this help message and exit
@@ -54,6 +55,10 @@ options:
54
55
  The branch to parse, if not the default branch
55
56
  -v, --verbose Whether to output debugging info
56
57
  -c, --clean Remove contributors without any contribuition
58
+ -a {developer,repository}, --aggregate {developer,repository}
59
+ Combine the results into one row per developer, summed
60
+ over all repositories, or one row per repository,
61
+ summed over its contributors
57
62
  --github Also report GitHub pull requests opened, merged,
58
63
  closed and self-merged per contributor, with their git
59
64
  and GitHub emails, their pushes to the analyzed branch
@@ -143,6 +148,17 @@ By default, every contributor in a repository's history is listed, with zeros fo
143
148
  gitlogstats -rf repos.txt -c
144
149
  ```
145
150
 
151
+ ### Totals per developer or per repository
152
+
153
+ By default there is one row per contributor per repository. Add `-a developer` to get one row per developer with their statistics summed over every repository, or `-a repository` to get one row per repository summed over its contributors.
154
+
155
+ ```
156
+ gitlogstats -rf repos.txt -a developer
157
+ gitlogstats -rf repos.txt -a repository
158
+ ```
159
+
160
+ Developer rows have no `repository` column; instead a `repositories` column counts the repositories the developer was active in during the date range. Repository rows have no username or email columns; a `contributors` column counts the contributors who were active in the repository during the date range. `-u` and `-c` are applied first, so `-u alice -a repository` shows Alice's share of each repository. With `--github`, a developer's rows are combined by GitHub username, so different git names linked to one GitHub account become one row, and their email addresses are listed together. A developer matched to a GitHub account in one repository but not in another (for example a repository not on GitHub) keeps a separate row for each.
161
+
146
162
  ### Cloned repositories
147
163
 
148
164
  Each repository is cloned into a `repos/` directory under the directory you run the command from, and later runs update (`git pull`) the existing clone instead of cloning again, which makes them much faster. Add `--clobber` to delete the clones the run used, once the results have been printed; `repos/` itself is removed too if nothing else is left in it. Clones from other runs are kept, and nothing is deleted if the run fails before printing, so you can inspect it. With `-v`, each deleted path is listed on standard error.
@@ -202,6 +202,16 @@ When several repositories are analyzed, the results are printed once, after all
202
202
 
203
203
  **Status:** Done
204
204
 
205
+ #### OUT-6 Aggregated results
206
+
207
+ `-a/--aggregate developer` prints one row per developer, summed over every repository analyzed, instead of one row per contributor per repository. Its columns are `username, start_date, end_date, commits, insertions, deletions, files, merges, repositories`, where `repositories` is the number of repositories in which the developer has any activity in the date range (a non-zero count, git or GitHub, as for `-c`); there is no `repository` column. 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` follow. Rows are combined when they share a GitHub username (ignoring case), or, for rows without one, the same git `username` exactly; a developer matched to a GitHub user in one repository but not in another is reported as two rows. `username` is the git username of the developer's row with the most commits (ties alphabetical, as in STAT-11), `github_username` the first non-empty one among the developer's rows, and `git_email` and `github_email` list every distinct email of the combined rows, sorted alphabetically (ignoring case) and separated by `; `.
208
+
209
+ `-a/--aggregate repository` prints one row per repository, summed over its contributors. Its columns are `repository, start_date, end_date, commits, insertions, deletions, files, merges, contributors`, where `contributors` is the number of its contributors with any activity in the date range; with `--github`, `prs_opened, prs_merged, prs_closed_unmerged, prs_self_merged, pushes_to_default, web_commits` follow. There are no `username`, `github_username`, `git_email` or `github_email` columns.
210
+
211
+ In both modes every count is the sum of the combined rows, so each total equals the sum of the rows printed without `--aggregate`. A GitHub count is empty (`null` in JSON) only when it is empty in every combined row (CLI-3). Aggregation is applied after `-u` and `-c`, so it combines only the rows those options keep. Rows are sorted by `username` (or `github_username`, for a developer with no git username) or by `repository`, ignoring case. The aggregated columns are printed in every output format, and in the CSV header even when there are no rows. Without `-a`, the output is unchanged.
212
+
213
+ **Status:** Done
214
+
205
215
  ### 5. Command-Line Interface
206
216
 
207
217
  #### CLI-1 Help and usage
@@ -24,7 +24,7 @@ You don't need to install `gitlogstats` yourself: the script installs or upgrade
24
24
  | `REPO_PREFIX` | The start of the repository names for this assignment | `web-app-exercise-` |
25
25
  | `START_DATE` | First day of the period, `mm/dd/yyyy` | `09/01/2029` |
26
26
  | `END_DATE` | Last day of the period, `mm/dd/yyyy` (included) | `10/16/2029` |
27
- | options | Anything after the four arguments is passed to `gitlogstats` unchanged | `--github -f markdown` |
27
+ | options | Anything after the four arguments is passed to `gitlogstats` unchanged | `-f markdown` |
28
28
 
29
29
  Run it from any directory. The results are printed to standard output, so redirect them to save them; progress messages go to standard error and stay on screen.
30
30
 
@@ -36,10 +36,10 @@ Save a CSV of one assignment's statistics:
36
36
  ./assignment-stats.sh software-students-fall2029 web-app-exercise- 09/01/2029 10/16/2029 > web-app-exercise.csv
37
37
  ```
38
38
 
39
- Include the GitHub columns (pull requests, pushes, website commits, emails) and print a Markdown table:
39
+ Print a Markdown table instead:
40
40
 
41
41
  ```bash
42
- ./assignment-stats.sh software-students-fall2029 web-app-exercise- 09/01/2029 10/16/2029 --github -f markdown
42
+ ./assignment-stats.sh software-students-fall2029 web-app-exercise- 09/01/2029 10/16/2029 -f markdown
43
43
  ```
44
44
 
45
45
  Leave out students with no activity in the period, as JSON:
@@ -57,7 +57,7 @@ See the main [README](../../README.md) for every `gitlogstats` option and what e
57
57
  1. Checks the arguments and that Python, git and `gh` are installed and `gh` is signed in. If anything is missing it stops with a message saying what.
58
58
  2. Installs or upgrades `gitlogstats` in a Python environment of its own, `~/.gitlogstats-venv`, reused by later runs. Set `GITLOGSTATS_VENV` to keep it elsewhere. A separate environment is needed because Homebrew Python and recent Debian and Ubuntu releases refuse a system-wide `pip install`.
59
59
  3. Lists the organization's repositories whose names start with `REPO_PREFIX`, into a temporary file that is deleted when the script ends, even after an error. An existing `repos.txt` in your directory is never touched. If no repository matches, it stops with an error.
60
- 4. Runs `gitlogstats` on those repositories with `--clobber`, so the clones it makes in `./repos/` are deleted once the results are printed.
60
+ 4. Runs `gitlogstats` on those repositories with `--github` and `--clobber`. `--github` adds the GitHub columns (pull requests, pushes, website commits and emails) and combines each student's git names into one row; `--clobber` deletes the clones it makes in `./repos/` once the results are printed.
61
61
 
62
62
  ### Troubleshooting
63
63
 
@@ -9,10 +9,11 @@
9
9
  #
10
10
  # Example:
11
11
  # ./assignment-stats.sh software-students-fall2029 web-app-exercise- 09/01/2029 10/16/2029 > stats.csv
12
- # ./assignment-stats.sh software-students-fall2029 web-app-exercise- 09/01/2029 10/16/2029 --github -f markdown
12
+ # ./assignment-stats.sh software-students-fall2029 web-app-exercise- 09/01/2029 10/16/2029 -f markdown
13
13
  #
14
- # Dates are mm/dd/yyyy and both days are included. Any options after the four
15
- # arguments are passed to gitlogstats unchanged (e.g. --github, -f json, -c).
14
+ # Dates are mm/dd/yyyy and both days are included. gitlogstats always runs
15
+ # with --github, so the GitHub columns are included. Any options after the four
16
+ # arguments are passed to gitlogstats unchanged (e.g. -f json, -c).
16
17
  # The results go to standard output; progress messages go to standard error.
17
18
  #
18
19
  # Requires: Python 3, git, and the GitHub CLI (gh), signed in with
@@ -109,5 +110,8 @@ if [ "$count" -eq 0 ]; then
109
110
  fi
110
111
  echo "Analyzing $count repositories from $START_DATE to $END_DATE..." >&2
111
112
 
112
- # --clobber deletes the clones once the results are printed
113
- "$VENV_PYTHON" -m gitlogstats -rf "$REPOS_FILE" -s "$START_DATE" -e "$END_DATE" --clobber "$@"
113
+ # --github adds the GitHub columns (pull requests, pushes, website commits,
114
+ # emails) and combines each student's git names into one row; --clobber
115
+ # deletes the clones once the results are printed
116
+ "$VENV_PYTHON" -m gitlogstats -rf "$REPOS_FILE" -s "$START_DATE" -e "$END_DATE" \
117
+ --github --clobber "$@"
@@ -0,0 +1,126 @@
1
+ username,repository,start_date,end_date,commits,insertions,deletions,files,merges,github_username,prs_opened,prs_merged,prs_closed_unmerged,prs_self_merged,git_email,github_email,pushes_to_default,web_commits
2
+ Jade Leong,specification-phase-exercise-sakjp,09/01/2026,10/16/2026,4,124,45,4,0,Jade-Leong,1,1,0,0,jyl9618@nyu.edu,,2,0
3
+ Khidir Ahmed,specification-phase-exercise-sakjp,09/01/2026,10/16/2026,4,9,6,4,0,khidirahmed,2,2,0,1,kka6822@nyu.edu,kka6822@nyu.edu,2,1
4
+ Pope,specification-phase-exercise-sakjp,09/01/2026,10/16/2026,6,31,5,6,3,pope-cruz,4,4,0,3,168572503+pope-cruz@users.noreply.github.com; popecruz822@gmail.com,,1,0
5
+ sethderusha,specification-phase-exercise-sakjp,09/01/2026,10/16/2026,10,55,34,10,7,sethderusha,2,2,0,2,165086066+sethderusha@users.noreply.github.com; srd6665@nyu.edu,,8,0
6
+ alissawu,specification-phase-exercise-sakjp,09/01/2026,10/16/2026,1,1,0,1,0,alissawu,1,1,0,0,alissawu77@gmail.com,alissawu77@gmail.com,0,0
7
+ bloombar,specification-phase-exercise-sakjp,09/01/2026,10/16/2026,1,15,0,1,0,bloombar,0,0,0,0,2209098+bloombar@users.noreply.github.com; amos@cs.nyu.edu,,0,0
8
+ Dialloni,specification-phase-exercise-leodiajulce,09/01/2026,10/16/2026,5,130,6,6,3,Dialloni,0,0,0,0,119769795+Dialloni@users.noreply.github.com; dialoabo@gmail.com,,0,0
9
+ Jrleitersdorf,specification-phase-exercise-leodiajulce,09/01/2026,10/16/2026,4,437,13,5,2,Jrleitersdorf,0,0,0,0,jrleitersdorf@gmail.com,,0,0
10
+ Leonid2004,specification-phase-exercise-leodiajulce,09/01/2026,10/16/2026,3,189,6,3,3,Leonid2004,0,0,0,0,72578139+Leonid2004@users.noreply.github.com; leophilo7@gmail.com,,0,0
11
+ bloombar,specification-phase-exercise-leodiajulce,09/01/2026,10/16/2026,1,15,0,1,0,bloombar,0,0,0,0,2209098+bloombar@users.noreply.github.com; amos@cs.nyu.edu,,0,0
12
+ carinutza,specification-phase-exercise-leodiajulce,09/01/2026,10/16/2026,13,146,33,18,4,carinutza,0,0,0,0,ilie.carina@gmail.com,,1,0
13
+ YusefMoustafa,specification-phase-exercise-waterfall,09/01/2026,10/16/2026,6,119,7,6,0,YusefMoustafa,0,0,0,0,158067706+YusefMoustafa@users.noreply.github.com,,6,6
14
+ bloombar,specification-phase-exercise-waterfall,09/01/2026,10/16/2026,1,15,0,1,0,bloombar,0,0,0,0,2209098+bloombar@users.noreply.github.com; amos@cs.nyu.edu,,0,0
15
+ krishivseth,specification-phase-exercise-waterfall,09/01/2026,10/16/2026,7,555,23,10,1,krishivseth,2,2,0,2,93362198+krishivseth@users.noreply.github.com,,1,7
16
+ monica9482,specification-phase-exercise-waterfall,09/01/2026,10/16/2026,15,77,97,15,1,monica9482,0,0,0,0,ml9482@nyu.edu,,16,0
17
+ sing1179,specification-phase-exercise-waterfall,09/01/2026,10/16/2026,5,91,19,5,0,sing1179,0,0,0,0,143670921+sing1179@users.noreply.github.com,,5,5
18
+ Sarah Toussaint,specification-phase-exercise-tigers,09/01/2026,10/16/2026,3,50,2,3,0,sarahtoussaint,1,0,1,0,sat10002@nyu.edu,,3,3
19
+ bloombar,specification-phase-exercise-tigers,09/01/2026,10/16/2026,1,15,0,1,0,bloombar,0,0,0,0,2209098+bloombar@users.noreply.github.com; amos@cs.nyu.edu,,0,0
20
+ izz04,specification-phase-exercise-tigers,09/01/2026,10/16/2026,14,175,70,14,4,izz04,6,6,0,2,73780798+izz04@users.noreply.github.com,,9,14
21
+ jk8308-jpg,specification-phase-exercise-tigers,09/01/2026,10/16/2026,2,48,1,2,3,jk8308-jpg,2,2,0,0,jk8308@nyu.edu,,0,0
22
+ nonyembaezue,specification-phase-exercise-tigers,09/01/2026,10/16/2026,7,32,6,7,1,nonyembaezue,0,0,0,0,cam9137@nyu.edu,,7,7
23
+ bloombar,specification-exercise-project-1,09/01/2026,10/16/2026,1,15,0,1,0,bloombar,0,0,0,0,2209098+bloombar@users.noreply.github.com; amos@cs.nyu.edu,,0,0
24
+ sallyhegab,specification-exercise-project-1,09/01/2026,10/16/2026,5,109,14,5,1,sallyhegab,0,0,0,0,158313024+sallyhegab@users.noreply.github.com; sh6568@nyu.edu,,5,0
25
+ sar9249,specification-exercise-project-1,09/01/2026,10/16/2026,1,1,1,1,0,sar9249,1,1,0,0,sar9249@nyu.edu,,0,1
26
+ Ryan Lin,specification-phase-exercise-codekitchen,09/01/2026,10/16/2026,2,3,1,2,0,ryanwlin,0,0,0,0,ryanandlin@gmail.com,,2,2
27
+ berrizy,specification-phase-exercise-codekitchen,09/01/2026,10/16/2026,2,2,3,2,0,berrizy,0,0,0,0,nahasselma@gmail.com,,2,0
28
+ bloombar,specification-phase-exercise-codekitchen,09/01/2026,10/16/2026,1,15,0,1,0,bloombar,0,0,0,0,2209098+bloombar@users.noreply.github.com; amos@cs.nyu.edu,,0,0
29
+ rishachada,specification-phase-exercise-codekitchen,09/01/2026,10/16/2026,6,114,37,6,0,rishachada,0,0,0,0,rc5294@nyu.edu,,6,2
30
+ schauhans,specification-phase-exercise-codekitchen,09/01/2026,10/16/2026,2,24,3,2,0,schauhans,0,0,0,0,139730145+schauhans@users.noreply.github.com; sc10486@nyu.edu,,2,1
31
+ yufeiihuang,specification-phase-exercise-codekitchen,09/01/2026,10/16/2026,9,65,13,9,0,yufeiihuang,0,0,0,0,yufeixyhuang@gmail.com,,9,9
32
+ AmitavNarayan,specification-phase-exercise-no-ai-team,09/01/2026,10/16/2026,26,489,443,26,7,AmitavNarayan,6,6,0,6,amitavnarayan2@gmail.com,,21,8
33
+ Trisolis,specification-phase-exercise-no-ai-team,09/01/2026,10/16/2026,8,221,109,8,3,Trisolis,3,3,0,2,papijackwaiter@gmail.com,papijackwaiter@gmail.com,5,0
34
+ bloombar,specification-phase-exercise-no-ai-team,09/01/2026,10/16/2026,1,15,0,1,0,bloombar,0,0,0,0,2209098+bloombar@users.noreply.github.com; amos@cs.nyu.edu,,0,0
35
+ rfang1224,specification-phase-exercise-no-ai-team,09/01/2026,10/16/2026,3,9,5,3,0,rfang1224,1,1,0,0,rf2789@nyu.edu,,2,2
36
+ Abel200595,specification-phase-exercise-smart_monkeys-,09/01/2026,10/16/2026,1,507,1,4,0,Abel200595,0,0,0,0,ym3057@nyu.edu,,0,0
37
+ Jialiang Cao,specification-phase-exercise-smart_monkeys-,09/01/2026,10/16/2026,4,9,19,4,0,jialiangcao,0,0,0,0,jcao2080@gmail.com,,3,0
38
+ RayyanAhmed21,specification-phase-exercise-smart_monkeys-,09/01/2026,10/16/2026,7,252,111,7,5,RayyanAhmed21,5,5,0,5,ra4073@nyu.edu,,0,3
39
+ bloombar,specification-phase-exercise-smart_monkeys-,09/01/2026,10/16/2026,1,15,0,1,1,bloombar,0,0,0,0,2209098+bloombar@users.noreply.github.com; amos@cs.nyu.edu,,0,0
40
+ rz3007,specification-phase-exercise-smart_monkeys-,09/01/2026,10/16/2026,2,54,4,2,0,rz3007,0,0,0,0,rz3007@nyu.edu,,0,2
41
+ Asritha Pannem,specification-phase-exercise-controller,09/01/2026,10/16/2026,1,42,0,1,0,,0,0,0,0,asp9404@nyu.edu,,0,0
42
+ NISUN05,specification-phase-exercise-controller,09/01/2026,10/16/2026,6,106,7,6,0,NISUN05,0,0,0,0,na3755@nyu.edu,,6,0
43
+ Nishika Gorla,specification-phase-exercise-controller,09/01/2026,10/16/2026,3,32,4,3,0,nishikagorla,2,2,0,0,nishika.gorla@gmail.com,nishika.gorla@gmail.com,3,0
44
+ Pranay Engineer,specification-phase-exercise-controller,09/01/2026,10/16/2026,6,70,11,6,5,PranayEng,3,3,0,3,88552049+PranayEng@users.noreply.github.com; Pme7634@nyu.edu,,1,1
45
+ ai2652-png,specification-phase-exercise-controller,09/01/2026,10/16/2026,8,317,213,8,5,ai2652-png,5,5,0,5,ai2652@nyu.edu,,6,8
46
+ bloombar,specification-phase-exercise-controller,09/01/2026,10/16/2026,1,15,0,1,0,bloombar,0,0,0,0,2209098+bloombar@users.noreply.github.com; amos@cs.nyu.edu,,0,0
47
+ ,specification-phase-exercise-controller,09/01/2026,10/16/2026,0,0,0,0,0,scanfasri,0,0,0,0,,,1,0
48
+ ShuoWu6529,specification-phase-exercise-forked,09/01/2026,10/16/2026,7,24,6,7,11,ShuoWu6529,6,6,0,4,sw6529@nyu.edu,sw6529@nyu.edu,1,0
49
+ bloombar,specification-phase-exercise-forked,09/01/2026,10/16/2026,1,15,0,1,0,bloombar,0,0,0,0,2209098+bloombar@users.noreply.github.com; amos@cs.nyu.edu,,0,0
50
+ ew2725,specification-phase-exercise-forked,09/01/2026,10/16/2026,6,24,14,6,6,ew2725,5,5,0,1,ew2725@nyu.edu,,0,0
51
+ jack-k-jiang,specification-phase-exercise-forked,09/01/2026,10/16/2026,8,37,14,8,6,jack-k-jiang,8,8,0,6,82908596+jack-k-jiang@users.noreply.github.com; jackj12358@gmail.com,,0,0
52
+ ronaldszeto,specification-phase-exercise-forked,09/01/2026,10/16/2026,7,24,3,7,1,ronaldszeto,5,5,0,1,rs9316@nyu.edu,,1,2
53
+ xuruikun,specification-phase-exercise-forked,09/01/2026,10/16/2026,23,93,61,23,3,xuruikun,7,7,0,7,88195416+xuruikun@users.noreply.github.com,,8,23
54
+ MarcoHGulino,specification-phase-exercise-team2,09/01/2026,10/16/2026,5,81,36,5,0,MarcoHGulino,0,0,0,0,mg6378@nyu.edu,,3,5
55
+ Sai Shettar,specification-phase-exercise-team2,09/01/2026,10/16/2026,12,46,14,12,0,saishettar,0,0,0,0,sai.r.shettar@gmail.com,saishettar@nyu.edu,4,12
56
+ SiennaSSM,specification-phase-exercise-team2,09/01/2026,10/16/2026,14,122,48,14,0,SiennaSSM,0,0,0,0,sm12131@nyu.edu,,4,14
57
+ bloombar,specification-phase-exercise-team2,09/01/2026,10/16/2026,1,15,0,1,0,bloombar,0,0,0,0,2209098+bloombar@users.noreply.github.com; amos@cs.nyu.edu,,0,0
58
+ elbow74,specification-phase-exercise-team2,09/01/2026,10/16/2026,5,86,48,5,0,elbow74,0,0,0,0,92692888+elbow74@users.noreply.github.com,,5,5
59
+ Abyssjac,specification-phase-exercise-sth,09/01/2026,10/16/2026,7,229,183,8,1,Abyssjac,0,0,0,0,131870078+Abyssjac@users.noreply.github.com; zuzhou.abyss@gmail.com,,7,0
60
+ Jessie Zhang,specification-phase-exercise-sth,09/01/2026,10/16/2026,7,24,10,7,0,jessiezhang218,1,1,0,1,jessiezhang0218@gmail.com; jz6102@nyu.edu,jessiezhang0218@gmail.com,1,7
61
+ Shiqi Wang,specification-phase-exercise-sth,09/01/2026,10/16/2026,5,132,66,5,4,ShiqiWang1115,4,4,0,4,sw6423@nyu.edu,,0,0
62
+ Sinu112,specification-phase-exercise-sth,09/01/2026,10/16/2026,2,74,2,2,0,Sinu112,2,2,0,2,xs2471@nyu.edu,,0,2
63
+ bloombar,specification-phase-exercise-sth,09/01/2026,10/16/2026,1,15,0,1,0,bloombar,0,0,0,0,2209098+bloombar@users.noreply.github.com; amos@cs.nyu.edu,,0,0
64
+ TracyWang0904,specification-phase-exercise-byteme,09/01/2026,10/16/2026,8,226,43,21,0,TracyWang0904,0,0,0,0,yw8252@nyu.edu,,8,0
65
+ bloombar,specification-phase-exercise-byteme,09/01/2026,10/16/2026,0,0,0,0,0,bloombar,0,0,0,0,2209098+bloombar@users.noreply.github.com; amos@cs.nyu.edu,,0,0
66
+ AliG643,specification-phase-exercise-purple-sasquatch,09/01/2026,10/16/2026,4,90,9,4,4,AliG643,1,1,0,0,sg7920@nyu.edu,sg7920@nyu.edu,0,0
67
+ S1rius-Yang,specification-phase-exercise-purple-sasquatch,09/01/2026,10/16/2026,1,3,1,1,0,S1rius-Yang,0,0,0,0,hy2743@nyu.edu,,0,1
68
+ Sirius,specification-phase-exercise-purple-sasquatch,09/01/2026,10/16/2026,3,6,4,3,0,,0,0,0,0,s1rius@SiriusdeMacBook-Pro.local,,0,0
69
+ Yuyang-Ding1102,specification-phase-exercise-purple-sasquatch,09/01/2026,10/16/2026,4,425,63,4,7,Yuyang-Ding1102,2,1,1,1,yd2734@nyu.edu,,0,4
70
+ Yvonne,specification-phase-exercise-purple-sasquatch,09/01/2026,10/16/2026,2,80,27,2,0,Lyvoj,0,0,0,0,261538801+Lyvoj@users.noreply.github.com,,0,0
71
+ Zihan Chen,specification-phase-exercise-purple-sasquatch,09/01/2026,10/16/2026,6,148,88,6,0,Vent1Zzz,0,0,0,0,203582969+Vent1Zzz@users.noreply.github.com,,1,0
72
+ bloombar,specification-phase-exercise-purple-sasquatch,09/01/2026,10/16/2026,1,15,0,1,0,bloombar,0,0,0,0,2209098+bloombar@users.noreply.github.com; amos@cs.nyu.edu,,0,0
73
+ Dizay-53,specification-exercise-hibiscus,09/01/2026,10/16/2026,3,46,3,3,0,Dizay-53,2,2,0,1,Ya2522@nyu.edu,,0,3
74
+ LefeiKe,specification-exercise-hibiscus,09/01/2026,10/16/2026,4,54,6,4,3,LefeiKe,4,4,0,1,87459566+LefeiKe@users.noreply.github.com; lk3104@nyu.edu,,0,0
75
+ ange285,specification-exercise-hibiscus,09/01/2026,10/16/2026,1,2,1,1,1,ange285,1,1,0,0,enjelia126@gmail.com,,0,1
76
+ bloombar,specification-exercise-hibiscus,09/01/2026,10/16/2026,1,15,0,1,0,bloombar,0,0,0,0,2209098+bloombar@users.noreply.github.com; amos@cs.nyu.edu,,0,0
77
+ sofia-matari,specification-exercise-hibiscus,09/01/2026,10/16/2026,2,46,11,2,6,sofia-matari,2,2,0,0,sbm9838@nyu.edu,,0,0
78
+ yasminek27,specification-exercise-hibiscus,09/01/2026,10/16/2026,9,118,28,9,5,yasminek27,9,9,0,0,yk2154@nyu.edu,,0,0
79
+ aw4580,specification-phase-exercise-search,09/01/2026,10/16/2026,6,57,30,6,0,aw4580,0,0,0,0,134334340+aw4580@users.noreply.github.com; aw4580@nyu.edu,,7,5
80
+ benzaslavsky,specification-phase-exercise-search,09/01/2026,10/16/2026,1,17,2,1,2,benzaslavsky,1,1,0,1,62486098+benzaslavsky@users.noreply.github.com; benzy.venue@gmail.com,,0,0
81
+ bloombar,specification-phase-exercise-search,09/01/2026,10/16/2026,1,15,0,1,0,bloombar,0,0,0,0,2209098+bloombar@users.noreply.github.com; amos@cs.nyu.edu,,0,0
82
+ iindes,specification-phase-exercise-search,09/01/2026,10/16/2026,1,1,1,1,1,iindes,1,1,0,1,59502115+iindes@users.noreply.github.com,,0,1
83
+ lkykathy,specification-phase-exercise-search,09/01/2026,10/16/2026,7,338,61,7,6,lkykathy,7,7,0,6,67247076+lkykathy@users.noreply.github.com; linkeying0425@gmail.com,,0,0
84
+ saramhersch,specification-phase-exercise-search,09/01/2026,10/16/2026,1,4,0,1,0,saramhersch,0,0,0,0,smh9864@nyu.edu,,1,1
85
+ SparkFan-ui,specification-phase-exercise-sttke,09/01/2026,10/16/2026,4,271,28,5,0,SparkFan-ui,0,0,0,0,jf4832@nyu.edu,,6,4
86
+ bloombar,specification-phase-exercise-sttke,09/01/2026,10/16/2026,1,15,0,1,0,bloombar,0,0,0,0,2209098+bloombar@users.noreply.github.com; amos@cs.nyu.edu,,0,0
87
+ kevinhan923,specification-phase-exercise-sttke,09/01/2026,10/16/2026,3,116,185,3,0,kevinhan923,0,0,0,0,69257524+kevinhan923@users.noreply.github.com,,4,0
88
+ 23matthewc,specification-phase-exercise-nocturnal,09/01/2026,10/16/2026,0,0,0,0,2,23matthewc,0,0,0,0,76405126+23matthewc@users.noreply.github.com,,0,0
89
+ ajoykumarrishi,specification-phase-exercise-nocturnal,09/01/2026,10/16/2026,14,310,24,14,0,ajoykumarrishi,1,1,0,0,151567609+ajoykumarrishi@users.noreply.github.com; rishi.ajoykumar9@gmail.com,,0,14
90
+ bloombar,specification-phase-exercise-nocturnal,09/01/2026,10/16/2026,0,0,0,0,0,bloombar,0,0,0,0,2209098+bloombar@users.noreply.github.com; amos@cs.nyu.edu,,0,0
91
+ christinalin06,specification-phase-exercise-nocturnal,09/01/2026,10/16/2026,7,119,31,7,1,christinalin06,2,2,0,0,Christinalin006@gmail.com,,3,1
92
+ DyingRavioli,specification-phase-exercise-goodTeam,09/01/2026,10/16/2026,5,147,87,5,3,DyingRavioli,5,5,0,5,109256424+DyingRavioli@users.noreply.github.com,,1,5
93
+ RayyanAhmed21,specification-phase-exercise-goodTeam,09/01/2026,10/16/2026,4,121,17,4,0,RayyanAhmed21,0,0,0,0,ra4073@nyu.edu,,0,0
94
+ UIYrj,specification-phase-exercise-goodTeam,09/01/2026,10/16/2026,3,38,4,3,2,UIYrj,2,2,0,2,158982316+UIYrj@users.noreply.github.com,,0,3
95
+ bloombar,specification-phase-exercise-goodTeam,09/01/2026,10/16/2026,1,15,0,1,1,bloombar,0,0,0,0,2209098+bloombar@users.noreply.github.com; amos@cs.nyu.edu,,0,0
96
+ rz3007,specification-phase-exercise-goodTeam,09/01/2026,10/16/2026,2,54,4,2,0,rz3007,0,0,0,0,rz3007@nyu.edu,,0,2
97
+ sanay-d-nyu,specification-phase-exercise-goodTeam,09/01/2026,10/16/2026,6,77,9,6,1,sanay-d-nyu,1,1,0,1,sd5113@nyu.edu,sd5113@nyu.edu,1,0
98
+ DaniyalMl,specification-phase-exercise-ddjz,09/01/2026,10/16/2026,1,1,0,1,0,DaniyalMlk,0,0,0,0,103329154+DaniyalMlk@users.noreply.github.com,,1,1
99
+ Jayyu2005,specification-phase-exercise-ddjz,09/01/2026,10/16/2026,1,6,1,1,0,Jayyu2005,0,0,0,0,jy4886@nyu.edu,,1,1
100
+ LefeiKe,specification-phase-exercise-ddjz,09/01/2026,10/16/2026,2,2,2,2,0,LefeiKe,0,0,0,0,lk3104@nyu.edu,,2,0
101
+ bloombar,specification-phase-exercise-ddjz,09/01/2026,10/16/2026,1,15,0,1,0,bloombar,1,1,0,0,2209098+bloombar@users.noreply.github.com; amos@cs.nyu.edu,,0,0
102
+ catw1thtea,specification-phase-exercise-ddjz,09/01/2026,10/16/2026,3,5,3,3,0,catw1thtea,0,0,0,0,ds7746@nyu.edu,danialsagymbayev@gmail.com,3,3
103
+ denisekos,specification-phase-exercise-ddjz,09/01/2026,10/16/2026,1,129,5,1,0,denisekos,0,0,0,0,denise.koswara28@gmail.com,,1,1
104
+ manzim7,specification-phase-exercise-ddjz,09/01/2026,10/16/2026,3,9,3,3,5,manzim7,0,0,0,0,mm13507@nyu.edu,,3,0
105
+ ,specification-phase-exercise-ddjz,09/01/2026,10/16/2026,0,0,0,0,0,mosada2515,1,0,1,0,,,0,0
106
+ ,specification-phase-exercise-ddjz,09/01/2026,10/16/2026,0,0,0,0,0,Yded21,1,0,0,0,,,0,0
107
+ LefeiKe,specification-phase-exercise-team1,09/01/2026,10/16/2026,2,2,2,2,0,LefeiKe,0,0,0,0,lk3104@nyu.edu,,0,0
108
+ Yded21,specification-phase-exercise-team1,09/01/2026,10/16/2026,1,187,14,1,0,Yded21,1,1,0,0,eddyzzy@outlook.com,,0,1
109
+ ac11238,specification-phase-exercise-team1,09/01/2026,10/16/2026,9,29,12,9,0,ac11238,0,0,0,0,ac11348@nyu.edu,,12,9
110
+ bloombar,specification-phase-exercise-team1,09/01/2026,10/16/2026,1,15,0,1,0,bloombar,0,0,0,0,2209098+bloombar@users.noreply.github.com; amos@cs.nyu.edu,,0,0
111
+ manzim7,specification-phase-exercise-team1,09/01/2026,10/16/2026,1,5,1,1,0,manzim7,0,0,0,0,mm13507@nyu.edu,,0,0
112
+ uwtz,specification-phase-exercise-team1,09/01/2026,10/16/2026,8,189,37,8,1,uwtz,0,0,0,0,86273231+uwtz@users.noreply.github.com,,8,8
113
+ SeanA12,specification-phase-exercise-GitLost,09/01/2026,10/16/2026,2,36,2,2,2,SeanA12,2,2,0,0,106447402+SeanA12@users.noreply.github.com; seanalliman112@gmai.com,,0,0
114
+ bloombar,specification-phase-exercise-GitLost,09/01/2026,10/16/2026,1,15,0,1,0,bloombar,0,0,0,0,2209098+bloombar@users.noreply.github.com; amos@cs.nyu.edu,,0,0
115
+ layan-al,specification-phase-exercise-GitLost,09/01/2026,10/16/2026,4,161,5,4,0,layan-al,5,4,1,0,103759519+layan-al@users.noreply.github.com,,0,0
116
+ ogngnaoh,specification-phase-exercise-GitLost,09/01/2026,10/16/2026,0,0,0,0,4,ogngnaoh,0,0,0,0,ngoviethoang9@gmail.com,,0,0
117
+ rayyang262,specification-phase-exercise-GitLost,09/01/2026,10/16/2026,1,2,2,1,0,rayyang262,0,0,0,0,rayyang2026@gmail.com,,1,1
118
+ BrianDai22,specification-phase-exercise-mihir-brian-yash-linh-theo,09/01/2026,10/16/2026,3,158,157,4,0,BrianDai22,0,0,0,0,mooprules@gmail.com,,0,0
119
+ Mihir Ganesan,specification-phase-exercise-mihir-brian-yash-linh-theo,09/01/2026,10/16/2026,6,243,35,6,0,,0,0,0,0,mihirganesan@10-17-27-151.dynapool.wireless.nyu.edu; mihirganesan@10-17-58-124.dynapool.wireless.nyu.edu,,0,0
120
+ bloombar,specification-phase-exercise-mihir-brian-yash-linh-theo,09/01/2026,10/16/2026,1,15,0,1,0,bloombar,0,0,0,0,2209098+bloombar@users.noreply.github.com; amos@cs.nyu.edu,,0,0
121
+ theoegoldstine,specification-phase-exercise-mihir-brian-yash-linh-theo,09/01/2026,10/16/2026,4,6,4,4,0,theoegoldstine,0,0,0,0,63026703+theoegoldstine@users.noreply.github.com,,0,4
122
+ yash-pandya9798,specification-phase-exercise-mihir-brian-yash-linh-theo,09/01/2026,10/16/2026,1,8,2,1,0,yash-pandya9798,0,0,0,0,yash.pandya9798@gmail.com,,0,1
123
+ RayyanAhmed21,specification-phase-exercise,09/01/2026,10/16/2026,5,175,18,5,4,RayyanAhmed21,4,4,0,4,ra4073@nyu.edu,,0,1
124
+ bloombar,specification-phase-exercise,09/01/2026,10/16/2026,1,15,0,1,1,bloombar,1,1,0,1,2209098+bloombar@users.noreply.github.com; amos@cs.nyu.edu,,0,0
125
+ rz3007,specification-phase-exercise,09/01/2026,10/16/2026,3,55,5,3,1,rz3007,1,1,0,1,rz3007@nyu.edu,,1,3
126
+ bloombar,specification-phase-exercise-helloworld,09/01/2026,10/16/2026,1,15,0,1,0,bloombar,0,0,0,0,2209098+bloombar@users.noreply.github.com; amos@cs.nyu.edu,,0,0
@@ -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.5.1"
8
+ version = "1.6.0"
9
9
  authors = [
10
10
  { name="Tobody Tothing", email="nobody.nothing@nowhere.com" },
11
11
  ]
@@ -132,6 +132,22 @@
132
132
  family: OUT
133
133
  phase: 1
134
134
  status: Done
135
+ - id: OUT-6
136
+ title: Aggregated results
137
+ full: -a/--aggregate developer prints one row per developer, summed over every
138
+ repository analyzed, instead of one row per contributor per repository. Its
139
+ columns are username, start_date, end_date, commits, insertions, deletions,
140
+ files, merges, repositories, where repositories is the number of
141
+ repositories in which the developer has any activity in the date range (a
142
+ non-zero count, git or GitHub, as for -c); there is no repository column.
143
+ With --github, the columns github_username, prs_opened, prs_merged,
144
+ prs_closed_unmerged, prs_self_merged, git_email, github_email,
145
+ pushes_to_default, web_commits follow. Rows are combined when they share a
146
+ GitHub username (ignoring case), or, for rows without…
147
+ section: §4 Output
148
+ family: OUT
149
+ phase: 1
150
+ status: Done
135
151
  - id: PKG-1
136
152
  title: Installable package
137
153
  full: The tool installs with pip install gitlogstats, which provides a
@@ -5,6 +5,7 @@ import datetime
5
5
  import re
6
6
  import sys
7
7
  from . import GitLogsParser
8
+ from . import aggregate
8
9
  from .clones import remove_clones
9
10
  from .git_logs_parser import FIELDNAMES
10
11
  from .github import (
@@ -146,6 +147,14 @@ def main():
146
147
  default=False,
147
148
  action="store_true",
148
149
  )
150
+ parser.add_argument(
151
+ "-a",
152
+ "--aggregate",
153
+ help="Combine the results into one row per developer, summed over all "
154
+ "repositories, or one row per repository, summed over its contributors",
155
+ default=None,
156
+ choices=aggregate.MODES,
157
+ )
149
158
  parser.add_argument(
150
159
  "--github",
151
160
  help="Also report GitHub pull requests opened, merged, closed and "
@@ -253,12 +262,17 @@ def main():
253
262
 
254
263
  # emit a single table (or JSON array) covering all repositories
255
264
  if last_parser is not None:
256
- # an empty CSV table still gets the GitHub columns with --github
257
- fieldnames = (
258
- FIELDNAMES + GITHUB_FIELDNAMES + EMAIL_FIELDNAMES + ACTIVITY_FIELDNAMES
259
- if args.github
260
- else None
261
- )
265
+ # an empty CSV table still gets the columns it would have had
266
+ if args.aggregate:
267
+ # OUT-6: one row per developer or per repository
268
+ all_results = aggregate.aggregate(all_results, args.aggregate, args.github)
269
+ fieldnames = aggregate.fieldnames(args.aggregate, args.github)
270
+ elif args.github:
271
+ fieldnames = (
272
+ FIELDNAMES + GITHUB_FIELDNAMES + EMAIL_FIELDNAMES + ACTIVITY_FIELDNAMES
273
+ )
274
+ else:
275
+ fieldnames = None
262
276
  output = last_parser.format_results(all_results, args.format, fieldnames)
263
277
  # end with exactly one newline: CSV and Markdown output already has one
264
278
  print(output, end="" if output.endswith("\n") else "\n")
@@ -0,0 +1,177 @@
1
+ """Combine result rows by developer or by repository (OUT-6).
2
+
3
+ Everything here is pure logic over the rows the rest of the tool produces: no
4
+ git, no network. Each aggregated row sums the counts of the rows it combines.
5
+ """
6
+
7
+ from gitlogstats.git_logs_parser import FIELDNAMES
8
+ from gitlogstats.github import (
9
+ ACTIVITY_FIELDNAMES,
10
+ EMAIL_FIELDNAMES,
11
+ GITHUB_FIELDNAMES,
12
+ )
13
+ from gitlogstats.identity import join_emails
14
+
15
+ DEVELOPER = "developer"
16
+ REPOSITORY = "repository"
17
+ MODES = [DEVELOPER, REPOSITORY]
18
+
19
+ # the git counts (commits to merges), summed in both modes
20
+ GIT_COUNTS = FIELDNAMES[4:]
21
+
22
+ # the GitHub pull-request counts (CLI-3), summed in both modes
23
+ PR_COUNTS = GITHUB_FIELDNAMES[1:]
24
+
25
+ # every count; a row with any of them non-zero is active, as for -c (FILT-4)
26
+ ALL_COUNTS = GIT_COUNTS + PR_COUNTS + ACTIVITY_FIELDNAMES
27
+
28
+
29
+ def fieldnames(mode, github):
30
+ """
31
+ The output columns of an aggregated table, in order.
32
+ @param mode: DEVELOPER or REPOSITORY
33
+ @param github: whether the GitHub columns are reported (--github)
34
+ @returns: a list of column names
35
+ """
36
+ dates = ["start_date", "end_date"]
37
+ if mode == DEVELOPER:
38
+ columns = ["username"] + dates + GIT_COUNTS + ["repositories"]
39
+ if github:
40
+ columns += ["github_username"] + PR_COUNTS + EMAIL_FIELDNAMES
41
+ columns += ACTIVITY_FIELDNAMES
42
+ else:
43
+ columns = ["repository"] + dates + GIT_COUNTS + ["contributors"]
44
+ if github:
45
+ columns += PR_COUNTS + ACTIVITY_FIELDNAMES
46
+ return columns
47
+
48
+
49
+ def developer_key(row):
50
+ """
51
+ The developer a row belongs to: its GitHub user, ignoring case, when it
52
+ has one, otherwise its exact git username.
53
+ @param row: a result row
54
+ @returns: a hashable key
55
+ """
56
+ login = row.get("github_username")
57
+ if login:
58
+ return ("github", login.casefold())
59
+ return ("git", row["username"])
60
+
61
+
62
+ def is_active(row):
63
+ """
64
+ Whether a row has any activity: a non-zero count, git or GitHub.
65
+ @param row: a result row
66
+ @returns: a bool
67
+ """
68
+ return any(row.get(field) for field in ALL_COUNTS)
69
+
70
+
71
+ def main_username(rows):
72
+ """
73
+ The git username of a developer's rows: the one with the most commits,
74
+ ties alphabetical, as when combining identities in one repository (STAT-11).
75
+ @param rows: the rows of one developer
76
+ @returns: a username, or "" if every row is GitHub-only
77
+ """
78
+ named = [row for row in rows if row["username"]]
79
+ if not named:
80
+ return ""
81
+ return min(named, key=lambda row: (-(row["commits"] or 0), row["username"]))[
82
+ "username"
83
+ ]
84
+
85
+
86
+ def sum_values(values):
87
+ """
88
+ Add up counts. An unavailable count (None, CLI-3) is skipped, so the total
89
+ is None only when every count is unavailable.
90
+ @param values: numbers or None
91
+ @returns: their sum, or None
92
+ """
93
+ present = [value for value in values if value is not None]
94
+ return sum(present) if present else None
95
+
96
+
97
+ def first_text(values):
98
+ """
99
+ The first non-empty text. When there is none, "" if any value is
100
+ available, or None if every value is unavailable.
101
+ @param values: strings or None
102
+ @returns: a string, or None
103
+ """
104
+ values = list(values)
105
+ for value in values:
106
+ if value:
107
+ return value
108
+ return None if all(value is None for value in values) else ""
109
+
110
+
111
+ def merge_emails(values):
112
+ """
113
+ Join '; '-separated email lists into one sorted list without repeats
114
+ (STAT-13), or None if every list is unavailable.
115
+ @param values: '; '-separated strings, "" or None
116
+ @returns: a '; '-separated string, or None
117
+ """
118
+ values = list(values)
119
+ if all(value is None for value in values):
120
+ return None
121
+ return join_emails(
122
+ email for value in values if value for email in value.split("; ")
123
+ )
124
+
125
+
126
+ def combine(rows, columns):
127
+ """
128
+ Build the aggregated row of one group.
129
+ @param rows: the rows of one developer or one repository
130
+ @param columns: the output columns, from fieldnames()
131
+ @returns: one row with those columns
132
+ """
133
+ active = [row for row in rows if is_active(row)]
134
+ out = {}
135
+ for field in columns:
136
+ if field == "username":
137
+ out[field] = main_username(rows)
138
+ elif field in ("repository", "start_date", "end_date"):
139
+ out[field] = rows[0][field]
140
+ elif field == "repositories":
141
+ out[field] = len({row["repository"] for row in active})
142
+ elif field == "contributors":
143
+ out[field] = len(active)
144
+ elif field == "github_username":
145
+ out[field] = first_text(row[field] for row in rows)
146
+ elif field in EMAIL_FIELDNAMES:
147
+ out[field] = merge_emails(row[field] for row in rows)
148
+ else:
149
+ out[field] = sum_values(row[field] for row in rows)
150
+ return out
151
+
152
+
153
+ def aggregate(rows, mode, github):
154
+ """
155
+ Combine result rows into one row per developer or one row per repository,
156
+ sorted by username (or GitHub username, for a developer with no git
157
+ username) or by repository, ignoring case.
158
+ @param rows: result rows of every repository, after -u and -c
159
+ @param mode: DEVELOPER or REPOSITORY
160
+ @param github: whether the rows carry the GitHub columns (--github)
161
+ @returns: the aggregated rows, sorted
162
+ """
163
+ groups = {}
164
+ for row in rows:
165
+ key = developer_key(row) if mode == DEVELOPER else row["repository"]
166
+ groups.setdefault(key, []).append(row)
167
+ columns = fieldnames(mode, github)
168
+ combined = [combine(group, columns) for group in groups.values()]
169
+
170
+ def sort_key(row):
171
+ if mode == DEVELOPER:
172
+ name = row["username"] or row.get("github_username") or ""
173
+ else:
174
+ name = row["repository"]
175
+ return (name.casefold(), name)
176
+
177
+ return sorted(combined, key=sort_key)