git-leftovers: Arbeit finden, die nur auf einem Rechner liegt

Ein kleines Werkzeug, das jedes Git-Repository unter einem Verzeichnis auf uncommittete Änderungen, ungepushte Commits, Stashes, fehlende Remotes und Zugangsdaten-Dateien in der Historie prüft.

Das Problem

Bevor ein Projekt übergeben wird, eine Stelle endet oder ein Laptop oder eine VM gelöscht wird, muss jemand herausfinden, welche Arbeit nur auf diesem Rechner liegt. git status beantwortet das für ein Repository und seinen aktuellen Branch. Auf einem Rechner mit vielen Repositories muss man die Prüfung in jedem einzelnen von Hand machen, und einige Arten von Resten übersieht man leicht: Commits auf lokalen Branches, die nie gepusht wurden, Stashes, Repositories, die nie ein Remote hatten, und Zugangsdaten-Dateien, die irgendwann committet und später gelöscht wurden.

Wie es funktioniert

git-leftovers durchläuft die angegebenen Verzeichnisse bis zur Tiefe 6 und behandelt jedes Verzeichnis mit einem .git-Eintrag als Repository. Das schließt verschachtelte Repositories und Worktrees ein. Verzeichnisse wie node_modules und .venv werden übersprungen.

Für jedes Repository ruft es nur lokale Git-Befehle auf:

  • git status --porcelain für geänderte und nicht versionierte Dateien
  • git stash list für Stashes
  • git for-each-ref mit dem Tracking-Zustand, der angibt, wie viele Commits ein Branch seinem Upstream voraus ist
  • für Branches ohne Upstream die Zahl der Commits, die in keinem Remote-Tracking-Branch enthalten sind (git rev-list --count <branch> --not --remotes)
  • git log --all --diff-filter=A --name-only, das jede jemals auf irgendeinem Branch hinzugefügte Datei auflistet; die Dateinamen werden mit Mustern wie .env, *.pem, id_rsa, *credentials*.json, .npmrc und .pgpass verglichen (.env.example und ähnliche Vorlagen sind ausgenommen)

Der Exit-Code ist 1, wenn ein Repository Reste hat, und 0, wenn alles gepusht ist. Das Werkzeug taugt damit auch als Prüfung in Skripten. --json gibt pro Repository eine Liste findings aus.

Das ist das Beispiel aus dem README (die Pfade sind dort als Beispielpfade gekennzeichnet):

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

Das Werkzeug braucht Python 3.10 oder neuer und git, sonst keine Abhängigkeiten.

Tests

Die Tests legen echte Git-Repositories in einem temporären Verzeichnis an, mit einem Bare-Repository als Remote. Ein vollständig gepushter Klon darf keine Befunde liefern. Davon ausgehend fügen die Tests eine geänderte Datei, eine nicht versionierte Datei und einen Stash hinzu, zwei ungepushte Commits auf main und einen lokalen Feature-Branch mit einem Commit, der als "branch not on any remote" gemeldet werden muss. Ein lokaler Branch ohne neue Commits darf nicht gemeldet werden, ein Repository ohne Remote dagegen schon.

Ein Test committet .env, löscht die Datei und pusht. Die Datei muss trotzdem aus der Historie gemeldet werden, und --no-history muss diese Prüfung überspringen. Eine Tabelle von Dateinamen prüft die Muster: deploy/.env.production und certs/server.pem passen, .env.example und src/credentials.py nicht. Ein weiterer Test prüft, dass ein verschachteltes Repository gefunden und eines in node_modules übersprungen wird. Der Kommandozeilentest prüft die Exit-Codes und die JSON-Ausgabe.

Es sind 16 Tests. Das CI führt sie auf Python 3.10, 3.11, 3.12 und 3.13 aus, zusammen mit ruff check und ruff format --check, und ruft das Werkzeug mit --all auf dem eigenen Checkout auf.

Grenzen

  • Es führt kein Fetch aus. Ein veralteter Remote-Tracking-Stand erscheint als "unpushed". Wenn das wichtig ist, sollte vorher git fetch --all laufen.
  • Die Prüfung auf Zugangsdaten vergleicht nur Dateinamen. Dateiinhalte liest sie nicht.
  • Eine verdächtige Datei in der Historie ist kein Beweis für ein Leck; Zertifikate (*.pem) sind oft öffentlich. Der Befund bedeutet, dass sich jemand die Datei ansehen sollte, bevor die Historie den Rechner verlässt.
  • Repositories, die tiefer liegen als die Suchtiefe (Standard 6), werden nicht gefunden.

Code und Installationsanleitung liegen unter github.com/Jwrede/git-leftovers.