Metadata-Version: 2.4
Name: git-city
Version: 0.5.0
Summary: High-level branch workflows for Git — simpler, forge-agnostic.
Author: Stefane Fermigier
Author-email: Stefane Fermigier <sf@abilian.com>
License-Expression: Apache-2.0
License-File: LICENSE
Requires-Dist: cyclopts>=4.17.0
Requires-Dist: inquirerpy>=0.3.4
Requires-Python: >=3.12
Description-Content-Type: text/markdown

# git-city

High-level branch workflows for Git — like [git-town](https://www.git-town.com), 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](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](https://docs.astral.sh/uv/).

From source:

```sh
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):

```sh
git city completions --install     # or: git city completions zsh > /path/to/completions
```

For development, skip the install and run it in place:

```sh
uv sync
uv run git-city            # or: uv run python -m git_city
```

---

## Quick start

```sh
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:

```sh
$ 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:

```sh
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
```

```sh
$ 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:

```sh
$ 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:

```sh
$ git city undo
✓ undid: sync add-login
```

### Conflicts

When a sync hits a conflict it stops cleanly and tells you exactly how to proceed:

```sh
$ 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:

```toml
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

```sh
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/`](docs/), a [Zensical](https://github.com/squidfunk/zensical) site — preview it with `uv run zensical serve`.

## License

Licensed under the [Apache License, Version 2.0](LICENSE). Copyright 2026 Stefane Fermigier.
