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.
Files changed (62) hide show
  1. {gitmole-0.7.0 → gitmole-0.7.1}/PKG-INFO +60 -42
  2. {gitmole-0.7.0 → gitmole-0.7.1}/README.md +59 -41
  3. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/__init__.py +1 -1
  4. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/banner.py +7 -4
  5. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/cli.py +3 -3
  6. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/render.py +7 -0
  7. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole.egg-info/PKG-INFO +60 -42
  8. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole.egg-info/SOURCES.txt +1 -0
  9. {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_banner.py +22 -0
  10. {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_cli.py +14 -0
  11. {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_render.py +26 -0
  12. gitmole-0.7.1/tests/test_render_examples.py +106 -0
  13. {gitmole-0.7.0 → gitmole-0.7.1}/LICENSE +0 -0
  14. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/__main__.py +0 -0
  15. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/backtest.py +0 -0
  16. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/blame.py +0 -0
  17. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/clean.py +0 -0
  18. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/coupling.py +0 -0
  19. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/deps.py +0 -0
  20. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/duplicates.py +0 -0
  21. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/filetypes.py +0 -0
  22. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/findings.py +0 -0
  23. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/functions.py +0 -0
  24. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/hotspots.py +0 -0
  25. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/identity.py +0 -0
  26. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/knowledge.py +0 -0
  27. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/leaks.py +0 -0
  28. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/load.py +0 -0
  29. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/loss.py +0 -0
  30. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/maat.py +0 -0
  31. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/run.py +0 -0
  32. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/textfmt.py +0 -0
  33. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/trend.py +0 -0
  34. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole/watch.py +0 -0
  35. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole.egg-info/dependency_links.txt +0 -0
  36. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole.egg-info/entry_points.txt +0 -0
  37. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole.egg-info/requires.txt +0 -0
  38. {gitmole-0.7.0 → gitmole-0.7.1}/gitmole.egg-info/top_level.txt +0 -0
  39. {gitmole-0.7.0 → gitmole-0.7.1}/pyproject.toml +0 -0
  40. {gitmole-0.7.0 → gitmole-0.7.1}/setup.cfg +0 -0
  41. {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_backtest.py +0 -0
  42. {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_blame.py +0 -0
  43. {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_clean.py +0 -0
  44. {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_coupling.py +0 -0
  45. {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_deps.py +0 -0
  46. {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_duplicates.py +0 -0
  47. {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_filetypes.py +0 -0
  48. {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_findings.py +0 -0
  49. {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_functions.py +0 -0
  50. {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_golden.py +0 -0
  51. {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_hotspots.py +0 -0
  52. {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_identity.py +0 -0
  53. {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_knowledge.py +0 -0
  54. {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_leaks.py +0 -0
  55. {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_load.py +0 -0
  56. {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_loss.py +0 -0
  57. {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_maat.py +0 -0
  58. {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_packaging.py +0 -0
  59. {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_run.py +0 -0
  60. {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_textfmt.py +0 -0
  61. {gitmole-0.7.0 → gitmole-0.7.1}/tests/test_trend.py +0 -0
  62. {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.0
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
- Any stack. Free. Offline. No token. No AI. Light.
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
- - **Free.** MIT licence, no paid tier, no account. The tools it runs are open source too.
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 token.** A local clone needs no credentials. The optional `owner/repo` shortcut uses the `gh` login you already have, and you ask for it.
35
- - **No AI.** Every finding is a plain rule over counts you can recompute by hand. The same clone gives the same report every time.
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
- Running `gitmole .` inside this repository:
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
- ╭─ gitmole ────────────────────────────────────────────────────────────────────────────────────────╮
79
- │ 185 commits · 2026-09-15 → 2026-09-17 · 1 identity · branch main │
80
- │ 9,475 lines in 52 files · Python, Ruby │
81
- │ most commits on Wed at 20:00 · 4% of commits are fixes · 100% of surviving code from 2026 │
82
- │ 3 warnings, 1 note │
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
- Below that: a watch list of the five files where the next bug is most
108
- likely, with the reasons in words and a backtest of how the list would have
109
- done; then tables for people, the knowledge map, the timeline, hotspots with
110
- their complexity trend, change coupling, complex functions and repo health.
111
- The full report is in
112
- [docs/example.md](https://github.com/antvinni/gitmole/blob/main/docs/example.md),
113
- and every section is explained in
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
- - [Full example report](https://github.com/antvinni/gitmole/blob/main/docs/example.md): the whole `gitmole .` output for this repository.
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
- Any stack. Free. Offline. No token. No AI. Light.
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
- - **Free.** MIT licence, no paid tier, no account. The tools it runs are open source too.
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 token.** A local clone needs no credentials. The optional `owner/repo` shortcut uses the `gh` login you already have, and you ask for it.
15
- - **No AI.** Every finding is a plain rule over counts you can recompute by hand. The same clone gives the same report every time.
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
- Running `gitmole .` inside this repository:
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
- ╭─ gitmole ────────────────────────────────────────────────────────────────────────────────────────╮
59
- │ 185 commits · 2026-09-15 → 2026-09-17 · 1 identity · branch main │
60
- │ 9,475 lines in 52 files · Python, Ruby │
61
- │ most commits on Wed at 20:00 · 4% of commits are fixes · 100% of surviving code from 2026 │
62
- │ 3 warnings, 1 note │
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
- Below that: a watch list of the five files where the next bug is most
88
- likely, with the reasons in words and a backtest of how the list would have
89
- done; then tables for people, the knowledge map, the timeline, hotspots with
90
- their complexity trend, change coupling, complex functions and repo health.
91
- The full report is in
92
- [docs/example.md](https://github.com/antvinni/gitmole/blob/main/docs/example.md),
93
- and every section is explained in
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
- - [Full example report](https://github.com/antvinni/gitmole/blob/main/docs/example.md): the whole `gitmole .` output for this repository.
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.
@@ -1,3 +1,3 @@
1
1
  """gitmole: offline git repository analysis with a terminal report."""
2
2
 
3
- __version__ = "0.7.0"
3
+ __version__ = "0.7.1"
@@ -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.0
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
- Any stack. Free. Offline. No token. No AI. Light.
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
- - **Free.** MIT licence, no paid tier, no account. The tools it runs are open source too.
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 token.** A local clone needs no credentials. The optional `owner/repo` shortcut uses the `gh` login you already have, and you ask for it.
35
- - **No AI.** Every finding is a plain rule over counts you can recompute by hand. The same clone gives the same report every time.
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
- Running `gitmole .` inside this repository:
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
- ╭─ gitmole ────────────────────────────────────────────────────────────────────────────────────────╮
79
- │ 185 commits · 2026-09-15 → 2026-09-17 · 1 identity · branch main │
80
- │ 9,475 lines in 52 files · Python, Ruby │
81
- │ most commits on Wed at 20:00 · 4% of commits are fixes · 100% of surviving code from 2026 │
82
- │ 3 warnings, 1 note │
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
- Below that: a watch list of the five files where the next bug is most
108
- likely, with the reasons in words and a backtest of how the list would have
109
- done; then tables for people, the knowledge map, the timeline, hotspots with
110
- their complexity trend, change coupling, complex functions and repo health.
111
- The full report is in
112
- [docs/example.md](https://github.com/antvinni/gitmole/blob/main/docs/example.md),
113
- and every section is explained in
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
- - [Full example report](https://github.com/antvinni/gitmole/blob/main/docs/example.md): the whole `gitmole .` output for this repository.
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.
@@ -53,6 +53,7 @@ tests/test_loss.py
53
53
  tests/test_maat.py
54
54
  tests/test_packaging.py
55
55
  tests/test_render.py
56
+ tests/test_render_examples.py
56
57
  tests/test_run.py
57
58
  tests/test_textfmt.py
58
59
  tests/test_trend.py
@@ -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