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.
- {gitmole-0.7.2 → gitmole-0.8.0}/PKG-INFO +40 -42
- {gitmole-0.7.2 → gitmole-0.8.0}/README.md +39 -41
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/__init__.py +1 -1
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/cli.py +15 -5
- gitmole-0.8.0/gitmole/evaluate.py +138 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/findings.py +71 -22
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/load.py +39 -13
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/maat.py +4 -4
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/render.py +19 -4
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/run.py +2 -2
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/watch.py +87 -28
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole.egg-info/PKG-INFO +40 -42
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole.egg-info/SOURCES.txt +2 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_backtest.py +1 -1
- {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_cli.py +43 -0
- gitmole-0.8.0/tests/test_evaluate.py +70 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_findings.py +23 -2
- {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_load.py +42 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_render.py +28 -10
- {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_run.py +18 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_watch.py +93 -13
- {gitmole-0.7.2 → gitmole-0.8.0}/LICENSE +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/__main__.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/backtest.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/banner.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/blame.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/clean.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/coupling.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/deps.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/duplicates.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/filetypes.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/functions.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/hotspots.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/identity.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/knowledge.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/leaks.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/loss.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/textfmt.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole/trend.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole.egg-info/dependency_links.txt +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole.egg-info/entry_points.txt +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole.egg-info/requires.txt +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/gitmole.egg-info/top_level.txt +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/pyproject.toml +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/setup.cfg +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_banner.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_blame.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_clean.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_coupling.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_deps.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_duplicates.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_filetypes.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_functions.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_golden.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_hotspots.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_identity.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_knowledge.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_leaks.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_loss.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_maat.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_packaging.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_render_examples.py +0 -0
- {gitmole-0.7.2 → gitmole-0.8.0}/tests/test_textfmt.py +0 -0
- {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.
|
|
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
|
|
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),
|
|
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
|
-
│
|
|
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
|
|
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,
|
|
83
|
+
│ 1 critical, 6 warnings, 9 notes │
|
|
84
84
|
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
85
85
|
|
|
86
86
|
◎ Watch list
|
|
87
87
|
file why
|
|
88
88
|
──────────────────────────────────────────────────────────────────────────────────────────────────
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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,
|
|
124
|
-
| [django](https://github.com/antvinni/gitmole/blob/main/docs/examples/django.md) |
|
|
125
|
-
| [react](https://github.com/antvinni/gitmole/blob/main/docs/examples/react.md) |
|
|
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
|
|
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
|
|
170
|
-
|
|
171
|
-
gitmole created and deletes them
|
|
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
|
|
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),
|
|
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
|
-
│
|
|
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
|
|
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,
|
|
63
|
+
│ 1 critical, 6 warnings, 9 notes │
|
|
64
64
|
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
65
65
|
|
|
66
66
|
◎ Watch list
|
|
67
67
|
file why
|
|
68
68
|
──────────────────────────────────────────────────────────────────────────────────────────────────
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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,
|
|
104
|
-
| [django](https://github.com/antvinni/gitmole/blob/main/docs/examples/django.md) |
|
|
105
|
-
| [react](https://github.com/antvinni/gitmole/blob/main/docs/examples/react.md) |
|
|
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
|
|
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
|
|
150
|
-
|
|
151
|
-
gitmole created and deletes them
|
|
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
|
|
@@ -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)
|
|
46
|
-
p.add_argument("--risk-threshold", type=float, metavar="N", help="with --risk: exit 3 when the
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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())
|