High-level branch workflows for Git — simpler, forge-agnostic.
Project description
git-city
High-level branch workflows for Git — like git-town, but simpler, forge-agnostic, written in Python, and obsessed with safety and UX.
git-city automates the recipes you retype every day — start a feature off an up-to-date trunk, bring my branch (or my whole stack) up to date, merge this branch down and clean up — as a handful of boring, guessable commands. Every command previews with --dry-run and reverses with undo.
0.5.0 — first public release. The local workflow (branches, stacks, sync, land, undo) is complete and heavily hardened. Forge integration (pull/merge requests, patch series) is intentionally not built yet — git-city works fully with only a plain Git remote, or none at all. See CHANGES.md.
Why
Git's plumbing is great; its porcelain for branch workflows is not. git-town solved this years ago, but it's tailored toward forge-centric, pull-request workflows and is opinionated in directions you may not share (seven branch types, ~30 commands). git-city keeps git-town's genuinely good ideas and sheds the rest:
- Forge-agnostic core. A forge is never required. Every command works on a plain Git remote — or no remote.
- Two branch types, not seven.
trunkandfeature. That's the whole mental model. - Rebase-first. Branches and stacks stay linear; syncing rebases onto the parent with
--force-with-lease. - Bulletproof undo. Every command is planned as a list of reversible steps, so
git city undoreverses the last one — and never silently destroys uncommitted work or a remote branch it didn't create. - Boring, guessable commands.
new,sync,land,switch— nothack,append,prepend.
Install
Requires Python ≥ 3.12 and Git. Built with uv.
From source:
git clone <this-repo> && cd git-city
uv tool install . # installs the `git-city` executable on your PATH
Because the executable is named git-city, Git automatically exposes it as a subcommand — so git city <command> and git-city <command> are equivalent. The docs use git city.
Optionally, set up tab-completion for your shell (bash, zsh, or fish):
git city completions --install # or: git city completions zsh > /path/to/completions
For development, skip the install and run it in place:
uv sync
uv run git-city # or: uv run python -m git_city
Quick start
git city init # record your trunk branch (interactive, or --trunk main)
git city new add-login # branch off an up-to-date trunk
# ...hack, commit...
git city sync # main moved on? rebase add-login onto it (and push)
git city land # fast-forward main to add-login, push, delete the branch
See where you are at any time:
$ git city info
git-city · my-project
trunk: main
remote: origin
on ● add-login (feature, parent: main)
working tree: clean
vs main: ↑2 ↓0
vs origin/add-login: ↑0 ↓0
stack:
main (trunk)
└─ ● add-login ↑2 ↓0
Stacked changes
Build a branch on top of another, then sync the whole stack at once:
git city new api # off main
git city new ui --onto api # ui stacks on api
git city sync --all # rebase the whole stack, parents before children
$ git city tree
main (trunk)
└─ api ↑3 ↓0
└─ ● ui ↑2 ↓0
Preview and undo
Every mutating command takes --dry-run and shows the exact Git commands it would run:
$ git city sync --dry-run
git fetch origin --prune
git branch -f main origin/main
git checkout add-login
git rebase --onto main <base> add-login
git push --force-with-lease=add-login:<sha> origin add-login
Nothing executed (--dry-run).
And undo reverses the last command — restoring moved refs, recreated branches, and even force-pushed remote branches:
$ git city undo
✓ undid: sync add-login
Conflicts
When a sync hits a conflict it stops cleanly and tells you exactly how to proceed:
$ git city sync
git-city: sync add-login is paused.
stopped on: git rebase --onto main <base> add-login
resolve the conflict and `git add`, then:
git city continue resume the operation
git city abort undo everything and return to the start
git city status show this again
Commands
| Command | What it does |
|---|---|
git city |
With no command, print the grouped list of commands (the help menu) |
git city info |
Dashboard: current branch, its place in the stack, ahead/behind, dirty state |
git city tree |
Show the branch hierarchy |
git city new <name> [--onto <branch>] |
Create a feature branch off the trunk (or stack it onto <branch>) |
git city switch [name] |
Switch branches; with no name, pick one with a fuzzy picker |
git city sync [branch] [--all] |
Rebase a branch (or every feature, parents first) onto its parent and remote |
git city land [branch] (alias merge) |
Fast-forward the parent to the branch, push, delete it (local + remote), re-home children |
git city delete [branch] |
Delete a branch without merging; re-home its children |
git city reparent <new-parent> [--branch <b>] |
Change a branch's parent (lineage only; the next sync moves the commits) |
git city insert <name> [--branch <b>] |
Insert a new branch between a branch and its parent |
git city squash [branch] [--message <m>] |
Compress a branch's commits into one |
git city undo |
Reverse the last git-city command |
git city continue / abort / status |
Resume, undo, or inspect a sync paused on a conflict |
git city init [--trunk <name>] [--remote <name>] |
Set up git-city for this repo (records the trunk, and a preferred remote) |
git city config |
Show the effective configuration and where it comes from |
git city completions [shell] |
Print a shell completion script (bash, zsh, fish); --install to set it up |
Every mutating command accepts --dry-run. Commands that move commits refuse to run on a dirty working tree (commit or stash first), so they never silently carry or discard your changes; switch and reparent, which don't touch your files, don't. The commands that delete a remote branch or rewrite published history — delete, land/merge, and squash — also confirm before running; pass --yes (or -y) to skip the prompt, which is required to run them non-interactively.
How it works
Branch types. Just two: a trunk (main, plus any configured perennials) is long-lived and never rebased; a feature is everything you work on, and it knows its parent. Follow the parent chain and you get a tree rooted at the trunk — a "stack" is a path down it.
Operations are reversible programs. Every command first compiles, from an immutable snapshot of the repo, into an explicit list of small, reversible steps (checkout, rebase --onto, push --force-with-lease, …). An interpreter then runs them. That single design is what gives --dry-run, undo, and conflict continue/abort for free, and what makes the planner exhaustively testable without touching Git.
Sync rebases. sync brings a feature up to date by rebasing it onto its (also-updated) parent, absorbing any commits the remote has that you don't, then force-pushing with a lease so it can never clobber unseen remote work. Trunks fast-forward from their remote; they're never rebased onto a child.
Undo is a guarantee, not best-effort. Inverses are captured before each step mutates, so undo restores prior ref positions exactly. It refuses rather than discard: it won't run on a dirty tree, and it won't delete a branch carrying commits that live on no other branch.
Configuration
Settings live in TOML, merged from a global user file ($XDG_CONFIG_HOME/git-city/config.toml, default ~/.config/...) and a local, committable git-city.toml at the repo root — local wins:
trunk = "main"
remote = "origin"
perennials = ["develop", "staging"]
git city init writes the local file for you (--remote records the remote too). Set remote when a repo has more than one — say a deploy target like piku alongside your upstream — so git-city pushes to the right place; it otherwise defaults to origin, or the first remote. Per-branch state (each branch's parent, and parked/private flags) lives in Git config under git-city.branch.<name>.* — local to your clone, never churning a tracked file. A parked branch is skipped by sync; a private branch syncs with its parent but is never pushed.
Compared to git-town
| x | git-town | git-city |
|---|---|---|
| Forge | central to the workflow | optional plugin (not built yet); core needs none |
| Branch types | 7 | 2 (+ parked/private flags) |
| Default sync | merge (features) | rebase, with --force-with-lease |
| Config | many Git-config keys | small global + local TOML |
| Language | Go | Python |
git-city borrows git-town's best ideas (high-level verbs, parent hierarchy, reversible-operation engine) — it is not a fork and makes no compatibility promise.
Development
uv sync
make test # the test suite (unit / integration / e2e)
make lint # ruff + ruff format + ty + pyrefly + zuban (incl. --strict)
The architecture is a strict functional core / imperative shell: pure planners turn an immutable repo snapshot into step lists; one thin shell (git.py) shells out to Git porcelain.
Full documentation is in docs/, a Zensical site — preview it with uv run zensical serve.
License
Licensed under the Apache License, Version 2.0. Copyright 2026 Stefane Fermigier.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file git_city-0.5.0.tar.gz.
File metadata
- Download URL: git_city-0.5.0.tar.gz
- Upload date:
- Size: 31.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f6414f62b4f8a88d95a8aac1b5ecfa5e6663d91244447dcca3710a953e0105ee
|
|
| MD5 |
89b198b6e022cd5b635bdd5f914a4f01
|
|
| BLAKE2b-256 |
7bf72f857cedc1e5ff4262ae936407b97b611ca6fda5e93938784395465087ec
|
File details
Details for the file git_city-0.5.0-py3-none-any.whl.
File metadata
- Download URL: git_city-0.5.0-py3-none-any.whl
- Upload date:
- Size: 37.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.11.16 {"installer":{"name":"uv","version":"0.11.16","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f6765b085cab61d5aab65d5ae02a889b437a9f03456447d018db030f222d4741
|
|
| MD5 |
01714a19599e0df5ce13ff9a782e024d
|
|
| BLAKE2b-256 |
b80b1a8a9631115162386001ff43087386a5d3b22adce79ba9cec8f5c58f0f3b
|