Skip to main content

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. trunk and feature. 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 undo reverses the last one — and never silently destroys uncommitted work or a remote branch it didn't create.
  • Boring, guessable commands. new, sync, land, switch — not hack, 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 and push (keeps the branch)
git city prune                     # later: sweep away branches you've landed

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 info       show this and the repo status

Commands

Command What it does
git city With no command, print the grouped list of commands (the help menu)
git city info [--verbose / --terse] Dashboard: current branch, its place in the stack, ahead/behind, dirty state — and any paused operation. -t collapses it to one line; -v adds shas and unparented branches
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, re-home children — and keep the branch (flagged landed); --delete/-d removes it then and there
git city prune Delete the branches you've landed once their work is in the parent (skips any you've since added commits to)
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 Resume, or roll back, a sync paused on a conflict (inspect it with git city info)
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 branch or rewrite published history — delete, prune, squash, and land --delete (plain land keeps the branch, so it doesn't prompt) — 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.

Metadata

Release files for git-city 0.5.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for git-city 0.5.1
File Size Uploaded
git_city-0.5.1.tar.gz 33.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for git-city 0.5.1
File Interpreter ABI Platform
git_city-0.5.1-py3-none-any.whl Python 3 none any Details

Total release size: 73.4 kB

Release files / git_city-0.5.1.tar.gz

Download URL git_city-0.5.1.tar.gz
Size 33.7 kB
Tags Source
SHA-256 checksum
How to use checksums
74af2ad7d75165d3f492db22835d27077de2ee08b33b770322e48a354c2b7d5a
BLAKE2b-256 checksum
How to use checksums
0c48adc65eca9e23e6d9ff9987427544284facf5c381a4a24f473bb9981271aa
Upload date
Uploaded using Trusted Publishing?
What is 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}

Release files / git_city-0.5.1-py3-none-any.whl

Download URL git_city-0.5.1-py3-none-any.whl
Size 39.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
753e1ecac9e493b84eab13e5596deccdfa9cd49fec87481fe441281b6281763f
BLAKE2b-256 checksum
How to use checksums
ff3c2dee845b19030b00bdf8176fefc78b81762bd68515d3a62837b81972a778
Upload date
Uploaded using Trusted Publishing?
What is 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}

Release history Release notifications | RSS feed

This release

0.5.1 This release

2 release files

0.5.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page