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, andundo, notgit 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.
-
A reversible-operation engine. Mutating commands are planned, then executed while recording how to reverse each step, so
git city undocan 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
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).
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:
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.
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.
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.