Rebase-first sync¶
git-city keeps your history linear. When you sync a feature, it is rebased onto its parent — not merged. There are no merge commits threaded between your work and the trunk, and a stack reads top to bottom as a clean sequence of commits.
This is a deliberate departure from tools that merge feature branches by default. Rebasing costs you the occasional conflict to resolve (handled gracefully — see conflicts), and buys you a history that is genuinely easy to read, bisect, and reason about.
Rebase-first is the default, not a mandate. A long-lived branch against a busy parent is exactly where rebasing hurts — you re-resolve the same conflicts each sync. For those, flip the branch to the merge sync strategy and resolve once. Most branches stay on rebase.
Why rebase¶
A merge-based workflow records the shape of your collaboration: every integration leaves a merge commit, and the graph forks and rejoins. That is faithful, but noisy — especially with stacked branches, where each feature depends on the one below it.
Rebasing instead replays your commits on top of an up-to-date parent, so the result is a straight line:
- Linear history. No merge commits.
git logis a flat list, andgit bisecthas no parallel paths to confuse it. - Clean stacks. A stack is just a path down the parent tree. Rebasing every feature onto its parent keeps that path tight — each branch sits directly on the tip of the one below it.
- Honest diffs. A feature's diff against its parent is exactly the work you added, not the work the parent happened to gain since you branched.
Note
git-city has only two branch types: a long-lived trunk that is never rebased and always fast-forwards from its remote, and a feature that knows its parent and is the thing you rebase. The rebase-first rule applies to features; trunks fast-forward. See the mental model.
What sync does for a feature¶
When you run git city sync on a feature branch, git-city performs an ordered sequence:
- Fetch the remote (with
--prune). - Update the parent, recursively — the parent is brought up to date first (and its parent, and so on up to the trunk), so you rebase onto current work, not a stale base.
- Absorb remote commits — if the feature's own remote tracking branch has commits you do not have locally, those are integrated first, so you never silently drop teammates' work on the same branch.
- Rebase onto the parent — your commits are replayed on top of the up-to-date parent.
- Force-push the rebased branch with
--force-with-lease.
Because a rebase rewrites commit shas, the local and remote branch diverge in shape — the same changes now sit on a new base. A force-push is the only way to publish that, and git-city always uses the leased form so it is safe (see below).
See it before you run it¶
Every mutating command accepts --dry-run, which prints the exact ordered git commands and executes nothing:
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).
Reading top to bottom: fetch and prune, fast-forward the parent main to its remote, check out the feature, rebase it onto main, then force-push with a lease. That is the whole feature sync.
Tip
Run --dry-run first whenever you are unsure what a sync will touch — particularly before the first sync of a freshly fetched repo, or on a branch with remote commits to absorb.
What sync does for a trunk¶
A trunk is never rebased. Syncing a trunk simply:
- Fetches the remote.
- Fast-forwards to the remote tip — or, if you have local commits on the trunk, rebases those local commits onto the remote.
There is no force-push for a trunk: it only ever moves forward.
| Branch type | Strategy on sync | Pushes |
|---|---|---|
| Trunk | Fetch, then fast-forward (or rebase local commits onto the remote) | No force-push; only fast-forward |
| Feature | Fetch, update parent recursively, absorb remote commits, rebase onto parent | Force-push with --force-with-lease |
Note
With git city sync --all, every feature is synced in topological order — parents before children — and each trunk is updated once. The whole tree is restacked in a single pass.
Why --force-with-lease is safe¶
A plain git push --force overwrites the remote unconditionally — including commits you have never seen, which is how a teammate's push gets clobbered.
--force-with-lease refuses unless the remote is where you last observed it. git-city always pushes the leased form, pinning the expected sha:
If someone else pushed to add-login after your last fetch, the lease check fails and the push is rejected rather than destroying their work. And because sync absorbs any remote commits before rebasing (step 3 above), the normal case is that you have already integrated whatever was on the remote — the lease simply guarantees nothing slipped in between.
Warning
--force-with-lease protects against clobbering unseen remote work, but it is not yet a guardrail against rewriting commits authored by others that you have already fetched. A dedicated rewrite guardrail is on the roadmap.
When a rebase conflicts¶
A rebase can stop on a conflict. git-city does not abandon the operation — it pauses, leaving the rebase in progress, and tells you exactly how to proceed:
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
Resolve the conflict, git add the result, and git city continue picks up the remaining steps. git city abort rolls the whole sync back to where it started.
Related¶
- The
synccommand — every flag and the full behavior, including--all. - Conflicts — pause, continue, abort, and how the engine resumes.
- Reversible operations — why every sync can be undone.
- The mental model — trunks, features, parents, and stacks.