Skip to main content

Rebase and Resolve Conflicts

What to do when the remote has moved: recovering from divergence, pulling and rebasing, force-with-lease, resolving conflicts and continuing or aborting, detached HEAD, and the errors that come with all of it.

Updated
Reviewed

Recovering from divergence

When a push is rejected because the remote moved on, the banner reads "Remote has N new commits. Pull and rebase, or force push to overwrite." Two recovery paths follow.

Pull and rebase

The primary option opens a confirmation: "Pull and rebase local commits? Replays N local commits on top of M incoming commits from the remote. Each replayed commit becomes a new commit with a different hash." The counts come from the worktree's ahead and behind state. Confirm and the rebase runs against the branch's upstream. If it hits conflicts mid-replay, the working tree drops into the conflict view below.

The heading names no ref. It used to interpolate the current branch, which read as "onto BRANCH" — the exact inversion of what a rebase does, since it replays onto the upstream and rewrites the branch. This surface has no upstream ref to put there instead, so it states the counts and stops. And the consequence is named concretely: "cannot be undone" is not something you can act on, where "the hashes change" is the fact that breaks anything pointing at the old commits.

Rebasing from outside Review Hub

The same rebase dispatched from the action palette, or from the recovery banner on a terminal push error, gets a fuller dialog. Where Review Hub has counts, this one previews the commits themselves.

A Rewrites row names your branch and an Onto row names the upstream — the ref the rebase integrates from, not the push destination, which in a fork workflow is a different repository. Under them, Commits to replay lists the real replay set: what git would actually pick up, measured --no-merges --cherry-pick --right-only against the upstream, so merge commits and commits the upstream already holds as equivalent patches are excluded. Up to twelve rows of short hash, subject and author, with a total badge and an "…and N more" tail.

That is a stricter set than the branch's recent history, which is what the dialog used to show: a list under a heading promising those commits were about to be rewritten, when the upstream already had most of them.

Warning

A broken read never renders as "nothing to replay". If git fails, the dialog reports "Couldn't read which commits this would replay" with git's own message and a Retry, and the primary stays disabled.

A measured-empty range is a settled answer and splits two ways: "Nothing to replay — BRANCH already matches origin/main" when the branch is level with the upstream, and "Nothing to replay — BRANCH is N behind origin/main and has no commit the rebase would replay on top of it" when it is not. An empty replay set alone cannot tell those apart, and calling either one "already matches" would be wrong half the time.

An upstream that is configured but has never been fetched into this worktree is a third thing again, and it blocks: "Nothing to compare against yet", with git fetch <remote> to run. There is no local ref to subtract from, so the set was not measured — and a dialog whose whole job is showing what gets rewritten will not approve past the one state where it cannot answer.

Neither empty state claims a fast-forward, and neither should be read as one. Because the set is measured --cherry-pick --right-only, a branch carrying merge commits, or commits the upstream already holds as equivalent patches, measures empty here too — and those are local-only commits the rebase drops rather than replays.

A detached HEAD ("No branch checked out") and a branch that tracks nothing ("No upstream to rebase onto", with git branch --set-upstream-to=<remote>/<branch>) each block the primary and name themselves rather than leaving it dead.

Force push

The secondary option appears only when Daintree captured a lease for the remote branch at the moment of rejection. Without that lease, --force-with-lease would silently degrade to a plain --force if a background fetch advanced the tracking ref between the rejection and your click, so the button is suppressed rather than offered unsafely.

It opens a dialog titled "Force push to REMOTE/BRANCH?" listing the Remote commits to discard with hash, message, and author, plus a tail count when the range runs longer than the list. Both the rows and the total come from the same range against the resolved destination, so the preview cannot describe a different repository from the one being written.

Warning
If the discard preview fails to load, the confirm button stays disabled. The lease still keeps the operation technically safe, but you would have no visibility into what it discards, and force pushing blind is not something Review Hub will let you do.

Force push uses --force-with-lease: it only overwrites the remote if it still matches what Daintree saw when the push was rejected, so it will not clobber a teammate's newer work. It will still discard every remote commit in the list. Pull and rebase is the safer default.

Integrating the base branch

The worktree card's Git submenu separates updating from your upstream from integrating the branch your work will eventually merge into. Pull and rebase follows the current branch's configured upstream. Rebase onto <base>… and Merge <base> in… integrate the base named on the card.

  1. Commit or stash uncommitted changes before starting a base integration.
  2. Use Git > Fetch when you need current remote refs. Fetch and prune also removes stale remote-tracking refs. Fetch does not merge into your branch or change working files.
  3. Choose rebase to replay the branch's work on the base, or merge to integrate it without rewriting existing commits. Review the resolved refs and preview in the confirmation before proceeding.
  4. If conflicts stop the operation, resolve them in Review Hub. The card menu changes to Continue and Abort… for the current operation.

Base integration does not fetch automatically. It uses the currently available comparison ref, preferring the base branch's remote-tracking target where available, and verifies the previewed refs before the write. Missing base information, detached HEAD, unknown status, a dirty tree, or a measured up-to-date branch explain why a menu row is disabled. The main-process checks also reject an existing repository operation and integrating a branch into itself.

These are different from Push, which publishes to the branch's push destination. In a fork workflow that destination may differ from both upstream and base. The card can disable Push when its upstream ahead count is zero; the command palette or Review Hub can still resolve and preview the actual push destination.

Source reference: Git write validation and base integration.

Resolving conflicts

Review Hub resolves conflicts in place. When a worktree is mid-conflict (from a merge, rebase, cherry-pick, or revert), the file list is replaced by a three-region conflict panel.

Operation chrome

An amber banner names the operation: "Resolve Merge Conflicts", "Resolve Rebase Conflicts", and so on. For a rebase it also shows a Step N of M badge and a running conflict count. An Abort button backs the whole operation out; its confirmation spells out exactly what gets discarded and restored for that operation type.

Rebase sequence rail

During a rebase, a vertical rail above the worklist lists the full commit sequence so you can see where you are. Done commits get a check and dim out, the current commit is bold with an accent chevron, pending commits show a dashed circle, and dropped commits are struck through. Fixup and squash commits sit indented under their parent.

The worklist

Each conflicted file shows a warning icon, its path, a label like "both modified", and a badge with the number of conflict regions. Per file you can:

  • Take ours / Take theirs: resolve the whole file one way, each behind a confirmation.
  • Open: jump to the file in your external editor at the first conflict marker.
  • Mark resolved: stage the file once you have handled it by hand.

Resolved files collapse into a Resolved (N) disclosure. Continue is enabled only once no conflicts remain.

Note
During a rebase, "ours" and "theirs" are inverted from what you would expect: "ours" is the branch you are rebasing onto, and "theirs" is the commit being replayed. The action tooltips spell this out so you do not pick the wrong side.

Detached HEAD

In a detached HEAD state the composer shows an amber warning and disables the textarea and the commit buttons. Detached HEAD is also a readiness blocker, so the rail says so before you reach for the composer.

Warning
Commits are blocked in detached HEAD. Check out a named branch first: git checkout -b BRANCH-NAME.

Errors and auto-refresh

If git status fails to load, a red banner with a Retry button replaces the file lists. A push that is rate-limited reports a rate-limit message rather than a raw error.

Review Hub watches the worktree and refreshes in the background, so the lists stay current while an agent writes files. Your scroll position is kept, and the surface takes a stale treatment (dimmed, and marked busy for assistive technology) while a refresh is in flight rather than tearing down to a skeleton.