🌐 Sprache / Language: English · Deutsch
“It's like a jungle sometimes”
— Grandmaster Flash and the Furious Five, “The Message”
A fast terminal overview (TUI) of every Git repository below the current directory, so you can see what still needs attention and tidy it up on the spot.
Green means clean and in sync with your remote; red and yellow mean something is
left over. One Python file, standard library only — no pip install, no daemon,
no registration of repositories. It scans whatever is below the directory you
start it in.
The screen above is generated from the real program on the --demo sandbox — python3 docs/make-screens.py (and --check in CI). No screenshots to retake when the UI changes.
Try it without touching your own repositories:
python3 gitmaster_flash.py --demo--demo builds a throwaway sandbox of fake repositories covering every state and
opens the UI on it. The folder lives in your temp directory; delete it when done.
- remote names are always visible — every line ends with all configured remotes, even when everything is synchronized. Their order is the private sync remote first, other remotes next, and GitHub at the far right.
- ↑n / ↓n next to a remote — commits ahead of / behind that exact remote for
the current branch, based on the last fetch.
Rrefreshes every remote in every repository withgit fetch --allwithout changing a working tree. - M / D / U — number of modified, deleted and untracked files.
- ⚑Stash:n — stashes that exist in the repo (easy to forget, so it is shown).
- ⚠conflict:n — unmerged files, e.g. after a
git stash popthat did not apply cleanly. Kept separate from "modified", because it needs different work. - Warnings such as "no sync remote" or "branch not on remote".
Repositories that need attention sort to the top, clean ones to the bottom.
Every shortcut is permanently visible in the footer, so there is nothing to
memorize. Case does not matter — f works like F.
| Key | Action |
|---|---|
| ↑ / ↓ | select a repository |
| → / ← | expand / collapse (files with M/D/U/C, stashes) |
| ⏎ | quit and cd into the repository (needs the gmf wrapper, see below) |
| E | open the repository in a configured app (add your own in config.json) |
| C | commit helper (see below) |
| P | safely push the current branch to the private sync remote |
| L | safely fast-forward the current branch from the private sync remote |
| G | guarded GitHub push with outgoing-commit/file preview and typed confirmation |
| H | show the Git safety rules inside the TUI |
| U | apply the latest stash (git stash pop, with confirmation) |
| S | view the latest stash as a diff (read-only, scrollable) |
| D | drop the latest stash (git stash drop, with confirmation) |
| R | reload everything including git fetch --all |
| Q | quit |
A stash is never popped onto a tree that already has conflicts — resolve those first. Its preview includes untracked and binary files; a failed or unexpectedly empty Git preview is labelled explicitly before the destructive drop action remains available.
Commit helper · api-gateway — review, then ⏎
M README.md ✔ commit
U notes.txt ✔ commit
U server.py ✔ commit
U build/out.o ✎ .gitignore: build/
␣ commit on/off · i gitignore on/off · ⏎ next · Esc cancel
- Every changed and new file is listed with a suggestion: typical junk
(
node_modules/,.DS_Store,__pycache__/,*.log,.env, …) is proposed for .gitignore, everything else for committing. Both are togglable per file (␣commit on/off,igitignore on/off). - Before you type the commit message, the repository's last five messages are shown as a style reference.
- Merge conflicts block the helper completely.
.gitignoreis extended atomically without following symlinks. The commit is built in a temporary index containing only the approved paths; an existing user index, including deliberately staged but excluded work, stays intact. Optionally the commit is pushed through the same guarded private sync path asPafterwards.
P and L are intentionally limited to a non-public sync remote. Both fetch
first, require a clean working tree and reject divergent history. Fetch and push
URLs must identify one identical credential-free host/repository target; multiple
or differing push URLs are blocked. Immediately before a confirmed mutation the
branch, HEAD, index, worktree, remote identity and target OID are checked again.
Pull merges only the approved immutable OID by fast-forward; push sends the approved
commit OID through an explicit refspec. An exact target-OID lease prevents a
remote deletion or concurrent move from turning it into an unreviewed update;
tags are never sent.
GitHub uses the separate G path. It works only when the same branch already
exists on one GitHub remote and the histories are related. Before publishing it
shows every outgoing commit and changed file name. The exact phrase
PUSH <remote> must then be typed. The final command still sends only the current
branch: approved source OID, exact target lease, no tags, no new branch. A remote
with multiple or differing fetch/push targets is blocked entirely, even if both
targets are on GitHub. Complex cases stay terminal-only.
Requires Python 3 and a terminal. Nothing else.
git clone https://github.com/DanielMuellerIR/gitmaster_flash.git
python3 gitmaster_flash/gitmaster_flash.pyFor the ⏎ = cd into the repository feature, source the shell wrapper — a child
process cannot change the working directory of the shell that started it, so a
small function has to do it. install.sh does that for you: it runs the
self-test, then registers the safely quoted absolute path to gmf.zsh in your
~/.zshrc (idempotent — a second run changes nothing, including from clone paths
with spaces or shell metacharacters):
gitmaster_flash/install.shOr add the line manually:
echo 'source /path/to/gitmaster_flash/gmf.zsh' >> ~/.zshrcIn a new shell, gmf then starts the tool (and cds where you asked it to):
cd ~/projects && gmfWithout the wrapper everything works the same, except that ⏎ prints the path instead of changing into it.
If you keep the same repos on more than one machine (laptop + desktop, Mac + Linux),
they drift apart in ways git never warns you about. Remotes live in .git/config
and git never transfers them — add a github remote on one machine and the other
simply doesn't have it, so a pending push is invisible there. Same for branches you
don't currently have checked out.
gitmaster_flash.py --diff mymac # compare ~/git here with ~/git on mymac
gitmaster_flash.py --diff mymac --json # machine-readable
gitmaster_flash.py --diff mymac:~/code # different directory on the other side
gitmaster_flash.py --diff mymac --fetch # refresh ahead/behind counts firstIt prints only the differences, split into classes — that split is the point, a report that lists everything gets ignored:
DRIFT favenio: remote 'github' only here (git never transfers remotes)
DRIFT notes: origin is 4 ahead/2 behind here, 0/0 on mymac
SYNC music: origin 0 ahead/3 behind on both machines
local webapp: [main] here, [feature/x] on mymac
local blog: 3 changed/new file(s) here
only on mymac: experiment
DRIFT also covers errors, conflicts, stashes, branch availability, remote safety
classification and credential-free endpoint fingerprints; raw remote URLs and
credentials never enter JSON. DRIFT = should be identical but isn't (worth
acting on). SYNC = both machines agree, but together they sit ahead/behind the
sync remote — invisible in a pure two-machine comparison, yet usually the number
you actually care about. local = explainable (different branch checked out,
dirty working tree). Exit code 0 =
nothing to report, 1 = findings (note: a SYNC line means the machines agree
with each other, so "identical machines" alone no longer guarantees exit 0),
2 = the other machine could not be reached.
Requirements: ssh HOST has to work — that's it. gitmaster_flash does not need
to be installed on the other machine: the script is piped over stdin, so the remote
only needs python3 and git, and both sides always run the exact same version (no
version drift to reason about). Works against Linux too.
It never changes anything — no fetching into your repos, no remotes added, nothing pushed. It tells you what differs; fixing is yours.
Tip: put your machines in ~/.ssh/config and add
ControlMaster auto / ControlPath ~/.ssh/cm-%C / ControlPersist 60s. Scanning many
repos opens many ssh connections at once, and the sshd default (MaxStartups 10:30:100)
drops some of them at random — which looks like a broken repo but isn't.
gitmaster_flash.py --list # colored text list
gitmaster_flash.py --json # machine-readable
gitmaster_flash.py --json --fetch # fetch each repo first
# Every output carries the version — so a diff of two machines' output shows
# whether the same build produced them:
# --list header: gitmaster_flash 0.6.0 · /Users/you/git · 61 repos
# --json (0.6.0+): {"version": "0.6.0", "root": "…", "repos": [ … ]}
# (before 0.6.0 --json printed a bare array)Exit code 0 means everything is clean and in sync, 1 means at least one repository needs attention. Without a TTY the tool prints the list instead of starting the UI, so a pipe does the sensible thing.
~/.config/gitmaster_flash/config.json, created on first run:
apps— key → application used to open a repository (macOSopen -a). The key shows up in the footer automatically, so{"Z": {"name": "Zed", "path": "/Applications/Zed.app"}}gives youZ Zed. Pick a key that is not already taken by the table above.sync_remote_names/sync_remote_hosts— how the private sync remote is recognized: by remote name, or by an exact normalized host in the remote URL (substring matches are never accepted). Defaults tooriginfor a generic installation. All remotes are displayed regardless; GitHub is recognized from its URL and sorted last.skip_dirs— directories the scan does not descend into.lang—"en","de", ornullto follow$LANG.git_timeout/fetch_timeout— seconds per git call.
python3 -m unittest discover -s testsThe logic (status parsing, heuristics, repo scan) is separated from the curses UI and tested headlessly against real temporary repositories.
A nod to Grandmaster Flash — the tool is mostly about quick cuts between many records.
WTFPL — see LICENSE.
