Skip to main content
CodeOath
← All posts

Git95 min total · 17 parts

Git Internals and Workflows: Branching, Merging, and Rebasing

Part 8 of 17 · ~4 min

Resolving a Rebase Conflict

Rewind. Same Wednesday afternoon, same two branches, same conflicting lines — but this time you abort the merge and rebase instead, because you and Theo agreed you would.

git merge --abort
git rebase main
Auto-merging src/roster/availability.js
CONFLICT (content): Merge conflict in src/roster/availability.js
error: could not apply 7c1f4ae... Block swaps inside the deadline window
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".

Same file, same lines, and you already know how to do this. Open it, keep HEAD's side plus your addition, exactly like last time.

Except here is what is actually in the file:

<<<<<<< HEAD
export function hoursUntil(shift, now) {
  const start = zonedTime(shift.startsAt, shift.site.timezone);
  return (start - now) / 3_600_000;
}
=======
export function hoursUntil(shift, now) {
  return (shift.startsAt - now) / 3_600_000;
}

export function canSwap(shift, now) {
  return hoursUntil(shift, now) >= SWAP_DEADLINE_HOURS;
}
>>>>>>> 7c1f4ae (Block swaps inside the deadline window)

The sides have swapped. HEAD is Nadia's timezone fix. Your work — the work you are in the middle of rebasing, on the branch you are standing on — is the incoming side, labelled with your own commit's hash and message.

If you resolved this the way you resolved the merge, on the assumption that HEAD means "mine," you would keep the timezone body and delete canSwap entirely: you would throw away the commit you are currently replaying and not notice until a test failed.

The reason is that a rebase does not move your commits. It checks out the new base and replays your commits onto it, one at a time, as if each were being applied by hand. So HEAD during a rebase is main — the branch you are rebasing onto, plus however many of your commits have already been replayed successfully. Your commit is the patch being applied to it. Ask "what is checked out right now?" and the labels stop being confusing: it is not your branch, it is the new base.

Confirm it yourself mid-conflict, which is a habit worth having:

git log --oneline -1
# e0b8d31 Make hoursUntil respect each site's timezone   ← that is main, not your branch

The same inversion applies to git checkout --ours and --theirs during a rebase, for the same reason: --ours is the upstream you are replaying onto, --theirs is your commit. This is not Git being inconsistent. It is Git being completely consistent about what HEAD means, in a situation where HEAD is somewhere you did not expect.

The resolution itself is identical to the merge — keep the zoned body, keep canSwap — and then:

git add src/roster/availability.js
git rebase --continue

A rebase is a sequence of separate replays, so it gives you two options a merge cannot:

git rebase --abort   # cancel the entire rebase; the branch returns exactly to its pre-rebase state
git rebase --skip    # drop the commit currently being replayed, discarding its changes, and carry on

--abort is genuinely safe and genuinely complete. It does not leave a half-finished rebase lying around, and it restores the original commits, not copies of them. Once a rebase costs more effort than it saves, that is your cue to bail out and merge instead — a normal outcome, not a defeat.

--skip is the dangerous one, and it is dangerous in a quiet way. It does not skip the conflict; it drops the entire commit. If that commit contained anything beyond the conflicting lines, that work is gone from the branch too. Reach for it only when you already know the commit is redundant — usually because the same change arrived on main by another route.

Two smaller things that save confusion. First, git status during a plain git rebase announces "interactive rebase in progress," because modern Git implements both with the same machinery. You did not accidentally start something interactive. Second, a six-commit rebase can stop six times, and each stop is a separate resolution — so the progress counter (Rebasing (2/6)) is worth reading, because "I've fixed this twice already" usually means you are on commit three of six rather than going in circles.