Multi-Repository Git Manager
Run git operations across dozens of repositories in parallel — checkout, pull, commit, stash, reset, track — from one command.
- Parallel by default — 10 concurrent git ops, live-streamed output.
- Safe — never force-resets your work; dirty repos are skipped, not clobbered.
- Interactive TUI — multi-select repos and files with bubbletea.
- Self-updating (manual installs) —
gitm upgradepulls signed binaries from GitHub Releases on macOS/Linux manual installs. - Zero config — single SQLite file at
~/.gitm/gitm.db, no daemons.
When working across many repositories, daily git operations become repetitive:
| Without gitm | With gitm |
|---|---|
cd repo1 && git checkout main && git pull × 23 repos |
gitm checkout master |
| Checkout a feature branch in specific repos interactively | gitm checkout |
| Checkout a branch across all repos at once | gitm checkout feature/JIRA-12345 |
Manually cd into 6 repos to create a feature branch |
gitm branch create feature/JIRA-123 |
| Manually rename a branch in each repo + update remote | gitm branch rename old-name new-name |
| Forget which repos are dirty or behind origin | gitm status |
Manually cd into each repo to stage + commit + push |
gitm commit |
| Stash changes in specific repos before switching branches | gitm stash |
| Re-apply stashed work after switching back | gitm stash pop |
| Undo local commits and clean up history before pushing | gitm reset |
| Rewrite pushed history safely across multiple repos | gitm reset --hard --commits 2 then approve force-push |
brew tap alexandreafj/gitm
brew install --cask gitmscoop bucket add gitm https://github.com/alexandreafj/scoop-gitm
scoop install gitmAuto-detects your OS and architecture, downloads the binary, verifies the SHA-256 checksum, and installs to /usr/local/bin:
curl -fsSL https://raw.githubusercontent.com/alexandreafj/gitm/master/install.sh | shTo install a specific version or to a custom directory:
curl -fsSL https://raw.githubusercontent.com/alexandreafj/gitm/master/install.sh | VERSION=v1.0.12 INSTALL_DIR="$HOME/.local/bin" shFor manual macOS/Linux installs, gitm can update itself with signature verification:
gitm upgradeIf installed via a package manager, use the package manager upgrade flow instead:
brew upgrade --cask gitm # Homebrew (macOS)
scoop update gitm # Scoop (Windows)Pre-built binaries for all major platforms are also available on the GitHub Releases page.
| Platform | Binary |
|---|---|
| macOS (Apple Silicon) | gitm-macos-arm64 |
| macOS (Intel) | gitm-macos-x86_64 |
| Linux (x86_64) | gitm-linux-amd64 |
| Linux (ARM64) | gitm-linux-arm64 |
| Windows (x86_64) | gitm-windows-amd64.exe |
# Example: macOS Apple Silicon
curl -L https://github.com/alexandreafj/gitm/releases/latest/download/gitm-macos-arm64 -o gitm
chmod +x gitm
sudo mv gitm /usr/local/bin/gitm --help- Go 1.26+
gitavailable in yourPATH
# Clone the repository
git clone https://github.com/alexandreafj/gitm.git
cd gitm
# Build and install to GOPATH/bin
make install
# Verify installation
gitm --helpOn manual macOS/Linux installs, gitm upgrade verifies the signature on checksums.txt against this repo's release workflow before installing any new binary:
- The release workflow signs
checksums.txtwith cosign in keyless mode (OIDC-bound torelease.ymlon a tagged push). The signature, certificate, and Rekor transparency-log proof are bundled intochecksums.txt.bundleand uploaded with each release. gitm upgradedownloads the bundle, verifies it against Sigstore's public-good trust root, and aborts on any failure.- Releases that predate signing (older than this feature) fall back to SHA-256 verification with a warning.
To verify a manually-downloaded binary outside of gitm upgrade:
# Download the binary, checksums, and bundle for your release of choice
curl -L -O https://github.com/alexandreafj/gitm/releases/download/<tag>/gitm-macos-arm64
curl -L -O https://github.com/alexandreafj/gitm/releases/download/<tag>/checksums.txt
curl -L -O https://github.com/alexandreafj/gitm/releases/download/<tag>/checksums.txt.bundle
# Verify the signature on checksums.txt (requires cosign installed)
cosign verify-blob --bundle checksums.txt.bundle \
--certificate-identity-regexp '^https://github\.com/alexandreafj/gitm/\.github/workflows/release\.yml@refs/tags/v.*$' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
checksums.txt
# Then verify the binary against the (now trusted) checksums file
sha256sum -c checksums.txt --ignore-missingmake build
# Binary will be at ./bin/gitm
./bin/gitm --helpexport PATH="$PATH:/path/to/gitm/bin"
# Add the above to your ~/.bashrc or ~/.zshrc to make it permanent# 1. Register your repositories
gitm repo add /path/to/api-gateway
gitm repo add /path/to/auth-service /path/to/frontend /path/to/payment-svc
# Add with a custom alias (useful when two repos share the same directory name)
gitm repo add /path/to/www-api/v1 --alias www-api-v1
gitm repo add /path/to/docs-api/v1 --alias docs-api-v1
# Or register the current directory
cd /path/to/my-repo && gitm repo add .
# Got a folder full of repos? Register them all at once
gitm repo add /path/to/projects --auto-detect
# 2. See all registered repos
gitm repo list
# 3. Sync everything to the default branch (main/master) and pull
gitm checkout master
# 4. Start a new task — create a branch in selected repos interactively
gitm branch create feature/JIRA-456
# 5. Check what all your repos are doing
gitm status
# 6. Pull the latest on whatever branch each repo is currently on
gitm updateRegister one or more local git repositories with gitm.
gitm repo add <path> [path...]
Arguments:
| Argument | Description |
|---|---|
<path> |
Absolute or relative path to a git repository. Use . for the current directory. |
Flags:
| Flag | Default | Description |
|---|---|---|
--alias |
(directory name) | Custom display name for the repository. Must be unique across all registered repos. Useful when two repos share the same directory name (e.g. two repos both named v1). Cannot be combined with --auto-detect. |
--auto-detect |
false | Scan the immediate subdirectories of the given path and register every git repository found. Skips plain directories and hidden directories (names starting with .). Cannot be combined with --alias. |
--depth |
1 | How many directory levels to scan when using --auto-detect. Use --depth 2 when repos are nested inside grouping folders (e.g. project/v1). Requires --auto-detect. |
Examples:
# Add a single repo
gitm repo add /home/user/work/api-gateway
# Add multiple repos at once
gitm repo add /home/user/work/api-gateway /home/user/work/auth-service /home/user/work/frontend
# Add the current directory
gitm repo add .
# Add two repos that share the same directory name using aliases
gitm repo add /home/user/work/www-api/v1 --alias www-api-v1
gitm repo add /home/user/work/docs-api/v1 --alias docs-api-v1
# Scan a parent folder and register every git repo found inside it
gitm repo add /home/user/work --auto-detect
# Scan two levels deep to find repos in subfolders (e.g. api-group/v1, api-group/v2)
gitm repo add /home/user/work --auto-detect --depth 2--auto-detect example output:
$ gitm repo add /home/user/work --auto-detect
Found 4 git repository(ies) in /home/user/work
✓ added api-gateway (default branch: main)
✓ added auth-service (default branch: master)
✓ added frontend (default branch: main)
✓ added payment-svc (default branch: main)
4 repository(ies) registered. Run `gitm repo list` to see all.
If some repos are already registered, they are reported as skipped (⚠) and do not cause an error:
$ gitm repo add /home/user/work --auto-detect
Found 4 git repository(ies) in /home/user/work
✓ added auth-service (default branch: master)
⚠ /home/user/work/api-gateway: already registered as "api-gateway"
⚠ /home/user/work/frontend: already registered as "frontend"
✓ added payment-svc (default branch: main)
2 repository(ies) registered. Run `gitm repo list` to see all.
Behaviour:
- Validates that the path exists and is a git repository (
git rev-parsecheck). - Auto-detects the default branch (
mainormaster) by inspectingorigin/HEAD. - The alias (display name) defaults to the directory base name. Use
--aliasto override — this is required when two repos share the same directory name. - If the alias is already taken by another path, prints a clear error with a suggested
--aliascommand. - Stores the alias, path, and default branch in
~/.gitm/gitm.db. - With
--auto-detect: scans subdirectories up to--depthlevels deep (default 1). When a git repo is found, its children are not scanned. Hidden directories (.git,.cache, etc.) are always skipped at every level.
List all registered repositories.
gitm repo list
Example output:
# ALIAS DEFAULT BRANCH PATH
1 api-gateway main /home/user/work/api-gateway
2 auth-service master /home/user/work/auth-service
3 docs-api-v1 master /home/user/work/docs-api/v1
4 frontend main /home/user/work/frontend
5 www-api-v1 main /home/user/work/www-api/v1
5 repository(ies) registered.
Unregister a repository by alias. This only removes it from gitm's database — it does not delete the repository from disk.
gitm repo remove <alias>
gitm repo rm <alias> # alias
Arguments:
| Argument | Description |
|---|---|
<alias> |
The repository alias as shown in gitm repo list. |
Examples:
gitm repo remove api-gateway
gitm repo rm www-api-v1Rename a registered repository's alias without removing and re-adding it. Useful for fixing ambiguous names on already-registered repos.
gitm repo rename <old-alias> <new-alias>
Arguments:
| Argument | Description |
|---|---|
<old-alias> |
The current alias as shown in gitm repo list. |
<new-alias> |
The new alias to use. Must not already be in use. |
Examples:
# Fix an ambiguous name after registration
gitm repo rename v1 www-api-v1
gitm repo rename v2 www-api-v2Switch repositories to a branch and pull. Three modes of operation. Runs in parallel.
gitm checkout [branch] [--repo alias1,alias2]
Modes:
| Invocation | Behaviour |
|---|---|
gitm checkout (no args) |
Interactive: multi-select repos, then type a branch name |
gitm checkout master or gitm checkout main |
Switch all repos to their configured default branch + pull |
gitm checkout <branch-name> |
Check out <branch-name> in all repos; skip with warning where it doesn't exist |
Flags:
| Flag | Shorthand | Description |
|---|---|---|
--repo |
-r |
Limit checkout to specific repository aliases (comma-separated) |
Behaviour (all modes):
- Repositories with uncommitted tracked changes are skipped (untracked files like
AGENTS.mdare safely ignored). - Branch existence is checked locally first, then on the remote — skipped with a warning if neither has it.
- After checkout, runs
git pull --ff-only. - Streams results live with a final summary.
Example — default branch:
$ gitm checkout master
Checking out default branch and pulling for 4 repositories…
[api-gateway ] ✓ on main — already up to date
[auth-service ] ✓ on master — 3 files changed, 47 insertions(+)
[frontend ] ⚠ SKIPPED: uncommitted changes (2 file(s)): M src/App.tsx, M package.json
[payment-svc ] ✓ on main — already up to date
Done: 3 succeeded, 1 skipped
Example — specific repos only:
$ gitm checkout master --repo=api-gateway,auth-service
Checking out default branch and pulling for 2 repositories…
[api-gateway ] ✓ on main — already up to date
[auth-service ] ✓ on master — 3 files changed, 47 insertions(+)
Done: 2 succeeded, 0 skipped
Example — specific branch:
$ gitm checkout feature/JIRA-12345
Checking out branch "feature/JIRA-12345" in 4 repositories…
[api-gateway ] ✓ on feature/JIRA-12345 — already up to date
[auth-service ] ✓ on feature/JIRA-12345 — pulled
[frontend ] ⚠ SKIPPED: branch "feature/JIRA-12345" not found (local or remote)
[payment-svc ] ⚠ SKIPPED: uncommitted changes (1 file(s))
Done: 2 succeeded, 2 skipped
Example — interactive:
$ gitm checkout
Select repositories to checkout
↑/↓ or j/k to move • space to toggle • a to select all • enter to confirm • q/esc to cancel
▶ [✓] api-gateway /home/user/work/api-gateway
[✓] auth-service /home/user/work/auth-service
[ ] frontend /home/user/work/frontend
2/3 selected
Branch to checkout
Type the branch name • enter to confirm • esc to cancel
feature/JIRA-12345
Checking out branch "feature/JIRA-12345" in 2 repositories…
Create a new branch in selected repositories. An interactive multi-select UI lets you choose which repositories to apply the operation to. Use --repo to skip the UI entirely and target specific repositories by alias.
gitm branch create <branch-name> [flags]
Arguments:
| Argument | Description |
|---|---|
<branch-name> |
The name of the new branch to create. |
Flags:
| Flag | Short | Default | Description |
|---|---|---|---|
--all |
-a |
false | Skip the selection UI and apply to all registered repositories. |
--from |
-f |
(repo default branch) | Base branch to create from instead of the repo's default branch. |
--repo |
-r |
(none) | Comma-separated list of repository aliases to target. Bypasses the interactive selection UI. Takes precedence over --all. |
Interactive UI:
When you run gitm branch create feature/JIRA-123, you'll see:
Select repositories for new branch: feature/JIRA-123
↑/↓ or j/k to move • space to toggle • a to select all • enter to confirm • q/esc to cancel
[ ] api-gateway /home/user/work/api-gateway
▶ [✓] auth-service /home/user/work/auth-service
[✓] frontend /home/user/work/frontend
[ ] payment-svc /home/user/work/payment-svc
2/4 selected
Keybindings:
| Key | Action |
|---|---|
↑ / k |
Move cursor up |
↓ / j |
Move cursor down |
Space |
Toggle selection |
a |
Select / deselect all |
Enter |
Confirm selection and proceed |
q / Esc |
Cancel |
Behaviour:
- For each selected repo (in parallel):
- Checks for uncommitted changes — skips if dirty.
- Checks out the base branch and pulls latest.
- Creates and checks out the new branch (
git checkout -b <branch-name>). - If the branch already exists, checks it out instead of failing.
- Streams results live.
Examples:
# Interactive selection
gitm branch create feature/JIRA-456
# Create in all repos without prompting
gitm branch create feature/JIRA-456 --all
# Create only in specific repos by alias (no prompt)
gitm branch create feature/AA-1 --repo api-gateway,auth-service
# Create from a specific base branch
gitm branch create hotfix/critical-bug --from develop
# Target specific repos and use a custom base branch
gitm branch create feature/AA-1 --repo api-gateway --from developRename a branch across selected repositories — both locally and on the remote. Runs in parallel.
gitm branch rename <old-name> <new-name> [flags]
Arguments:
| Argument | Description |
|---|---|
<old-name> |
The current branch name. |
<new-name> |
The new branch name. |
Flags:
| Flag | Short | Default | Description |
|---|---|---|---|
--all |
-a |
false | Apply to all repositories that have the old branch. |
--no-remote |
— | false | Only rename locally. Skip deleting the old remote branch and pushing the new one. |
--repo |
-r |
(none) | Comma-separated list of repository aliases to target. Bypasses the interactive selection UI. Takes precedence over --all. |
Behaviour:
- Filters the registered repositories to only those that have a local branch named
<old-name>. - Opens the interactive multi-select UI showing only the matching repositories.
- For each selected repo (in parallel):
git branch -m <old-name> <new-name>— renames locally.git push origin --delete <old-name>— deletes the old remote branch (if it exists).git push --set-upstream origin <new-name>— pushes the new branch and sets tracking.
- Streams results live.
Examples:
# Interactive: rename feature/JIRA-123 to feature/JIRA-456 in selected repos
gitm branch rename feature/JIRA-123 feature/JIRA-456
# Apply to all repos that have the branch
gitm branch rename feature/JIRA-123 feature/JIRA-456 --all
# Rename only in specific repos by alias (no prompt)
gitm branch rename feature/JIRA-123 feature/JIRA-456 --repo api-gateway,auth-service
# Local rename only (skip remote)
gitm branch rename old-name new-name --no-remoteExample output:
Renaming "feature/JIRA-123" → "feature/JIRA-456" in 2 repository(ies)…
[auth-service ] ✓ renamed feature/JIRA-123 → feature/JIRA-456 (local + remote)
[frontend ] ✓ renamed feature/JIRA-123 → feature/JIRA-456 (local + remote)
Done: 2 succeeded
Show a summary of all registered repositories: current branch, dirty state, and commits ahead/behind origin. Runs in parallel with no network calls by default.
gitm status [flags]
Flags:
| Flag | Default | Description |
|---|---|---|
--fetch |
false | Run git fetch on all repos first for up-to-date remote numbers (slower, requires network). |
Example output (fast mode, no network):
Collecting status for 11 repositories…
REPO BRANCH DIRTY REMOTE
────────────────────────────────────────────────────────────────────────────────
api-gateway feature/PROJ-101 2 modified up to date
auth-service feature/PROJ-202 1 modified up to date
billing master clean 14 behind
data-pipeline feature/PROJ-101 1 modified up to date
frontend master 1 modified up to date
notifications feature/PROJ-303 clean up to date
payments feature/PROJ-101 2 modified up to date
reporting master 2 modified up to date
search master 12 modified 4 behind
user-service feature/PROJ-303 1 modified up to date
worker master 1 modified up to date
Column descriptions:
| Column | Description |
|---|---|
REPO |
Repository name |
BRANCH |
Currently checked-out branch |
DIRTY |
clean if no uncommitted changes; otherwise shows the number of modified files |
REMOTE |
Commits ahead/behind origin. Based on the last known remote state (no network call). Use --fetch for current numbers. |
Examples:
# Fast: instant, uses cached remote tracking info
gitm status
# Accurate: fetch from origin first, then show status (takes a few seconds)
gitm status --fetchPerformance note: By default,
gitm statusis near-instant because it doesn't fetch from origin. The ahead/behind numbers reflect the last known state of remote branches. Use--fetchif you need up-to-the-second accuracy from the remote.
Interactively select which repositories and which files to discard uncommitted changes in. Only repositories that actually have changes are shown in the selection list — if none of your repos have uncommitted changes, the command exits immediately with a message.
gitm discard [flags]
WARNING: This operation is irreversible. Discarded changes cannot be recovered.
Flags:
| Flag | Short | Default | Description |
|---|---|---|---|
--repo |
-r |
(all repos) | Limit to specific repository aliases (comma-separated), bypasses interactive repo selection. |
What it does per selected file (based on status):
| File status | Git command(s) | Effect |
|---|---|---|
Modified tracked ( M, M , MM) |
git reset HEAD -- <file> + git checkout -- <file> |
Reverts to last committed version |
Staged new file (A ) |
git reset HEAD -- <file> + git clean -fd -- <file> |
Unstages and removes the file |
Untracked file/dir (??) |
git clean -fd -- <file> |
Removes the file or directory |
Behaviour:
- Scans all registered repositories (or those specified with
--repo) for uncommitted changes. - If none are dirty, prints
Nothing to discard — all repositories are clean.and exits. - If some are dirty, shows a summary of how many files each has modified, then opens the interactive multi-select showing only the dirty repos (skipped when
--repois used). - For each selected repo, opens a file picker where you choose exactly which files to discard. No files are pre-selected — you must explicitly pick every file you want gone.
- Discards only the selected files in each repo.
- Prints a per-repo summary.
Example flow:
3 repositories with uncommitted changes:
api-gateway 2 file(s) modified
auth-service 5 file(s) modified
frontend 12 file(s) modified
WARNING: Select repositories to discard changes in (irreversible)
↑/↓ or j/k to move • space to toggle • a to select all • enter to confirm • q/esc to cancel
[ ] api-gateway /home/user/work/api-gateway
▶ [✓] auth-service /home/user/work/auth-service
[ ] frontend /home/user/work/frontend
1/3 selected
After selecting a repo, the file picker appears:
━━━ auth-service ━━━
Select files to discard in auth-service (irreversible)
↑/↓ or j/k to move • space to toggle • a to select all • enter to confirm • q/esc to cancel
▶ [ ] M src/handler.go
[✓] M src/config.go
[ ] ?? tmp/debug.log
[✓] A src/new_service.go
2/4 selected
After confirming:
✓ Discarded 2 file(s):
src/config.go
src/new_service.go
Summary
───────────────────────
✓ auth-service (2 file(s) discarded)
1 discarded 0 skipped 0 failed
Example with --repo flag:
$ gitm discard --repo api-gateway
$ gitm discard -r api-gateway,auth-service
Example when nothing to discard:
$ gitm discard
Nothing to discard — all repositories are clean.
Pull the latest changes on the current branch of every repository in parallel. Unlike checkout master, this does not switch branches.
gitm update [flags]
Flags:
| Flag | Short | Default | Description |
|---|---|---|---|
--repo |
-r |
(all repos) | Limit update to specific repository aliases (comma-separated). |
Behaviour:
- If
--repois specified, only the listed repos are updated. Otherwise, all registered repos are updated. - For each repository (in parallel):
- Checks for uncommitted changes — skips if dirty.
- Runs
git pull --ff-onlyon the current branch.
- Streams results live with a summary.
- If a
--repoalias doesn't match any registered repository, the command exits with an error before pulling anything.
Use case: You've been working on a feature branch for a while and want to pull in the latest changes your teammates pushed to the same branch across multiple repos.
Examples:
# Update all registered repos
gitm update
# Update a single repo by alias
gitm update --repo=api-gateway
# Update multiple specific repos
gitm update --repo=api-gateway,auth-service
# Short form
gitm update -r api-gateway,auth-serviceExample output:
Pulling current branch for 4 repositories…
[api-gateway ] ✓ on main — already up to date
[auth-service ] ✓ on feature/JIRA-456 — pulled
[frontend ] ⚠ SKIPPED: uncommitted changes — stash or commit first
[payment-svc ] ✓ on main — already up to date
Done: 3 succeeded, 1 skipped
Interactively stage files and commit across dirty repositories. Walks you through each selected repository sequentially — pick files, write a message, and push. Use --repo to skip the selection UI and target specific repositories by alias.
gitm commit [flags]
Flags:
| Flag | Short | Default | Description |
|---|---|---|---|
--no-push |
— | false | Commit but skip git push after each commit. |
--repo |
-r |
(none) | Comma-separated list of repository aliases to target. Bypasses the interactive multi-select UI. Non-dirty repos in the list are silently skipped. |
What it does:
- Scans the registered repositories (all, or only those in
--repo) for uncommitted changes. - Filters to dirty repos only — repos on their default branch are shown but marked
⛔ protected branchand cannot be selected. - Multi-select UI — pick which repos you want to commit. (Skipped when
--repois provided — all dirty, unprotected matches proceed automatically.) - For each selected repo, sequentially:
- File picker — shows all dirty files with colour-coded status prefixes (yellow
M, greenA, redD, dim??). Nothing is pre-selected. - Commit message input — single-line text input; rejects empty messages.
git add -- <selected files>git commit -m "<message>"git push --set-upstream origin <branch>(skipped with--no-push)- Live result printed per repo.
- File picker — shows all dirty files with colour-coded status prefixes (yellow
- Final summary —
N committed, N skipped, N failed.
Example flow:
Scanning repositories for uncommitted changes…
Select repositories to commit
↑/↓ or j/k to move • space to toggle • a to select all • enter to confirm • q/esc to cancel
[ ] api-gateway /home/user/work/api-gateway
▶ [ ] auth-service /home/user/work/auth-service
[ ] main-repo /home/user/work/main-repo ⛔ protected branch
0/2 selected
After selecting repos, for each one:
━━━ auth-service ━━━
Select files to stage for auth-service
↑/↓ or j/k to move • space to toggle • a to select all • enter to confirm • q/esc to cancel
[✓] M src/auth/login.go
[ ] ?? scratch.txt
1/2 selected
Commit message for auth-service
Type your commit message • enter to confirm • esc to cancel
fix: correct token expiry check
✓ Staged 1 file(s)
✓ Committed: [feature/JIRA-456 3a4b5c6] fix: correct token expiry check
✓ Pushed
Final summary:
Summary
───────────────────────
✓ auth-service (committed + pushed)
~ api-gateway
1 committed 0 skipped 0 failed
Protected branch behaviour:
Repos that are currently on their configured default branch (e.g. main or master) are shown greyed out in the selection list with an ⛔ protected branch label and cannot be toggled. This prevents accidental direct commits to the default branch.
Examples:
# Interactive commit + push
gitm commit
# Interactive commit, skip push
gitm commit --no-push
# Commit only specific repos by alias (no selection prompt)
gitm commit --repo api-gateway,auth-service
# Commit specific repos and skip push
gitm commit --repo api-gateway --no-pushManage git stashes across selected repositories. All subcommands require you to select repos via the interactive multi-select TUI before anything happens.
gitm stash
gitm stash apply
gitm stash pop
gitm stash list
Scans all repos for uncommitted changes (including untracked files), shows only dirty repos in the multi-select, then runs git stash push --include-untracked with an auto-generated message on each selected repo in parallel.
$ gitm stash
Scanning repositories for uncommitted changes…
Select repositories to stash
↑/↓ or j/k • space to toggle • a to select all • enter to confirm • q/esc to cancel
▶ [✓] repo1 /home/user/work/repo1
[ ] repo2 /home/user/work/repo2
1/2 selected
Stashing changes in 1 repository(ies)…
[repo1 ] ✓ stashed (gitm stash on feature/JIRA-456)
Done: 1 succeeded
Scans all repos for stash entries, shows only repos with stashes in the multi-select, then runs git stash apply (keeps the stash) on each selected repo in parallel.
Same as apply, but runs git stash pop — applies and removes the stash entry.
Prints a table of all repos that have stash entries, with the count and top stash message.
$ gitm stash list
REPO STASHES TOP STASH
────────────────────────────────────────────────────
repo1 1 On feature/JIRA-456: gitm stash on feature/JIRA-456
repo2 2 On master: gitm stash on master
2 repository(ies) with stash entries.
Undo the last N commits across selected repositories in three safe modes: soft, mixed (default), or hard. Perfect for undoing local commits before pushing.
gitm reset [flags]
Modes:
| Mode | Effect | Use case |
|---|---|---|
--soft |
Moves HEAD back; keeps changes staged and ready to re-commit | Squash, amend, or reorganize commits before pushing |
| (default, mixed) | Moves HEAD back; unstages changes but keeps them in working tree | Undo commits and re-stage selectively |
--hard |
Moves HEAD back AND discards all changes irreversibly | Completely discard unwanted commits and changes |
Flags:
| Flag | Default | Description |
|---|---|---|
--commits |
1 | Number of commits to undo (reset back N commits) |
--soft |
false | Keep changes staged after reset |
--hard |
false | Discard all changes (irreversible) |
⚠️ WARNING:--hardis irreversible. Discarded changes cannot be recovered. Only use this when you're certain.
Pre-flight Check:
Before applying the reset:
- Shows a summary table of:
- Each repository and the commits that will be undone (by hash + message)
- Which commits are already pushed to origin (
⚠️ red flag)
- Opens the interactive multi-select UI to choose which repos to reset
- If any commits are already pushed, you'll be prompted once: "Force-push to clean remote history? [y/N]"
--force-with-leaseis used (the safest form) to rewrite history safely- Only offered if you own those branches (careful: shared branches will break teammates' clones!)
Behaviour:
-
For each selected repo (in parallel):
- Moves HEAD back N commits
- Applies the chosen reset mode (soft/mixed/hard)
- Reports which commits were undone
-
If any undone commits were already pushed:
- You're warned with a red caution box
- Prompted once for all repos: approve or skip the force-push
- If approved:
git push --force-with-leaseis used to rewrite remote history
Examples:
# Undo last commit, keep changes staged (safest)
gitm reset --soft
# Undo last commit, unstage changes, keep in working tree (default)
gitm reset
# Undo last 3 commits, unstage changes
gitm reset --commits 3
# Undo last 2 commits, discard all changes (IRREVERSIBLE)
gitm reset --hard --commits 2Example flow:
$ gitm reset --commits 2
Mode: mixed — HEAD moves back; changes are unstaged but kept in working tree
Scope: last 2 commit(s) per repository
api-gateway [2 commit(s) already pushed — remote will need force-push]
↩ a1b2c3d feat: add authentication
↩ e4f5g6h fix: correct token validation
Select repositories to reset
↑/↓ or j/k to move • space to toggle • a to select all • enter to confirm • q/esc to cancel
[✓] api-gateway /home/user/work/api-gateway
1/1 selected
Applying mixed reset (HEAD~2) to 1 repository(ies)…
[api-gateway ] ✓ mixed reset — undid 2 commit(s):
↩ a1b2c3d feat: add authentication
↩ e4f5g6h fix: correct token validation
changes are unstaged but present in the working tree
Done: 1 succeeded
┌──────────────────────────────────────────────────────┐
│ CAUTION: Remote history rewrite │
│ │
│ 1 of the reset repo(s) had already-pushed commits │
│ undone. Force-pushing will rewrite the remote │
│ branch history. Anyone who has already pulled │
│ those commits will need to hard-reset their local │
│ branch. Only do this on branches that you own and │
│ no one else is using. │
└──────────────────────────────────────────────────────┘
The following 1 repo(s) will be force-pushed:
api-gateway /home/user/work/api-gateway
Force-push to clean remote history? [y/N] y
Force-pushing 1 repository(ies)…
[api-gateway ] ✓ force-pushed branch master to origin
Done: 1 succeeded
Start tracking untracked files across multiple repositories. Only repositories with untracked files are shown in the selection list.
gitm track [flags]
Flags:
| Flag | Short | Default | Description |
|---|---|---|---|
--repo |
-r |
(all repos) | Limit to specific repository aliases (comma-separated). |
What it does:
- Scans all registered repositories for untracked files.
- If none have untracked files, prints a message and exits.
- Shows a summary of how many untracked files each repo has.
- Opens the interactive multi-select to choose which repos to track files in.
- For each selected repo, opens the file picker showing only untracked files.
- Runs
git addon the selected files.
Example flow:
$ gitm track
2 repositories with untracked files:
api-gateway 3 untracked file(s)
auth-service 1 untracked file(s)
Select repositories to track files in
↑/↓ or j/k to move • space to toggle • a to select all • enter to confirm • q/esc to cancel
▶ [✓] api-gateway /home/user/work/api-gateway
[ ] auth-service /home/user/work/auth-service
1/2 selected
Select files to track for api-gateway
↑/↓ or j/k to move • space to toggle • a to select all • enter to confirm • q/esc to cancel
[✓] ?? src/new-handler.go
[✓] ?? src/new-handler_test.go
[ ] ?? scratch.txt
2/3 selected
[api-gateway ] ✓ tracked 2 file(s)
Done: 1 succeeded
Examples:
# Interactive — select repos and files
gitm track
# Track files only in specific repos by alias
gitm track --repo api-gateway,auth-serviceStop tracking files across multiple repositories. Files are removed from the git index but remain on disk. This is useful for accidentally committed files like .env, logs, or build artifacts.
gitm untrack [flags]
Flags:
| Flag | Short | Default | Description |
|---|---|---|---|
--repo |
-r |
(all repos) | Limit to specific repository aliases (comma-separated). |
--path |
-p |
(all files) | Filter files by glob pattern or path prefix (e.g. "*.env", "public/"). |
What it does:
- Opens the interactive multi-select to choose which repos to untrack files from.
- For each selected repo, shows tracked files in the file picker (filtered by
--pathif provided). - Runs
git rm --cachedon the selected files — removes from git's index only. - The files remain on disk untouched.
Tip: After untracking a file, add it to
.gitignoreto prevent it from being tracked again.
Example flow:
$ gitm untrack
Select repositories to untrack files from
↑/↓ or j/k to move • space to toggle • a to select all • enter to confirm • q/esc to cancel
▶ [✓] api-gateway /home/user/work/api-gateway
[ ] auth-service /home/user/work/auth-service
1/1 selected
Select files to untrack for api-gateway (files stay on disk)
↑/↓ or j/k to move • space to toggle • a to select all • enter to confirm • q/esc to cancel
[ ] T go.mod
[ ] T go.sum
[✓] T .env
[✓] T debug.log
2/15 selected
[api-gateway ] ✓ untracked 2 file(s)
Done: 1 succeeded
Examples:
# Interactive — select repos and files
gitm untrack
# Untrack files only in specific repos by alias
gitm untrack --repo api-gateway
# Filter to only show .env files
gitm untrack --path "*.env"
# Filter to only show files under public/
gitm untrack --path "public/"
# Combine repo and path filters
gitm untrack --repo api-gateway --path "*.log"Self-update gitm to the latest release from GitHub for manual macOS/Linux installs. Downloads the correct binary for your platform, verifies the checksum, and replaces the current binary — no manual download needed.
gitm upgrade
If you installed gitm with a package manager, use:
brew upgrade --cask gitm # Homebrew (macOS)
scoop update gitm # Scoop (Windows)What it does:
- Queries the GitHub Releases API for the latest version.
- Compares against the currently installed version (
gitm --version). - Detects your OS and architecture to download the correct binary.
- Downloads the binary and
checksums.txt, then verifies SHA256 integrity. - Atomically replaces the current binary (backs up the old one, swaps in the new one).
- Sets executable permissions on Linux/macOS (
chmod 755).
Supported platforms:
| Platform | Binary |
|---|---|
| macOS (Apple Silicon) | gitm-macos-arm64 |
| macOS (Intel) | gitm-macos-x86_64 |
| Linux (x86_64) | gitm-linux-amd64 |
| Linux (ARM64) | gitm-linux-arm64 |
| Windows (x86_64) | gitm-windows-amd64.exe |
Example — upgrade available:
$ gitm upgrade
Checking for updates... found v1.1.0
Downloading gitm-macos-arm64... done
Verifying checksum... ok
Updated gitm: v1.0.6 → v1.1.0
Example — already up to date:
$ gitm upgrade
Checking for updates... already up to date (v1.1.0)
Version check:
# See your current version
gitm --versionNote: This command does not require database access — it works even if
~/.gitm/gitm.dbdoesn't exist yet.Note:
gitm upgradeis disabled for package-managed installs (Homebrew/Scoop) and disabled on Windows.
Every multi-repo operation uses a concurrent worker pool (golang.org/x/sync/errgroup) with a default concurrency limit of 10 parallel git operations. Results are streamed to the terminal as each operation completes, so you don't wait for a slow repository to see the others' results.
gitm status is optimized for speed:
- By default, it does not fetch from origin, making it nearly instant (~2 seconds for 11 repos) because it only reads local git state and uses cached remote tracking info.
- Use the
--fetchflag if you need accurate up-to-the-second ahead/behind numbers from the remote (requires network calls).
Why this matters: When you're checking the status of 20+ repos multiple times a day, you want it to be fast. The cached remote state is accurate enough for most daily workflows — you only need --fetch when preparing to merge or push.
gitm auto-detects each repository's default branch using this fallback chain:
git symbolic-ref refs/remotes/origin/HEAD— reads what origin considers the default.- Checks if a local branch named
mainexists. - Checks if a local branch named
masterexists. - Falls back to the current
HEADbranch.
This is stored in the SQLite database when the repo is added and used by checkout master and branch create.
gitm never force-resets or stashes your work. If a repository has uncommitted changes when a checkout or pull is attempted, it is skipped and reported in the summary. Your work is always safe.
gitm stores repository configuration in a SQLite database at:
~/.gitm/gitm.db
The database is created automatically on first run. It contains a single table:
CREATE TABLE repositories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL, -- auto-detected directory name
alias TEXT NOT NULL UNIQUE, -- display name (user-controlled)
path TEXT NOT NULL UNIQUE, -- absolute path
default_branch TEXT NOT NULL, -- auto-detected: main or master
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);To back up or migrate your configuration:
# Backup
cp ~/.gitm/gitm.db ~/gitm-backup.db
# Move to a new machine: copy the binary and the .db file
scp ~/.gitm/gitm.db newmachine:~/.gitm/gitm.db# Run all tests with race detection
make test
# Run tests verbosely
go test ./... -v -race -timeout 60s
# Run a specific package's tests
go test ./internal/cli/... -v -race
# Run a single test by name
go test ./internal/cli/... -v -race -run TestResetSoft| Metric | Count |
|---|---|
| Test files | 38 |
| Test functions | 330 |
| Language | Go |
cli-git-commands/
├── cmd/
│ └── gitm/
│ └── main.go # Entry point
├── internal/
│ ├── cli/
│ │ ├── root.go # Root cobra command
│ │ ├── repo.go # repo add/list/remove/rename
│ │ ├── checkout.go # checkout master
│ │ ├── branch.go # branch create/rename
│ │ ├── status.go # status
│ │ ├── update.go # update
│ │ ├── discard.go # discard
│ │ ├── commit.go # commit
│ │ ├── stash.go # stash / stash apply / stash pop / stash list
│ │ ├── reset.go # reset --soft / --hard with force-push support
│ │ ├── track.go # start tracking untracked files
│ │ ├── untrack.go # stop tracking files (git rm --cached)
│ │ └── upgrade.go # self-update from GitHub releases
│ ├── config/
│ │ └── config.go # App config & data dir
│ ├── db/
│ │ ├── db.go # SQLite connection & migrations
│ │ └── repository.go # Repository CRUD
│ ├── git/
│ │ └── git.go # Git operations
│ ├── runner/
│ │ └── parallel.go # Parallel execution engine
│ └── tui/
│ ├── multiselect.go # Bubbletea multi-select UI (with disabled-item support)
│ ├── fileselect.go # File picker UI (porcelain status, colour-coded)
│ └── textinput.go # Single-line commit message input
├── Makefile
├── go.mod
├── go.sum
└── README.md
make build # Compile to ./bin/gitm
make install # Install to GOPATH/bin
make test # Run all tests with race detector
make lint # Run go vet + staticcheck
make clean # Remove ./bin/
make tidy # Tidy go.mod
make help # Show all targets- Create a feature branch:
git checkout -b feat/<command-name>. - Create
internal/cli/<command>.go. - Define a function
func <command>Cmd() *cobra.Command. - Register it in
internal/cli/root.goby addingroot.AddCommand(<command>Cmd()). - If the command doesn't need DB access, add its name to the skip list in
PersistentPreRunE. - Create
internal/cli/<command>_test.gowith unit tests (real git repos, no mocks). - Update this
README.md: add to Table of Contents, Commands Reference, and Project Structure. - Run
make test && make lintbefore committing.
| Package | Version | Purpose |
|---|---|---|
github.com/spf13/cobra |
v1.x | CLI framework |
modernc.org/sqlite |
v1.x | Pure-Go SQLite (no CGO) |
github.com/charmbracelet/bubbletea |
v1.x | TUI framework |
github.com/charmbracelet/bubbles |
v1.x | TUI components |
github.com/charmbracelet/lipgloss |
v1.x | Terminal styling |
github.com/fatih/color |
v1.x | Colored output |
golang.org/x/sync |
latest | errgroup for parallel ops |
See AGENTS.md for the development workflow, coding standards, and the (short) list of approved dependencies. Briefly: feature branch off master, real git in tests (no mocks), make lint && make test before pushing.
MIT — see the LICENSE file for details.