gitmole 0.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. gitmole-0.3.0/LICENSE +21 -0
  2. gitmole-0.3.0/PKG-INFO +469 -0
  3. gitmole-0.3.0/README.md +449 -0
  4. gitmole-0.3.0/gitmole/__init__.py +3 -0
  5. gitmole-0.3.0/gitmole/__main__.py +6 -0
  6. gitmole-0.3.0/gitmole/backtest.py +82 -0
  7. gitmole-0.3.0/gitmole/banner.py +107 -0
  8. gitmole-0.3.0/gitmole/blame.py +135 -0
  9. gitmole-0.3.0/gitmole/cli.py +439 -0
  10. gitmole-0.3.0/gitmole/filetypes.py +78 -0
  11. gitmole-0.3.0/gitmole/findings.py +405 -0
  12. gitmole-0.3.0/gitmole/functions.py +108 -0
  13. gitmole-0.3.0/gitmole/hotspots.py +18 -0
  14. gitmole-0.3.0/gitmole/identity.py +66 -0
  15. gitmole-0.3.0/gitmole/knowledge.py +55 -0
  16. gitmole-0.3.0/gitmole/leaks.py +124 -0
  17. gitmole-0.3.0/gitmole/load.py +253 -0
  18. gitmole-0.3.0/gitmole/loss.py +44 -0
  19. gitmole-0.3.0/gitmole/maat.py +299 -0
  20. gitmole-0.3.0/gitmole/render.py +736 -0
  21. gitmole-0.3.0/gitmole/run.py +407 -0
  22. gitmole-0.3.0/gitmole/textfmt.py +91 -0
  23. gitmole-0.3.0/gitmole/trend.py +154 -0
  24. gitmole-0.3.0/gitmole/watch.py +172 -0
  25. gitmole-0.3.0/gitmole.egg-info/PKG-INFO +469 -0
  26. gitmole-0.3.0/gitmole.egg-info/SOURCES.txt +51 -0
  27. gitmole-0.3.0/gitmole.egg-info/dependency_links.txt +1 -0
  28. gitmole-0.3.0/gitmole.egg-info/entry_points.txt +2 -0
  29. gitmole-0.3.0/gitmole.egg-info/requires.txt +5 -0
  30. gitmole-0.3.0/gitmole.egg-info/top_level.txt +1 -0
  31. gitmole-0.3.0/pyproject.toml +34 -0
  32. gitmole-0.3.0/setup.cfg +4 -0
  33. gitmole-0.3.0/tests/test_backtest.py +122 -0
  34. gitmole-0.3.0/tests/test_banner.py +126 -0
  35. gitmole-0.3.0/tests/test_blame.py +108 -0
  36. gitmole-0.3.0/tests/test_cli.py +658 -0
  37. gitmole-0.3.0/tests/test_filetypes.py +96 -0
  38. gitmole-0.3.0/tests/test_findings.py +580 -0
  39. gitmole-0.3.0/tests/test_functions.py +122 -0
  40. gitmole-0.3.0/tests/test_golden.py +100 -0
  41. gitmole-0.3.0/tests/test_hotspots.py +22 -0
  42. gitmole-0.3.0/tests/test_identity.py +75 -0
  43. gitmole-0.3.0/tests/test_knowledge.py +64 -0
  44. gitmole-0.3.0/tests/test_leaks.py +153 -0
  45. gitmole-0.3.0/tests/test_load.py +338 -0
  46. gitmole-0.3.0/tests/test_loss.py +70 -0
  47. gitmole-0.3.0/tests/test_maat.py +352 -0
  48. gitmole-0.3.0/tests/test_packaging.py +27 -0
  49. gitmole-0.3.0/tests/test_render.py +913 -0
  50. gitmole-0.3.0/tests/test_run.py +585 -0
  51. gitmole-0.3.0/tests/test_textfmt.py +79 -0
  52. gitmole-0.3.0/tests/test_trend.py +127 -0
  53. gitmole-0.3.0/tests/test_watch.py +166 -0
gitmole-0.3.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 antvinni
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
gitmole-0.3.0/PKG-INFO ADDED
@@ -0,0 +1,469 @@
1
+ Metadata-Version: 2.4
2
+ Name: gitmole
3
+ Version: 0.3.0
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
+ > Any stack. Free. Offline. No token. No AI. Light.
30
+
31
+ ## Principles
32
+
33
+ - **Free.** MIT licence, no paid tier, no account, nothing to sign up for.
34
+ The tools it runs are open source too.
35
+ - **Any stack.** It reads what every repository has: the git log, git blame
36
+ and the files themselves. Python, Vue, Terraform or a Makefile get the same
37
+ treatment; there is no language it has to support first.
38
+ - **Offline.** Everything runs against a clone on your machine. Nothing is
39
+ uploaded, nothing is fetched, nothing phones home. Pull the network cable
40
+ and every number comes out the same.
41
+ - **No token.** A local clone needs no GitHub token, no API access, no
42
+ credentials of any kind. The optional `owner/repo` shortcut clones with the
43
+ `gh` login you already have; that is the one network call, and you ask for
44
+ it.
45
+ - **No AI.** Every finding is a plain rule over counts you can recompute by
46
+ hand: revisions, lines, dates, names. No model, no prompt, no guessing. The
47
+ same clone gives the same report every time, and the report says what each
48
+ number is.
49
+ - **Light.** A run on a 4,400-commit repository takes under thirty seconds.
50
+ The package is a few thousand lines of Python plus two libraries, and
51
+ nothing new gets in unless it changes what you do next.
52
+
53
+ ## The tool set
54
+
55
+ One tool per question; together they cover what a single command can tell
56
+ you about a clone.
57
+
58
+ | Question | Tool | Install |
59
+ |---|---|---|
60
+ | What is this repo, at a glance; who commits, when, how much churn | gitmole itself, from the git log | built in |
61
+ | How big is the codebase, per language | [scc](https://github.com/boyter/scc) | brew |
62
+ | Is the repo itself healthy (huge blobs, deep trees) | [git-sizer](https://github.com/github/git-sizer) | brew |
63
+ | Where is the risk: hotspots, coupling, ownership | gitmole's own change analysis over `git log --numstat` | built in |
64
+ | How old is the surviving code, per year and author | gitmole's own blame pass (one `git blame` per file at HEAD) | built in |
65
+ | Code-age and survival plots over time | [git-of-theseus](https://github.com/erikbern/git-of-theseus) | pip, opt-in with `--plots` |
66
+ | Per-function complexity, length, parameters; duplicated blocks with `--duplicates` | [lizard](https://github.com/terryyin/lizard) | pip, installed with gitmole; tracked code files only |
67
+ | Have secrets ever been committed | [gitleaks](https://github.com/gitleaks/gitleaks) | brew |
68
+
69
+ Why these and not others: [docs/tools.md](https://github.com/antvinni/gitmole/blob/main/docs/tools.md).
70
+
71
+ ## Install
72
+
73
+ gitmole needs git, Python 3.9 or newer, and three tools on your PATH:
74
+ [scc](https://github.com/boyter/scc) for size,
75
+ [git-sizer](https://github.com/github/git-sizer) for repository health and
76
+ [gitleaks](https://github.com/gitleaks/gitleaks) for secrets. gitmole itself
77
+ is a Python package; install it with [pipx](https://pipx.pypa.io) so it gets
78
+ its own environment and a `gitmole` command.
79
+
80
+ ### macOS
81
+
82
+ Homebrew installs gitmole and the three tools in one go. The tap lives in
83
+ this repository, so the first command names it by URL; the second marks it
84
+ trusted, which Homebrew 7 requires before it will install from a third-party
85
+ tap; after that the short name works everywhere, `brew upgrade` included.
86
+
87
+ ```bash
88
+ brew tap antvinni/gitmole https://github.com/antvinni/gitmole
89
+ brew trust antvinni/gitmole
90
+ brew install gitmole
91
+ ```
92
+
93
+ Without Homebrew, install the three tools yourself and use pipx:
94
+
95
+ ```bash
96
+ pipx ensurepath # once; then open a new shell
97
+ pipx install gitmole
98
+ ```
99
+
100
+ ### Linux
101
+
102
+ With Homebrew on Linux the same three commands work unchanged; all three tools
103
+ are bottled there. Without Homebrew, take the tools from your package manager
104
+ where it has them and from the projects' release pages otherwise; each ships
105
+ a static binary, so dropping it into `~/.local/bin` is enough.
106
+
107
+ ```bash
108
+ # Debian and Ubuntu: git-sizer and pipx are packaged
109
+ sudo apt install git git-sizer pipx
110
+ pipx ensurepath # once; then open a new shell
111
+
112
+ # scc and gitleaks: one static binary each, from their release pages
113
+ # https://github.com/boyter/scc/releases (the Linux x86_64 or arm64 archive)
114
+ # https://github.com/gitleaks/gitleaks/releases (the linux x64 or arm64 archive)
115
+ # unpack and move the binary into ~/.local/bin, then:
116
+ chmod +x ~/.local/bin/scc ~/.local/bin/gitleaks
117
+
118
+ pipx install gitmole
119
+ ```
120
+
121
+ On a distribution without a `pipx` package, `python3 -m pip install --user
122
+ pipx` installs it. Some distributions package scc or gitleaks as well; if
123
+ yours does, prefer that to a downloaded binary.
124
+
125
+ ### Check
126
+
127
+ ```bash
128
+ scc --version && git-sizer --version && gitleaks version && gitmole --version
129
+ gitmole . # a report of the clone you are in
130
+ ```
131
+
132
+ `gitmole` reports any tool it cannot find on the first run.
133
+
134
+ ### Other ways to install
135
+
136
+ ```bash
137
+ pipx install 'gitmole[plots]' # adds git-of-theseus for --plots
138
+ pipx install git+https://github.com/antvinni/gitmole # main, unreleased
139
+ ```
140
+
141
+ `python -m gitmole` works too. From a checkout, `pip install -e .` in a
142
+ virtual environment gives an editable install. Use pip 22 or newer: the pip
143
+ that ships with macOS's system Python is older and silently builds an empty
144
+ package called UNKNOWN from modern project files. pipx brings its own current
145
+ pip, and `python3 -m pip install -U pip` fixes a plain venv.
146
+
147
+ ## Run
148
+
149
+ ```bash
150
+ gitmole . # the clone you are in
151
+ gitmole /path/to/clone # any local clone
152
+ gitmole owner/repo # clones with gh into a temp dir first
153
+ gitmole https://github.com/o/r # same, from a URL
154
+ gitmole 'owner/*' # every non-archived repo of a user or org
155
+ ```
156
+
157
+ The last form is portfolio mode: each repository is cloned and analysed in
158
+ turn into `analysis-<owner>/<repo>/`, then one table summarises them all with
159
+ commits, people, the top author's share of surviving code, secrets found,
160
+ size, and the worst finding per repo. `--markdown` and `--json` write a
161
+ portfolio document with every repo's findings; `--fail-on` looks across all
162
+ of them.
163
+
164
+ Options: `--full` for every column and every row (the default report keeps the
165
+ columns you read, caps each table, and elides long paths in the middle),
166
+ `--out DIR` to choose the output directory, `--no-run DIR` to re-render the
167
+ report from an earlier run, `--since 2y` (or `18m`, `90d`, a date) to bound
168
+ the history by author date so people, activity, timeline, hotspots and
169
+ coupling describe the current team rather than the founders (file ages and
170
+ code age always cover the whole history; identity aliases are still merged
171
+ over all of it; an empty window is an error), `--plots` to also draw the
172
+ git-of-theseus code-age and survival charts, `--file-types py,sql` to choose
173
+ which files count as code (or `all`; `--list-file-types` shows what is in the
174
+ tree and what the default includes), `--duplicates` to also look for
175
+ duplicated blocks, `--workers N` to change how many tools run at once,
176
+ `--timeout S` to cap any single tool (default 15 minutes), `--gone MONTHS` to
177
+ change how long without a commit counts as gone (default 12, measured before
178
+ the last commit), `--risk BASE` to score the files changed since BASE (the
179
+ merge base with HEAD) with the watch list's score, in one extra section with
180
+ a total; it works with `--no-run` and the JSON carries the number for CI.
181
+ Ctrl-C kills every running step, including their child processes, and exits
182
+ with code 130.
183
+
184
+ All tools run concurrently, so a run takes about as long as the slowest tool.
185
+ Tool stderr goes to `run.log` in the output directory, not the terminal.
186
+
187
+ ### Exports and CI
188
+
189
+ ```bash
190
+ gitmole . --markdown report.md # the same report as a Markdown document
191
+ gitmole . --json report.json # every table, the watch list and the findings, machine-readable
192
+ gitmole . --markdown - | pbcopy # - means stdout; banner and progress go to stderr
193
+ gitmole . --fail-on warning # exit 3 if any finding is a warning or worse
194
+ gitmole . --risk main --risk-threshold 5 # exit 3 if the changed files are too risky
195
+ ```
196
+
197
+ `--fail-on` accepts `critical`, `warning`, or `info`. A CI job that runs
198
+ `gitmole . --fail-on critical --markdown - >> "$GITHUB_STEP_SUMMARY"` blocks
199
+ on secrets in source files and still posts the report. Secrets found only in
200
+ test files are a warning, so gate on `warning` to block on those too. Both exports also work with
201
+ `--no-run` against an earlier output directory. `--risk-threshold` needs `--risk`: it exits 3 when
202
+ the files changed since main add up to more than 5 on the watch-list scale; the total prints in the
203
+ Change risk caption.
204
+
205
+ ### Big repositories
206
+
207
+ Blame and lizard's duplicate finder are the two costs that scale with repo
208
+ size. gitmole keeps them in check:
209
+
210
+ - the code-age table comes from one `git blame` per tracked code file at
211
+ HEAD, run on all but two CPU cores at low priority so the machine stays
212
+ usable. That is all the table needs;
213
+ - blame cost depends on file size and history depth, not file count, so
214
+ gitmole times a sample of 25 blames first and projects the whole pass. If
215
+ the projection exceeds `--time-budget` (default 60 seconds) the pass is
216
+ skipped with a message, and the report shows net lines added per year from
217
+ the change log instead, labelled as an approximation;
218
+ - the plots need history, so `--plots` runs git-of-theseus with monthly
219
+ sampling (tracked files × samples blames) on top, skipped above
220
+ `--budget` (default 50,000 blames);
221
+ - `--deep` forces both regardless of the budgets;
222
+ - the duplicate finder is off by default. It keeps a hash node per token,
223
+ so on a repo of a few thousand files it runs for minutes at one or two
224
+ gigabytes per worker, which is why `--duplicates` also caps that step at
225
+ two workers. Function metrics without it take a second or two;
226
+ - `--ignore-data` excludes data-like files (csv, json, lock files, minified
227
+ and vendored assets) from blame and from the function metrics, and
228
+ `--ignore GLOB` adds your own patterns, repeatable. Both shrink the blame
229
+ count a lot on repos full of exports and fixtures.
230
+
231
+ Two steps read history rather than the working tree, and both are bounded.
232
+ The trend behind the hotspots' `trend` column runs scc over the ten top
233
+ hotspots at up to twelve sampled commits, one run per sample, not one per
234
+ file. The backtest behind the watch list's caption is a second change
235
+ analysis over the same log with the window closed six months before the
236
+ last commit, plus one checkout of the tree as it was then, exported under
237
+ the output directory and removed again when the step ends.
238
+
239
+ A tool that exceeds `--timeout` is killed along with its child processes,
240
+ marked in the report, and the rest of the report still renders.
241
+
242
+ ## Example
243
+
244
+ Running `gitmole .` inside this repository:
245
+
246
+ ```text
247
+ ╭─ gitmole ────────────────────────────────────────────────────────────────────────────────────────╮
248
+ │ 115 commits · 2026-09-15 → 2026-09-16 · 1 identity · branch main │
249
+ │ 6,944 lines in 44 files · Python, Ruby │
250
+ │ most commits on Wed at 20:00 · 3% of commits are fixes · 100% of surviving code from 2026 │
251
+ │ 3 warnings, 1 note │
252
+ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
253
+ ╭─ Findings (4) ───────────────────────────────────────────────────────────────────────────────────╮
254
+ │ ▲ Bus factor of one │
255
+ │ vinni wrote 100% of the code that survives today │
256
+ │ ↳ Pair someone with vinni on gitmole/ and build/ first; they are 100% and 100% theirs. │
257
+ │ ▲ Hotspots getting more complex │
258
+ │ 4 of the 10 top source hotspots grew by 25% or more in a year: gitmole/render.py (+150%), │
259
+ │ gitmole/cli.py (+32%), gitmole/findings.py (+266%), gitmole/run.py (+26%) │
260
+ │ ↳ Split gitmole/render.py before the next change; its complexity grew 150% in a year. │
261
+ │ ▲ Knowledge islands │
262
+ │ 2 area(s) with at least 200 lines were written almost entirely by one person: gitmole/ (vinni │
263
+ │ 100%); build/ (vinni 100%). That is 97% of all lines added │
264
+ │ ↳ Pair someone with vinni on gitmole/ first; it is the largest at 5,458 lines. │
265
+ │ ● Bug magnets │
266
+ │ 1 file(s) were fixed 3+ times in the last six months: gitmole/run.py (3 recent, 3 total) │
267
+ │ ↳ Review gitmole/run.py before the next release; expect the next bug there. │
268
+ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
269
+
270
+ ◎ Watch list
271
+ file why
272
+ ──────────────────────────────────────────────────────────────────────────────────────────────────
273
+ gitmole/render.py changed 38 times · fixed twice in six months · only vinni has touched it ·
274
+ hotspots_section() complexity 23 · changes with gitmole/cli.py (63%) and 3
275
+ others
276
+ gitmole/cli.py changed 38 times · fixed twice in six months · only vinni has touched it ·
277
+ main() complexity 17 · changes with gitmole/render.py (63%) and 1 other
278
+ gitmole/run.py changed 25 times · fixed 3 times in six months · only vinni has touched it ·
279
+ collect_meta() complexity 23 · changes with gitmole/load.py (67%) and 2
280
+ others
281
+ gitmole/findings.py changed 25 times · fixed twice in six months · only vinni has touched it ·
282
+ complexity_growth() complexity 18 · changes with gitmole/load.py (62%) and 1
283
+ other
284
+ gitmole/load.py changed 17 times · fixed twice in six months · only vinni has touched it ·
285
+ parse_git_sizer() complexity 15 · changes with gitmole/run.py (67%) and 2
286
+ others
287
+ ranked by churn × recent fixes × complexity × single ownership
288
+ too little history to backtest
289
+ ```
290
+
291
+ The full report is in [docs/example.md](https://github.com/antvinni/gitmole/blob/main/docs/example.md).
292
+
293
+ ### The terminal report
294
+
295
+ 1. **Header**: commits, date span, identities, branch, size, top languages,
296
+ one line for the busiest day and hour, the share of fix commits, the
297
+ share that are reverts when there are any, and the year most surviving
298
+ code was written (or why the blame pass did not run), and a one-line
299
+ tally of the findings.
300
+ 2. **Findings**: anything the heuristics flagged, worst first. Findings of
301
+ the same kind are grouped into one entry with a list, and every finding
302
+ ends with a next step that names the file, area or person to start with,
303
+ on its own line under the facts. Currently:
304
+ secrets in history (see below), an unconfigured git identity
305
+ (example.com and the like), one author owning most surviving code,
306
+ git-sizer concerns, one file dominating the churn, bug magnets (source
307
+ files fixed three or more times in the last six months; a warning at
308
+ five), reverts (5% of commits or five of them; a warning at 10%; names
309
+ the file most often backed out), brain methods (functions with
310
+ complexity 15+ and 100+ lines; a warning when one sits in a hotspot),
311
+ hotspots getting more complex (three or more of the top ten hotspots
312
+ grew by a quarter in a year; a warning when the top one did),
313
+ tightly coupled file pairs (a file and its test are expected to change
314
+ together, so those pairs are left out), duplicated blocks of 30+ lines
315
+ (with `--duplicates`), a large share of stale files (files still in the
316
+ tree; deleted paths do not count), knowledge islands: areas of at least
317
+ 200 lines written almost entirely by one person (a warning when such
318
+ areas hold most of the code), and knowledge loss (people with no commits
319
+ in the twelve months before the last commit who wrote 10% or more of the
320
+ surviving code; a warning at 30%). An unconfigured identity is only
321
+ flagged when it made at least 1% of the commits.
322
+
323
+ Secrets are grouped by value, so one key copied into ten files is one
324
+ entry with its places counted. A value found in any source file is
325
+ critical. A value found only in test files, such as fixtures and saved
326
+ web pages, is a warning. Version strings and tokens shortened with "..."
327
+ cannot be live secrets, so they are left out and counted on the footer
328
+ line. Nothing is skipped by prefix. To silence a false positive for
329
+ good, copy its fingerprint from `secrets.json` into a `.gitleaksignore`
330
+ at the repository root; gitleaks reads it on the next run.
331
+
332
+ A commit counts as a fix when its subject starts with `fix:`, `hotfix:` or
333
+ `bugfix:` in the conventional style, or mentions fix, bug, hotfix,
334
+ regression or crash. Test files are left out of every finding that names a
335
+ file, area or function: they change with every fix, and owning the tests is
336
+ not the knowledge risk. The default tables leave them out too; `--full`
337
+ shows them.
338
+ 3. **Watch list**: the five files where the next bug is most likely, with
339
+ the reasons in words. Every source file still in the tree that changed
340
+ more than once is scored churn × (1 + recent fixes) × (1 + complexity),
341
+ times 1.5 when one person wrote 90% or more of it, each factor scaled to
342
+ the worst file in the repo. Churn is the base because a file nobody
343
+ changes is not where the next bug lands; complexity is scc's per-file
344
+ total, one scale for every file, while the most complex function lizard
345
+ found is named in the reasons. The reasons name the fix count, the sole
346
+ owner, the function and the files it always changes with. Test files are
347
+ left out. Under `--since`, churn and ownership are windowed and the list
348
+ says so. `--full` and the exports show fifteen. With `--risk BASE`, a
349
+ Change risk section follows: every file changed since BASE with its watch
350
+ score as a bar and the reasons, or why it has none (new file, changed
351
+ once, test file, not scored).
352
+
353
+ Under the watch list, one line says how the list would have done:
354
+ gitmole reruns the change analysis as of six months before the last
355
+ commit, with scc on the tree at that time, ranks the watch list from
356
+ that, and counts how many of the files fixed since were on it, next to
357
+ what a random list of the same size, drawn from the files that had
358
+ changed more than once, would score. Repositories with under a year
359
+ of history say `too little history to backtest`.
360
+ 4. **Tables**: people (identities merged by name and email similarity on
361
+ top of `.mailmap`, and the caption says whose; bots such as renovate,
362
+ dependabot and GitHub Actions are counted apart in the caption and kept
363
+ out of the timeline), a knowledge map (lines added per area of the tree
364
+ and who wrote them), a timeline of commits per author over the last
365
+ twelve months, hotspots ranked by revisions times lines of code with the
366
+ number of fix commits alongside, change coupling, the most complex
367
+ functions, repo health. Hotspots carry a `trend` column, sampled for the
368
+ top ten hotspots: the change in complexity over the last year from scc on
369
+ the file at sampled commits (`--full` shows the whole series as a
370
+ sparkline). The knowledge map marks owners who have stopped committing
371
+ with `(gone)`, and under `--full` shows the share of each area's lines
372
+ that they wrote. With `--full`: size by language, activity by weekday
373
+ with the busiest hour and the share of commits that are fixes, and
374
+ surviving code by year.
375
+
376
+ Size, hotspots, coupling, ownership, code age and the watch list analyse
377
+ source files: a built-in list of code extensions plus names like Makefile
378
+ and Dockerfile (`--file-types all` counts everything). In the default
379
+ report, the hotspots and complex functions tables hide test files, and
380
+ the change coupling table hides pairs with a test file; the captions
381
+ show how many are hidden, and `--full` shows them. Activity and the
382
+ timeline cover the whole history.
383
+ 5. **Footer**: where the files and plots are.
384
+
385
+ The files each run writes, and how to read them: [docs/output.md](https://github.com/antvinni/gitmole/blob/main/docs/output.md).
386
+
387
+ ## Development
388
+
389
+ Developer setup: Homebrew for the three tools,
390
+ `brew install scc git-sizer gitleaks`. Then either a virtual environment
391
+ with `pip install -e .`, or the checkout style:
392
+ `python3 -m pip install --user rich lizard` and
393
+ `ln -sfn "$PWD/bin/gitmole" "$(brew --prefix)/bin/gitmole"`, which makes
394
+ the checkout what runs.
395
+
396
+ ```bash
397
+ python3 -m unittest discover -s tests -t .
398
+ ```
399
+
400
+ `tests/test_golden.py` builds a small synthetic repository, runs the whole
401
+ pipeline with the real tools, and compares the plain-text report against
402
+ `tests/golden/report.txt`. It skips itself when the tools are not installed.
403
+ When a change to the report is intended, regenerate the stored file and
404
+ review the diff:
405
+
406
+ ```bash
407
+ UPDATE_GOLDEN=1 python3 -m unittest tests.test_golden
408
+ ```
409
+
410
+ `GITMOLE_NOW=YYYY-MM-DD` fixes the reference date for file ages, which is
411
+ what keeps that report stable. gitmole validates it, announces it at the
412
+ start of a run, and records it in `meta.json`, so a forgotten export cannot
413
+ silently skew a real report.
414
+
415
+ ### Releases
416
+
417
+ Versions are git tags. To release: bump `__version__` in `gitmole/__init__.py`,
418
+ merge, then tag that commit `vX.Y.Z` and push the tag. CI runs the tests, checks
419
+ that the tag matches `__version__`, builds the sdist and wheel, and creates the
420
+ GitHub release with notes generated from the merged pull requests and the
421
+ artefacts attached. The release job then bumps `Formula/gitmole.rb` on main
422
+ to the new release, so `brew upgrade gitmole` follows within minutes; the tag
423
+ also publishes to PyPI. `pipx install gitmole==X.Y.Z` installs a release with
424
+ pipx. Releases are listed at
425
+ https://github.com/antvinni/gitmole/releases.
426
+
427
+ `bin/render-banner` regenerates `docs/banner.svg` from the banner code.
428
+ The code lives in `gitmole/`: `run.py` plans and executes the tools,
429
+ `maat.py` is the standalone change analysis (revisions, coupling, authors,
430
+ age, ownership over the numstat log; the file names still say maat because
431
+ the layout matches what code-maat produced), `blame.py` is the standalone
432
+ code-age pass (its output mimics git-of-theseus so one loader serves both),
433
+ `identity.py` merges author aliases, `load.py` parses the outputs, `findings.py` holds the heuristics,
434
+ and `render.py` draws the report. `bin/gitmole` is a thin launcher.
435
+
436
+ ## Safety notes
437
+
438
+ - Everything here is offline except the optional clone step, which uses
439
+ your existing gh auth. None of the tools send data anywhere.
440
+ - Remote targets are cloned into a fresh temp directory. Local clones are
441
+ only read, but the log export and the gitleaks scan touch all branches.
442
+ - Secret values never reach the output directory. gitleaks writes its report
443
+ to gitmole in memory, and gitmole stores a short keyed hash of each value
444
+ in place of the value, the matched text and the commit message. The key is
445
+ random, made for that one report and never saved, so a stored hash cannot
446
+ be checked against a list of common passwords. It only tells you which
447
+ hits in one report share a value.
448
+ - Install from the official repos or Homebrew with pinned versions, not from
449
+ forks.
450
+
451
+ ## License
452
+
453
+ gitmole is released under the [MIT License](https://github.com/antvinni/gitmole/blob/main/LICENSE).
454
+
455
+ It does not bundle any of the tools it wraps; gitmole runs them as
456
+ separate processes. Their licences:
457
+
458
+ | Tool | Licence |
459
+ |---|---|
460
+ | scc | MIT |
461
+ | git-sizer | MIT |
462
+ | gitleaks | MIT |
463
+ | rich | MIT |
464
+ | lizard | MIT |
465
+ | git-of-theseus | Apache-2.0 |
466
+
467
+ The change analysis (hotspots, coupling, ownership, age) is gitmole's own
468
+ code, written after the ideas in Adam Tornhill's code-maat but sharing no
469
+ code with it.