Gites is a safe multi-repo Git CLI for developers who manage many related repositories outside a monorepo. It gives a repo family one short name, shows a single table of branch/dirty/ahead/behind state, and can commit and push the eligible repositories with one deterministic checkpoint command.
Use it when you have a directory full of Git repos and need a practical way to
inspect, checkpoint, and push them without hand-running git status, git commit, and git push in every folder.
The interface is intentionally context-like: register a directory as a named
instance, list the instances, select the active one, then run short commands
against it. If you are used to Docker contexts or named environments, gites
applies that same small mental model to repo families.
- Define a directory of repositories as a named instance, then select it from a list.
- One command shows a table for every repo in a saved root.
- One command previews or applies a bulk Git checkpoint.
- Unsafe repos are refused instead of force-pushed or silently modified.
- Large repo roots are inspected in parallel with timeouts.
- Local run ledgers stay ignored and private.
- Works well for WSL users who keep large Git repo families on native Linux
paths instead of slow
/mnt/c/...checkouts.
Typical use cases include multi-repo project families, research/code archives, documentation surface repos, generated repo sets, and teams that want some of the operational convenience of a monorepo without merging repositories together.
The name carries the idea: Git plus easy, and also gites as in small houses
or lodgings. A gites root is meant to feel like a modest home for related
repositories: each repo keeps its own room, while the front door gives you one
place to check what changed and send the whole house home safely.
For regular CLI use, pipx is recommended:
pipx install gites
pipx upgrade gites --pip-args="--no-cache-dir"pip also works:
pip install gitesFor local development:
pip install -e .The normal workflow is to register a repo root once, give it a short name, and then operate on that named object.
Register the current directory as a repo root:
cd ~/repos/work
gites init --name work --branch mainOr register an explicit path:
gites init ~/repos/work --name work --branch mainList saved roots:
gites dirsSwitch active root:
gites use workShow the current repo-state table for a saved root:
gites view
gites view work
gites status work
gites workViewing a named instance prints the selected directory, optional progress, and then a single table for the repositories inside that instance:
$ gites view work
dir: work root: ~/repos/work branch: main
untracked scan: skipped (use --untracked for full scan)
Inspecting 4 repos with 15 worker(s)...
[1/4] api-service -> noop
[2/4] web-app -> sync
[3/4] docs-site -> noop
[4/4] local-tooling -> refuse
repo branch ahead behind dirty untracked action reason
------------- ------ ----- ------ ----- --------- ------ --------------------------------
api-service main 0 0 no no noop working tree clean
web-app main 1 0 yes no sync
docs-site main 0 0 no no noop working tree clean
local-tooling main 0 0 no no refuse missing origin remote
view, status, and object shortcuts inspect repositories in parallel, show
progress by default, use 15 workers, apply a 60 second per-Git-command timeout,
and skip expensive untracked-file enumeration unless requested:
gites view work
gites view work --untracked
gites view work --no-progress
gites view work --jobs 30 --timeout 120Commit and push eligible repositories:
gites push
gites push workpush applies by default and generates a per-repo checkpoint message when
-m/--message is omitted. Its output summarizes clean repositories in one line
and expands only the repositories that were pushed, refused, or failed:
$ gites push work
dir: work root: ~/repos/work branch: main mode: apply
Using generated per-repo commit messages.
run_id: 2b3389c7-6cf7-4cb6-86d5-2a7fdcf8d6b9
applied: yes
summary: clean=2, pushed=1, refused=1, failed=0
clean: 2 repo(s) had no changes.
pushed:
- web-app: 3 file(s): 2 modified, 1 untracked; 287d9c3ecebc -> 8f17ad280c44; commit: chore(gites): checkpoint 3 files
refused:
- local-tooling: no file changes; missing origin remote
Preview without committing or pushing:
gites push work --dry-runOverride the generated message for all synced repositories:
gites push work -m "chore: checkpoint repo family 2026-05-11"The saved directory config lives outside the repo at ~/.config/gites/config.json.
The status table is meant to be the main decision surface:
repo branch ahead behind dirty untracked action reason
-------------------- ------ ----- ------ ----- --------- ------ ------------------
api-service main 0 0 no no noop working tree clean
web-app main 1 0 yes no sync
local-tooling main 0 0 no no refuse missing origin remote
Actions:
noop: clean and already aligned with upstreamsync: eligible for commit and pushrefuse: not safe to modify automatically; read the reason column
gites refuses local-only repositories because it cannot safely push them until
they have an origin remote and an upstream branch.
For large repo roots on WSL, prefer native Linux paths such as:
~/repos/workAvoid using /mnt/c/... as the active root for very large repositories.
Windows-mounted paths can make git status and git commit slow because Git
must perform many metadata checks through the WSL-to-Windows filesystem bridge.
The same repo can be much faster when cloned or moved under the native WSL
filesystem.
The lower-level plan and sync commands remain available for scripts and
one-off roots. Most interactive use should prefer view and push.
Preview repositories under a root directory:
gites plan --root ~/repos/work --branch mainDry-run a checkpoint:
gites sync --root ~/repos/work --branch main --dry-runApply a checkpoint with an explicit commit message:
gites sync --root ~/repos/work \
--branch main \
--apply \
--message "chore: checkpoint repo family 2026-05-10"Read local run ledgers:
gites ledger list --root ~/repos/work
gites ledger show RUN_ID --root ~/repos/workLedgers are written under .gites/ledgers/ inside the selected root. That directory is intentionally ignored by Git.
gites sync --apply refuses a repository when it detects:
- detached
HEAD - merge, rebase, cherry-pick, or revert in progress
- unresolved conflicts
- wrong branch
- missing
origin - missing upstream branch
- branch behind upstream
- branch diverged from upstream
- protected path changes such as
.env,secrets/,private/, orinternal/ - changed files larger than the configured size limit
- missing commit message in apply mode
Gites never force-pushes by default.
Real manifests should stay local and ignored, for example my_gites.json or gites.local.json.
Create a local template:
gites config init my_gites.jsonValidate a manifest:
gites config validate my_gites.jsonUse a family from a manifest:
gites sync --manifest my_gites.json \
--family default \
--dry-runA sanitized public example is available at examples/example.gites.json.
Run tests:
python -m unittest discover -vDo not commit real manifests, ledgers, credentials, ChatGPT exports, private notes, or local editor state. The repository .gitignore blocks the intended local-only paths.
