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 --porcelainfor modified and untracked filesgit stash listfor stashesgit for-each-refwith 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,.npmrcand.pgpass(.env.exampleand 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 --allshould 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.