Skip to content

Conflicts and held rewrites

When a replay conflicts, fufu reports a held rewrite: the requested rewrite is waiting for you to resolve it. ff status shows held:, the command that caused it, the conflicting commit and files, and what to do next.

That branch's conflicting replay has not advanced its tip or put markers in your working copy. Earlier successful branch updates in the same command can stand. You can keep working at the existing tip, or resolve the conflict now.

Resolve, edit, finish

On the held branch, ff resolve opens a resolution session and puts the surviving conflicts into files as labeled markers. Edit those files, remove the markers, then run ff done to apply the fixes and return to the original branch.

ff resolve
# Edit the conflicting files and remove the conflict markers.
ff done

You do not need to stage the fixes. If another branch is held, first use ff switch to select it. A parked change that conflicts when you switch back is a distinct case, resolved in place.

The session

A rewrite resolution session uses an automatically named branch whose starting commit contains the marker tree. Your original branch keeps its tip and hold; its open change parks there. Status shows resolving: while the session is open.

The current side of a marker is labeled the rewrite so far. The incoming side identifies the replayed commit, for example >>>>>>> rebasing "add parser options" (3/10). These labels associate each fix with the step that needs it.

You can switch away from the session and return later. Its unfinished edits park and resume like other branch work. The temporary session commit contains literal markers that ordinary Git can read; the original branch does not contain that marker commit.

Landing the session

ff done applies each fix to its corresponding replay step and reruns the rewrite. When it succeeds, the rewritten branch receives the clean commits, the session branch is deleted, and you return to the original branch with its parked work restored or reported as a held arrival. Branches based on the rewritten branch can then follow through a cascade.

Some conflicts require more than one round. When two replay steps conflict over the same unresolved region, fufu stops before creating overlapping marker blocks. Resolve the presented part and run ff done; any remaining work is held for another ff resolve round.

Abandoning or undoing a resolution

ff resolve --abandon drops the held rewrite and an open resolution session, returning from the session if needed. It works from the session or the held branch.

Opening a rewrite session takes two operations: creating the session branch and switching to it. One ff undo returns to the original branch; another removes the newly created session. Landing or abandoning is one operation, so one undo restores the session, hold, and recorded fixes. Snapshot coverage and retention apply.

Parked-change arrival

A held arrival happens when ff switch cannot replay parked work over a branch tip that has moved. The branch switch still completes and reports exit 3, but the parked edits wait for resolution.

Here ff resolve lays the parked change into the current working copy with markers. It does not create a resolution-session branch: the result is an open change. Edit the markers, then continue working or use ff commit when ready to record it. There is no session to finish with ff done.

If the branch already has another open change, resolve refuses to overwrite it. Commit that work before retrying; switching away only parks it on the same branch, so returning resumes the blocker. ff resolve --abandon drops a still-held arrival and reports the saved parked commit; it does not apply that work.

Reading conflict reports

The command announces the hold when it is created. Status keeps showing it until it is resolved or abandoned, and ff branch marks held branches and unfinished sessions in its list.

Exit 3 reports a held primary replay for ff pull, ff restack, ff done, ff absorb, and ff lift. Pull, restack, and ff fold also exit 3 for holds in their cascades. A successful absorb, lift, or session landing can return 0 with a downstream branch held, because its primary change landed. A reword through ff describe currently returns 0 even when its cascade holds; scripts must inspect reword.cascade.held. Read the branch reports as well as the exit code.

What a hold blocks, and what it does not

ff push refuses to send a held branch. Resolve or abandon its pending rewrite before pushing. This is a guard on fufu's push command; Git compatibility and policy explain what happens with other callers.

A hold does not itself prevent local commits or branch switches. You can keep building on the existing tip, but the requested post-rewrite state is not available until it applies successfully. Other command-specific guards still apply.

What a hold records

A hold saves the requested rewrite's intent: its branch and target. Resolution recomputes the replay against current inputs instead of resuming an old partial plan. Work committed at the existing tip can therefore be included when you resolve later.

If the rewrite now applies cleanly, ff resolve releases the hold and tells you to rerun the original command. If its target disappeared or no longer belongs to the required history, it reports that the hold expired rather than using a stale plan.

How conflicts reach the session

fufu replays commits in memory before updating the branch. During resolution it carries unresolved regions forward as literal marker content, letting later commits apply around them. A conflict already fixed by a later commit can disappear before the session opens. The remaining regions are presented together, except where overlapping conflicts require another round.

For the design choice between a pending rewrite and jj's conflict objects, see fufu vs jj. The storage model explains how session and parked-change objects remain readable by Git.