gitmole 0.6.15__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 (65) hide show
  1. gitmole-0.7.1/PKG-INFO +184 -0
  2. gitmole-0.7.1/README.md +164 -0
  3. {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/__init__.py +1 -1
  4. {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/banner.py +7 -4
  5. {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/cli.py +24 -15
  6. gitmole-0.7.1/gitmole/deps.py +186 -0
  7. gitmole-0.7.1/gitmole/duplicates.py +153 -0
  8. {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/findings.py +62 -7
  9. {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/functions.py +11 -39
  10. {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/load.py +30 -2
  11. {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/render.py +57 -8
  12. {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/run.py +32 -17
  13. gitmole-0.7.1/gitmole.egg-info/PKG-INFO +184 -0
  14. {gitmole-0.6.15 → gitmole-0.7.1}/gitmole.egg-info/SOURCES.txt +5 -0
  15. {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_banner.py +22 -0
  16. {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_cli.py +59 -15
  17. gitmole-0.7.1/tests/test_deps.py +160 -0
  18. gitmole-0.7.1/tests/test_duplicates.py +168 -0
  19. {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_findings.py +68 -2
  20. {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_functions.py +9 -33
  21. {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_load.py +24 -2
  22. {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_render.py +58 -0
  23. gitmole-0.7.1/tests/test_render_examples.py +106 -0
  24. {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_run.py +32 -20
  25. gitmole-0.6.15/PKG-INFO +0 -162
  26. gitmole-0.6.15/README.md +0 -142
  27. gitmole-0.6.15/gitmole.egg-info/PKG-INFO +0 -162
  28. {gitmole-0.6.15 → gitmole-0.7.1}/LICENSE +0 -0
  29. {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/__main__.py +0 -0
  30. {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/backtest.py +0 -0
  31. {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/blame.py +0 -0
  32. {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/clean.py +0 -0
  33. {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/coupling.py +0 -0
  34. {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/filetypes.py +0 -0
  35. {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/hotspots.py +0 -0
  36. {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/identity.py +0 -0
  37. {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/knowledge.py +0 -0
  38. {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/leaks.py +0 -0
  39. {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/loss.py +0 -0
  40. {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/maat.py +0 -0
  41. {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/textfmt.py +0 -0
  42. {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/trend.py +0 -0
  43. {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/watch.py +0 -0
  44. {gitmole-0.6.15 → gitmole-0.7.1}/gitmole.egg-info/dependency_links.txt +0 -0
  45. {gitmole-0.6.15 → gitmole-0.7.1}/gitmole.egg-info/entry_points.txt +0 -0
  46. {gitmole-0.6.15 → gitmole-0.7.1}/gitmole.egg-info/requires.txt +0 -0
  47. {gitmole-0.6.15 → gitmole-0.7.1}/gitmole.egg-info/top_level.txt +0 -0
  48. {gitmole-0.6.15 → gitmole-0.7.1}/pyproject.toml +0 -0
  49. {gitmole-0.6.15 → gitmole-0.7.1}/setup.cfg +0 -0
  50. {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_backtest.py +0 -0
  51. {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_blame.py +0 -0
  52. {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_clean.py +0 -0
  53. {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_coupling.py +0 -0
  54. {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_filetypes.py +0 -0
  55. {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_golden.py +0 -0
  56. {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_hotspots.py +0 -0
  57. {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_identity.py +0 -0
  58. {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_knowledge.py +0 -0
  59. {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_leaks.py +0 -0
  60. {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_loss.py +0 -0
  61. {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_maat.py +0 -0
  62. {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_packaging.py +0 -0
  63. {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_textfmt.py +0 -0
  64. {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_trend.py +0 -0
  65. {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_watch.py +0 -0
gitmole-0.7.1/PKG-INFO ADDED
@@ -0,0 +1,184 @@
1
+ Metadata-Version: 2.4
2
+ Name: gitmole
3
+ Version: 0.7.1
4
+ Summary: Offline git repository analysis with a terminal report: hotspots, coupling, ownership, code age, secrets, repo health.
5
+ License: MIT
6
+ Project-URL: Homepage, https://github.com/antvinni/gitmole
7
+ Keywords: git,analysis,hotspots,code-age,repository,metrics
8
+ Classifier: Environment :: Console
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Topic :: Software Development :: Version Control :: Git
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: rich>=13
16
+ Requires-Dist: lizard>=1.24
17
+ Provides-Extra: plots
18
+ Requires-Dist: git-of-theseus; extra == "plots"
19
+ Dynamic: license-file
20
+
21
+ <img src="https://raw.githubusercontent.com/antvinni/gitmole/main/docs/banner.svg" width="912" alt="gitmole">
22
+
23
+ # gitmole
24
+
25
+ A toolkit for digging into any cloned git repository: who works on it,
26
+ where the risk is, how old the code is, whether the repo itself is healthy,
27
+ and whether anything sensitive was ever committed.
28
+
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
+
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.
34
+ - **Any stack.** It reads what every repository has: the git log, git blame and the files themselves.
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.
38
+
39
+ ## Install
40
+
41
+ ```bash
42
+ # macOS, or Linux with Homebrew: gitmole and the five tools it runs
43
+ brew tap antvinni/gitmole https://github.com/antvinni/gitmole
44
+ brew trust antvinni/gitmole
45
+ brew install gitmole
46
+
47
+ # anywhere else: scc, git-sizer, betterleaks, jscpd and osv-scanner on your PATH, then
48
+ pipx install gitmole
49
+ ```
50
+
51
+ Linux package names, the release binaries, `--plots` and the pip caveats:
52
+ [docs/install.md](https://github.com/antvinni/gitmole/blob/main/docs/install.md).
53
+
54
+ ## Usage
55
+
56
+ ```bash
57
+ gitmole . # the clone you are in
58
+ gitmole /path/to/clone # any local clone
59
+ gitmole owner/repo # clones into a temp dir first, with gh or plain git
60
+ gitmole 'owner/*' # every non-archived repo of a user or org, one summary table
61
+
62
+ gitmole . --markdown report.md # the same report as a Markdown document
63
+ gitmole . --json report.json # every table, the watch list and the findings
64
+ gitmole . --fail-on warning # exit 3 if any finding is a warning or worse
65
+ gitmole . --risk main --risk-threshold 5 # exit 3 if the files changed since main are too risky
66
+ gitmole . --since 2y --full # the current team, every row and column
67
+ gitmole --clean # list what gitmole left behind, delete on a yes
68
+ ```
69
+
70
+ A CI job that runs `gitmole . --fail-on critical --markdown - >> "$GITHUB_STEP_SUMMARY"`
71
+ blocks on secrets in source files and still posts the report. Every option:
72
+ [docs/cli.md](https://github.com/antvinni/gitmole/blob/main/docs/cli.md).
73
+
74
+ ## What you get
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:
78
+
79
+ ```text
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 │
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)
108
+ ```
109
+
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
117
+ [docs/output.md](https://github.com/antvinni/gitmole/blob/main/docs/output.md).
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
+
134
+ ## The tool set
135
+
136
+ One tool per question; together they cover what a single command can tell
137
+ you about a clone.
138
+
139
+ | Question | Tool | Install |
140
+ |---|---|---|
141
+ | What is this repo, at a glance; who commits, when, how much churn | gitmole itself, from the git log | built in |
142
+ | How big is the codebase, per language | [scc](https://github.com/boyter/scc) | brew |
143
+ | Is the repo itself healthy (huge blobs, deep trees) | [git-sizer](https://github.com/github/git-sizer) | brew |
144
+ | Where is the risk: hotspots, coupling, ownership | gitmole's own change analysis over `git log --numstat` | built in |
145
+ | How old is the surviving code, per year and author | gitmole's own blame pass (one `git blame` per file at HEAD) | built in |
146
+ | Code-age and survival plots over time | [git-of-theseus](https://github.com/erikbern/git-of-theseus) | pip, opt-in with `--plots` |
147
+ | Per-function complexity, length, parameters | [lizard](https://github.com/terryyin/lizard) | pip, installed with gitmole; tracked code files only |
148
+ | Which blocks of code appear more than once | [jscpd](https://github.com/kucherenko/jscpd) | brew |
149
+ | Have secrets ever been committed | [betterleaks](https://github.com/betterleaks/betterleaks) | brew |
150
+ | Do the dependencies have known vulnerabilities | [osv-scanner](https://github.com/google/osv-scanner), offline against a local copy of the OSV database | brew, plus a one-time database download |
151
+
152
+ Why these and not others: [docs/tools.md](https://github.com/antvinni/gitmole/blob/main/docs/tools.md).
153
+
154
+ ## Docs
155
+
156
+ - [Install](https://github.com/antvinni/gitmole/blob/main/docs/install.md): macOS, Linux, pipx, the check, pinned releases.
157
+ - [Command line](https://github.com/antvinni/gitmole/blob/main/docs/cli.md): every option, portfolio mode, exports and CI gates, big repositories.
158
+ - [The report and the output files](https://github.com/antvinni/gitmole/blob/main/docs/output.md): what each section and each file means.
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`.
160
+ - [Why these tools](https://github.com/antvinni/gitmole/blob/main/docs/tools.md): the rationale, what was left out, licences.
161
+ - [Development](https://github.com/antvinni/gitmole/blob/main/docs/development.md): setup, tests, releases, code layout.
162
+ - [Contributing](https://github.com/antvinni/gitmole/blob/main/CONTRIBUTING.md): bugs, ideas, pull requests, security reports.
163
+
164
+ ## Safety
165
+
166
+ - Everything is offline except the optional clone step, which uses your
167
+ existing gh auth. None of the tools send data anywhere; osv-scanner runs
168
+ against a local copy of its database that you download once, and gitmole
169
+ never downloads it for you.
170
+ - Remote targets are cloned into a fresh temp directory that is removed when
171
+ the run ends. Local clones are only read, but the log export and the
172
+ secrets scan touch all branches. `gitmole --clean` lists every directory
173
+ gitmole created and deletes them after a y/N question.
174
+ - Secret values never reach the output directory. betterleaks reports to
175
+ gitmole in memory, and gitmole stores a short keyed hash in place of the
176
+ value, the matched text and the commit message. The key is random, made
177
+ for that one report and never saved.
178
+
179
+ ## License
180
+
181
+ [MIT](https://github.com/antvinni/gitmole/blob/main/LICENSE). gitmole runs
182
+ the tools it wraps as separate processes and bundles none of them; their
183
+ licences are listed in
184
+ [docs/tools.md](https://github.com/antvinni/gitmole/blob/main/docs/tools.md#licences).
@@ -0,0 +1,164 @@
1
+ <img src="https://raw.githubusercontent.com/antvinni/gitmole/main/docs/banner.svg" width="912" alt="gitmole">
2
+
3
+ # gitmole
4
+
5
+ A toolkit for digging into any cloned git repository: who works on it,
6
+ where the risk is, how old the code is, whether the repo itself is healthy,
7
+ and whether anything sensitive was ever committed.
8
+
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
+
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.
14
+ - **Any stack.** It reads what every repository has: the git log, git blame and the files themselves.
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.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ # macOS, or Linux with Homebrew: gitmole and the five tools it runs
23
+ brew tap antvinni/gitmole https://github.com/antvinni/gitmole
24
+ brew trust antvinni/gitmole
25
+ brew install gitmole
26
+
27
+ # anywhere else: scc, git-sizer, betterleaks, jscpd and osv-scanner on your PATH, then
28
+ pipx install gitmole
29
+ ```
30
+
31
+ Linux package names, the release binaries, `--plots` and the pip caveats:
32
+ [docs/install.md](https://github.com/antvinni/gitmole/blob/main/docs/install.md).
33
+
34
+ ## Usage
35
+
36
+ ```bash
37
+ gitmole . # the clone you are in
38
+ gitmole /path/to/clone # any local clone
39
+ gitmole owner/repo # clones into a temp dir first, with gh or plain git
40
+ gitmole 'owner/*' # every non-archived repo of a user or org, one summary table
41
+
42
+ gitmole . --markdown report.md # the same report as a Markdown document
43
+ gitmole . --json report.json # every table, the watch list and the findings
44
+ gitmole . --fail-on warning # exit 3 if any finding is a warning or worse
45
+ gitmole . --risk main --risk-threshold 5 # exit 3 if the files changed since main are too risky
46
+ gitmole . --since 2y --full # the current team, every row and column
47
+ gitmole --clean # list what gitmole left behind, delete on a yes
48
+ ```
49
+
50
+ A CI job that runs `gitmole . --fail-on critical --markdown - >> "$GITHUB_STEP_SUMMARY"`
51
+ blocks on secrets in source files and still posts the report. Every option:
52
+ [docs/cli.md](https://github.com/antvinni/gitmole/blob/main/docs/cli.md).
53
+
54
+ ## What you get
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:
58
+
59
+ ```text
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 │
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)
88
+ ```
89
+
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
97
+ [docs/output.md](https://github.com/antvinni/gitmole/blob/main/docs/output.md).
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
+
114
+ ## The tool set
115
+
116
+ One tool per question; together they cover what a single command can tell
117
+ you about a clone.
118
+
119
+ | Question | Tool | Install |
120
+ |---|---|---|
121
+ | What is this repo, at a glance; who commits, when, how much churn | gitmole itself, from the git log | built in |
122
+ | How big is the codebase, per language | [scc](https://github.com/boyter/scc) | brew |
123
+ | Is the repo itself healthy (huge blobs, deep trees) | [git-sizer](https://github.com/github/git-sizer) | brew |
124
+ | Where is the risk: hotspots, coupling, ownership | gitmole's own change analysis over `git log --numstat` | built in |
125
+ | How old is the surviving code, per year and author | gitmole's own blame pass (one `git blame` per file at HEAD) | built in |
126
+ | Code-age and survival plots over time | [git-of-theseus](https://github.com/erikbern/git-of-theseus) | pip, opt-in with `--plots` |
127
+ | Per-function complexity, length, parameters | [lizard](https://github.com/terryyin/lizard) | pip, installed with gitmole; tracked code files only |
128
+ | Which blocks of code appear more than once | [jscpd](https://github.com/kucherenko/jscpd) | brew |
129
+ | Have secrets ever been committed | [betterleaks](https://github.com/betterleaks/betterleaks) | brew |
130
+ | Do the dependencies have known vulnerabilities | [osv-scanner](https://github.com/google/osv-scanner), offline against a local copy of the OSV database | brew, plus a one-time database download |
131
+
132
+ Why these and not others: [docs/tools.md](https://github.com/antvinni/gitmole/blob/main/docs/tools.md).
133
+
134
+ ## Docs
135
+
136
+ - [Install](https://github.com/antvinni/gitmole/blob/main/docs/install.md): macOS, Linux, pipx, the check, pinned releases.
137
+ - [Command line](https://github.com/antvinni/gitmole/blob/main/docs/cli.md): every option, portfolio mode, exports and CI gates, big repositories.
138
+ - [The report and the output files](https://github.com/antvinni/gitmole/blob/main/docs/output.md): what each section and each file means.
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`.
140
+ - [Why these tools](https://github.com/antvinni/gitmole/blob/main/docs/tools.md): the rationale, what was left out, licences.
141
+ - [Development](https://github.com/antvinni/gitmole/blob/main/docs/development.md): setup, tests, releases, code layout.
142
+ - [Contributing](https://github.com/antvinni/gitmole/blob/main/CONTRIBUTING.md): bugs, ideas, pull requests, security reports.
143
+
144
+ ## Safety
145
+
146
+ - Everything is offline except the optional clone step, which uses your
147
+ existing gh auth. None of the tools send data anywhere; osv-scanner runs
148
+ against a local copy of its database that you download once, and gitmole
149
+ never downloads it for you.
150
+ - Remote targets are cloned into a fresh temp directory that is removed when
151
+ the run ends. Local clones are only read, but the log export and the
152
+ secrets scan touch all branches. `gitmole --clean` lists every directory
153
+ gitmole created and deletes them after a y/N question.
154
+ - Secret values never reach the output directory. betterleaks reports to
155
+ gitmole in memory, and gitmole stores a short keyed hash in place of the
156
+ value, the matched text and the commit message. The key is random, made
157
+ for that one report and never saved.
158
+
159
+ ## License
160
+
161
+ [MIT](https://github.com/antvinni/gitmole/blob/main/LICENSE). gitmole runs
162
+ the tools it wraps as separate processes and bundles none of them; their
163
+ licences are listed in
164
+ [docs/tools.md](https://github.com/antvinni/gitmole/blob/main/docs/tools.md#licences).
@@ -1,3 +1,3 @@
1
1
  """gitmole: offline git repository analysis with a terminal report."""
2
2
 
3
- __version__ = "0.6.15"
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
@@ -37,7 +37,7 @@ def parse_args(argv):
37
37
  p.add_argument("--list-file-types", action="store_true", help="list the file types in the repository, with counts and whether they count as code, then exit")
38
38
  p.add_argument("--clean", action="store_true", help="list the directories gitmole created (temp clones, analysis-* under the target) and delete them after a y/N question, then exit")
39
39
  p.add_argument("--yes", action="store_true", help="with --clean: delete without asking")
40
- p.add_argument("--duplicates", action="store_true", help="also look for duplicated code blocks (minutes and gigabytes on a large repo; function metrics alone take seconds)")
40
+ p.add_argument("--duplicates", action="store_true", help=argparse.SUPPRESS) # duplicates always run now; kept so older scripts still parse
41
41
  p.add_argument("--full", action="store_true", help="every column and every row in the terminal report, and the test files the default tables hide (the default is the tighter, readable one)")
42
42
  p.add_argument("--json", metavar="PATH", help="write the report and findings as JSON to PATH, or - for stdout")
43
43
  p.add_argument("--markdown", metavar="PATH", help="write the report as Markdown to PATH, or - for stdout")
@@ -100,7 +100,7 @@ def main(argv=None, console: Console = None, tool_check=run.missing_tools, plann
100
100
  missing = tool_check(plots=args.plots)
101
101
  if missing:
102
102
  err.print("[red]missing tools:[/red] " + ", ".join(missing))
103
- err.print("brew install scc git-sizer betterleaks; see README.md for other ways")
103
+ err.print(f"brew install {' '.join(run.REQUIRED_TOOLS)}; see README.md for other ways")
104
104
  return 2
105
105
  args.lizard = lizard_check() # decided once, for every repository this run analyses
106
106
 
@@ -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
 
@@ -269,12 +269,17 @@ def _list_file_types(repo_dir: str, args, console: Console) -> int:
269
269
  return 0
270
270
 
271
271
 
272
- def _budgets(args, estimate, ui) -> tuple[bool, bool, float]:
273
- """Decide whether code age and plots fit their time/size budgets, printing a skip notice for each
274
- one cut. The projected blame time comes back with them: meta.json records it."""
272
+ def _budgets(args, estimate, ui) -> tuple[bool, bool, float, bool]:
273
+ """Decide whether code age, plots and the duplicates step fit their time/size budgets, printing a
274
+ skip notice for each one cut. The projected blame time comes back with them: meta.json records it."""
275
275
  projected = float(estimate.get("seconds", 0.0))
276
276
  age_ok = args.deep or projected <= args.time_budget
277
277
  plots_ok = args.plots and (args.deep or estimate["blames"] <= args.budget)
278
+ text_mb = estimate.get("text_bytes", 0) / 1e6
279
+ duplicates_ok = args.deep or text_mb <= run.DUPLICATES_BUDGET_MB
280
+ if not duplicates_ok:
281
+ ui.print(f"[yellow]duplicates skipped:[/yellow] {text_mb:,.0f} MB of tracked text is over the {run.DUPLICATES_BUDGET_MB} MB budget; "
282
+ f"jscpd would need about {text_mb / 25:,.0f} GB of memory. Rerun with --deep to force it, or --ignore-data to shrink it.")
278
283
  if not age_ok:
279
284
  ui.print(f"[yellow]code age skipped:[/yellow] a blame pass over {estimate.get('code_files', estimate['files']):,} files is projected "
280
285
  f"to take about {projected:,.0f}s, over the {args.time_budget:,.0f}s time budget. "
@@ -283,10 +288,10 @@ def _budgets(args, estimate, ui) -> tuple[bool, bool, float]:
283
288
  ui.print(f"[yellow]plots skipped:[/yellow] about {estimate['blames']:,} git blames "
284
289
  f"({estimate['files']:,} files × {estimate['samples']} samples) exceeds the budget of {args.budget:,}. "
285
290
  f"Rerun with --deep to force them, or --ignore-data to shrink them.")
286
- return age_ok, plots_ok, projected
291
+ return age_ok, plots_ok, projected, duplicates_ok
287
292
 
288
293
 
289
- def _meta_for_run(repo_dir: str, args, estimate, age_ok: bool, plots_ok: bool, projected: float) -> tuple[dict, str | None]:
294
+ def _meta_for_run(repo_dir: str, args, estimate, age_ok: bool, plots_ok: bool, projected: float, duplicates_ok: bool = True) -> tuple[dict, str | None]:
290
295
  """Collect this run's meta.json: repo facts plus a planned status record for every optional step.
291
296
  Raises NoCommits when --since leaves no commits to analyse."""
292
297
  types_spec = _types_spec(args.file_types)
@@ -308,6 +313,8 @@ def _meta_for_run(repo_dir: str, args, estimate, age_ok: bool, plots_ok: bool, p
308
313
  meta["plots"] = {"status": "run" if plots_ok else "skipped", "blames": estimate["blames"], "samples": estimate["samples"], "budget": args.budget}
309
314
  lizard_ok = args.lizard
310
315
  meta["functions"] = {"status": "planned" if lizard_ok else "skipped"} # "run" only once the step has finished
316
+ meta["duplicates"] = {"status": "planned" if duplicates_ok else "skipped", "text_mb": round(estimate.get("text_bytes", 0) / 1e6, 1),
317
+ "budget_mb": run.DUPLICATES_BUDGET_MB}
311
318
  meta["trend"] = {"status": "planned"}
312
319
  from . import maat as _maat
313
320
  cut = _maat.months_before(meta["last_date"], 6) if meta["last_date"] else None
@@ -320,7 +327,7 @@ def _meta_for_run(repo_dir: str, args, estimate, age_ok: bool, plots_ok: bool, p
320
327
  return meta, cut
321
328
 
322
329
 
323
- def _record_statuses(meta, results, age_ok: bool, plots_ok: bool, lizard_ok: bool, cut) -> None:
330
+ def _record_statuses(meta, results, age_ok: bool, plots_ok: bool, lizard_ok: bool, cut, duplicates_ok: bool = True) -> None:
324
331
  """Turn each planned step's exit code into its final status: run, timeout or failed."""
325
332
  def status(step, default="run"):
326
333
  rc = results.get(step, 0)
@@ -331,6 +338,8 @@ def _record_statuses(meta, results, age_ok: bool, plots_ok: bool, lizard_ok: boo
331
338
  meta["plots"]["status"] = status("git-of-theseus")
332
339
  if lizard_ok:
333
340
  meta["functions"]["status"] = status("functions")
341
+ if duplicates_ok and "duplicates" in results:
342
+ meta["duplicates"]["status"] = status("duplicates")
334
343
  if "trend" in results:
335
344
  meta["trend"]["status"] = status("trend")
336
345
  if cut and "backtest" in results:
@@ -345,14 +354,14 @@ def _analyse(repo_dir: str, out_dir: str, args, ui: Console, planner, estimator)
345
354
 
346
355
  ignore = list(run.DATA_IGNORES if args.ignore_data else []) + list(args.ignore)
347
356
  estimate = estimator(repo_dir, run.MONTH, ignore=ignore, types=filetypes.parse(args.file_types))
348
- age_ok, plots_ok, projected = _budgets(args, estimate, ui)
357
+ age_ok, plots_ok, projected, duplicates_ok = _budgets(args, estimate, ui)
349
358
 
350
- meta, cut = _meta_for_run(repo_dir, args, estimate, age_ok, plots_ok, projected)
359
+ meta, cut = _meta_for_run(repo_dir, args, estimate, age_ok, plots_ok, projected, duplicates_ok)
351
360
  types_spec = meta["file_types"]
352
361
  lizard_ok = args.lizard
353
362
  run.clear_outputs(out_dir)
354
363
  steps = planner(repo_dir, out_dir, branch=meta["branch"], age=age_ok, plots=plots_ok, ignore=ignore, types=types_spec, now=args.now,
355
- since=args.since_date, lizard=lizard_ok, duplicates=args.duplicates, backtest=cut)
364
+ since=args.since_date, lizard=lizard_ok, duplicates=duplicates_ok, backtest=cut)
356
365
  run.save_meta(meta, out_dir)
357
366
  results = _execute(steps, log_path, repo_dir, args.workers, ui, timeout=args.timeout)
358
367
  if _control.cancelled.is_set():
@@ -360,7 +369,7 @@ def _analyse(repo_dir: str, out_dir: str, args, ui: Console, planner, estimator)
360
369
  ui.print(f"[red]interrupted:[/red] killed {len(killed)} step(s)")
361
370
  raise Interrupted()
362
371
 
363
- _record_statuses(meta, results, age_ok, plots_ok, lizard_ok, cut)
372
+ _record_statuses(meta, results, age_ok, plots_ok, lizard_ok, cut, duplicates_ok)
364
373
  run.save_meta(meta, out_dir)
365
374
 
366
375
  failed = [n for n, rc in results.items() if rc != 0]
@@ -428,7 +437,7 @@ def _execute(steps, log_path, repo_dir, workers, console, timeout=None) -> dict:
428
437
  active, lock = set(), threading.Lock()
429
438
  started = time.monotonic()
430
439
  spinner = Spinner("dots", style="cyan")
431
- frame = banner.frames()
440
+ frame = banner.frames(version=__version__)
432
441
 
433
442
  def label() -> str:
434
443
  with lock:
@@ -457,7 +466,7 @@ def _execute(steps, log_path, repo_dir, workers, console, timeout=None) -> dict:
457
466
  while worker.is_alive():
458
467
  live.update(view())
459
468
  worker.join(0.1)
460
- 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(""))
461
470
  console.print(f"[dim]{len(steps)} steps in {time.monotonic() - started:.1f}s[/dim]\n")
462
471
  return results
463
472