Skip to content

Compared to git-town

If you have used git-town, git-city will feel familiar — and deliberately smaller. git-town pioneered the idea of high-level branch-workflow verbs (hack, sync, ship, kill) layered over plain Git, the parent hierarchy that makes stacks tractable, and a fully reversible operation engine. git-city keeps all three of those ideas and strips almost everything else away.

This page lays out the concrete differences and the reasoning behind them, so you can decide whether git-city's narrower, more opinionated shape fits how you work.

Note

git-city is not a fork of git-town. It shares none of its Go code and makes no compatibility promise — you cannot point git-city at a repo configured by git-town and expect it to read that configuration. It borrows git-town's best ideas and re-derives them in Python around a single immutable-snapshot engine.


At a glance

Dimension git-town git-city
Forge integration Central to the workflow — propose / ship talk to GitHub, GitLab, Gitea, Bitbucket. Optional, and not built yet. Works fully against a plain Git remote, or none at all.
Branch types Seven (main, perennial, feature, observed, contribution, parked, prototype). Two — trunk and feature — plus two modifier flags (parked, private).
Default sync for features Merge by default (rebase opt-in). Rebase by default, pushed with --force-with-lease.
Configuration Many Git-config keys per setting. A small global + local TOML (trunk, perennials) merged key-by-key, plus per-branch lineage in Git config.
Language Go (single static binary). Python (≥ 3.12), managed with uv.

What git-city keeps

These are the ideas git-city considers load-bearing and adopts wholesale.

  • High-level verbs over plain Git. You think in terms of new, sync, land, and undo, not git rebase --onto <base> <branch>. The tool computes the exact Git commands; you state the intent.
  • The parent hierarchy. Every feature records exactly one parent, so your branches form a tree rooted at the trunk, and a stack is simply a path down that tree. This is the same conceptual core git-town uses to keep stacked changes coherent.

    The mental model

  • A reversible-operation engine. Mutating commands are planned, then executed while recording how to reverse each step, so git city undo can roll the last command back — restoring moved refs, recreated branches, even force-pushed remote branches.


What git-city changes, and why

Fewer branch types

git-town distinguishes seven branch types to express subtly different sync behaviors. git-city collapses that to two types and two flags: a branch is either a long-lived trunk (your main, plus any configured perennials) or a feature you build and rewrite. The behaviors git-town encodes as separate types — "don't push this", "keep this local but current" — become per-branch flags instead:

Flag Effect on sync
parked The branch is skipped entirely.
private The branch is rebased onto its parent but never pushed.

The flags live in Git config (git-city.branch.<name>.parked / .private). There is no toggle command yet — set them directly for now:

git config --type=bool git-city.branch.ui.parked true
git config --type=bool git-city.branch.ui.private true

Branch types reference

Rebase by default

git-town merges feature branches by default and treats rebase as a mode you opt into. git-city inverts that: features are rebased onto their parent and force-pushed with --force-with-lease, so a stack stays linear and each branch shows a clean ahead/behind count against its parent. The --force-with-lease guard means a rebase-and-push can never clobber remote work you have not seen.

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

Rebase-first, and why

A small TOML instead of many Git-config keys

git-town stores its project settings across a number of Git-config keys. git-city keeps the project-wide truth in a tiny, committable TOML file with just two keys, trunk and perennials:

trunk = "main"
perennials = ["release-1.x"]

A global file at ~/.config/git-city/config.toml is merged with a local, in-repo git-city.toml, with the local file overriding key-by-key. Only per-branch lineage (each feature's parent) lives in Git config, where it stays local to your clone and never churns a tracked file.

Configuration

Forge-agnostic by design

This is the biggest philosophical divergence. For git-town, the forge is where the workflow culminates — ship and propose are first-class. git-city treats the forge as optional, and today the integration is simply not built: every command works against a plain Git remote (or no remote at all). land fast-forwards the parent and pushes; it does not open or merge a pull request.

Warning

There is no forge integration yet — no propose command, no PR or MR creation. If your workflow depends on the tool opening and merging pull requests for you, git-city does not do that today. A propose command is on the roadmap.

Forge-agnostic

Python instead of Go

git-town ships as a single static Go binary. git-city is written in Python (≥ 3.12) and managed with uv, which makes it easy to read, hack on, and extend, at the cost of needing a Python runtime. It depends on cyclopts for the CLI and InquirerPy for the interactive pickers.


Which should you use?

Reach for git-town if forge integration is central to your day — you want the tool to open and merge pull requests across GitHub, GitLab, or others — and you value a single dependency-free binary.

Reach for git-city if you want a smaller surface area: two branch types, rebase-first stacks, a committable two-key config, and a safety-first reversible engine, all working against a plain Git remote. Just remember the status: the local workflow is complete and heavily tested, while forge integration is still to come.

The mental model · Forge-agnostic · Roadmap