Skip to main content
CodeOath
← All posts

Git95 min total · 17 parts

Git Internals and Workflows: Branching, Merging, and Rebasing

Part 7 of 17 · ~4 min

Resolving a Merge Conflict

You go with merge first.

git merge main
Auto-merging src/roster/availability.js
CONFLICT (content): Merge conflict in src/roster/availability.js
Automatic merge failed; fix conflicts and then commit the result.

This was always going to happen, and it is worth being clear that nothing has gone wrong. A conflict is Git declining to guess. It merges changes to different parts of a file automatically, all day, without telling you. It stops only when two branches edited identical lines inside one file, because at that point no automatic answer exists that is not a judgement call, and judgement calls about your code are not Git's to make.

You and Nadia both rewrote hoursUntil(). Here is what is in the file now:

export const SWAP_DEADLINE_HOURS = 12;

<<<<<<< HEAD
export function hoursUntil(shift, now) {
  return (shift.startsAt - now) / 3_600_000;
}

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

Three markers, two versions. Read them like this:

  • Everything between <<<<<<< HEAD and ======= is the branch you are currently on — your shift-swap work, because that is where you ran the merge from.
  • Everything between ======= and >>>>>>> main is the branch you are merging in — Nadia's timezone fix.
  • The label after >>>>>>> tells you what the incoming side is. Here it is a branch name. It can also be a commit hash, which matters enormously in the next chapter.

Now the actual work, and this is the part that has nothing to do with Git. Look at what each side was for. Nadia's side fixes a real correctness bug: without the timezone conversion, a clinic in Auckland gets the wrong answer. Your side adds a function that did not exist before. Neither side is wrong, and picking one wholesale silently destroys the other's intent. Take Nadia's body, keep your addition:

export const SWAP_DEADLINE_HOURS = 12;

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

export function canSwap(shift, now) {
  return hoursUntil(shift, now) >= SWAP_DEADLINE_HOURS;
}

The markers are gone, because they were never code. <<<<<<<, =======, and >>>>>>> are scratch marks Git writes into the file so it can hand you the decision inside your own editor. Leave one in and you will ship a syntax error.

Then tell Git you are done:

git add src/roster/availability.js
git commit                       # completes the merge, with a pre-filled message

git merge --continue does the same thing and reads more clearly next to git rebase --continue. Either way, that final commit is the merge commit, with both parents attached.

A few things worth knowing while you are in the middle of one:

git status                       # which files are still unresolved ("both modified")
git diff                         # in a conflict, shows a combined diff of the unresolved hunks
git merge --abort                # back out entirely, exactly as things were before you started
git checkout --ours  <file>      # take your current branch's whole version of this file
git checkout --theirs <file>     # take the incoming branch's whole version of this file

--ours and --theirs are for the cases where one side genuinely does win outright — a generated file, a lockfile, a whole module one branch deleted. They are the wrong tool for availability.js, where the answer is "both."

Your editor almost certainly renders those markers as Accept Current / Accept Incoming / Accept Both buttons, and using them is fine. Knowing what the markers mean is what makes those buttons trustworthy rather than a coin flip, because "current" and "incoming" are exactly the two sides above — and in the next chapter they swap places.

One more thing to reach for if you find yourself resolving the same conflict repeatedly, which happens on long-lived branches:

git config rerere.enabled true

rerere is "reuse recorded resolution." With it on, Git remembers how you resolved a given conflict and replays that resolution automatically the next time it sees the identical one. On a branch you rebase onto main every morning, it turns a daily chore into nothing.