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

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.1.tar.gz (33.7 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.1-py3-none-any.whl (39.7 kB view details)

Uploaded Python 3

File details

Details for the file git_city-0.5.1.tar.gz.

File metadata

  • Download URL: git_city-0.5.1.tar.gz
  • Upload date:
  • Size: 33.7 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.1.tar.gz
Algorithm Hash digest
SHA256 74af2ad7d75165d3f492db22835d27077de2ee08b33b770322e48a354c2b7d5a
MD5 6c7fd8a13973cd0104733ae081f4060a
BLAKE2b-256 0c48adc65eca9e23e6d9ff9987427544284facf5c381a4a24f473bb9981271aa

See more details on using hashes here.

File details

Details for the file git_city-0.5.1-py3-none-any.whl.

File metadata

  • Download URL: git_city-0.5.1-py3-none-any.whl
  • Upload date:
  • Size: 39.7 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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 753e1ecac9e493b84eab13e5596deccdfa9cd49fec87481fe441281b6281763f
MD5 5327c4f3ca16e0249d33019124bac304
BLAKE2b-256 ff3c2dee845b19030b00bdf8176fefc78b81762bd68515d3a62837b81972a778

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