gitmole 0.7.0__tar.gz → 0.7.1__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.0 → gitmole-0.7.1}/PKG-INFO +60 -42
- {gitmole-0.7.0 → gitmole-0.7.1}/README.md +59 -41
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/__init__.py +1 -1
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/banner.py +7 -4
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/cli.py +3 -3
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/render.py +7 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole.egg-info/PKG-INFO +60 -42
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole.egg-info/SOURCES.txt +1 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_banner.py +22 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_cli.py +14 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_render.py +26 -0
- gitmole-0.7.1/tests/test_render_examples.py +106 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/LICENSE +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/__main__.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/backtest.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/blame.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/clean.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/coupling.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/deps.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/duplicates.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/filetypes.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/findings.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/functions.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/hotspots.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/identity.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/knowledge.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/leaks.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/load.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/loss.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/maat.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/run.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/textfmt.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/trend.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/watch.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole.egg-info/dependency_links.txt +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole.egg-info/entry_points.txt +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole.egg-info/requires.txt +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/gitmole.egg-info/top_level.txt +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/pyproject.toml +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/setup.cfg +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_backtest.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_blame.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_clean.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_coupling.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_deps.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_duplicates.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_filetypes.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_findings.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_functions.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_golden.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_hotspots.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_identity.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_knowledge.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_leaks.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_load.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_loss.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_maat.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_packaging.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_run.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_textfmt.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_trend.py +0 -0
- {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_watch.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitmole
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.1
|
|
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
|
|
@@ -26,14 +26,15 @@ A toolkit for digging into any cloned git repository: who works on it,
|
|
|
26
26
|
where the risk is, how old the code is, whether the repo itself is healthy,
|
|
27
27
|
and whether anything sensitive was ever committed.
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
<img src="https://raw.githubusercontent.com/antvinni/gitmole/main/docs/report.svg" width="912" alt="gitmole on react: the summary, and the watch list of files where the next bug is likely, with a backtest">
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Free. Any Stack. Local. Offline. Deterministic. Fast.
|
|
32
|
+
|
|
33
|
+
- **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
34
|
- **Any stack.** It reads what every repository has: the git log, git blame and the files themselves.
|
|
33
|
-
- **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
|
-
- **No
|
|
35
|
-
- **
|
|
36
|
-
- **Light.** A 4,400-commit repository takes under thirty seconds. A few thousand lines of Python plus two libraries.
|
|
35
|
+
- **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.
|
|
36
|
+
- **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.
|
|
37
|
+
- **Fast.** A 4,400-commit repository takes under thirty seconds.
|
|
37
38
|
|
|
38
39
|
## Install
|
|
39
40
|
|
|
@@ -72,47 +73,64 @@ blocks on secrets in source files and still posts the report. Every option:
|
|
|
72
73
|
|
|
73
74
|
## What you get
|
|
74
75
|
|
|
75
|
-
|
|
76
|
+
The opening of the report for [react](https://github.com/facebook/react), 35,263 commits
|
|
77
|
+
since 2013, at a pinned commit; the picture above, as text:
|
|
76
78
|
|
|
77
79
|
```text
|
|
78
|
-
╭─
|
|
79
|
-
│
|
|
80
|
-
│
|
|
81
|
-
│ most commits on Wed at
|
|
82
|
-
│
|
|
83
|
-
|
|
84
|
-
╭─ Findings (4) ───────────────────────────────────────────────────────────────────────────────────╮
|
|
85
|
-
│ ▲ Bus factor of one │
|
|
86
|
-
│ vinni wrote 100% of the code that survives today │
|
|
87
|
-
│ ↳ Pair someone with vinni on gitmole/ first; it is 100% theirs. │
|
|
88
|
-
│ ▲ Hotspots getting more complex │
|
|
89
|
-
│ 4 of the 10 top source hotspots grew by 25% or more in a year: gitmole/render.py (+194%), │
|
|
90
|
-
│ gitmole/findings.py (+360%), gitmole/cli.py (+64%), gitmole/run.py (+34%) │
|
|
91
|
-
│ ↳ Split gitmole/render.py before the next change; its complexity grew 194% in a year. │
|
|
92
|
-
│ ▲ Knowledge islands │
|
|
93
|
-
│ 1 area(s) with at least 200 lines were written almost entirely by one person: gitmole/ (vinni │
|
|
94
|
-
│ 100%). That is 98% of all lines added │
|
|
95
|
-
│ ↳ Pair someone with vinni on gitmole/ first; it is the largest at 7,607 lines. │
|
|
96
|
-
│ ● Bug magnets │
|
|
97
|
-
│ 5 file(s) were fixed 3+ times in the last six months: gitmole/load.py (4 recent, 4 total); │
|
|
98
|
-
│ gitmole/cli.py (3 recent, 3 total); gitmole/findings.py (3 recent, 3 total); gitmole/render.py │
|
|
99
|
-
│ (3 recent, 3 total); gitmole/run.py (3 recent, 3 total) │
|
|
100
|
-
│ ↳ Review gitmole/load.py and gitmole/cli.py before the next release; expect the next bug │
|
|
101
|
-
│ there. │
|
|
102
|
-
│ ✔ No secrets in history │
|
|
103
|
-
│ betterleaks scanned every commit on every branch; 28 placeholder-shaped hits left out │
|
|
80
|
+
╭─ react ──────────────────────────────────────────────────────────────────────────────────────────╮
|
|
81
|
+
│ 35263 commits · 2013-05-28 → 2026-09-16 · 1880 identities · branch main │
|
|
82
|
+
│ 681,078 lines in 4781 files · JavaScript, TypeScript, Rust, CSS │
|
|
83
|
+
│ most commits on Wed at 16:00 · 13% of commits are fixes · 1% of commits are reverts · 19% │
|
|
84
|
+
│ of surviving code from 2026 │
|
|
85
|
+
│ 1 critical, 6 warnings, 8 notes │
|
|
104
86
|
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
87
|
+
|
|
88
|
+
◎ Watch list
|
|
89
|
+
file why
|
|
90
|
+
──────────────────────────────────────────────────────────────────────────────────────────────────
|
|
91
|
+
compiler/packages/babel-plugin-react-compiler/s changed 331 times · fixed twice in six months ·
|
|
92
|
+
rc/Inference/InferMutationAliasingEffects.ts Joe Savona wrote 98% of it ·
|
|
93
|
+
findNonMutatedDestructureSpreads() complexity 39
|
|
94
|
+
packages/react-server/src/ReactFlightServer.js changed 377 times · fixed once in six months ·
|
|
95
|
+
renderModelDestructive() complexity 544
|
|
96
|
+
packages/shared/forks/ReactFeatureFlags.www.js changed 583 times · fixed once in six months ·
|
|
97
|
+
changes with
|
|
98
|
+
packages/shared/forks/ReactFeatureFlags.test-ren
|
|
99
|
+
derer.www.js (77%) and 5 others
|
|
100
|
+
packages/shared/ReactFeatureFlags.js changed 575 times · fixed 6 times · changes with
|
|
101
|
+
packages/shared/forks/ReactFeatureFlags.test-ren
|
|
102
|
+
derer.js (80%) and 4 others
|
|
103
|
+
packages/react-reconciler/src/ReactFiberWorkLoo changed 342 times · fixed 4 times in six months
|
|
104
|
+
p.js · flushSpawnedWork() complexity 48
|
|
105
|
+
ranked by churn × recent fixes × complexity × single ownership
|
|
106
|
+
6 months ago this list would have named 6 of the 211 files fixed since (a random 15 of the 1979
|
|
107
|
+
files that had changed more than once would name 0.3)
|
|
105
108
|
```
|
|
106
109
|
|
|
107
|
-
|
|
108
|
-
likely,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
and
|
|
110
|
+
The watch list is the point: the five files where the next bug is most
|
|
111
|
+
likely, the reasons in words, and a backtest that says how the same list,
|
|
112
|
+
drawn six months earlier, would have done against the fixes that followed.
|
|
113
|
+
Between the header and that list the full report puts its findings, 15 for
|
|
114
|
+
react (1 critical, 6 warnings, 8 notes); below it, tables for people, the
|
|
115
|
+
knowledge map, the timeline, hotspots with their complexity trend, change
|
|
116
|
+
coupling, complex functions and repo health. Every section is explained in
|
|
114
117
|
[docs/output.md](https://github.com/antvinni/gitmole/blob/main/docs/output.md).
|
|
115
118
|
|
|
119
|
+
Reports on repositories you know, each at a pinned commit with a fixed
|
|
120
|
+
reference date, published as gitmole wrote them; the repo-health numbers
|
|
121
|
+
come from git-sizer over the whole clone, so a fresh clone can differ there:
|
|
122
|
+
|
|
123
|
+
| Repository | Commits | Lines | Watch list backtest |
|
|
124
|
+
|---|---:|---:|---|
|
|
125
|
+
| [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 |
|
|
126
|
+
| [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 |
|
|
127
|
+
| [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 |
|
|
128
|
+
| [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 |
|
|
129
|
+
|
|
130
|
+
kubernetes's code-age step was skipped on gitmole's default time budget and
|
|
131
|
+
its report says so; the duplicates step was skipped on the memory budget too
|
|
132
|
+
and produced no section.
|
|
133
|
+
|
|
116
134
|
## The tool set
|
|
117
135
|
|
|
118
136
|
One tool per question; together they cover what a single command can tell
|
|
@@ -138,7 +156,7 @@ Why these and not others: [docs/tools.md](https://github.com/antvinni/gitmole/bl
|
|
|
138
156
|
- [Install](https://github.com/antvinni/gitmole/blob/main/docs/install.md): macOS, Linux, pipx, the check, pinned releases.
|
|
139
157
|
- [Command line](https://github.com/antvinni/gitmole/blob/main/docs/cli.md): every option, portfolio mode, exports and CI gates, big repositories.
|
|
140
158
|
- [The report and the output files](https://github.com/antvinni/gitmole/blob/main/docs/output.md): what each section and each file means.
|
|
141
|
-
- [
|
|
159
|
+
- [Example reports](https://github.com/antvinni/gitmole/tree/main/docs/examples): curl, django, react and kubernetes at pinned commits, regenerated by `bin/render-examples`.
|
|
142
160
|
- [Why these tools](https://github.com/antvinni/gitmole/blob/main/docs/tools.md): the rationale, what was left out, licences.
|
|
143
161
|
- [Development](https://github.com/antvinni/gitmole/blob/main/docs/development.md): setup, tests, releases, code layout.
|
|
144
162
|
- [Contributing](https://github.com/antvinni/gitmole/blob/main/CONTRIBUTING.md): bugs, ideas, pull requests, security reports.
|
|
@@ -6,14 +6,15 @@ A toolkit for digging into any cloned git repository: who works on it,
|
|
|
6
6
|
where the risk is, how old the code is, whether the repo itself is healthy,
|
|
7
7
|
and whether anything sensitive was ever committed.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
<img src="https://raw.githubusercontent.com/antvinni/gitmole/main/docs/report.svg" width="912" alt="gitmole on react: the summary, and the watch list of files where the next bug is likely, with a backtest">
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Free. Any Stack. Local. Offline. Deterministic. Fast.
|
|
12
|
+
|
|
13
|
+
- **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
14
|
- **Any stack.** It reads what every repository has: the git log, git blame and the files themselves.
|
|
13
|
-
- **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
|
-
- **No
|
|
15
|
-
- **
|
|
16
|
-
- **Light.** A 4,400-commit repository takes under thirty seconds. A few thousand lines of Python plus two libraries.
|
|
15
|
+
- **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.
|
|
16
|
+
- **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.
|
|
17
|
+
- **Fast.** A 4,400-commit repository takes under thirty seconds.
|
|
17
18
|
|
|
18
19
|
## Install
|
|
19
20
|
|
|
@@ -52,47 +53,64 @@ blocks on secrets in source files and still posts the report. Every option:
|
|
|
52
53
|
|
|
53
54
|
## What you get
|
|
54
55
|
|
|
55
|
-
|
|
56
|
+
The opening of the report for [react](https://github.com/facebook/react), 35,263 commits
|
|
57
|
+
since 2013, at a pinned commit; the picture above, as text:
|
|
56
58
|
|
|
57
59
|
```text
|
|
58
|
-
╭─
|
|
59
|
-
│
|
|
60
|
-
│
|
|
61
|
-
│ most commits on Wed at
|
|
62
|
-
│
|
|
63
|
-
|
|
64
|
-
╭─ Findings (4) ───────────────────────────────────────────────────────────────────────────────────╮
|
|
65
|
-
│ ▲ Bus factor of one │
|
|
66
|
-
│ vinni wrote 100% of the code that survives today │
|
|
67
|
-
│ ↳ Pair someone with vinni on gitmole/ first; it is 100% theirs. │
|
|
68
|
-
│ ▲ Hotspots getting more complex │
|
|
69
|
-
│ 4 of the 10 top source hotspots grew by 25% or more in a year: gitmole/render.py (+194%), │
|
|
70
|
-
│ gitmole/findings.py (+360%), gitmole/cli.py (+64%), gitmole/run.py (+34%) │
|
|
71
|
-
│ ↳ Split gitmole/render.py before the next change; its complexity grew 194% in a year. │
|
|
72
|
-
│ ▲ Knowledge islands │
|
|
73
|
-
│ 1 area(s) with at least 200 lines were written almost entirely by one person: gitmole/ (vinni │
|
|
74
|
-
│ 100%). That is 98% of all lines added │
|
|
75
|
-
│ ↳ Pair someone with vinni on gitmole/ first; it is the largest at 7,607 lines. │
|
|
76
|
-
│ ● Bug magnets │
|
|
77
|
-
│ 5 file(s) were fixed 3+ times in the last six months: gitmole/load.py (4 recent, 4 total); │
|
|
78
|
-
│ gitmole/cli.py (3 recent, 3 total); gitmole/findings.py (3 recent, 3 total); gitmole/render.py │
|
|
79
|
-
│ (3 recent, 3 total); gitmole/run.py (3 recent, 3 total) │
|
|
80
|
-
│ ↳ Review gitmole/load.py and gitmole/cli.py before the next release; expect the next bug │
|
|
81
|
-
│ there. │
|
|
82
|
-
│ ✔ No secrets in history │
|
|
83
|
-
│ betterleaks scanned every commit on every branch; 28 placeholder-shaped hits left out │
|
|
60
|
+
╭─ react ──────────────────────────────────────────────────────────────────────────────────────────╮
|
|
61
|
+
│ 35263 commits · 2013-05-28 → 2026-09-16 · 1880 identities · branch main │
|
|
62
|
+
│ 681,078 lines in 4781 files · JavaScript, TypeScript, Rust, CSS │
|
|
63
|
+
│ most commits on Wed at 16:00 · 13% of commits are fixes · 1% of commits are reverts · 19% │
|
|
64
|
+
│ of surviving code from 2026 │
|
|
65
|
+
│ 1 critical, 6 warnings, 8 notes │
|
|
84
66
|
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
67
|
+
|
|
68
|
+
◎ Watch list
|
|
69
|
+
file why
|
|
70
|
+
──────────────────────────────────────────────────────────────────────────────────────────────────
|
|
71
|
+
compiler/packages/babel-plugin-react-compiler/s changed 331 times · fixed twice in six months ·
|
|
72
|
+
rc/Inference/InferMutationAliasingEffects.ts Joe Savona wrote 98% of it ·
|
|
73
|
+
findNonMutatedDestructureSpreads() complexity 39
|
|
74
|
+
packages/react-server/src/ReactFlightServer.js changed 377 times · fixed once in six months ·
|
|
75
|
+
renderModelDestructive() complexity 544
|
|
76
|
+
packages/shared/forks/ReactFeatureFlags.www.js changed 583 times · fixed once in six months ·
|
|
77
|
+
changes with
|
|
78
|
+
packages/shared/forks/ReactFeatureFlags.test-ren
|
|
79
|
+
derer.www.js (77%) and 5 others
|
|
80
|
+
packages/shared/ReactFeatureFlags.js changed 575 times · fixed 6 times · changes with
|
|
81
|
+
packages/shared/forks/ReactFeatureFlags.test-ren
|
|
82
|
+
derer.js (80%) and 4 others
|
|
83
|
+
packages/react-reconciler/src/ReactFiberWorkLoo changed 342 times · fixed 4 times in six months
|
|
84
|
+
p.js · flushSpawnedWork() complexity 48
|
|
85
|
+
ranked by churn × recent fixes × complexity × single ownership
|
|
86
|
+
6 months ago this list would have named 6 of the 211 files fixed since (a random 15 of the 1979
|
|
87
|
+
files that had changed more than once would name 0.3)
|
|
85
88
|
```
|
|
86
89
|
|
|
87
|
-
|
|
88
|
-
likely,
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
and
|
|
90
|
+
The watch list is the point: the five files where the next bug is most
|
|
91
|
+
likely, the reasons in words, and a backtest that says how the same list,
|
|
92
|
+
drawn six months earlier, would have done against the fixes that followed.
|
|
93
|
+
Between the header and that list the full report puts its findings, 15 for
|
|
94
|
+
react (1 critical, 6 warnings, 8 notes); below it, tables for people, the
|
|
95
|
+
knowledge map, the timeline, hotspots with their complexity trend, change
|
|
96
|
+
coupling, complex functions and repo health. Every section is explained in
|
|
94
97
|
[docs/output.md](https://github.com/antvinni/gitmole/blob/main/docs/output.md).
|
|
95
98
|
|
|
99
|
+
Reports on repositories you know, each at a pinned commit with a fixed
|
|
100
|
+
reference date, published as gitmole wrote them; the repo-health numbers
|
|
101
|
+
come from git-sizer over the whole clone, so a fresh clone can differ there:
|
|
102
|
+
|
|
103
|
+
| Repository | Commits | Lines | Watch list backtest |
|
|
104
|
+
|---|---:|---:|---|
|
|
105
|
+
| [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 |
|
|
106
|
+
| [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 |
|
|
107
|
+
| [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 |
|
|
108
|
+
| [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 |
|
|
109
|
+
|
|
110
|
+
kubernetes's code-age step was skipped on gitmole's default time budget and
|
|
111
|
+
its report says so; the duplicates step was skipped on the memory budget too
|
|
112
|
+
and produced no section.
|
|
113
|
+
|
|
96
114
|
## The tool set
|
|
97
115
|
|
|
98
116
|
One tool per question; together they cover what a single command can tell
|
|
@@ -118,7 +136,7 @@ Why these and not others: [docs/tools.md](https://github.com/antvinni/gitmole/bl
|
|
|
118
136
|
- [Install](https://github.com/antvinni/gitmole/blob/main/docs/install.md): macOS, Linux, pipx, the check, pinned releases.
|
|
119
137
|
- [Command line](https://github.com/antvinni/gitmole/blob/main/docs/cli.md): every option, portfolio mode, exports and CI gates, big repositories.
|
|
120
138
|
- [The report and the output files](https://github.com/antvinni/gitmole/blob/main/docs/output.md): what each section and each file means.
|
|
121
|
-
- [
|
|
139
|
+
- [Example reports](https://github.com/antvinni/gitmole/tree/main/docs/examples): curl, django, react and kubernetes at pinned commits, regenerated by `bin/render-examples`.
|
|
122
140
|
- [Why these tools](https://github.com/antvinni/gitmole/blob/main/docs/tools.md): the rationale, what was left out, licences.
|
|
123
141
|
- [Development](https://github.com/antvinni/gitmole/blob/main/docs/development.md): setup, tests, releases, code layout.
|
|
124
142
|
- [Contributing](https://github.com/antvinni/gitmole/blob/main/CONTRIBUTING.md): bugs, ideas, pull requests, security reports.
|
|
@@ -83,8 +83,9 @@ def _sprite_rows(look: int = 0) -> list:
|
|
|
83
83
|
return rows
|
|
84
84
|
|
|
85
85
|
|
|
86
|
-
def neon(offset: int = 0, look: int = 0) -> Text:
|
|
87
|
-
"""The banner with the palette rotated down by `offset` rows, and the mole beside it.
|
|
86
|
+
def neon(offset: int = 0, look: int = 0, version: str = None) -> Text:
|
|
87
|
+
"""The banner with the palette rotated down by `offset` rows, and the mole beside it.
|
|
88
|
+
With `version`, a dim `v1.2.3` row underneath: the report's reproducibility stamp."""
|
|
88
89
|
text = Text()
|
|
89
90
|
sprite = _sprite_rows(look)
|
|
90
91
|
for i, row in enumerate(ART.split("\n")):
|
|
@@ -93,15 +94,17 @@ def neon(offset: int = 0, look: int = 0) -> Text:
|
|
|
93
94
|
text.append(" " * GAP)
|
|
94
95
|
text.append_text(sprite[i])
|
|
95
96
|
text.append("\n")
|
|
97
|
+
if version:
|
|
98
|
+
text.append(f"v{version}\n", style="dim")
|
|
96
99
|
return text
|
|
97
100
|
|
|
98
101
|
|
|
99
102
|
LOOK_EVERY = 5 # frames per glance; at 10 fps the eyes move every half second
|
|
100
103
|
|
|
101
104
|
|
|
102
|
-
def frames():
|
|
105
|
+
def frames(version: str = None):
|
|
103
106
|
"""Endless generator of banner frames: gradient flowing down, eyes glancing side to side."""
|
|
104
107
|
n = 0
|
|
105
108
|
while True:
|
|
106
|
-
yield neon(offset=n % len(NEON), look=(n // LOOK_EVERY) % 2)
|
|
109
|
+
yield neon(offset=n % len(NEON), look=(n // LOOK_EVERY) % 2, version=version)
|
|
107
110
|
n += 1
|
|
@@ -171,7 +171,7 @@ def _no_run(args, console, ui, err) -> int:
|
|
|
171
171
|
err.print(f"[red]no gitmole output found in {out_dir}[/red] (expected meta.json)")
|
|
172
172
|
return 2
|
|
173
173
|
if ui.is_terminal:
|
|
174
|
-
ui.print(banner.neon())
|
|
174
|
+
ui.print(banner.neon(version=__version__))
|
|
175
175
|
return _render(out_dir, console, ui, args, err)
|
|
176
176
|
|
|
177
177
|
|
|
@@ -437,7 +437,7 @@ def _execute(steps, log_path, repo_dir, workers, console, timeout=None) -> dict:
|
|
|
437
437
|
active, lock = set(), threading.Lock()
|
|
438
438
|
started = time.monotonic()
|
|
439
439
|
spinner = Spinner("dots", style="cyan")
|
|
440
|
-
frame = banner.frames()
|
|
440
|
+
frame = banner.frames(version=__version__)
|
|
441
441
|
|
|
442
442
|
def label() -> str:
|
|
443
443
|
with lock:
|
|
@@ -466,7 +466,7 @@ def _execute(steps, log_path, repo_dir, workers, console, timeout=None) -> dict:
|
|
|
466
466
|
while worker.is_alive():
|
|
467
467
|
live.update(view())
|
|
468
468
|
worker.join(0.1)
|
|
469
|
-
live.update(Group(banner.neon(), Text("")) if console.is_terminal else Text(""))
|
|
469
|
+
live.update(Group(banner.neon(version=__version__), Text("")) if console.is_terminal else Text(""))
|
|
470
470
|
console.print(f"[dim]{len(steps)} steps in {time.monotonic() - started:.1f}s[/dim]\n")
|
|
471
471
|
return results
|
|
472
472
|
|
|
@@ -809,6 +809,13 @@ def report(report: dict, findings: list, console: Console, full: bool = False, r
|
|
|
809
809
|
console.print(Text(f"Full results and plots in {report['out_dir']}", style="dim"), soft_wrap=True)
|
|
810
810
|
|
|
811
811
|
|
|
812
|
+
def excerpt(report: dict, findings: list, console: Console, full: bool = False) -> None:
|
|
813
|
+
"""The report's opening on its own: the header, whose tally counts the findings, and the watch
|
|
814
|
+
list. What the README's picture shows; the findings themselves are in the full report."""
|
|
815
|
+
console.print(header(report, findings))
|
|
816
|
+
print_section(console, watch_section(report, full=full, width=console.width))
|
|
817
|
+
|
|
818
|
+
|
|
812
819
|
# --- markdown / json -------------------------------------------------------
|
|
813
820
|
|
|
814
821
|
def _md_cell(cell: str) -> str:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: gitmole
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.1
|
|
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
|
|
@@ -26,14 +26,15 @@ A toolkit for digging into any cloned git repository: who works on it,
|
|
|
26
26
|
where the risk is, how old the code is, whether the repo itself is healthy,
|
|
27
27
|
and whether anything sensitive was ever committed.
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
<img src="https://raw.githubusercontent.com/antvinni/gitmole/main/docs/report.svg" width="912" alt="gitmole on react: the summary, and the watch list of files where the next bug is likely, with a backtest">
|
|
30
30
|
|
|
31
|
-
|
|
31
|
+
Free. Any Stack. Local. Offline. Deterministic. Fast.
|
|
32
|
+
|
|
33
|
+
- **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
34
|
- **Any stack.** It reads what every repository has: the git log, git blame and the files themselves.
|
|
33
|
-
- **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
|
-
- **No
|
|
35
|
-
- **
|
|
36
|
-
- **Light.** A 4,400-commit repository takes under thirty seconds. A few thousand lines of Python plus two libraries.
|
|
35
|
+
- **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.
|
|
36
|
+
- **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.
|
|
37
|
+
- **Fast.** A 4,400-commit repository takes under thirty seconds.
|
|
37
38
|
|
|
38
39
|
## Install
|
|
39
40
|
|
|
@@ -72,47 +73,64 @@ blocks on secrets in source files and still posts the report. Every option:
|
|
|
72
73
|
|
|
73
74
|
## What you get
|
|
74
75
|
|
|
75
|
-
|
|
76
|
+
The opening of the report for [react](https://github.com/facebook/react), 35,263 commits
|
|
77
|
+
since 2013, at a pinned commit; the picture above, as text:
|
|
76
78
|
|
|
77
79
|
```text
|
|
78
|
-
╭─
|
|
79
|
-
│
|
|
80
|
-
│
|
|
81
|
-
│ most commits on Wed at
|
|
82
|
-
│
|
|
83
|
-
|
|
84
|
-
╭─ Findings (4) ───────────────────────────────────────────────────────────────────────────────────╮
|
|
85
|
-
│ ▲ Bus factor of one │
|
|
86
|
-
│ vinni wrote 100% of the code that survives today │
|
|
87
|
-
│ ↳ Pair someone with vinni on gitmole/ first; it is 100% theirs. │
|
|
88
|
-
│ ▲ Hotspots getting more complex │
|
|
89
|
-
│ 4 of the 10 top source hotspots grew by 25% or more in a year: gitmole/render.py (+194%), │
|
|
90
|
-
│ gitmole/findings.py (+360%), gitmole/cli.py (+64%), gitmole/run.py (+34%) │
|
|
91
|
-
│ ↳ Split gitmole/render.py before the next change; its complexity grew 194% in a year. │
|
|
92
|
-
│ ▲ Knowledge islands │
|
|
93
|
-
│ 1 area(s) with at least 200 lines were written almost entirely by one person: gitmole/ (vinni │
|
|
94
|
-
│ 100%). That is 98% of all lines added │
|
|
95
|
-
│ ↳ Pair someone with vinni on gitmole/ first; it is the largest at 7,607 lines. │
|
|
96
|
-
│ ● Bug magnets │
|
|
97
|
-
│ 5 file(s) were fixed 3+ times in the last six months: gitmole/load.py (4 recent, 4 total); │
|
|
98
|
-
│ gitmole/cli.py (3 recent, 3 total); gitmole/findings.py (3 recent, 3 total); gitmole/render.py │
|
|
99
|
-
│ (3 recent, 3 total); gitmole/run.py (3 recent, 3 total) │
|
|
100
|
-
│ ↳ Review gitmole/load.py and gitmole/cli.py before the next release; expect the next bug │
|
|
101
|
-
│ there. │
|
|
102
|
-
│ ✔ No secrets in history │
|
|
103
|
-
│ betterleaks scanned every commit on every branch; 28 placeholder-shaped hits left out │
|
|
80
|
+
╭─ react ──────────────────────────────────────────────────────────────────────────────────────────╮
|
|
81
|
+
│ 35263 commits · 2013-05-28 → 2026-09-16 · 1880 identities · branch main │
|
|
82
|
+
│ 681,078 lines in 4781 files · JavaScript, TypeScript, Rust, CSS │
|
|
83
|
+
│ most commits on Wed at 16:00 · 13% of commits are fixes · 1% of commits are reverts · 19% │
|
|
84
|
+
│ of surviving code from 2026 │
|
|
85
|
+
│ 1 critical, 6 warnings, 8 notes │
|
|
104
86
|
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
|
87
|
+
|
|
88
|
+
◎ Watch list
|
|
89
|
+
file why
|
|
90
|
+
──────────────────────────────────────────────────────────────────────────────────────────────────
|
|
91
|
+
compiler/packages/babel-plugin-react-compiler/s changed 331 times · fixed twice in six months ·
|
|
92
|
+
rc/Inference/InferMutationAliasingEffects.ts Joe Savona wrote 98% of it ·
|
|
93
|
+
findNonMutatedDestructureSpreads() complexity 39
|
|
94
|
+
packages/react-server/src/ReactFlightServer.js changed 377 times · fixed once in six months ·
|
|
95
|
+
renderModelDestructive() complexity 544
|
|
96
|
+
packages/shared/forks/ReactFeatureFlags.www.js changed 583 times · fixed once in six months ·
|
|
97
|
+
changes with
|
|
98
|
+
packages/shared/forks/ReactFeatureFlags.test-ren
|
|
99
|
+
derer.www.js (77%) and 5 others
|
|
100
|
+
packages/shared/ReactFeatureFlags.js changed 575 times · fixed 6 times · changes with
|
|
101
|
+
packages/shared/forks/ReactFeatureFlags.test-ren
|
|
102
|
+
derer.js (80%) and 4 others
|
|
103
|
+
packages/react-reconciler/src/ReactFiberWorkLoo changed 342 times · fixed 4 times in six months
|
|
104
|
+
p.js · flushSpawnedWork() complexity 48
|
|
105
|
+
ranked by churn × recent fixes × complexity × single ownership
|
|
106
|
+
6 months ago this list would have named 6 of the 211 files fixed since (a random 15 of the 1979
|
|
107
|
+
files that had changed more than once would name 0.3)
|
|
105
108
|
```
|
|
106
109
|
|
|
107
|
-
|
|
108
|
-
likely,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
and
|
|
110
|
+
The watch list is the point: the five files where the next bug is most
|
|
111
|
+
likely, the reasons in words, and a backtest that says how the same list,
|
|
112
|
+
drawn six months earlier, would have done against the fixes that followed.
|
|
113
|
+
Between the header and that list the full report puts its findings, 15 for
|
|
114
|
+
react (1 critical, 6 warnings, 8 notes); below it, tables for people, the
|
|
115
|
+
knowledge map, the timeline, hotspots with their complexity trend, change
|
|
116
|
+
coupling, complex functions and repo health. Every section is explained in
|
|
114
117
|
[docs/output.md](https://github.com/antvinni/gitmole/blob/main/docs/output.md).
|
|
115
118
|
|
|
119
|
+
Reports on repositories you know, each at a pinned commit with a fixed
|
|
120
|
+
reference date, published as gitmole wrote them; the repo-health numbers
|
|
121
|
+
come from git-sizer over the whole clone, so a fresh clone can differ there:
|
|
122
|
+
|
|
123
|
+
| Repository | Commits | Lines | Watch list backtest |
|
|
124
|
+
|---|---:|---:|---|
|
|
125
|
+
| [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 |
|
|
126
|
+
| [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 |
|
|
127
|
+
| [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 |
|
|
128
|
+
| [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 |
|
|
129
|
+
|
|
130
|
+
kubernetes's code-age step was skipped on gitmole's default time budget and
|
|
131
|
+
its report says so; the duplicates step was skipped on the memory budget too
|
|
132
|
+
and produced no section.
|
|
133
|
+
|
|
116
134
|
## The tool set
|
|
117
135
|
|
|
118
136
|
One tool per question; together they cover what a single command can tell
|
|
@@ -138,7 +156,7 @@ Why these and not others: [docs/tools.md](https://github.com/antvinni/gitmole/bl
|
|
|
138
156
|
- [Install](https://github.com/antvinni/gitmole/blob/main/docs/install.md): macOS, Linux, pipx, the check, pinned releases.
|
|
139
157
|
- [Command line](https://github.com/antvinni/gitmole/blob/main/docs/cli.md): every option, portfolio mode, exports and CI gates, big repositories.
|
|
140
158
|
- [The report and the output files](https://github.com/antvinni/gitmole/blob/main/docs/output.md): what each section and each file means.
|
|
141
|
-
- [
|
|
159
|
+
- [Example reports](https://github.com/antvinni/gitmole/tree/main/docs/examples): curl, django, react and kubernetes at pinned commits, regenerated by `bin/render-examples`.
|
|
142
160
|
- [Why these tools](https://github.com/antvinni/gitmole/blob/main/docs/tools.md): the rationale, what was left out, licences.
|
|
143
161
|
- [Development](https://github.com/antvinni/gitmole/blob/main/docs/development.md): setup, tests, releases, code layout.
|
|
144
162
|
- [Contributing](https://github.com/antvinni/gitmole/blob/main/CONTRIBUTING.md): bugs, ideas, pull requests, security reports.
|
|
@@ -122,5 +122,27 @@ class BannerInCli(unittest.TestCase):
|
|
|
122
122
|
self.assertNotIn("███╗", self._run(terminal=False))
|
|
123
123
|
|
|
124
124
|
|
|
125
|
+
class VersionLine(unittest.TestCase):
|
|
126
|
+
def test_version_row_sits_under_the_letters(self):
|
|
127
|
+
rows = banner.neon(version="1.2.3").plain.rstrip("\n").split("\n")
|
|
128
|
+
self.assertEqual(len(rows), 7)
|
|
129
|
+
self.assertEqual(rows[6], "v1.2.3")
|
|
130
|
+
|
|
131
|
+
def test_version_row_is_dim(self):
|
|
132
|
+
text = banner.neon(version="1.2.3")
|
|
133
|
+
start = text.plain.index("v1.2.3")
|
|
134
|
+
style = next(str(sp.style) for sp in text.spans if sp.start == start)
|
|
135
|
+
self.assertIn("dim", style)
|
|
136
|
+
|
|
137
|
+
def test_without_a_version_nothing_changes(self):
|
|
138
|
+
self.assertEqual(banner.neon().plain, banner.neon(version=None).plain)
|
|
139
|
+
self.assertNotIn("v", banner.neon().plain.split("\n")[-2])
|
|
140
|
+
|
|
141
|
+
def test_every_frame_carries_the_version(self):
|
|
142
|
+
gen = banner.frames(version="1.2.3")
|
|
143
|
+
for _ in range(3):
|
|
144
|
+
self.assertTrue(next(gen).plain.rstrip("\n").endswith("v1.2.3"))
|
|
145
|
+
|
|
146
|
+
|
|
125
147
|
if __name__ == "__main__":
|
|
126
148
|
unittest.main()
|
|
@@ -30,6 +30,18 @@ class NoRun(unittest.TestCase):
|
|
|
30
30
|
self.assertEqual(rc, 2)
|
|
31
31
|
self.assertIn("/nonexistent/analysis-x", c.export_text())
|
|
32
32
|
|
|
33
|
+
def test_terminal_no_run_prints_the_banner_with_the_version(self):
|
|
34
|
+
from gitmole import __version__
|
|
35
|
+
with tempfile.TemporaryDirectory() as out:
|
|
36
|
+
with open(os.path.join(out, "meta.json"), "w") as fh:
|
|
37
|
+
json.dump({"name": "demo", "commits": 5, "identities": []}, fh)
|
|
38
|
+
c = Console(file=io.StringIO(), width=100, record=True, force_terminal=True, color_system="truecolor")
|
|
39
|
+
rc = cli.main([out, "--no-run"], console=c)
|
|
40
|
+
text = c.export_text()
|
|
41
|
+
self.assertEqual(rc, 0)
|
|
42
|
+
self.assertIn("███╗ ███╗", text)
|
|
43
|
+
self.assertIn(f"v{__version__}", text)
|
|
44
|
+
|
|
33
45
|
|
|
34
46
|
class LiveRun(unittest.TestCase):
|
|
35
47
|
def test_full_run_on_a_terminal_console_prints_banner_and_report(self):
|
|
@@ -44,6 +56,8 @@ class LiveRun(unittest.TestCase):
|
|
|
44
56
|
text = c.export_text()
|
|
45
57
|
self.assertEqual(rc, 0)
|
|
46
58
|
self.assertIn("███╗ ███╗", text)
|
|
59
|
+
from gitmole import __version__
|
|
60
|
+
self.assertIn(f"v{__version__}", text)
|
|
47
61
|
self.assertIn("1 steps in", text)
|
|
48
62
|
self.assertIn(os.path.basename(d), text)
|
|
49
63
|
|
|
@@ -1155,5 +1155,31 @@ class ChangeRisk(unittest.TestCase):
|
|
|
1155
1155
|
self.assertNotIn("change_risk", render.to_json(sample_report(), []))
|
|
1156
1156
|
|
|
1157
1157
|
|
|
1158
|
+
class Excerpt(unittest.TestCase):
|
|
1159
|
+
def _text(self, findings=()):
|
|
1160
|
+
console = Console(file=io.StringIO(), width=100, record=True, force_terminal=False, color_system=None)
|
|
1161
|
+
render.excerpt(sample_report(), list(findings), console)
|
|
1162
|
+
return console.export_text()
|
|
1163
|
+
|
|
1164
|
+
def test_prints_header_and_watch_list(self):
|
|
1165
|
+
text = self._text()
|
|
1166
|
+
self.assertIn("demo", text) # header panel title
|
|
1167
|
+
self.assertIn("363 commits", text)
|
|
1168
|
+
self.assertIn("Watch list", text)
|
|
1169
|
+
self.assertIn("static/apps-metadata.json", text) # the top watch row: 128 revisions
|
|
1170
|
+
|
|
1171
|
+
def test_prints_nothing_else(self):
|
|
1172
|
+
text = self._text()
|
|
1173
|
+
for heading in ("Findings", "Hotspots", "People", "Knowledge map", "Timeline", "Change coupling", "Repo health", "Full results"):
|
|
1174
|
+
self.assertNotIn(heading, text)
|
|
1175
|
+
|
|
1176
|
+
def test_findings_are_tallied_in_the_header_not_listed(self):
|
|
1177
|
+
found = [{"severity": "warning", "title": "Bus factor of one", "detail": "Ann wrote 80% of the code",
|
|
1178
|
+
"advice": "Pair someone with Ann."}]
|
|
1179
|
+
text = self._text(found)
|
|
1180
|
+
self.assertIn("1 warning", text)
|
|
1181
|
+
self.assertNotIn("Bus factor of one", text)
|
|
1182
|
+
|
|
1183
|
+
|
|
1158
1184
|
if __name__ == "__main__":
|
|
1159
1185
|
unittest.main()
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""The pure parts of bin/render-examples: the committed document's shape and the pins' shape."""
|
|
2
|
+
import importlib.machinery
|
|
3
|
+
import importlib.util
|
|
4
|
+
import os
|
|
5
|
+
import unittest
|
|
6
|
+
|
|
7
|
+
PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "bin", "render-examples")
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load():
|
|
11
|
+
loader = importlib.machinery.SourceFileLoader("render_examples", PATH)
|
|
12
|
+
spec = importlib.util.spec_from_loader("render_examples", loader)
|
|
13
|
+
module = importlib.util.module_from_spec(spec)
|
|
14
|
+
loader.exec_module(module)
|
|
15
|
+
return module
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
MARKDOWN = """# curl
|
|
19
|
+
|
|
20
|
+
35000 commits · 1999-12-29 → 2026-09-17 · 900 identities · branch master
|
|
21
|
+
200,000 lines in 1,200 files · C
|
|
22
|
+
|
|
23
|
+
## Findings
|
|
24
|
+
|
|
25
|
+
- something
|
|
26
|
+
|
|
27
|
+
## Watch list
|
|
28
|
+
|
|
29
|
+
| file | why |
|
|
30
|
+
| --- | --- |
|
|
31
|
+
| lib/url.c | changed 800 times |
|
|
32
|
+
|
|
33
|
+
betterleaks scanned every commit on every branch
|
|
34
|
+
|
|
35
|
+
Full results and plots in /private/tmp/x/gitmole-examples/analysis-curl
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class Document(unittest.TestCase):
|
|
40
|
+
def setUp(self):
|
|
41
|
+
self.mod = load()
|
|
42
|
+
self.doc = self.mod.document("curl/curl", "a" * 40, MARKDOWN)
|
|
43
|
+
|
|
44
|
+
def test_keeps_the_title_first(self):
|
|
45
|
+
self.assertTrue(self.doc.startswith("# curl\n"))
|
|
46
|
+
|
|
47
|
+
def test_provenance_names_version_target_commit_and_date(self):
|
|
48
|
+
from gitmole import __version__
|
|
49
|
+
line = self.doc.split("\n")[2]
|
|
50
|
+
self.assertIn(f"gitmole {__version__}", line)
|
|
51
|
+
self.assertIn("[curl/curl](https://github.com/curl/curl)", line)
|
|
52
|
+
self.assertIn("https://github.com/curl/curl/commit/" + "a" * 40, line)
|
|
53
|
+
self.assertIn("`" + "a" * 12 + "`", line)
|
|
54
|
+
self.assertIn(self.mod.NOW, line)
|
|
55
|
+
self.assertIn("bin/render-examples", line)
|
|
56
|
+
self.assertIn("(https://github.com/antvinni/gitmole#readme)", line)
|
|
57
|
+
|
|
58
|
+
def test_drops_the_machine_specific_last_line(self):
|
|
59
|
+
self.assertNotIn("Full results and plots", self.doc)
|
|
60
|
+
self.assertNotIn("/private/tmp", self.doc)
|
|
61
|
+
self.assertTrue(self.doc.endswith("every commit on every branch\n"))
|
|
62
|
+
|
|
63
|
+
def test_body_is_otherwise_unchanged(self):
|
|
64
|
+
self.assertIn("| lib/url.c | changed 800 times |", self.doc)
|
|
65
|
+
self.assertIn("## Watch list", self.doc)
|
|
66
|
+
|
|
67
|
+
def test_strips_the_footer_render_markdown_actually_emits(self):
|
|
68
|
+
from tests.test_render import sample_report
|
|
69
|
+
from gitmole import render
|
|
70
|
+
markdown = render.markdown(sample_report(), [])
|
|
71
|
+
self.assertIn("Full results and plots in", markdown) # the fixture copies this sentence
|
|
72
|
+
doc = self.mod.document("demo/demo", "b" * 40, markdown)
|
|
73
|
+
self.assertNotIn("Full results and plots in", doc)
|
|
74
|
+
self.assertNotIn(sample_report()["out_dir"], doc)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class Pins(unittest.TestCase):
|
|
78
|
+
def test_every_example_is_a_github_target_with_a_full_sha(self):
|
|
79
|
+
mod = load()
|
|
80
|
+
self.assertGreaterEqual(len(mod.EXAMPLES), 3)
|
|
81
|
+
for target, sha in mod.EXAMPLES:
|
|
82
|
+
self.assertRegex(target, r"^[\w.-]+/[\w.-]+$")
|
|
83
|
+
self.assertRegex(sha, r"^[0-9a-f]{40}$", f"{target} is not pinned to a full commit hash")
|
|
84
|
+
|
|
85
|
+
def test_names_are_unique_and_featured_is_one_of_them(self):
|
|
86
|
+
mod = load()
|
|
87
|
+
from gitmole import run
|
|
88
|
+
names = [run.repo_name(t) for t, _ in mod.EXAMPLES]
|
|
89
|
+
self.assertEqual(len(names), len(set(names)))
|
|
90
|
+
self.assertIn(mod.FEATURED, names)
|
|
91
|
+
|
|
92
|
+
def test_constants(self):
|
|
93
|
+
mod = load()
|
|
94
|
+
self.assertRegex(mod.NOW, r"^\d{4}-\d{2}-\d{2}$")
|
|
95
|
+
self.assertEqual(mod.WIDTH, 100)
|
|
96
|
+
self.assertGreaterEqual(mod.TIMEOUT, 900)
|
|
97
|
+
|
|
98
|
+
def test_unknown_name_exits_2_and_names_the_choices(self):
|
|
99
|
+
mod = load()
|
|
100
|
+
import io, contextlib
|
|
101
|
+
err = io.StringIO()
|
|
102
|
+
with contextlib.redirect_stderr(err):
|
|
103
|
+
rc = mod.main(["nope"])
|
|
104
|
+
self.assertEqual(rc, 2)
|
|
105
|
+
self.assertIn("nope", err.getvalue())
|
|
106
|
+
self.assertIn("curl", err.getvalue())
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|