Skip to main content

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. 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, 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

git_city-0.5.0.tar.gz (31.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

git_city-0.5.0-py3-none-any.whl (37.4 kB view details)

Uploaded Python 3

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

Hashes for git_city-0.5.0.tar.gz
Algorithm Hash digest
SHA256 f6414f62b4f8a88d95a8aac1b5ecfa5e6663d91244447dcca3710a953e0105ee
MD5 89b198b6e022cd5b635bdd5f914a4f01
BLAKE2b-256 7bf72f857cedc1e5ff4262ae936407b97b611ca6fda5e93938784395465087ec

See more details on using hashes here.

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

Hashes for git_city-0.5.0-py3-none-any.whl
Algorithm Hash digest
SHA256 f6765b085cab61d5aab65d5ae02a889b437a9f03456447d018db030f222d4741
MD5 01714a19599e0df5ce13ff9a782e024d
BLAKE2b-256 b80b1a8a9631115162386001ff43087386a5d3b22adce79ba9cec8f5c58f0f3b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page