Algorithms and design decisions
Rename tracking
git log -M --numstat reports a rename as old => new, abbreviated with braces when paths share parts:
| numstat path | old | new |
|---|---|---|
src/{catalog => inventory}/search.py |
src/catalog/search.py |
src/inventory/search.py |
{ => sub}/f.py |
f.py |
sub/f.py |
a.py => b.py |
a.py |
b.py |
Empty brace halves leave a double slash, which is collapsed.
Commits are processed newest to oldest, keeping an alias table. Seeing a rename old -> new records alias[old] = alias.get(new, new); every older appearance of old then resolves to the newest name. Worked example, newest first:
a.py+2 (a different file that re-used the name)- rename
a.py -> b.py a.py+1
Step 1 resolves to a.py (no alias yet), step 2 sets alias[a.py] = b.py, step 3 resolves to b.py. Walking oldest to newest would wrongly merge step 1 into the old file.
The cache stores raw paths and old_path; identities are resolved on every load, which is what makes incremental updates correct: a rename in a new commit retroactively relabels old rows without rewriting them.
Cache
meta(head, version), commits(seq, sha, name, email, date, message), changes(seq, raw_path, old_path, added, deleted). seq ascends from newest to oldest. On a run:
- HEAD equals the cached head: load only.
- cached head is an ancestor of HEAD (
git merge-base --is-ancestor): parsecached..HEAD, insert with sequence numbers below the current minimum, so order stays newest-first. - otherwise (rebase, force-push, other repo state): wipe and rescan.
Filters and mailmap are applied after loading, so one cache serves every query.
Squarified treemap
Input values are scaled so their areas sum to the bounding rectangle. Values are sorted descending and placed in rows along the shorter side. An item joins the current row while the worst aspect ratio in the row does not get worse:
worst(row, side) = max(side^2 * max / sum^2, sum^2 / (side^2 * min))
When adding an item would worsen it, the row is laid out as a strip (column when the free rectangle is wider than tall, row otherwise) and the remaining rectangle shrinks. The result tiles the rectangle exactly with areas proportional to values (tested as invariants: proportional areas, in bounds, no overlap).
The hierarchical layout nests by directory: each directory is laid out in its parent's rectangle by total value, then its children are laid out inside it after reserving a header strip and padding. Chains of single-child directories are merged (src/app) so space is not wasted. Because of the padding, areas are exactly proportional within one directory but only approximately across the whole tree.
Bus factor, worked example
Directory d with four files:
| file | alice | bob | carol |
|---|---|---|---|
| d/1 | 90% | 10% | |
| d/2 | 80% | 20% | |
| d/3 | 100% | ||
| d/4 | 100% |
Knowledge units: alice 1.7, bob 0.3, carol 2.0 out of 4. Sorted: carol 50%, alice 42.5%, bob 7.5%. Carol alone holds exactly 50%, not more than 50%, so alice is added: bus factor 2. Weighting files equally means a single enormous file cannot decide the answer.
Coupling
Per commit the set of existing files is enumerated pairwise (n * (n-1) / 2 pairs); --max-files bounds that by dropping bulk commits. With A in 10 commits, B in 7, shared 6: degree 6 / 8.5 = 70.6%, A to B 60%, B to A 85.7%. Asymmetric confidence shows dependence direction: if every change to B also touches A but not the reverse, B depends on A.
Hotspot scoring
Both factors are normalised by the maximum in the analysed set, then multiplied, so a file needs to be both frequently changed and complex to rank high. A trivially edited huge file and a complex file nobody touches both score near zero.