gitmole 0.7.2__tar.gz → 0.8.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 (64) hide show
  1. {gitmole-0.7.2 → gitmole-0.8.0}/PKG-INFO +40 -42
  2. {gitmole-0.7.2 → gitmole-0.8.0}/README.md +39 -41
  3. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/__init__.py +1 -1
  4. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/cli.py +15 -5
  5. gitmole-0.8.0/gitmole/evaluate.py +138 -0
  6. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/findings.py +71 -22
  7. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/load.py +39 -13
  8. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/maat.py +4 -4
  9. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/render.py +19 -4
  10. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/run.py +2 -2
  11. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/watch.py +87 -28
  12. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole.egg-info/PKG-INFO +40 -42
  13. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole.egg-info/SOURCES.txt +2 -0
  14. {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_backtest.py +1 -1
  15. {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_cli.py +43 -0
  16. gitmole-0.8.0/tests/test_evaluate.py +70 -0
  17. {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_findings.py +23 -2
  18. {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_load.py +42 -0
  19. {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_render.py +28 -10
  20. {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_run.py +18 -0
  21. {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_watch.py +93 -13
  22. {gitmole-0.7.2 → gitmole-0.8.0}/LICENSE +0 -0
  23. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/__main__.py +0 -0
  24. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/backtest.py +0 -0
  25. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/banner.py +0 -0
  26. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/blame.py +0 -0
  27. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/clean.py +0 -0
  28. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/coupling.py +0 -0
  29. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/deps.py +0 -0
  30. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/duplicates.py +0 -0
  31. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/filetypes.py +0 -0
  32. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/functions.py +0 -0
  33. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/hotspots.py +0 -0
  34. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/identity.py +0 -0
  35. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/knowledge.py +0 -0
  36. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/leaks.py +0 -0
  37. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/loss.py +0 -0
  38. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/textfmt.py +0 -0
  39. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/trend.py +0 -0
  40. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole.egg-info/dependency_links.txt +0 -0
  41. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole.egg-info/entry_points.txt +0 -0
  42. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole.egg-info/requires.txt +0 -0
  43. {gitmole-0.7.2 → gitmole-0.8.0}/gitmole.egg-info/top_level.txt +0 -0
  44. {gitmole-0.7.2 → gitmole-0.8.0}/pyproject.toml +0 -0
  45. {gitmole-0.7.2 → gitmole-0.8.0}/setup.cfg +0 -0
  46. {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_banner.py +0 -0
  47. {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_blame.py +0 -0
  48. {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_clean.py +0 -0
  49. {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_coupling.py +0 -0
  50. {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_deps.py +0 -0
  51. {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_duplicates.py +0 -0
  52. {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_filetypes.py +0 -0
  53. {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_functions.py +0 -0
  54. {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_golden.py +0 -0
  55. {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_hotspots.py +0 -0
  56. {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_identity.py +0 -0
  57. {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_knowledge.py +0 -0
  58. {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_leaks.py +0 -0
  59. {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_loss.py +0 -0
  60. {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_maat.py +0 -0
  61. {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_packaging.py +0 -0
  62. {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_render_examples.py +0 -0
  63. {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_textfmt.py +0 -0
  64. {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_trend.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gitmole
3
- Version: 0.7.2
3
+ Version: 0.8.0
4
4
  Summary: Offline git repository analysis with a terminal report: hotspots, coupling, ownership, code age, secrets, repo health.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/antvinni/gitmole
@@ -31,7 +31,7 @@ Free. Any Stack. Local. Offline. Deterministic. Fast.
31
31
  - **Free.** MIT licence, no paid tier, no account, no token. A local clone needs no credentials, and a public `owner/repo` is cloned with plain git. Your `gh` login is only used for private repositories and for `owner/*`, and only when you ask for them. The tools it runs are open source too.
32
32
  - **Any stack.** It reads what every repository has: the git log, git blame and the files themselves.
33
33
  - **Local & Offline.** Everything runs against a clone on your machine. Nothing is uploaded, nothing phones home; the vulnerability database is a copy you download once.
34
- - **Deterministic.** No AI at runtime. Every finding is a plain rule over counts you can recompute by hand. The same clone gives the same report every time.
34
+ - **Deterministic.** No AI at runtime. Every finding is a plain rule over counts you can recompute by hand. The JSON export carries each finding's rule and the numbers it fired on. The same clone gives the same report every time.
35
35
  - **Fast.** A 4,400-commit repository takes under thirty seconds.
36
36
 
37
37
  ## Install
@@ -60,7 +60,7 @@ gitmole 'owner/*' # every non-archived repo of a user or or
60
60
  gitmole . --markdown report.md # the same report as a Markdown document
61
61
  gitmole . --json report.json # every table, the watch list and the findings
62
62
  gitmole . --fail-on warning # exit 3 if any finding is a warning or worse
63
- gitmole . --risk main --risk-threshold 5 # exit 3 if the files changed since main are too risky
63
+ gitmole . --risk main --risk-threshold 10 # exit 3 if the files changed since main hold over 10% of the risk
64
64
  gitmole . --since 2y --full # the current team, every row and column
65
65
  gitmole --clean # list what gitmole left behind, delete on a yes
66
66
  ```
@@ -71,47 +71,48 @@ blocks on secrets in source files and still posts the report. Every option:
71
71
 
72
72
  ## What you get
73
73
 
74
- The opening of the report for [react](https://github.com/facebook/react), 35,263 commits
74
+ The opening of the report for [react](https://github.com/facebook/react), 21,703 commits
75
75
  since 2013, at a pinned commit:
76
76
 
77
77
  ```text
78
78
  ╭─ react ──────────────────────────────────────────────────────────────────────────────────────────╮
79
- 35263 commits · 2013-05-28 → 2026-09-16 · 1880 identities · branch main │
79
+ 21703 commits · 2013-05-28 → 2026-09-16 · 1843 identities · branch main │
80
80
  │ 681,078 lines in 4781 files · JavaScript, TypeScript, Rust, CSS │
81
- │ most commits on Wed at 16:00 · 13% of commits are fixes · 1% of commits are reverts · 19% │
81
+ │ most commits on Wed at 15:00 · 14% of commits are fixes · 1% of commits are reverts · 19% │
82
82
  │ of surviving code from 2026 │
83
- │ 1 critical, 6 warnings, 8 notes │
83
+ │ 1 critical, 6 warnings, 9 notes │
84
84
  ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
85
85
 
86
86
  ◎ Watch list
87
87
  file why
88
88
  ──────────────────────────────────────────────────────────────────────────────────────────────────
89
- compiler/packages/babel-plugin-react-compiler/s changed 331 times · fixed twice in six months ·
90
- rc/Inference/InferMutationAliasingEffects.ts Joe Savona wrote 98% of it ·
91
- findNonMutatedDestructureSpreads() complexity 39
92
- packages/react-server/src/ReactFlightServer.js changed 377 times · fixed once in six months ·
93
- renderModelDestructive() complexity 544
94
- packages/shared/forks/ReactFeatureFlags.www.js changed 583 times · fixed once in six months ·
95
- changes with
96
- packages/shared/forks/ReactFeatureFlags.test-ren
97
- derer.www.js (77%) and 5 others
98
- packages/shared/ReactFeatureFlags.js changed 575 times · fixed 6 times · changes with
99
- packages/shared/forks/ReactFeatureFlags.test-ren
100
- derer.js (80%) and 4 others
101
- packages/react-reconciler/src/ReactFiberWorkLoo changed 342 times · fixed 4 times in six months
89
+ packages/react-server/src/ReactFlightServer.js changed 319 times · fixed once in six months ·
90
+ visitAsyncNodeImpl() complexity 46
91
+ packages/react-server/src/ReactFizzServer.js changed 297 times · fixed twice in six months ·
92
+ retryNode() complexity 41
93
+ packages/react-reconciler/src/ReactFiberWorkLoo changed 312 times · fixed 4 times in six months
102
94
  p.js · flushSpawnedWork() complexity 48
103
- ranked by churn × recent fixes × complexity × single ownership
104
- 6 months ago this list would have named 6 of the 211 files fixed since (a random 15 of the 1979
105
- files that had changed more than once would name 0.3)
95
+ packages/react-reconciler/src/ReactFiberCommitW changed 284 times · fixed 28 times ·
96
+ ork.js commitLayoutEffectOnFiber() complexity 72
97
+ packages/react-reconciler/src/ReactFiberBeginWo changed 361 times · fixed once in six months ·
98
+ rk.js beginWork() complexity 52
99
+ ranked by revisions × lines of code; the reasons say what else counts against each file
100
+ 6 months ago this list would have named 11 of the 46 files fixed since (a random 15 of the 1802
101
+ files that had changed more than once would name 0.3; the 15 most changed would name 7)
106
102
  ```
107
103
 
108
- The watch list is the point: the five files where the next bug is most
109
- likely, the reasons in words, and a backtest that says how the same list,
110
- drawn six months earlier, would have done against the fixes that followed.
111
- Between the header and that list the full report puts its findings, 15 for
112
- react (1 critical, 6 warnings, 8 notes); below it, tables for people, the
113
- knowledge map, the timeline, hotspots with their complexity trend, change
114
- coupling, complex functions and repo health. Every section is explained in
104
+ The watch list is the point: the five source files most likely to need a fix
105
+ next, the reasons in words, and a backtest that says how the same list, drawn
106
+ six months earlier, would have done against the fixes that followed. The list
107
+ ranks by revisions × lines of code: measured at six cut-offs on three
108
+ repositories
109
+ ([validation](https://github.com/antvinni/gitmole/blob/main/docs/validation.md)),
110
+ that named more of the files fixed next than churn alone, size alone or a
111
+ weighted product of fixes, complexity and ownership. Between the header and
112
+ that list the full report puts its findings, 16 for react (1 critical, 6
113
+ warnings, 9 notes); below it, tables for people, the knowledge map, the
114
+ timeline, hotspots with their complexity trend, change coupling, complex
115
+ functions and repo health. Every section is explained in
115
116
  [docs/output.md](https://github.com/antvinni/gitmole/blob/main/docs/output.md).
116
117
 
117
118
  Reports on repositories you know, each at a pinned commit with a fixed
@@ -120,14 +121,9 @@ come from git-sizer over the whole clone, so a fresh clone can differ there:
120
121
 
121
122
  | Repository | Commits | Lines | Watch list backtest |
122
123
  |---|---:|---:|---|
123
- | [curl](https://github.com/antvinni/gitmole/blob/main/docs/examples/curl.md) | 39,894 | 247,179 | named 15 of the 239 files fixed in the next six months; a random pick would name 5.0 |
124
- | [django](https://github.com/antvinni/gitmole/blob/main/docs/examples/django.md) | 52,832 | 431,749 | named 13 of the 213 files fixed in the next six months; a random pick would name 2.8 |
125
- | [react](https://github.com/antvinni/gitmole/blob/main/docs/examples/react.md) | 35,263 | 681,078 | named 6 of the 211 files fixed in the next six months; a random pick would name 0.3 |
126
- | [kubernetes](https://github.com/antvinni/gitmole/blob/main/docs/examples/kubernetes.md) | 161,803 | 4,180,715 | named 7 of the 338 files fixed in the next six months; a random pick would name 0.4 |
127
-
128
- kubernetes's code-age step was skipped on gitmole's default time budget and
129
- its report says so; the duplicates step was skipped on the memory budget too
130
- and produced no section.
124
+ | [curl](https://github.com/antvinni/gitmole/blob/main/docs/examples/curl.md) | 39,758 | 247,179 | named 15 of the 238 files fixed in the next six months; a random pick would name 4.9, the 15 most changed 15 |
125
+ | [django](https://github.com/antvinni/gitmole/blob/main/docs/examples/django.md) | 34,933 | 431,749 | named 15 of the 213 files fixed in the next six months; a random pick would name 3.1, the 15 most changed 13 |
126
+ | [react](https://github.com/antvinni/gitmole/blob/main/docs/examples/react.md) | 21,703 | 681,078 | named 11 of the 46 files fixed in the next six months; a random pick would name 0.3, the 15 most changed 7 |
131
127
 
132
128
  ## The tool set
133
129
 
@@ -154,7 +150,8 @@ Why these and not others: [docs/tools.md](https://github.com/antvinni/gitmole/bl
154
150
  - [Install](https://github.com/antvinni/gitmole/blob/main/docs/install.md): macOS, Linux, pipx, the check, pinned releases.
155
151
  - [Command line](https://github.com/antvinni/gitmole/blob/main/docs/cli.md): every option, portfolio mode, exports and CI gates, big repositories.
156
152
  - [The report and the output files](https://github.com/antvinni/gitmole/blob/main/docs/output.md): what each section and each file means.
157
- - [Example reports](https://github.com/antvinni/gitmole/tree/main/docs/examples): curl, django, react and kubernetes at pinned commits, regenerated by `bin/render-examples`.
153
+ - [Example reports](https://github.com/antvinni/gitmole/tree/main/docs/examples): curl, django and react at pinned commits, regenerated by `bin/render-examples`.
154
+ - [Validation](https://github.com/antvinni/gitmole/blob/main/docs/validation.md): the watch list against other ways of ranking the same files at six cut-offs on three repositories.
158
155
  - [Why these tools](https://github.com/antvinni/gitmole/blob/main/docs/tools.md): the rationale, what was left out, licences.
159
156
  - [Development](https://github.com/antvinni/gitmole/blob/main/docs/development.md): setup, tests, releases, code layout.
160
157
  - [Contributing](https://github.com/antvinni/gitmole/blob/main/CONTRIBUTING.md): bugs, ideas, pull requests, security reports.
@@ -166,9 +163,10 @@ Why these and not others: [docs/tools.md](https://github.com/antvinni/gitmole/bl
166
163
  against a local copy of its database that you download once, and gitmole
167
164
  never downloads it for you.
168
165
  - Remote targets are cloned into a fresh temp directory that is removed when
169
- the run ends. Local clones are only read, but the log export and the
170
- secrets scan touch all branches. `gitmole --clean` lists every directory
171
- gitmole created and deletes them after a y/N question.
166
+ the run ends. Local clones are only read. The secrets scan reads every
167
+ branch; everything else describes the branch that is checked out.
168
+ `gitmole --clean` lists every directory gitmole created and deletes them
169
+ after a y/N question.
172
170
  - Secret values never reach the output directory. betterleaks reports to
173
171
  gitmole in memory, and gitmole stores a short keyed hash in place of the
174
172
  value, the matched text and the commit message. The key is random, made
@@ -11,7 +11,7 @@ Free. Any Stack. Local. Offline. Deterministic. Fast.
11
11
  - **Free.** MIT licence, no paid tier, no account, no token. A local clone needs no credentials, and a public `owner/repo` is cloned with plain git. Your `gh` login is only used for private repositories and for `owner/*`, and only when you ask for them. The tools it runs are open source too.
12
12
  - **Any stack.** It reads what every repository has: the git log, git blame and the files themselves.
13
13
  - **Local & Offline.** Everything runs against a clone on your machine. Nothing is uploaded, nothing phones home; the vulnerability database is a copy you download once.
14
- - **Deterministic.** No AI at runtime. Every finding is a plain rule over counts you can recompute by hand. The same clone gives the same report every time.
14
+ - **Deterministic.** No AI at runtime. Every finding is a plain rule over counts you can recompute by hand. The JSON export carries each finding's rule and the numbers it fired on. The same clone gives the same report every time.
15
15
  - **Fast.** A 4,400-commit repository takes under thirty seconds.
16
16
 
17
17
  ## Install
@@ -40,7 +40,7 @@ gitmole 'owner/*' # every non-archived repo of a user or or
40
40
  gitmole . --markdown report.md # the same report as a Markdown document
41
41
  gitmole . --json report.json # every table, the watch list and the findings
42
42
  gitmole . --fail-on warning # exit 3 if any finding is a warning or worse
43
- gitmole . --risk main --risk-threshold 5 # exit 3 if the files changed since main are too risky
43
+ gitmole . --risk main --risk-threshold 10 # exit 3 if the files changed since main hold over 10% of the risk
44
44
  gitmole . --since 2y --full # the current team, every row and column
45
45
  gitmole --clean # list what gitmole left behind, delete on a yes
46
46
  ```
@@ -51,47 +51,48 @@ blocks on secrets in source files and still posts the report. Every option:
51
51
 
52
52
  ## What you get
53
53
 
54
- The opening of the report for [react](https://github.com/facebook/react), 35,263 commits
54
+ The opening of the report for [react](https://github.com/facebook/react), 21,703 commits
55
55
  since 2013, at a pinned commit:
56
56
 
57
57
  ```text
58
58
  ╭─ react ──────────────────────────────────────────────────────────────────────────────────────────╮
59
- 35263 commits · 2013-05-28 → 2026-09-16 · 1880 identities · branch main │
59
+ 21703 commits · 2013-05-28 → 2026-09-16 · 1843 identities · branch main │
60
60
  │ 681,078 lines in 4781 files · JavaScript, TypeScript, Rust, CSS │
61
- │ most commits on Wed at 16:00 · 13% of commits are fixes · 1% of commits are reverts · 19% │
61
+ │ most commits on Wed at 15:00 · 14% of commits are fixes · 1% of commits are reverts · 19% │
62
62
  │ of surviving code from 2026 │
63
- │ 1 critical, 6 warnings, 8 notes │
63
+ │ 1 critical, 6 warnings, 9 notes │
64
64
  ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
65
65
 
66
66
  ◎ Watch list
67
67
  file why
68
68
  ──────────────────────────────────────────────────────────────────────────────────────────────────
69
- compiler/packages/babel-plugin-react-compiler/s changed 331 times · fixed twice in six months ·
70
- rc/Inference/InferMutationAliasingEffects.ts Joe Savona wrote 98% of it ·
71
- findNonMutatedDestructureSpreads() complexity 39
72
- packages/react-server/src/ReactFlightServer.js changed 377 times · fixed once in six months ·
73
- renderModelDestructive() complexity 544
74
- packages/shared/forks/ReactFeatureFlags.www.js changed 583 times · fixed once in six months ·
75
- changes with
76
- packages/shared/forks/ReactFeatureFlags.test-ren
77
- derer.www.js (77%) and 5 others
78
- packages/shared/ReactFeatureFlags.js changed 575 times · fixed 6 times · changes with
79
- packages/shared/forks/ReactFeatureFlags.test-ren
80
- derer.js (80%) and 4 others
81
- packages/react-reconciler/src/ReactFiberWorkLoo changed 342 times · fixed 4 times in six months
69
+ packages/react-server/src/ReactFlightServer.js changed 319 times · fixed once in six months ·
70
+ visitAsyncNodeImpl() complexity 46
71
+ packages/react-server/src/ReactFizzServer.js changed 297 times · fixed twice in six months ·
72
+ retryNode() complexity 41
73
+ packages/react-reconciler/src/ReactFiberWorkLoo changed 312 times · fixed 4 times in six months
82
74
  p.js · flushSpawnedWork() complexity 48
83
- ranked by churn × recent fixes × complexity × single ownership
84
- 6 months ago this list would have named 6 of the 211 files fixed since (a random 15 of the 1979
85
- files that had changed more than once would name 0.3)
75
+ packages/react-reconciler/src/ReactFiberCommitW changed 284 times · fixed 28 times ·
76
+ ork.js commitLayoutEffectOnFiber() complexity 72
77
+ packages/react-reconciler/src/ReactFiberBeginWo changed 361 times · fixed once in six months ·
78
+ rk.js beginWork() complexity 52
79
+ ranked by revisions × lines of code; the reasons say what else counts against each file
80
+ 6 months ago this list would have named 11 of the 46 files fixed since (a random 15 of the 1802
81
+ files that had changed more than once would name 0.3; the 15 most changed would name 7)
86
82
  ```
87
83
 
88
- The watch list is the point: the five files where the next bug is most
89
- likely, the reasons in words, and a backtest that says how the same list,
90
- drawn six months earlier, would have done against the fixes that followed.
91
- Between the header and that list the full report puts its findings, 15 for
92
- react (1 critical, 6 warnings, 8 notes); below it, tables for people, the
93
- knowledge map, the timeline, hotspots with their complexity trend, change
94
- coupling, complex functions and repo health. Every section is explained in
84
+ The watch list is the point: the five source files most likely to need a fix
85
+ next, the reasons in words, and a backtest that says how the same list, drawn
86
+ six months earlier, would have done against the fixes that followed. The list
87
+ ranks by revisions × lines of code: measured at six cut-offs on three
88
+ repositories
89
+ ([validation](https://github.com/antvinni/gitmole/blob/main/docs/validation.md)),
90
+ that named more of the files fixed next than churn alone, size alone or a
91
+ weighted product of fixes, complexity and ownership. Between the header and
92
+ that list the full report puts its findings, 16 for react (1 critical, 6
93
+ warnings, 9 notes); below it, tables for people, the knowledge map, the
94
+ timeline, hotspots with their complexity trend, change coupling, complex
95
+ functions and repo health. Every section is explained in
95
96
  [docs/output.md](https://github.com/antvinni/gitmole/blob/main/docs/output.md).
96
97
 
97
98
  Reports on repositories you know, each at a pinned commit with a fixed
@@ -100,14 +101,9 @@ come from git-sizer over the whole clone, so a fresh clone can differ there:
100
101
 
101
102
  | Repository | Commits | Lines | Watch list backtest |
102
103
  |---|---:|---:|---|
103
- | [curl](https://github.com/antvinni/gitmole/blob/main/docs/examples/curl.md) | 39,894 | 247,179 | named 15 of the 239 files fixed in the next six months; a random pick would name 5.0 |
104
- | [django](https://github.com/antvinni/gitmole/blob/main/docs/examples/django.md) | 52,832 | 431,749 | named 13 of the 213 files fixed in the next six months; a random pick would name 2.8 |
105
- | [react](https://github.com/antvinni/gitmole/blob/main/docs/examples/react.md) | 35,263 | 681,078 | named 6 of the 211 files fixed in the next six months; a random pick would name 0.3 |
106
- | [kubernetes](https://github.com/antvinni/gitmole/blob/main/docs/examples/kubernetes.md) | 161,803 | 4,180,715 | named 7 of the 338 files fixed in the next six months; a random pick would name 0.4 |
107
-
108
- kubernetes's code-age step was skipped on gitmole's default time budget and
109
- its report says so; the duplicates step was skipped on the memory budget too
110
- and produced no section.
104
+ | [curl](https://github.com/antvinni/gitmole/blob/main/docs/examples/curl.md) | 39,758 | 247,179 | named 15 of the 238 files fixed in the next six months; a random pick would name 4.9, the 15 most changed 15 |
105
+ | [django](https://github.com/antvinni/gitmole/blob/main/docs/examples/django.md) | 34,933 | 431,749 | named 15 of the 213 files fixed in the next six months; a random pick would name 3.1, the 15 most changed 13 |
106
+ | [react](https://github.com/antvinni/gitmole/blob/main/docs/examples/react.md) | 21,703 | 681,078 | named 11 of the 46 files fixed in the next six months; a random pick would name 0.3, the 15 most changed 7 |
111
107
 
112
108
  ## The tool set
113
109
 
@@ -134,7 +130,8 @@ Why these and not others: [docs/tools.md](https://github.com/antvinni/gitmole/bl
134
130
  - [Install](https://github.com/antvinni/gitmole/blob/main/docs/install.md): macOS, Linux, pipx, the check, pinned releases.
135
131
  - [Command line](https://github.com/antvinni/gitmole/blob/main/docs/cli.md): every option, portfolio mode, exports and CI gates, big repositories.
136
132
  - [The report and the output files](https://github.com/antvinni/gitmole/blob/main/docs/output.md): what each section and each file means.
137
- - [Example reports](https://github.com/antvinni/gitmole/tree/main/docs/examples): curl, django, react and kubernetes at pinned commits, regenerated by `bin/render-examples`.
133
+ - [Example reports](https://github.com/antvinni/gitmole/tree/main/docs/examples): curl, django and react at pinned commits, regenerated by `bin/render-examples`.
134
+ - [Validation](https://github.com/antvinni/gitmole/blob/main/docs/validation.md): the watch list against other ways of ranking the same files at six cut-offs on three repositories.
138
135
  - [Why these tools](https://github.com/antvinni/gitmole/blob/main/docs/tools.md): the rationale, what was left out, licences.
139
136
  - [Development](https://github.com/antvinni/gitmole/blob/main/docs/development.md): setup, tests, releases, code layout.
140
137
  - [Contributing](https://github.com/antvinni/gitmole/blob/main/CONTRIBUTING.md): bugs, ideas, pull requests, security reports.
@@ -146,9 +143,10 @@ Why these and not others: [docs/tools.md](https://github.com/antvinni/gitmole/bl
146
143
  against a local copy of its database that you download once, and gitmole
147
144
  never downloads it for you.
148
145
  - Remote targets are cloned into a fresh temp directory that is removed when
149
- the run ends. Local clones are only read, but the log export and the
150
- secrets scan touch all branches. `gitmole --clean` lists every directory
151
- gitmole created and deletes them after a y/N question.
146
+ the run ends. Local clones are only read. The secrets scan reads every
147
+ branch; everything else describes the branch that is checked out.
148
+ `gitmole --clean` lists every directory gitmole created and deletes them
149
+ after a y/N question.
152
150
  - Secret values never reach the output directory. betterleaks reports to
153
151
  gitmole in memory, and gitmole stores a short keyed hash in place of the
154
152
  value, the matched text and the commit message. The key is random, made
@@ -1,3 +1,3 @@
1
1
  """gitmole: offline git repository analysis with a terminal report."""
2
2
 
3
- __version__ = "0.7.2"
3
+ __version__ = "0.8.0"
@@ -42,8 +42,8 @@ def parse_args(argv):
42
42
  p.add_argument("--json", metavar="PATH", help="write the report and findings as JSON to PATH, or - for stdout")
43
43
  p.add_argument("--markdown", metavar="PATH", help="write the report as Markdown to PATH, or - for stdout")
44
44
  p.add_argument("--fail-on", choices=findings.SEVERITIES, help="exit 3 if any finding is at this severity or worse")
45
- p.add_argument("--risk", metavar="BASE", help="score the files changed since BASE (merge base with HEAD) with the watch list's score; needs a local path")
46
- p.add_argument("--risk-threshold", type=float, metavar="N", help="with --risk: exit 3 when the change-risk total exceeds N")
45
+ p.add_argument("--risk", metavar="BASE", help="score the files changed since BASE (merge base with HEAD) by their share of the repository's revisions × lines of code; needs a local path")
46
+ p.add_argument("--risk-threshold", type=float, metavar="N", help="with --risk: exit 3 when the changed files hold more than N percent of the repository's revisions × lines of code")
47
47
  p.add_argument("--version", action="version", version=f"gitmole {__version__}")
48
48
  return p.parse_args(argv)
49
49
 
@@ -328,7 +328,7 @@ def _meta_for_run(repo_dir: str, args, estimate, age_ok: bool, plots_ok: bool, p
328
328
 
329
329
 
330
330
  def _record_statuses(meta, results, age_ok: bool, plots_ok: bool, lizard_ok: bool, cut, duplicates_ok: bool = True) -> None:
331
- """Turn each planned step's exit code into its final status: run, timeout or failed."""
331
+ """Turn each step's exit code into its final status: run, skipped, timeout or failed."""
332
332
  def status(step, default="run"):
333
333
  rc = results.get(step, 0)
334
334
  return default if rc == 0 else ("timeout" if rc == "timeout" else "failed")
@@ -344,6 +344,8 @@ def _record_statuses(meta, results, age_ok: bool, plots_ok: bool, lizard_ok: boo
344
344
  meta["trend"]["status"] = status("trend")
345
345
  if cut and "backtest" in results:
346
346
  meta["backtest"]["status"] = status("backtest")
347
+ # every step, not only the optional ones above: a killed scc is otherwise a report of "0 lines" with no reason
348
+ meta["steps"] = {name: "run" if rc == 0 else (rc if isinstance(rc, str) else "failed") for name, rc in results.items()}
347
349
 
348
350
 
349
351
  def _analyse(repo_dir: str, out_dir: str, args, ui: Console, planner, estimator) -> None:
@@ -411,7 +413,11 @@ def _portfolio(owner: str, args, console: Console, ui: Console, planner, estimat
411
413
  except NoCommits as e:
412
414
  ui.print(f"[yellow]{name}:[/yellow] {e}; skipped")
413
415
  continue
414
- report = load.load_report(out_dir)
416
+ try:
417
+ report = load.load_report(out_dir)
418
+ except load.Unreadable as e:
419
+ ui.print(f"[yellow]{name}:[/yellow] {e}; skipped", soft_wrap=True)
420
+ continue
415
421
  reports.append((name, report, findings.evaluate(report)))
416
422
  finally:
417
423
  shutil.rmtree(parent, ignore_errors=True) # the temp clones; nothing reads them after the run
@@ -484,7 +490,11 @@ def _render(out_dir: str, console: Console, ui: Console, args, err: Console) ->
484
490
 
485
491
  from . import render
486
492
 
487
- report = load.load_report(out_dir)
493
+ try:
494
+ report = load.load_report(out_dir)
495
+ except load.Unreadable as e:
496
+ err.print(f"[red]{e}[/red]", soft_wrap=True)
497
+ return 2
488
498
  found = findings.evaluate(report)
489
499
  risk = None
490
500
  if args.risk:
@@ -0,0 +1,138 @@
1
+ #!/usr/bin/env python3
2
+ """How the watch list would have done at several cut-off dates, next to the factor products it
3
+ replaced and the simpler baselines.
4
+
5
+ A development tool, not a pipeline step: `python -m gitmole.evaluate REPO OUT_DIR [--windows 6]
6
+ [--horizon 6] [--top 15]`, where OUT_DIR is a finished gitmole output directory for REPO (its log.txt
7
+ and meta.json are read). For each cut-off T, counted back from the last commit in steps of `horizon`
8
+ months, the change analysis is rebuilt from the commits before T, scc measures the tree at T, every
9
+ variant names its `top` files, and the source files a fix commit touched in [T, T + horizon) are the
10
+ outcome. A fix is a commit whose subject says so (maat.is_fix): a proxy, not a bug tracker, and the
11
+ table is only as good as the repository's commit subjects. Prints Markdown: one row per variant, one
12
+ column per cut-off, and the total; then how many commits `--all` adds to HEAD's."""
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import calendar
17
+ import datetime as dt
18
+ import os
19
+ import subprocess
20
+ import sys
21
+
22
+ from . import backtest, filetypes, identity, load, maat, trend, watch
23
+
24
+
25
+ def cutoffs(last_date: str, windows: int, horizon: int) -> list:
26
+ """The cut-off dates, oldest first: the last commit's date less one, two, ... horizons."""
27
+ return sorted(maat.months_before(last_date, horizon * k) for k in range(1, windows + 1))
28
+
29
+
30
+ def months_after(date: str, months: int) -> str:
31
+ """The ISO date `months` whole months after `date`, day clamped to the month's length."""
32
+ d = dt.date.fromisoformat(date)
33
+ years, month = divmod(d.month - 1 + months, 12)
34
+ y, m = d.year + years, month + 1
35
+ return dt.date(y, m, min(d.day, calendar.monthrange(y, m)[1])).isoformat()
36
+
37
+
38
+ def fixed_between(commits: list, start: str, end: str) -> set:
39
+ """Source files a fix commit touched on or after `start` and before `end`. Test files change with every fix."""
40
+ return {p for c in maat.in_window(commits, start, end) if maat.is_fix(c.get("subject", ""))
41
+ for p, _, _ in c["files"] if not filetypes.is_test_path(p)}
42
+
43
+
44
+ def report_at(commits: list, t: str, size: dict, meta: dict) -> dict:
45
+ """The report watch.risks reads, from the commits before `t` and scc's listing of the tree at `t`.
46
+ No coupling and no functions: neither enters the score, and the pipeline's own backtest has no functions either."""
47
+ past = maat.in_window(commits, until=t)
48
+ bots = {b["name"] for b in meta.get("bots") or []}
49
+ ownership = [r for r in maat.entity_ownership(past) if r["author"] not in bots and not identity.is_bot(r["author"])]
50
+ return {"meta": {"now": t, "generated": meta.get("generated") or []}, "size": size, "revisions": maat.revisions(past),
51
+ "plumbing": maat.plumbing(past), "authors": maat.authors(past), "ownership": ownership,
52
+ "fixes": maat.fixes(past, now=t), "coupling": [], "functions": []}
53
+
54
+
55
+ def variants(report: dict) -> dict:
56
+ """variant -> file names, best first, every one drawn from the pool the watch list draws from."""
57
+ rows = watch.risks(report)
58
+ out = {"watch list (hotspot)": [r["file"] for r in rows],
59
+ "factor product (max-scaled)": [r["file"] for r in watch.risks(report, scoring="max")],
60
+ "factor product (rank-scaled)": [r["file"] for r in watch.risks(report, scoring="rank")]}
61
+ for name, key in watch.BASELINES.items():
62
+ out[name] = watch.ranked_by(rows, key)
63
+ out["recent fixes"] = watch.ranked_by(rows, lambda r: (r["recent_fixes"], r["revs"]))
64
+ return out
65
+
66
+
67
+ def score(report: dict, fixed: set, top: int) -> dict:
68
+ """variant -> how many of its first `top` files were fixed, plus what a random `top` of the pool would name."""
69
+ lists = variants(report)
70
+ pool = lists["churn"]
71
+ out = {name: len(fixed.intersection(files[:top])) for name, files in lists.items()}
72
+ out["random (expected)"] = round(min(top, len(pool)) * len(fixed.intersection(pool)) / len(pool), 1) if pool else 0.0
73
+ return out
74
+
75
+
76
+ def table(results: list) -> str:
77
+ """results: [(t, files fixed that were in the pool, pool size, {variant: hits})], oldest first -> Markdown."""
78
+ names = list(results[0][3]) if results else []
79
+ head = "| variant | " + " | ".join(f"{t} ({fixed} of {pool} fixed)" for t, fixed, pool, _ in results) + " | total |"
80
+ rule = "|---|" + "---:|" * (len(results) + 1)
81
+ rows = []
82
+ for name in names:
83
+ cells = [hits[name] for _, _, _, hits in results]
84
+ total = round(sum(cells), 1) if any(isinstance(c, float) for c in cells) else sum(cells)
85
+ rows.append(f"| {name} | " + " | ".join(str(c) for c in cells) + f" | {total} |")
86
+ return "\n".join([head, rule, *rows])
87
+
88
+
89
+ def ref_spread(repo: str) -> dict:
90
+ """Commits and fix commits reachable from every ref (what `git log --all` exports: remote release
91
+ branches with their backports, unmerged work, the stash) against those reachable from HEAD."""
92
+ def subjects(*revs):
93
+ out = subprocess.run([*filetypes.GIT, "log", *revs, "--format=%s"], cwd=repo, check=True, capture_output=True).stdout
94
+ return out.decode("utf-8", "replace").split("\n")[:-1]
95
+ everything, head = subjects("--all"), subjects("HEAD")
96
+ return {"all": len(everything), "head": len(head), "fix_all": sum(map(maat.is_fix, everything)), "fix_head": sum(map(maat.is_fix, head))}
97
+
98
+
99
+ def main(argv=None) -> int:
100
+ p = argparse.ArgumentParser(description=__doc__.split("\n")[0])
101
+ p.add_argument("repo")
102
+ p.add_argument("out")
103
+ p.add_argument("--windows", type=int, default=6)
104
+ p.add_argument("--horizon", type=int, default=6, metavar="MONTHS")
105
+ p.add_argument("--top", type=int, default=watch.WATCH_TOP)
106
+ args = p.parse_args(argv)
107
+ meta = load._read_json(args.out, "meta.json", {})
108
+ log_path = os.path.join(args.out, "log.txt")
109
+ if not meta.get("last_date") or not os.path.exists(log_path):
110
+ print("evaluate: a finished output directory is needed (meta.json with last_date, and log.txt)", file=sys.stderr)
111
+ return 2
112
+ types = filetypes.parse(meta.get("file_types"))
113
+ aliases = maat.aliases_from_meta(os.path.join(args.out, "meta.json")) if "aliases" in meta else None
114
+ with open(log_path, encoding="utf-8", errors="replace", newline="") as fh:
115
+ commits = maat.parse_log(fh.read(), aliases, types)
116
+ results = []
117
+ for t in cutoffs(meta["last_date"], args.windows, args.horizon):
118
+ rev = trend.rev_before(args.repo, t, end_of_day=False)
119
+ if not rev:
120
+ continue # the history does not reach back this far
121
+ size = load.parse_scc(backtest.size_at(args.repo, rev, args.out), types)
122
+ report = report_at(commits, t, size, meta)
123
+ fixed = fixed_between(commits, t, months_after(t, args.horizon))
124
+ pool = set(variants(report)["churn"])
125
+ results.append((t, len(fixed & pool), len(pool), score(report, fixed, args.top)))
126
+ print(f"evaluate: {t} done", file=sys.stderr)
127
+ if not results:
128
+ print("evaluate: no cut-off falls inside the history", file=sys.stderr)
129
+ return 2
130
+ spread = ref_spread(args.repo)
131
+ print(f"### {meta.get('name', args.repo)}, top {args.top}, {args.horizon}-month horizon\n")
132
+ print(table(results))
133
+ print(f"\n`--all` exports {spread['all']:,} commits ({spread['fix_all']:,} fixes); HEAD reaches {spread['head']:,} ({spread['fix_head']:,} fixes).")
134
+ return 0
135
+
136
+
137
+ if __name__ == "__main__":
138
+ sys.exit(main())