git-leftovers: finding work that exists only on one machine

A small tool that checks every Git repository under a directory for uncommitted changes, unpushed commits, stashes, missing remotes and credential files in the history.

The problem

Before a project is handed over, a job ends, or a laptop or VM is wiped, someone has to find out which work exists only on that machine. git status answers this for one repository and its current branch. On a machine with many repositories the check has to be done by hand in each of them, and several kinds of leftovers are easy to miss: commits on local branches that were never pushed, stashes, repositories that never had a remote, and credential files that were committed at some point and deleted later.

How it works

git-leftovers walks the given directories up to a depth of 6 and treats every directory with a .git entry as a repository, which includes nested repositories and worktrees. Directories such as node_modules and .venv are skipped.

For each repository it runs local Git commands only:

  • git status --porcelain for modified and untracked files
  • git stash list for stashes
  • git for-each-ref with the upstream tracking state, which gives the number of commits a branch is ahead of its upstream
  • for branches without an upstream, the number of commits that no remote-tracking branch contains (git rev-list --count <branch> --not --remotes)
  • git log --all --diff-filter=A --name-only, which lists every file ever added on any branch; the file names are matched against patterns such as .env, *.pem, id_rsa, *credentials*.json, .npmrc and .pgpass (.env.example and similar templates are excluded)

The exit code is 1 when any repository has leftovers and 0 when everything is pushed, so the tool also works as a check in scripts. --json prints a findings list per repository.

This is the example from the README (the README marks the paths as illustrative):

$ git-leftovers ~/work
/home/dev/work/billing-service  [main]  (no remote)
  - no remote: the whole repository exists only here
  - 1 modified file(s)
  - branch main: 4 unpushed commit(s)
  - secret-looking file in history: .env
/home/dev/work/data-pipeline  [main]
  - 1 untracked file(s)
  - branch fix/rounding: 1 commit(s), branch not on any remote
/home/dev/work/warehouse  [main]
  - 1 stash(es)

12 repositories, 3 with work that exists only here.

The tool needs Python 3.10 or newer and git, and has no other dependencies.

Testing

The tests create real Git repositories in a temporary directory, with a bare repository as the remote. A fully pushed clone must produce no findings. From there the tests add a modified file, an untracked file and a stash, two unpushed commits on main, and a local feature branch with one commit, which must be reported as "branch not on any remote". A local branch without new commits must not be reported, and a repository without a remote must be.

One test commits .env, deletes it, and pushes; the file must still be reported from the history, and --no-history must skip that check. A table of file names checks the patterns: deploy/.env.production and certs/server.pem match, .env.example and src/credentials.py do not. Another test checks that a nested repository is found while one inside node_modules is skipped. The command-line test checks the exit codes and the JSON output.

There are 16 tests. CI runs them on Python 3.10, 3.11, 3.12 and 3.13, together with ruff check and ruff format --check, and runs the tool with --all on its own checkout.

Limitations

  • It does not fetch. A stale remote-tracking state shows up as "unpushed", so git fetch --all should run first if that matters.
  • The credential check matches file names only. It does not look at file contents.
  • A secret-looking file in the history is not proof of a leak; certificates (*.pem) are often public. The finding means someone should look at the file before the history leaves the machine.
  • Repositories deeper than the search depth (default 6) are not found.

The code and installation instructions are at github.com/Jwrede/git-leftovers.