Skip to content

Merging branches and resolving conflicts

docs.scrimba.com

Your feature branch is finished. The toggle works, you have committed the changes, and now you want that work sitting in main where the rest of the team's code lives. Most of the time bringing it in is quick and quiet: Git compares the two branches, finds nothing that overlaps, and folds the work in with no extra steps. Sometimes, though, you and a teammate changed the exact same lines on different branches, and Git cannot guess which version you want kept. That situation shows up in almost every project with more than one person committing to it. It is a normal, everyday part of branching, and it means Git found two ideas about the same lines and wants your call on which one wins.

Bringing a branch back with git merge

Say you built the Fahrenheit toggle on its own branch, add-fahrenheit-toggle, and main has not moved since you branched off it. Switch to main and run git merge with the name of the branch you want to bring in:

bash
$ git switch main
$ git merge add-fahrenheit-toggle
Updating 4f2a891..8c3d1a0
Fast-forward
 src/index.js | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

git merge <branch> always merges the named branch into whichever branch you are currently standing on, so switching to main first matters. Whichever branch you're standing on is the one that receives the merge. After this runs, main has every commit from add-fahrenheit-toggle. The feature branch itself is untouched: you can keep working on it or delete it now that its work lives on main too.

That Fast-forward line in the output is worth reading closely. It means main had not gained any new commits since you branched off, so Git did not need to combine anything: it moved the main label forward to point at the same commit add-fahrenheit-toggle already pointed at. A fast-forward merge is Git catching a label up to where it already needed to be, nothing more.

If main had picked up other commits while you were working, say a teammate merged a bug fix in the meantime, Git can no longer move the label forward, because main and your branch have gone in different directions since you split off. Instead it creates a new commit that ties both histories together, called a merge commit:

bash
$ git switch main
$ git merge add-fahrenheit-toggle
Merge made by the 'ort' strategy.
 src/index.js | 12 +++++++++---
 1 file changed, 9 insertions(+), 3 deletions(-)

git log --graph --oneline after a merge like this shows the fork and the join: two lines of history running side by side, meeting back up at the merge commit. Neither outcome asks anything extra of you. Git decides which one applies based on whether the branches diverged, and either way your feature is now part of main.

Both outcomes above rest on the same idea underneath: the 3-way merge. To combine two branches, Git compares more than their two tips. It first finds the commit both branches share as a common starting point, the merge base, then looks at three snapshots: the base, and the tip of each branch. Comparing each tip against the shared base tells Git exactly which lines each side changed, which is what lets it combine two sets of edits automatically instead of asking you every time.

A fast-forward is the case where one of those three snapshots turns out to be redundant: the base and one tip are the same commit, so there is nothing on that side to combine, and Git moves the label without creating anything new. A merge commit is the general case, where both sides changed something since the base, and Git records a new snapshot with two parents, one pointing back into each branch's history. Every merge conflict you meet in the next section comes from this same 3-way comparison finding that both sides touched the same lines, with no way to combine them on its own.

JunoBringing a branch back with git mergegit merge branch-name brings that branch's commits into whichever branch you're currently on, so switch to main first. Most merges happen quietly in the background and you move on with your day. Nothing about the feature branch itself changes; you can keep using it or delete it once main has the work.
JunoBringing a branch back with git merge A fast-forward means main had not moved, so Git only slid the label forward. Once main picks up other commits in the meantime, merging creates a real merge commit with two parents instead. Either way the command is the same git merge branch-name; Git decides which kind of merge fits.
JunoBringing a branch back with git merge A 3-way merge compares the shared base commit against each branch tip to work out what changed on each side, which is what makes combining two branches automatic most of the time. Fast-forward is the special case where the base and one tip already match. I still catch myself pausing to work out which commit counts as the base on a messy graph, so draw it out if you're unsure.

What a merge conflict looks like

A conflict happens when the same lines change on both sides of a merge, and Git has no way to tell which version you want. Say a teammate merged a loading spinner into main that changed a function in src/index.js, and your add-fahrenheit-toggle branch changed the same function to add the toggle. Merging now stops partway through instead of finishing:

bash
$ git switch main
$ git merge add-fahrenheit-toggle
Auto-merging src/index.js
CONFLICT (content): Merge conflict in src/index.js
Automatic merge failed; fix conflicts and then commit the result.

That is a merge conflict. Open the file and you will find Git has marked exactly where the two versions disagree:

<<<<<<< HEAD
  showLoadingSpinner(true);
=======
  const tempF = celsiusToFahrenheit(tempC);
>>>>>>> add-fahrenheit-toggle

<<<<<<< HEAD marks the start of what is currently on your branch (main, in this case). ======= divides the two versions. >>>>>>> add-fahrenheit-toggle marks the end of the incoming branch's version. Everything between the markers is the same handful of lines, written two different ways.

A conflict means Git needs your judgment; nothing is broken. Git found two changes to the same lines and has no way to guess which you want, so it pauses and asks you to decide. Edit the file until it reads the way you want, keeping either version, both, or something new that combines them, then delete the marker lines themselves:

  showLoadingSpinner(true);
  const tempF = celsiusToFahrenheit(tempC);

Once the file looks right, stage and commit it exactly like any other change:

bash
$ git add src/index.js
$ git commit

Running git commit with no message here opens your editor with a message Git already prepared, describing the merge. Save and close it, and the merge is complete.

While a conflict is open, git status is the first thing worth checking. It lists every file still needing attention under "Unmerged paths," so on a bigger merge with several conflicting files you know exactly how many are left to fix:

bash
$ git status
On branch main
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)
	both modified:   src/index.js

git add tells Git a file's conflict is resolved. It does not check that you actually removed the markers, so read the file over before staging it. If the merge looks messier than you expected, or you picked the wrong branch, git merge --abort backs out completely and returns your working directory to how it looked before you ran git merge, with nothing half-finished left behind.

Conflicts are not unique to git merge. Rebase is the other way to bring one line of history up to date with another, and it hits the same kind of clash, with a different outcome once it is resolved. A merge ties two histories together with a merge commit that has two parents, preserving the fact that a branch existed and exactly when it rejoined. A rebase instead replays your branch's commits, one at a time, onto the tip of the other branch, producing a straight line of history with no merge commit and no trace that the two ever diverged:

bash
$ git switch add-fahrenheit-toggle
$ git rebase main

The trade-off is real. A linear history from rebase reads cleanly in git log, one commit after another, which some teams prefer for a tidy story. A merge keeps the true shape of what happened, including the exact point a feature branch rejoined main, which some teams prefer for the accuracy. Neither choice is wrong; pick one convention per team and stay consistent.

One rule holds regardless of which convention you pick: never rebase a branch other people have already pulled or built work on top of. Rebase rewrites commits into new ones with new IDs, and if a shared branch's history changes underneath a collaborator, their local copy and the rewritten remote one no longer agree, forcing a manual fix on their end. Rebase freely on your own branch before anyone else has pulled it. Once it is shared, merge.

JunoWhat a merge conflict looks like A merge conflict means two branches changed the same lines, and Git needs you to pick the result. Open the file, look for <<<<<<<, =======, and >>>>>>>, edit until it reads how you want, then delete those marker lines. Finish with git add and git commit. My first conflict felt like an emergency; it turned out to be Git asking me a fairly ordinary question.
JunoWhat a merge conflict looks likegit status lists every unresolved file under unmerged paths, handy the moment a conflict touches more than one. git add marks a file resolved without checking your work, so read it over before staging. If a merge goes sideways, git merge --abort gets you back to a clean state with nothing half-done.
JunoWhat a merge conflict looks like Rebase runs into the same clashes as merge; it replays commits onto a new base instead of tying histories together, trading a preserved branch shape for a straight line. Keep that trade to branches only you are using. The moment a branch is shared, rewriting its commits with rebase makes life difficult for anyone who already pulled it, so reach for merge instead.

Bringing it back together

Merging is what makes branching worth doing: you experiment safely on your own line of history, covered in Branches, then fold that work back into main once it is ready, conflicts and all. On GitHub, the same operation usually happens through a pull request instead of a local git merge on your machine; the pull request flow chapter covers proposing, reviewing, and merging a change on a shared project. If a merge ever lands somewhere you did not intend after you have already committed it, undoing things covers how to back out safely.