npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

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.

npm version npm downloads CI license

npx git-regrets

That 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 blame and git log. Nothing is sent anywhere.
  • Zero dependencies. One small package, plain Node, nothing to audit.
  • Comment-aware. todoList and "TODO" inside a string are not regrets. // TODO is. 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-regrets

Requires 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 help

Examples:

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

  1. git grep -n -I -E -i runs once over every tracked file with one loose pattern that covers all markers.
  2. Each hit is checked in JavaScript against a precise pattern and the comment heuristic.
  3. Surviving lines are grouped by file and passed to git blame --porcelain -w -M with one -L range per line, eight files at a time.
  4. 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, pack

The 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.

License

MIT