git-regrets
v0.1.2
Published
The leaderboard of shame for any git repo: your oldest TODO, FIXME, HACK and "temporary" fixes, git-blamed with age and author. Zero config, zero dependencies, works offline.
Maintainers
Readme
git-regrets
The leaderboard of shame for any git repository. git-regrets finds every TODO, FIXME, HACK, XXX, "temporary" fix, "quick fix", "for now", "remove me" and "do not merge" in your code, runs git blame on each one, and ranks them by age. Then it tells you who wrote the oldest one and whether they still work here.
npx git-regretsThat is the whole setup. No config, no install, no network, no dependencies. It reads the git data you already have.
🥇 git-regrets · monolith
the leaderboard of shame
Your oldest "temporary" fix is 9.2 years old, written by someone who left in 2019.
87 regrets · 61 files · 14 authors · 143 years of combined age · median 1.8 years
🥇 9.2y TEMPORARY src/legacy/auth.js:142 Alice Smith (left in 2019)
// TEMPORARY fix until the new auth service ships
🥈 7.8y HACK lib/parser.rb:88 Bob Jones (still here)
# HACK: monkey-patch Date because of a tz bug
🥉 7.8y QUICK FIX src/launch.js:12 Bob Jones (still here)
// Quick fix for now, remove this before launch
4. 6.1y FOR NOW api/routes/billing.ts:301 Carol Diaz (left in 2022)
// for now we just retry forever
5. 5.4y XXX core/scheduler.c:77 Dmitri V (left in 2021)
/* XXX no idea why this works, do not touch */
…
Hall of shame
Bob Jones 31 regrets oldest 7.8y still here
Alice Smith 12 regrets oldest 9.2y left in 2019
Carol Diaz 9 regrets oldest 6.1y left in 2022
TODO 48 · FIXME 17 · HACK 9 · TEMPORARY 6 · WORKAROUND 4 · FOR NOW 3
Screenshot it. Share it. Then go fix number one.Why
Every codebase has a comment that says "temporary" and has outlived three reorgs. Nobody knows how old it is, because nobody has looked. git-regrets looks. It takes the thing engineers joke about and puts a number, a name and a year on it, in the time it takes git blame to run.
- Zero config. Point it at a repository, or run it inside one. Done.
- Works offline. Everything comes from
git grep,git blameandgit log. Nothing is sent anywhere. - Zero dependencies. One small package, plain Node, nothing to audit.
- Comment-aware.
todoListand"TODO"inside a string are not regrets.// TODOis. Prose files and vendored code are skipped by default. - Reformat-proof. Blame ignores whitespace-only changes and in-file moves, so running Prettier does not launder anyone's confession.
- Screenshot-ready. Medals, colours, one sentence you can paste anywhere.
Install
You do not have to. npx git-regrets runs the latest version. If you want it on your PATH:
npm install -g git-regretsRequires Node 18 or newer and git on your PATH.
Usage
npx git-regrets [path] [options]
--top <n> How many regrets to show (default 10)
--all Show every regret
--json Print the full result as JSON instead of the leaderboard
--markers <list> Comma-separated markers to hunt instead of the defaults
--exclude <spec> Extra git pathspec to skip; repeatable
--no-default-excludes Also scan vendor/, node_modules/, lockfiles, minified files, prose
--loose Match markers anywhere on a line, not only after a comment marker
--no-authors Hide the per-author hall of shame
--no-color Plain text (also honours NO_COLOR)
-v, --version Print the version
-h, --help Show this helpExamples:
npx git-regrets # the repo you are standing in
npx git-regrets ~/work/monolith # any other repo
npx git-regrets --top 25 # a longer leaderboard
npx git-regrets --all --json > regrets.json
npx git-regrets --markers "TODO,FIXME,@ts-ignore,eslint-disable"
npx git-regrets --exclude 'test/**' --exclude 'fixtures/**'What counts as a regret
By default git-regrets hunts for these, case-insensitively, when they appear after a comment marker (//, #, /*, *, --, <!--, ;, %, ''', """ and friends):
| Marker | Also matches |
| -------------- | -------------------------------------------------------------------------------------- |
| TODO | |
| FIXME | FIX ME |
| HACK | HACKY, HACKS |
| XXX | |
| KLUDGE | |
| TEMPORARY | TEMP in capitals, or temporary fix, temporary workaround, temporarily disabled |
| WORKAROUND | work-around, work around |
| QUICK FIX | quick-fix, quickfix |
| FOR NOW | |
| REMOVE ME | remove this, delete me, remove before, delete later |
| DO NOT MERGE | don't ship, do not commit, NOCOMMIT |
| TECH DEBT | technical debt |
| DO NOT TOUCH | don't touch |
| DON'T ASK | |
| NO IDEA WHY | not sure why |
| FORGIVE ME | god help, here be dragons |
"temporary" only counts when it is shouted (TEMP, TEMPORARY) or followed by a confession noun, so # remove temporary files is not on your leaderboard but // temporary hack is.
When two regrets are exactly the same age, the more embarrassing marker ranks higher. TEMPORARY beats TODO.
--markers replaces this table with your own words. Each is matched literally, so --markers "@ts-ignore,eslint-disable,any" gives you a leaderboard of suppressed type errors.
--loose drops the comment requirement, which finds more and lies more.
What is skipped
Only tracked files are scanned, so anything in .gitignore never shows up. On top of that these are excluded by default, because a TODO in a README is a plan and a TODO in vendor/ is somebody else's:
node_modules/,vendor/,vendored/,third_party/,third-party/*.min.*,*.map,*.lock,*-lock.json,*.snap*.md,*.mdx,*.markdown,*.rst,*.txt,*.adoc*.svg,*.pdf,*.csv, and lines longer than 400 characters
--exclude adds a git pathspec; a bare glob like test/** is wrapped as :(glob,exclude)test/**. --no-default-excludes scans everything git tracks.
"left in 2019"
An author has "left" when they have no commit on the current branch in the last 365 days. Their most recent commit year is what gets printed. It is a heuristic, not an HR record: someone who moved to another repository, or who is on the branch you did not check out, will be reported as gone. A .mailmap is honoured, so if one person has committed under several e-mail addresses, map them and the numbers merge.
Uncommitted lines show up too, attributed to you, aged today. There is still time.
JSON output
--json prints everything the leaderboard is built from, so you can feed it into a dashboard, a CI check, or a very specific Slack bot:
{
"repo": { "root": "/work/monolith", "name": "monolith", "files": 4112, "commits": true },
"regrets": [
{
"file": "src/legacy/auth.js",
"line": 142,
"text": "// TEMPORARY fix until the new auth service ships",
"marker": "TEMPORARY",
"sha": "e3e8b11ac82be302b5e4b0c931784e9de3dbb4f6",
"author": "Alice Smith",
"email": "[email protected]",
"time": 1488326400,
"ageDays": 3484.6,
"summary": "initial auth",
"uncommitted": false,
"lastCommit": 1560000000,
"left": true
}
],
"authors": [
{
"name": "Alice Smith",
"email": "[email protected]",
"count": 12,
"oldestAgeDays": 3484.6,
"left": true
}
],
"totals": {
"count": 87,
"files": 61,
"authors": 14,
"combinedAgeDays": 52230,
"averageAgeDays": 600.3,
"byMarker": { "TODO": 48 }
}
}Regrets are always sorted oldest first.
Programmatic use
import { scan, renderReport, punchline } from 'git-regrets';
const result = await scan({ cwd: '/work/monolith' });
console.log(punchline(result));
// Your oldest "temporary" fix is 9.2 years old, written by someone who left in 2019.
console.log(renderReport(result, { top: 5, color: true }));scan accepts cwd, markers, excludes, defaultExcludes, loose, now (unix seconds, for reproducible ages) and concurrency (parallel git blame processes, default 8). It returns the same shape as --json.
Ideas for a CI check
Fail the build when a new regret older than a release cycle sneaks in, or just post the punchline on every pull request:
npx git-regrets --json | node -e '
const r = JSON.parse(require("fs").readFileSync(0, "utf8"));
const tooOld = r.regrets.filter((x) => x.ageDays > 365 * 2);
if (tooOld.length) { console.error(`${tooOld.length} regrets older than two years`); process.exit(1); }
'How it works
git grep -n -I -E -iruns once over every tracked file with one loose pattern that covers all markers.- Each hit is checked in JavaScript against a precise pattern and the comment heuristic.
- Surviving lines are grouped by file and passed to
git blame --porcelain -w -Mwith one-Lrange per line, eight files at a time. git log --format='%at %aE'is streamed newest-first and stopped as soon as every author on the leaderboard has been seen, which is how "left in 2019" stays cheap on huge histories.
On a repository with a few thousand files and a hundred regrets it finishes in well under a second.
Contributing
pnpm install
pnpm test
pnpm check # lint, format, test, packThe test suite builds throwaway git repositories with backdated commits, so it needs git and a writable temp directory, and nothing else.
The Debt Layer
git-regrets is chapter one of The Debt Layer, automated: before you can pay down technical debt you have to make it visible, and nothing makes it more visible than a leaderboard with names on it.
