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.
- gitmole-0.7.1/PKG-INFO +184 -0
- gitmole-0.7.1/README.md +164 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/__init__.py +1 -1
- {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/banner.py +7 -4
- {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/cli.py +24 -15
- gitmole-0.7.1/gitmole/deps.py +186 -0
- gitmole-0.7.1/gitmole/duplicates.py +153 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/findings.py +62 -7
- {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/functions.py +11 -39
- {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/load.py +30 -2
- {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/render.py +57 -8
- {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/run.py +32 -17
- gitmole-0.7.1/gitmole.egg-info/PKG-INFO +184 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/gitmole.egg-info/SOURCES.txt +5 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_banner.py +22 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_cli.py +59 -15
- gitmole-0.7.1/tests/test_deps.py +160 -0
- gitmole-0.7.1/tests/test_duplicates.py +168 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_findings.py +68 -2
- {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_functions.py +9 -33
- {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_load.py +24 -2
- {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_render.py +58 -0
- gitmole-0.7.1/tests/test_render_examples.py +106 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_run.py +32 -20
- gitmole-0.6.15/PKG-INFO +0 -162
- gitmole-0.6.15/README.md +0 -142
- gitmole-0.6.15/gitmole.egg-info/PKG-INFO +0 -162
- {gitmole-0.6.15 → gitmole-0.7.1}/LICENSE +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/__main__.py +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/backtest.py +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/blame.py +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/clean.py +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/coupling.py +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/filetypes.py +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/hotspots.py +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/identity.py +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/knowledge.py +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/leaks.py +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/loss.py +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/maat.py +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/textfmt.py +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/trend.py +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/gitmole/watch.py +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/gitmole.egg-info/dependency_links.txt +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/gitmole.egg-info/entry_points.txt +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/gitmole.egg-info/requires.txt +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/gitmole.egg-info/top_level.txt +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/pyproject.toml +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/setup.cfg +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_backtest.py +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_blame.py +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_clean.py +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_coupling.py +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_filetypes.py +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_golden.py +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_hotspots.py +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_identity.py +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_knowledge.py +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_leaks.py +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_loss.py +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_maat.py +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_packaging.py +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_textfmt.py +0 -0
- {gitmole-0.6.15 → gitmole-0.7.1}/tests/test_trend.py +0 -0
- {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).
|
gitmole-0.7.1/README.md
ADDED
|
@@ -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).
|
|
@@ -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=
|
|
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
|
|
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
|
|
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=
|
|
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
|
|