Skip to content

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 log is a flat list, and git bisect has 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:

  1. Fetch the remote (with --prune).
  2. 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.
  3. 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.
  4. Rebase onto the parent — your commits are replayed on top of the up-to-date parent.
  5. 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 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).

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:

  1. Fetches the remote.
  2. 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:

git push --force-with-lease=add-login:<sha> origin add-login

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.

Resolving conflicts


The sync command