Handbooks / Git / Chapter 3

Resolving Merge Conflicts

39 pages · ~72 min✓ Reviewed

Builds on Stash & Worktrees. Next up: Rebase vs Merge.

Part 1 · Resolving Merge Conflicts

Resolving Git Merge Conflicts with Confidence

Sooner or later every Git user sees the message CONFLICT (content): Merge conflict in app.py followed by Automatic merge failed; fix conflicts and then commit the result. For many learners it feels like something broke. It did not. Git merges two lines of history by combining changes automatically, and a conflict is simply the point where it cannot decide which change should win, so it hands that decision to you.

Conflicts matter because they show up exactly when teamwork starts: two people edit the same lines, a long-running branch drifts away from main, or a rebase replays your commits on top of someone else's work. If you do not understand what Git is asking, it is tempting to delete lines at random, commit the <<<<<<< markers by accident, or throw the whole branch away. A calm, repeatable routine turns a conflict from a panic into a two-minute edit.

In this chapter you will learn why conflicts happen, how to trigger one safely in a practice repository, and how to read the markers Git writes into your files. You will then resolve conflicts by hand in an editor and with git mergetool, finish the merge, or back out cleanly with an abort. After that we cover conflicts during a rebase, habits that prevent most conflicts, and the mistakes students make most often, along with how to recover from each.

Before you start

You need Git 2.35 or newer. Check with git --version. That release added the zdiff3 conflict style we recommend later, and Git 2.34 and later use the ort merge strategy by default, so older versions print recursive instead. You should already be comfortable with git add, git commit, git branch and git switch. Do all the practice in a throwaway repository, not in a project you care about, so you can break things freely.

Part 2 · Why Merge Conflicts Happen

Git refuses to guess

When you run git merge, Git tries to combine the work of two branches into one history. Most of the time it succeeds without asking you anything. A merge conflict is the one case where it cannot: both branches changed the same lines differently, or one branch edited something the other deleted. Git has no way to know which version you want, so it stops and hands the decision to you instead of guessing.

To judge who changed what, Git does not simply compare the two branch tips. It looks back in history for the last commit both branches share, and measures each side's changes against that starting point.

How two branches drift apart
  1. 1Common ancestorthe last commit both branches share
  2. 2main moves ona teammate commits changes
  3. 3feature moves onyou commit changes
  4. 4git mergeGit compares both against the ancestor

The three inputs of a three-way merge

Git's merge is called a three-way merge because it reads three versions of every file, not two. The extra one, the common ancestor, is what lets Git tell the difference between a line that one side changed and a line that both sides changed.

NameWhat it isHow to picture it
baseThe merge-base commit, the most recent ancestor shared by both branchesThe file as it was before the two lines of work split
oursThe tip of the branch you are standing on (the current branch, HEAD)Your side of the story
theirsThe tip of the branch you are merging inThe other side of the story
Remember

A conflict is not Git being broken or picky. It is Git saying: base, ours and theirs disagree here, and I will not pick a winner for you.

When Git merges alone and when it stops

Git checks the file piece by piece, in regions called hunks. For each hunk it asks a simple question: compared with the base, who changed it? If only one side did, Git quietly takes that side's version. It only stops when both sides changed the same region and ended up with different results.

Base vs oursBase vs theirsWhat Git does
unchangedunchangedkeeps the base text
changedunchangedtakes ours, merges automatically
unchangedchangedtakes theirs, merges automatically
changed the same waychanged the same waytakes the shared result, no conflict
changed one waychanged another waystops with a conflict
Git's decision for each hunk

Touching the same file is not a conflict

A common misunderstanding is that a conflict means both people edited the same file. That is not the rule. If you change a function near the top of a file and a teammate changes a different function near the bottom, those are separate hunks and Git combines them cleanly. The conflict only appears when the edits overlap on the same lines or sit right next to each other.

output
$ git merge feature/logging
Auto-merging app.js
Merge made by the 'ort' strategy.
 app.js | 4 ++++
 1 file changed, 4 insertions(+)

Both branches edited app.js, in different places, so nothing went wrong.

A real conflict, and the other kinds

Here is the classic case. In config.js the base commit sets the port to 3000. You change it to 4000 on your branch. At the same time a teammate changes the same line to 5000 on theirs. Both changes are valid, but they cannot both be the final value.

VersionLine in config.js
baseconst port = 3000;
ours (your branch)const port = 4000;
theirs (teammate's branch)const port = 5000;

The line differs from the base on both sides, and the two results are not the same, so Git stops. It reports the problem and leaves the file marked up for you to fix.

output
$ git merge teammate-port
Auto-merging config.js
CONFLICT (content): Merge conflict in config.js
Automatic merge failed; fix conflicts and then commit the result.

Overlapping line edits are the most common cause, but not the only one. Git also stops when the histories disagree about what a file even is, and when a file is not made of lines at all.

Conflict kindWhat happenedWhy Git cannot decide
modify/deleteOne branch edited a file, the other deleted itKeeping the file discards the deletion, deleting it throws away the edit
rename/renameBoth branches renamed the same file to different namesThere can only be one final name
add/addBoth branches created a new file at the same path with different contentsNeither has a base to compare against, so the two texts clash
binary fileBoth branches changed an image, PDF or other binaryGit compares lines, and a binary has no lines to combine
Conflicts are routine

Whenever several people work in parallel, overlapping edits will eventually happen. A conflict does not mean your repository is damaged, and nothing is lost: both versions are still safe in history. It is a normal step in the workflow that you resolve, then carry on.

Common mistake

Believing a conflict means you did something wrong, and then panicking or deleting the repository. Git has paused the merge for your decision. Your commits on both branches are untouched.

Part 3 · Triggering a Conflict Safely

Building a practice repo that conflicts on purpose

The best way to stop fearing conflicts is to cause one in a throwaway folder where nothing can be lost. A conflict needs only two things: two branches that both changed the same lines of the same file since they split apart. The recipe below builds exactly that, in five short steps.

The recipe at a glance
  1. 1git initnew empty repo on main
  2. 2First commitgreeting.txt on main
  3. 3git switch -c featurebranch off from that commit
  4. 4Edit line 1 on featurecommit it
  5. 5Edit line 1 on maincommit it differently

Start by creating the repository and a small file with two lines. The commit on main is the common ancestor: the last point both branches agree on. Then git switch -c feature creates a new branch at that commit and moves you onto it in one step. We also add a second file, notes.txt, on feature. It will never conflict, and later it shows how Git treats files that merge cleanly.

bash
git init -b main practice-merge
cd practice-merge

echo 'Hello, world' > greeting.txt
echo 'Welcome to Git' >> greeting.txt
git add greeting.txt
git commit -m 'Add greeting'

git switch -c feature

Steps 1 and 2: the repo, the common ancestor and the feature branch

Now the important part: change line 1 on both branches, but to different text. On feature we also add notes.txt. The sed command replaces line 1 of the file (it works in Git Bash; in any editor you can simply retype the first line). After committing on feature, switch back to main and rewrite the very same line another way.

bash
# on feature: change line 1, add a harmless second file
sed -i '1s/.*/Hello from feature/' greeting.txt
echo 'Remember to merge' > notes.txt
git add greeting.txt notes.txt
git commit -m 'Greet from feature'

# back on main: change the same line differently
git switch main
sed -i '1s/.*/Hello from main/' greeting.txt
git commit -am 'Greet from main'

Step 3: two different edits to line 1

Why this is safe

Everything lives in the practice-merge folder. If you get tangled, delete the folder and rerun the recipe. You can also use the same repo again later to practise resolving the conflict.

Running the merge

You are on main, so you merge feature into it. Git compares both branches against the common ancestor. notes.txt only exists on one side, so it is added without fuss. Line 1 of greeting.txt was changed on both sides to different text, and Git refuses to guess which version you want.

bash
git merge feature
output
Auto-merging greeting.txt
CONFLICT (content): Merge conflict in greeting.txt
Automatic merge failed; fix conflicts and then commit the result.

Read these three lines carefully, because you will see them often. Auto-merging says Git tried. CONFLICT (content) names the file and the kind of problem: both sides changed its content. Automatic merge failed tells you the merge has stopped halfway and is waiting for you. Nothing is broken, and no work is lost.

Common mistake: thinking the merge destroyed something

A conflict message is not an error that damaged your repo. Both branches still hold their own commits untouched. Only your working folder is in a half-merged state, and you can leave it with git merge --abort, covered in a later section.

Inspecting the repo mid-merge

After the failed merge your repo is in a special in-between state. Four quick checks tell you exactly where you stand. Get used to running them, because they answer the question every student asks at this point: what do I have to fix?

git status: your map

git status is the first command to run. It now reports You have unmerged paths and groups files by what happened to them. The conflicted file appears under both modified, meaning both branches changed it. Files that merged cleanly are already staged by Git, so they sit under Changes to be committed.

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

Changes to be committed:
	new file:   notes.txt

Unmerged paths:
  (use "git add <file>..." to mark resolution)
	both modified:   greeting.txt

no changes added to commit (use "git commit" and/or "git commit -a")
What status showsWhat it meansDo you need to act?
Changes to be committed: new file: notes.txtMerged cleanly and was staged by Git for youNo
Unmerged paths: both modified: greeting.txtBoth branches changed the same linesYes, resolve it by hand
(use "git merge --abort" to abort the merge)Your escape hatch back to before the mergeOnly if you want to give up

The takeaway is that a merge with ten changed files and one conflict leaves you with one file to think about. Everything under Changes to be committed is finished work. Only the files listed under Unmerged paths need your attention.

Listing only the unresolved files

In a real project the status output can run to many lines. To get just the files still waiting for you, ask for the unmerged ones with a diff filter. U stands for unmerged, and --name-only prints bare file names that are easy to read or pass to other tools.

bash
git diff --name-only --diff-filter=U
output
greeting.txt

This list shrinks as you resolve files. When it prints nothing, no conflicted file remains, which makes it a handy final check before you commit.

The merge is still in progress

Git remembers that a merge is under way by writing a file called MERGE_HEAD inside the .git folder. It holds the hash of the commit you are merging in, here the tip of feature. As long as the file exists, Git knows this will become a merge commit with two parents.

bash
ls .git/MERGE_HEAD
git rev-parse MERGE_HEAD feature

The second command prints two hashes; yours will differ

output
.git/MERGE_HEAD
3f9c2a1d8b7e4c6a5f0e1d2b3c4a5968778695a4
3f9c2a1d8b7e4c6a5f0e1d2b3c4a5968778695a4

The ls command confirms the file is there. git rev-parse resolves names to hashes, and it prints two identical lines because MERGE_HEAD and feature point to the same commit. While this state lasts, Git blocks ordinary commits so you cannot record a half-resolved merge by accident.

bash
git commit -m 'try to commit'
output
error: Committing is not possible because you have unmerged files.
hint: Fix them up in the work tree, and then use 'git add/rm <file>'
hint: as appropriate to mark resolution and make a commit.
fatal: Exiting because of an unresolved conflict.
Common mistake: deleting .git/MERGE_HEAD by hand

Some students remove the file to make the error go away. That leaves the conflict markers in the file and Git with no memory of the merge. Resolve the file and commit, or run git merge --abort. Never edit the .git folder yourself.

Remember

A conflicted merge leaves a clear trail: status shows both modified, git diff --name-only --diff-filter=U lists what is left, and MERGE_HEAD marks the merge as unfinished until you resolve it.

Part 4 · Reading Conflict Markers

What Git Writes Into Your File

When Git cannot combine two edits on its own, it stops and writes both versions into the file with three kinds of marker lines. They look strange at first, but there are only three of them, and each one has a fixed job.

Anatomy of one conflict block
  1. 1<<<<<<< HEADOpens the block. Your side starts here.
  2. 2Your linesFrom the branch you are on (ours)
  3. 3=======Divider between the two versions
  4. 4Incoming linesFrom the branch being merged in (theirs)
  5. 5>>>>>>> featureCloses the block, labelled with the incoming branch

The opening line <<<<<<< HEAD marks the start of your side. HEAD is whatever you currently have checked out, so the lines right below it are the version from the branch you are standing on. Git calls this side ours.

The line ======= is only a divider. Everything above it is ours, and everything below it belongs to the other side.

The closing line >>>>>>> feature ends the incoming side. The text after the arrows is the name of the branch (or the commit) being merged in, so you always know whose lines you are reading. Git calls this side theirs.

MarkerMeaningWhich lines follow
<<<<<<< HEADStart of your sideOurs: the branch you are on
=======DividerSwitches from ours to theirs
>>>>>>> featureEnd of the incoming sideTheirs: the branch or commit being merged in

An annotated example

Imagine you changed the server port to 4000 on main, while a teammate changed the very same line to 5000 on feature. After git merge feature, the file contains this block. It is only three lines of content plus the markers.

output
<<<<<<< HEAD
const port = 4000;
=======
const port = 5000;
>>>>>>> feature

What server.js looks like after a conflicting merge

RegionLinesWho wrote it
Oursconst port = 4000;You, on the branch you are on
Theirsconst port = 5000;Your teammate, on feature
MarkersThe <<<<<<<, ======= and >>>>>>> linesGit, to show you where the disagreement is

Git does not know which port is right. Both lines changed the same spot, so it keeps both and asks you to decide. The decision is yours: keep 4000, keep 5000, or write something new.

The file is now broken on purpose

The markers are plain text sitting inside your file. Nothing special hides them. Until you clean them up, a compiler, an interpreter or a JSON parser will reject the file with a syntax error. That error is a good sign that you are not finished yet.

Seeing the Original With diff3 and zdiff3

The default markers only show the two finished versions. That can leave you guessing: did someone change 3000 to 4000, or did both sides add the line from scratch? To help, Git can also show the common ancestor, the original text both branches started from. You turn it on with the merge.conflictStyle setting.

bash
git config --global merge.conflictStyle diff3

Show the original lines in every conflict from now on

With diff3, a new section starts at a line beginning with |||||||. It holds the original lines, so a block now has three parts instead of two.

output
<<<<<<< HEAD
const port = 4000;
||||||| merged common ancestors
const port = 3000;
=======
const port = 5000;
>>>>>>> feature

The same conflict with diff3 markers

Now the intent is clear. The original was 3000, you moved it to 4000, and your teammate moved it to 5000. You can see what each side changed, not just what each side ended up with. That makes it much easier to combine the two changes correctly.

zdiff3: the tidier variant

zdiff3 shows the same base section, but it is smarter about lines that both sides changed in exactly the same way. Plain diff3 drags those shared lines into the block on all three sides. zdiff3 pulls them outside the markers, so the block only contains the lines that really disagree.

bash
git config --global merge.conflictStyle zdiff3

Same base section, less clutter

output
<<<<<<< HEAD
const port = 4000;
||||||| merged common ancestors
const port = 3000;
=======
const port = 5000;
>>>>>>> feature
const host = '0.0.0.0';

zdiff3: the host line was changed identically on both sides, so it sits below the block

StyleSections in a blockShared edits
merge (default)Ours and theirsPulled inside the block on both sides
diff3Ours, base and theirsPulled inside the block on all three sides
zdiff3Ours, base and theirsMoved outside the block
Set it once

Most people turn on zdiff3 once with the command above and never look back. The base section costs one extra line of reading and saves a lot of guessing.

Editing Rules and the Rebase Swap

When you resolve a conflict, there is one rule to keep in mind: only edit the lines between the markers. The area between them is where the disagreement lives, and Git has already merged everything outside it. Your job is to replace the whole block with the single version you want.

The markers themselves are not part of your code, so all of them must be deleted before you commit. That means the <<<<<<< line, the ======= line, the >>>>>>> line, and the ||||||| line plus the base lines under it if you use diff3 or zdiff3. Here is the earlier block after a resolution, where the team agreed on port 4000.

output
const port = 4000;

Resolved: one version, no markers left

Common mistake: committing the markers

If you stage a file that still contains <<<<<<<, ======= or >>>>>>> lines, Git will happily commit them. The build then breaks for everyone. Before staging, search the file for those markers, or run git diff --check, which warns about leftover conflict markers.

Pitfall: ours and theirs swap during a rebase

Everything above assumes a merge, where ours is the branch you are on. A rebase works differently. Git checks out the branch you are rebasing onto, then replays your commits on top of it one at a time. So while a conflict is open, HEAD is the branch you are rebasing onto, and the commit being replayed is your own work.

During a mergeDuring a rebase
Ours (above the divider)The branch you are onThe branch you are rebasing onto
Theirs (below the divider)The branch being merged inYour own commit being replayed
Label after >>>>>>>The incoming branch nameA short commit hash and your commit subject
Common mistake: keeping the wrong side in a rebase

It is natural to think that the top half is yours. In a rebase it is not. If you pick the top half to keep your work, you will throw your own change away and keep the other branch's version. Read the label after >>>>>>> to see whose commit is being replayed, and remember that the same swap applies to git checkout --ours and --theirs.

Reading checklist

Find the three markers, work out which side is yours for this operation (merge or rebase), read the base section if you have one, and keep only the lines that should survive. Then delete every marker.

Part 5 · Resolving by Hand in an Editor

The four steps and three choices

When a merge stops with a conflict, Git has already merged everything it could and left the hard parts in the file for you. Resolving by hand means editing that file until it contains the text you actually want, with no trace of Git's bookkeeping. Any text editor can do this, because the conflict is just ordinary text.

Resolving one conflicted file
  1. 1Open the filefind each conflict block
  2. 2Decide the final textwhat should the code say?
  3. 3Delete the marker linesall three of them
  4. 4Savethe file is now plain code

The second step is the only one that needs thought. The other three are mechanical. Each conflict block has an ours half, above the ======= line, and a theirs half, below it. You have three choices for every block.

ChoiceWhat you doUse it when
Keep oursKeep the top half, delete the bottom halfThe change from the other branch is not needed or is already covered
Keep theirsKeep the bottom half, delete the top halfThe other branch has the better version of these lines
Combine bothWrite new code that contains the intent of both halvesBoth branches changed something that should survive

Whichever you pick, the result must have all three marker lines removed: the <<<<<<< line, the ======= line and the >>>>>>> line. Deleting only the half you do not want is not enough, because the markers would still sit in your file and break it.

You are the editor

Git cannot know which version is right. Your job is to produce the final text the file should have, as if you were writing it fresh with both changes in front of you.

Example: combining two signature edits

The combine choice is the one beginners find hardest, so here is a concrete case. On your branch you added a retries parameter to a function. On the branch you are merging, a teammate added an html parameter to the same line. Git cannot merge two edits to one line, so it stops and writes this into the file.

text
<<<<<<< HEAD
def send_email(to, subject, retries=3):
=======
def send_email(to, subject, html=False):
>>>>>>> feature/html-mail
    kind = "html" if html else "plain"
    return f"{kind} mail to {to}: {subject} (retries={retries})"

The conflicted file, as Git leaves it

Neither half is correct alone. Keeping ours would lose html, and the body below already uses html. Keeping theirs would lose retries, and the body uses that too. The right answer is one new line with both parameters, and with the markers and both old lines gone the file looks like this.

python
def send_email(to, subject, retries=3, html=False):
    kind = "html" if html else "plain"
    return f"{kind} mail to {to}: {subject} (retries={retries})"

print(send_email("ana@example.com", "Hello"))
print(send_email("ana@example.com", "Hello", retries=5, html=True))

The resolved file, with a call at the bottom to try it

output
plain mail to ana@example.com: Hello (retries=3)
html mail to ana@example.com: Hello (retries=5)
Common mistake: leaving both def lines

Deleting only the markers and keeping both def lines produces two definitions of the same function, with the second silently replacing the first. Merge them into one line instead.

Using VS Code and checking your work

Editing markers by hand is fine, but VS Code makes the common cases a single click. It draws a small row of links directly above each conflict block, and each link performs one of the choices from the previous page.

Link above the blockWhat it doesSame as
Accept Current ChangeKeeps the top half (ours) and removes the markers and the other halfKeep ours
Accept Incoming ChangeKeeps the bottom half (theirs) and removes the markers and the other halfKeep theirs
Accept Both ChangesKeeps both halves one after the other and removes only the markersCombine, but only by stacking
Compare ChangesOpens a side-by-side view of the two halvesHelps you decide

Accept Both simply puts the two halves one after the other. For the signature example that would give two def lines, so you would still edit the result into a single line. Treat the buttons as a fast start, then read what is left.

Before you tell Git you are finished, confirm that no markers survive. Search the file for <<<<<<< , ======= and >>>>>>>, or let the shell do it for the whole project. Note that a ======= line can legitimately appear in some documents, such as heading underlines, so look at each hit.

bash
git diff --check
grep -rn -E "^(<<<<<<<|=======|>>>>>>>)" .

The first command reports leftover markers; the second lists every line that looks like one

A file with no markers is only textually clean. It may still be wrong, because Git does not understand your code. A caller may use the old signature, or two merged halves may use the same variable name. So run the tests or the build after editing.

python
def send_email(to, subject, retries=3, html=False):
    kind = "html" if html else "plain"
    return f"{kind} mail to {to}: {subject} (retries={retries})"

# an old call from before the merge, passing html as the third argument
print(send_email("ana@example.com", "Hi", True))

A caller that no longer means what it did, though no marker is left

output
plain mail to ana@example.com: Hi (retries=True)

The merged file has no markers and runs without any error, yet the call now passes True as retries instead of turning on HTML. Only a test or a careful read would catch that.

Marking it resolved, or taking a whole side

Once the file is correct, saving it is not enough. Git still lists the path as unmerged until you say otherwise. Staging the file is how you tell Git that this path is resolved.

bash
git add mail.py
git status

After git add, the file moves from unmerged paths to changes to be committed

Sometimes a block-by-block merge is wasted effort. You may know that one branch's whole version of a file is the right one, such as a generated file or a file the other branch rewrote completely. In that case ask Git for one entire side, then stage it.

bash
git checkout --ours mail.py
git checkout --theirs config.json
git add mail.py config.json

Take our whole file for one path and their whole file for another

CommandFile ends up asDoes it keep the other side's edits?
git checkout --ours <file>The version from your current branchNo, they are discarded for this file
git checkout --theirs <file>The version from the branch being merged inNo, your edits are discarded for this file
Which side is which

During a merge, ours is the branch you are standing on and theirs is the branch you are bringing in. During a rebase the two swap, which a later section explains.

Common mistake: using --ours or --theirs on a mixed file

These commands replace the entire file, not just the conflicted blocks. Any non-conflicting change from the discarded side is lost too, so use them only when you really want one side wholesale.

Common mistake: forgetting git add

Fixing and saving the file does not mark it resolved. Without git add <file>, the merge cannot be finished, and Git will keep reporting the path as unmerged.

Part 6 · Resolving with git mergetool

What git mergetool does and how to set it up

Reading conflict markers in an editor works well when a conflict is small. When a file has many conflicting regions, the markers get hard to follow. git mergetool handles that case. After a merge stops with conflicts, you run it once. Git finds every unmerged file and opens each one in a visual three-way merge tool, one after another.

bash
git merge feature/login
git status
git mergetool

The merge stops, status lists the unmerged paths, and mergetool walks through them.

Git does not ship its own merge window. It launches a tool that is already on your machine and hands it the pieces of the conflict. So the first step is to tell Git which tool to use. You do that once with git config --global merge.tool, followed by the tool name. Common choices are vscode, meld, kdiff3 and vimdiff.

bash
git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait --merge $REMOTE $LOCAL $BASE $MERGED'

The second line is the mergetool.<name>.cmd setting. It is only needed when Git has no built-in command for the tool.

Git already knows how to start tools such as meld, kdiff3 and vimdiff, so for those the merge.tool line is enough. For a tool Git does not know, or one installed in an unusual place, you add mergetool.<name>.cmd. This is the exact command Git runs. It receives four variables: $LOCAL, $BASE, $REMOTE and $MERGED. The --wait flag matters for VS Code, because Git must pause until you close the editor tab. Otherwise Git thinks you finished immediately.

VariableWhat it holdsEditable?
$LOCALYour side of the conflict (ours), from the branch you are onNo
$BASEThe common ancestor both branches started fromNo
$REMOTEThe incoming side (theirs), from the branch being mergedNo
$MERGEDThe working file where the final result is writtenYes

You may not know which tools your machine can use. Run git mergetool --tool-help to find out. Git prints the tools it supports and splits them into those that are available (installed and found) and those it could run if you installed them.

bash
git mergetool --tool-help

Lists the merge tools installed on your machine, so you can pick a name for merge.tool.

Pick a tool you already use

If you live in VS Code, use vscode. If you prefer the terminal, vimdiff needs no extra install on most systems. The best tool is the one you will actually open.

Working through a conflict in the tool

When the tool opens, you usually see four panes. They are the same four pieces that Git passed in through the variables above. Most tools place three read-only versions on top and the file you edit at the bottom, so you can compare the sides and then build the result below them.

PaneMeaningPosition (typical)
LOCALYour version, the branch you are on (ours)Top left
BASEThe common ancestor of both changesTop middle
REMOTEThe incoming version (theirs)Top right
MERGEDThe editable result that Git will keepBottom

The BASE pane is the main reason to use a mergetool. Plain conflict markers show only your side and their side. In the base you can see what the line looked like before either of you touched it. That tells you whether each side changed something or only one did, and which change to keep.

Your job is to edit the bottom MERGED pane until it holds the correct final text. Most tools let you click an arrow beside a hunk to copy it from LOCAL or REMOTE into MERGED. You can also type in MERGED directly to combine both sides. Then you save and quit the tool. Git sees that the tool exited cleanly, marks that file as resolved, and moves on to the next unmerged file.

What happens when you run git mergetool
  1. 1Run git mergetoolGit lists the unmerged files
  2. 2Tool opens a fileLOCAL, BASE, REMOTE and MERGED
  3. 3You edit MERGEDPick or combine the changes
  4. 4Save and quitGit marks the file resolved
  5. 5Next fileRepeat until none are left

If you quit the tool without saving, Git asks whether the file was merged. Answer no and the file stays unresolved, so you can run git mergetool again later. Once every file is resolved, you finish the merge with a normal commit, which the next section covers.

Common mistake: closing the tool before saving

Closing the window without saving leaves MERGED unchanged, with the conflict markers still inside. If you then tell Git the file is resolved, those <<<<<<< lines get committed. Always save first, and check the file once afterwards with git diff.

Backup files, comparison and keeping .orig out of Git

Each time a tool resolves a file, Git first keeps a copy of the conflicted version. It saves it next to the original with an .orig suffix, so a conflict in app.py leaves an app.py.orig file behind. The backup is a safety net in case you resolve badly. After a few merges these files show up as untracked in git status and clutter the output.

bash
git status --short
StatusPath
Mapp.py
??app.py.orig

There are two ways to stop that. If you do not want backups at all, turn them off with git config --global mergetool.keepBackup false. Git then deletes the .orig file once the tool finishes. If you would rather keep the safety net, leave the setting alone and tell Git to ignore the files, by adding the pattern *.orig to your .gitignore. Then the backups are never committed by accident, even if you run git add ..

bash
git config --global mergetool.keepBackup false
echo '*.orig' >> .gitignore

Use the first line to stop creating backups, the second to ignore any that exist.

You now have two ways to resolve a conflict, so how do you choose? Both end in the same place, a clean file with no markers. They differ in how much effort they cost for the size of the conflict.

Manual editinggit mergetool
Best forSmall conflicts, one or two hunksLarge files and many hunks
SpeedFastest, as there is nothing to openSlower to start, faster on big files
Sees the base?No, only ours and theirs markersYes, in a dedicated BASE pane
RiskLeaving markers behind by handBackup files and tool setup
Setup neededNonemerge.tool and sometimes a cmd
Choosing between them

Fix a quick one-hunk conflict by hand. Reach for git mergetool when a file has many conflicting regions or when you need to see the base to understand what each side meant.

Common mistake: committing .orig files

If you neither disable backups nor ignore them, git add . stages every .orig file and they end up in the repository history. Set mergetool.keepBackup false or add *.orig to .gitignore before your first mergetool session.

Part 7 · Finishing the Merge

Telling Git the Conflicts Are Solved

Editing a file until the markers are gone does not finish a merge. Git still lists that file as a conflict until you say otherwise. Finishing is a short routine: check the status, stage every fixed file, create the merge commit, then confirm the result is sound.

From resolved files to a finished merge
  1. 1git statussee what is still unmerged
  2. 2git addmark each file resolved
  3. 3git commitor git merge --continue
  4. 4Verifylog graph and tests

Step 1: Read git status

While a merge is stopped on conflicts, git status is your progress report. Every conflicted file sits under an Unmerged paths heading. The goal is to move each of them out of that list and into Changes to be committed. Right after the conflict, with app.py still unresolved, you see this.

output
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:   app.py

Step 2: Stage each fixed file

Git has no separate "resolved" command. Staging is how you tell Git a conflict is resolved. When you run git add app.py, Git takes the file as it is on disk and records it as the final answer for that path. Do this once for every file that was in conflict.

bash
git add app.py
git status
output
On branch main
All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:
	modified:   app.py

The wording has changed from "You have unmerged paths" to "All conflicts fixed but you are still merging". The file moved from Unmerged paths to Changes to be committed. Once the Unmerged paths section is gone, Git is ready for the merge commit.

Status headingWhat it means for you
Unmerged pathsGit still treats the file as a conflict, so you must fix it and stage it
Changes to be committedThe file is staged and will be part of the merge commit
Changes not staged for commitYou edited the file again after staging, so stage it once more
Staging a file that still has markers

Git does not look inside the file when you run git add. If <<<<<<<, ======= or >>>>>>> lines are still in the file, they get staged as if they were real code. Run git diff --check before you stage. It reports leftover conflict markers.

Creating the Merge Commit

With nothing left under Unmerged paths, you can record the merge. There are two ways to do it, and they end in the same commit.

Option A: git commit with no message

Run git commit without -m. Git sees that a merge is in progress and opens your editor with a message already written. The first line is Merge branch 'feature'. Below it is a list of the files that conflicted, shown as comment lines so you can see what you resolved.

output
Merge branch 'feature'

# Conflicts:
#	app.py
#
# It looks like you may be committing a merge.
# If this is not correct, please run
#	git update-ref -d MERGE_HEAD
# and try again.

You can accept the first line as it is or reword it, for example to say what you decided in the conflict. Save and close the editor, and the merge commit is created. A default message is fine here, because the commit's two parents already record what was merged. If you pass -m, you skip the pre-filled text and the Conflicts list.

Option B: git merge --continue

The second route is git merge --continue. It runs the same commit and opens the same editor with the same message. What it adds is a statement of intent: you are finishing a merge, not making an ordinary commit.

Both commands behave the same when you are not ready. If any path is still unmerged, each one stops with the same error, and no commit is created.

output
error: Committing is not possible because you have unmerged files.
hint: Fix them up in the work tree, and then use 'git add/rm <file>'
hint: as appropriate to mark resolution and make a commit.
fatal: Exiting because of an unresolved conflict.

The real difference shows up when no merge is running. git commit still works as an ordinary commit. git merge --continue refuses, because it only makes sense during a merge.

output
fatal: There is no merge in progress (MERGE_HEAD missing).
git commitgit merge --continue
During a merge, all files stagedOpens the editor, makes the merge commitDoes the same
During a merge, a file still unmergedRefuses with the unmerged-files errorRefuses with the same error
Outside a mergeMakes an ordinary commitFails with "There is no merge in progress"
What it tells the readerNothing specific, it is a general commandYou meant to finish a merge
Which one to use

Neither is safer than the other. Pick git merge --continue if you want the command to say what you are doing, and git commit if you prefer one command for everything.

Checking the Result Before You Push

See what will be committed

Before you commit, git diff --cached shows the combined result that the merge commit will contain. During a merge, it compares your staged files with your current branch tip. So you see exactly what the merged code changes for your branch, with the other branch's work and your conflict decisions blended in. If a line you meant to keep is missing or a marker line slipped through, you spot it here, while it is still cheap to fix.

bash
git diff --cached
output
diff --git a/app.py b/app.py
--- a/app.py
+++ b/app.py
@@ -1,4 +1,5 @@
 def greet(name, shout=False):
-    return 'Hello ' + name
+    msg = 'Hello, ' + name + '!'
+    return msg.upper() if shout else msg

Check that the commit has two parents

After the commit, git log --oneline --graph draws the history as lines. A real merge commit has two parents, so you see two lines join into one at the merge. The first parent is the branch you were on, and the second is the branch you merged in.

bash
git log --oneline --graph
output
*   a1b2c3d (HEAD -> main) Merge branch 'feature'
|\
| * 9f8e7d6 (feature) Add shout option
* | 4c5d6e7 Change greeting text
|/
* 1a2b3c4 Initial commit

To list the parents directly, ask for just that field. Two hashes mean a two-parent merge commit.

bash
git log -1 --format=%p
output
4c5d6e7 9f8e7d6

Run the tests on the merge commit

A clean merge in Git is not the same as working code. Each branch passed its own tests, but the merged result is new code that nobody has run yet. Two changes can fit together without any conflict and still break each other, for example when one branch renames a function that the other branch calls. So run the tests on the merge commit before you push.

Pushing straight after the merge

The most common slip is to push as soon as Git stops complaining. If the tests fail on the merge commit, your teammates pull broken code. If they fail, fix the problem and commit the fix as a new commit on top of the merge.

Finishing checklist

Check git status, stage each fixed file, run git commit or git merge --continue, look at git log --oneline --graph for the two-parent merge, and run the tests before you push.

Part 8 · Aborting and Undoing a Merge

Backing Out of an Unfinished Merge

Sooner or later you start a merge, see a screen full of conflicts, and think: not today. Git lets you walk away. While a merge is stopped on conflicts, Git remembers where you started, so one command can put everything back exactly as it was before you typed git merge.

git merge --abort

git merge --abort throws away the half-finished merge. Your branch goes back to the commit it was on, the index is reset, and every file in the working tree returns to its pre-merge content, including the files Git had already merged cleanly. It is safe to run at any moment while the merge is unfinished: right after the conflict message, halfway through editing files, or after you have staged some of them.

bash
git merge feature/login
git status
git merge --abort
git status

Conflict, look around, give up, confirm everything is back to normal

output
Auto-merging app.py
CONFLICT (content): Merge conflict in app.py
Automatic merge failed; fix conflicts and then commit the result.
On branch main
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
        both modified:   app.py

On branch main
nothing to commit, working tree clean

Notice that Git even suggests the command in its own status message. After the abort, feature/login is untouched and still waiting for you; nothing about it changed. You can try the merge again later, perhaps after talking to the teammate who edited the same lines.

git reset --merge, the older way

Before --abort existed, people used git reset --merge. It does the same job during a conflicted merge: it resets the index and the files that the merge touched, and it keeps unrelated uncommitted changes in files the merge never involved. You will still see it in older tutorials and scripts, and it works fine, but git merge --abort says what you mean and is the one to reach for.

bash
git reset --merge

Equivalent to git merge --abort while a merge is in progress

Start from a clean working tree

Run git status before every merge and commit or stash anything you are carrying. If you begin a merge with uncommitted edits, Git may not be able to separate your edits from the merge changes when you abort, and your work can be lost or tangled up. With a clean tree, abort has nothing to lose and always returns you to a spotless starting point.

Undoing a Merge That Is Already Finished

The abort command only works while Git is still holding the paused merge. Once you run git commit (or git merge --continue) and the merge commit exists, there is nothing left to abort, and Git says so.

bash
git merge --abort

Run after the merge commit has been made

output
fatal: There is no merge to abort (MERGE_HEAD missing).

From here the right tool depends on one question: has the merge commit been pushed, so that other people may have it?

Not pushed yet: reset --hard ORIG_HEAD

Before a risky operation like a merge, Git saves the old position of your branch in a special name called ORIG_HEAD. If the merge commit is only on your machine, you can move the branch straight back there. The merge commit disappears from your branch as if it never happened.

bash
git log --oneline -3
git reset --hard ORIG_HEAD
git log --oneline -3

Undo a finished merge that nobody else has seen

output
a41c9e2 Merge branch 'feature/login'
7d03b18 Add health check endpoint
e92f6c0 Fix typo in README
7d03b18 Add health check endpoint
e92f6c0 Fix typo in README
3b8a051 Initial project layout

Run it straight after the merge. Other commands, such as git pull, git rebase and another git reset, overwrite ORIG_HEAD, and then it no longer points where you expect. If in doubt, read the log first and use the commit hash from before the merge instead.

Already pushed: revert -m 1

Once teammates may have pulled the merge, you must not erase it. Instead you add a new commit that cancels its effect. A merge commit has two parents, so Git needs to know which side to keep. The flag -m 1 means: keep parent number 1, which is the branch you were standing on when you merged, and undo the changes that came in from the other branch.

bash
git revert -m 1 a41c9e2

Adds a new commit that undoes the merge; history is preserved

output
[main c70de15] Revert "Merge branch 'feature/login'"
 3 files changed, 0 insertions(+), 87 deletions(-)
Merging the branch again later

After a revert, Git considers those commits already merged, so merging feature/login again brings nothing back. To re-apply the work, first revert the revert commit, then merge.

Choosing the right undo

git merge --abortgit reset --hard ORIG_HEADgit revert -m 1 <merge>
Use whenMerge still stopped on conflictsMerge commit made, not pushedMerge commit already pushed
What it doesDiscards the unfinished mergeMoves the branch back before the mergeAdds a new commit that cancels the merge
History afterwardsAs if you never startedMerge commit is goneMerge and revert both stay visible
Safe for shared branchesYes, nothing was sharedNo, rewrites historyYes, only adds history
Which undo do I need?
Common mistake: reset --hard on a pushed branch

Running git reset --hard on a branch that is already pushed rewrites shared history. Teammates still have the old merge commit, and your next push needs --force, which can wipe out their work. On any pushed branch use git revert -m 1 instead.

Redoing One File and Staying Safe

Sometimes the merge as a whole is fine, but you botched a single file: you deleted the wrong side, left a stray marker, or saved over the wrong version. You do not have to abort everything and redo the other files. While the merge is still unfinished, you can ask Git to rebuild the conflict for just that file.

bash
git checkout -m app.py
grep -n "<<<<<<<\|>>>>>>>" app.py

Recreate the conflict markers for one file

output
12:<<<<<<< HEAD
18:>>>>>>> feature/login

The -m flag (short for --merge) tells Git to redo the three-way merge for that path and write the conflict markers back into the file. It works even if you already ran git add on the file during this merge. On newer Git you can write the same thing as git restore --merge app.py. Then resolve the file again the usual way, stage it, and carry on.

Common mistake: forgetting that checkout -m overwrites your edits

The file is rebuilt from the original two sides, so any work you did in it, good or bad, is replaced by fresh conflict markers. Copy anything worth keeping before you run it.

What to remember

Unfinished merge: git merge --abort. Finished but not pushed: git reset --hard ORIG_HEAD. Finished and pushed: git revert -m 1. One bad file: git checkout -m <file>. And always begin a merge with a clean working tree.

Part 9 · Conflicts During Rebase

Why a Rebase Can Stop More Than Once

A merge combines two branches in a single step, so you settle every disagreement once. A rebase works differently. It lifts your commits off the branch, moves to the new base, and replays your commits one by one on top of it. Each replay is a small merge of its own, so a conflict can appear on any commit. If three of your commits touch the same lines that the upstream changed, you may have to resolve three times.

What git rebase main does to your branch
  1. 1Move to the new basethe tip of main
  2. 2Replay commit 1clean, or stops on a conflict
  3. 3Replay commit 2may conflict again
  4. 4Replay commit 3and so on, until the last one

When a replayed commit cannot be applied cleanly, Git stops in the middle of the job. It prints a line starting with CONFLICT and then tells you which commit it could not apply, using the short hash and the original commit message.

bash
git switch feature
git rebase main
output
Auto-merging app.py
CONFLICT (content): Merge conflict in app.py
error: could not apply 3f2a1b9... Change greeting
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".
Could not apply 3f2a1b9... Change greeting

The hash in could not apply tells you which of your commits is being replayed right now. Run git status to see where you are in the larger job. It says that a rebase is in progress, shows how many commits are done and how many remain, and lists the files that are still unmerged.

bash
git status
output
interactive rebase in progress; onto 8c1d4e7
Last command done (1 command done):
   pick 3f2a1b9 Change greeting
Next commands to do (1 remaining command):
   pick 5b7e0c2 Add farewell
You are currently rebasing branch 'feature' on '8c1d4e7'.
  (fix conflicts and then run "git rebase --continue")

Unmerged paths:
  both modified:   app.py

The Resolve Loop, Abort and Skip

Resolving a rebase conflict uses the same editing skills as a merge conflict, but the finishing step is different. After you fix the file, you stage it and tell the rebase to carry on. You do not run git commit here, because Git re-creates the commit for you from the original message. The loop repeats until no commits are left to replay.

The rebase resolve loop
bash
# fix app.py in your editor, then:
git add app.py
git rebase --continue

If things go badly, you have two exits. git rebase --abort throws away the whole rebase and puts your branch back in the exact state it had before you started, so it is always safe to use. git rebase --skip is narrower: it drops only the commit currently being replayed, along with its changes, and moves on to the next one.

CommandWhat it doesWhen to use it
git rebase --continueRecords the resolved commit and replays the next oneAfter you edit and git add the files
git rebase --abortReturns the branch to its state before the rebaseWhen you are lost or want to start over
git rebase --skipDrops the current commit and its changesWhen the upstream already contains the same change
Common mistake: git commit in the middle of a rebase

Running git commit after resolving a conflict is what you do in a merge, but in a rebase it is the wrong step. Use git rebase --continue instead. Also remember that --skip really deletes your work from the rewritten branch, so use it only when you are sure the commit is not needed.

Ours and Theirs Are Flipped

During a merge, HEAD (ours) is your branch and the incoming side is the other branch. A rebase turns this around. Git first checks out the new base and then applies your commits onto it, so ours is the upstream you are rebasing onto, and theirs is your own commit that is being replayed. The names describe what Git is doing at that moment, not whose work it is.

MergeRebase
Ours (HEAD side)Your current branchThe upstream you are rebasing onto
Theirs (incoming side)The branch you are merging inYour own commit being replayed

The markers in the file follow the same rule. The top half, labelled HEAD, holds the upstream version. The bottom half is labelled with the hash and message of your commit. Here is the block from the app.py conflict above, with all three markers in place.

text
<<<<<<< HEAD
greeting = "Hello, team"
=======
greeting = "Hi there"
>>>>>>> 3f2a1b9 (Change greeting)

Top: upstream (ours). Bottom: your commit being replayed (theirs).

If you pick a whole side from the command line, the flip applies there too. git checkout --ours app.py keeps the upstream version, and git checkout --theirs app.py keeps the version from your commit. Reading the labels on the markers is the safest habit.

Rebase or merge?

Both commands bring two lines of work together, and both can conflict. They differ in how the effort is spent and in what history you end up with.

MergeRebase
Conflicts resolvedAll at once, in one goOnce per replayed commit that conflicts
ResultOne merge commit that joins both linesYour commits rewritten on top of the new base
History shapeBranching, shows when the lines joinedLinear, a single straight line
Original commitsKept unchangedReplaced by new commits with new hashes

Shared Branches and Remembering Resolutions

A rebase does not move your commits. It creates new copies with new hashes and abandons the old ones. That is harmless on a branch only you use. It is a serious problem for commits that were already pushed and shared, because teammates still have the old commits and their history now disagrees with yours.

Common mistake: rebasing shared commits

Never rebase commits that other people have pulled. Rewriting them forces everyone else to untangle duplicated commits and fresh conflicts. Rebase your private feature branch before you share it, and merge once it is public.

In a long rebase you may hit the same conflict again and again, for example when several commits edit the same line. Git can remember how you resolved it. The setting is called rerere, short for reuse recorded resolution. After you turn it on, Git stores each resolution, and when it sees the same conflict later it applies your earlier answer on its own.

bash
git config rerere.enabled true

Add --global to turn it on for every repository.

With rerere on, the first conflict is resolved by hand as usual. When a later commit in the same rebase produces the identical conflict, Git fills in the saved resolution and reports it. You still check the result, run git add, and continue, but the repeated typing is gone.

Rebase conflicts in short

A rebase replays your commits one by one, so you may resolve several times. Edit, git add, then git rebase --continue, and never git commit. Use --abort to go back and --skip to drop a commit. Remember that ours is the upstream and theirs is your commit. Keep rebasing to your own unshared branches, and turn on rerere when conflicts repeat.

Part 10 · Preventing Conflicts

Keep Changes Small and Fresh

A conflict happens when two branches change the same lines, or one branch changes lines the other deleted. Every prevention habit in this section works by shrinking the chance of that overlap. The first three habits are about size and time: how big each change is, and how long it sits on your branch before it meets the rest of the team's work.

Commit small and often

A small commit touches a few lines for one reason. A large commit touches many lines for many reasons, so it has more chances to overlap with a teammate's work. When an overlap does happen, a small commit is also easier to resolve, because you can tell what each side was trying to do.

Small, frequent commitsOne big commit at the end
Lines touchedA handful per commitHundreds across many files
Chance of overlapLowHigh
Reading a conflictClear: one purpose per changeHard: several purposes mixed together
Undoing a mistakeRevert one commitPick apart a tangle by hand
bash
git add src/cart.js
git commit -m "Round cart total to two decimals"
git add src/tax.js
git commit -m "Read tax rate from config"

Two focused commits instead of one commit named 'cart work'

Sync with main at least daily

While you work, main keeps moving. The longer your branch ignores it, the further the two drift apart and the more lines can collide. Bring main into your branch every day, even when nothing seems to have changed. A small conflict today is cheaper than a large one next week, and you resolve it while the code is still fresh in your head.

bash
git fetch origin
git merge origin/main

Run this at the start of each working day on your feature branch

Keep feature branches short-lived

Divergence grows with time. A branch that lives for a few hours has almost nothing to collide with. A branch that lives for two weeks has collected two weeks of other people's commits to disagree with. Split large features into pieces that can be merged separately, and merge each piece as soon as it works.

Risk grows with branch age
  1. 1HoursFew commits on main, rare overlap
  2. 2DaysSeveral teammates have merged
  3. 3WeeksMany overlaps, hard to resolve

Divide the Work and Agree on Rules

The next habits are about people. Git cannot know that you and a teammate are both about to edit the same function, but you can. A little coordination removes most conflicts before any code is written.

Own different files, and keep reformatting separate

When you plan the work, give each person their own files or modules where you can. Two people editing different files can never conflict. Be careful with reformatting too. If you re-indent a whole file while also changing its logic, every line of that file is now different, and any teammate who touched the file will get a conflict. Do the reformatting in its own commit, merge it quickly, and only then start the logic change.

Reformat first, alone

Make a commit that contains only formatting changes, merge it to main, and tell the team to sync. Logic changes that follow are then small and easy to read.

Share one formatter configuration

If your editor uses tabs and a teammate's uses spaces, Git sees every changed line as different. A shared formatter such as Prettier for JavaScript or Black for Python removes this problem. Commit its configuration file to the repository so that everyone formats the same way automatically.

bash
# .prettierrc, committed to the repository
{
  "semi": true,
  "singleQuote": true,
  "tabWidth": 2
}

One shared config means no whitespace-only differences between teammates

Tell the team before touching shared files

Some files attract everyone: package.json, configuration files and large modules that many features pass through. These are the places where conflicts cluster. Before you change one, say so in the team chat. Then a teammate can merge their pending edit first, or you can agree on an order, and nobody is surprised later.

Regenerate lockfiles, do not hand-merge them

Files such as package-lock.json are generated by a tool and contain long machine-written blocks. Merging them by hand is error-prone, and a wrong guess can leave you with a lockfile that does not match package.json. Instead, merge package.json first, then let the tool rebuild the lockfile.

bash
git checkout --theirs package-lock.json
npm install
git add package-lock.json

Take either side to clear the conflict, then let npm rebuild the file from package.json

Common mistake: hand-editing a lockfile

Deleting conflict markers inside a lockfile can produce a file that looks valid but pins the wrong versions. Resolve package.json by hand, then regenerate the lockfile with the package manager.

Choosing How to Bring in Changes

There are three common ways to get a teammate's work into your local branch. They differ in how much control you have and in what your history looks like afterwards. Knowing the difference helps you pick the one that fits the moment.

CommandWhat it doesHistory afterwardsBest when
git pullFetches, then merges the remote branch into yoursA merge commit if both sides movedYou want the simplest default
git pull --rebaseFetches, then replays your commits on top of the remote branchA straight line, no merge commitYou have local commits and want a tidy history
git fetch, then reviewDownloads only; nothing in your branch changes yetUnchanged until you decideYou want to look before you combine

The third option is the safest because it splits the job in two. First you download, then you look at what arrived, and only then do you merge or rebase. Since git pull is just a fetch followed by a merge, the careful version costs only one extra command.

Which way should I update?
bash
git fetch origin
git log --oneline main..origin/main
git merge origin/main

Fetch, read the incoming commits, then merge when you are ready

Common mistake: rebasing shared commits

Use git pull --rebase for commits that exist only on your machine. Rebasing commits that teammates already pulled rewrites history they depend on and creates new conflicts for them.

Preview Overlap Before You Merge

You do not have to find out about a conflict by running the merge. Git can show you in advance which files both sides have changed. Two commands do this. The first finds the point where the branches split, and the second shows only your branch's own changes since that point.

Find the common ancestor

git merge-base main feature prints the commit where the two branches last agreed. Everything after that commit, on either side, is what Git has to combine.

bash
git merge-base main feature
output
4be1c9a7d2e03f5a8b6c1d9e0f7a2b3c4d5e6f70

See what your branch changed

git diff main...feature uses three dots. It compares the feature branch with that common ancestor, so it shows only what the feature added and ignores what main did meanwhile. Run the same command from the other direction, git diff feature...main, to see what main changed. Any file that appears in both lists is a candidate for a conflict.

bash
git diff main...feature --stat
output
 src/cart.js | 9 ++++++---
 src/tax.js  | 4 ++--
 2 files changed, 8 insertions(+), 5 deletions(-)

Here src/cart.js has 6 insertions and 3 deletions, and src/tax.js has 2 insertions and 2 deletions, which add up to the summary line. If the same two files show up in the diff from main's side, expect a conflict and talk to the teammate first.

The habits in short

Each habit below reduces either how much you change or how long you wait before meeting other people's changes.

  • Commit small and often so each change touches few lines.
  • Merge main into your branch daily and keep branches short.
  • Give people different files, and keep reformatting in its own commit.
  • Share a formatter config and announce edits to shared files.
  • Regenerate lockfiles instead of merging them by hand.
  • Fetch and preview overlap before you merge.
Why it works

Conflicts need two branches editing the same lines. Small, fresh, well-divided changes make that overlap rare, and the preview commands tell you early when it is still going to happen.

Part 11 · Common Mistakes and Recovery

Mistakes that slip into a commit

Most merge disasters are not caused by the conflict itself. They come from small slips made while resolving it, or just after. The first three are the ones students hit most often, because each of them looks like a finished job.

Markers left inside a file

Git does not stop you from committing a file that still contains <<<<<<<, ======= and >>>>>>>. Once you stage the file, it counts as resolved, whatever is inside it. The markers then end up in your history, and a Python file with them will not even import. The cheap habit is to search for them before every commit.

bash
grep -rn '<<<<<<<' .
git diff --cached --check

The first line searches the working tree. The second checks only what is staged and also reports leftover conflict markers.

Typing this by hand works until the day you forget. A pre-commit hook makes Git do it for you: the commit is refused while a marker is staged.

bash
#!/bin/sh
# .git/hooks/pre-commit  (make it executable with chmod +x)
if git diff --cached | grep -n '^+<<<<<<<'; then
  echo 'Conflict markers are still staged. Fix them first.'
  exit 1
fi

A non-zero exit from a pre-commit hook cancels the commit.

Common mistake: staging before reading

Running git add . and git commit straight after a conflict, without opening the files, is how markers reach the repository. Search first, then stage.

Accept Current, blindly

Editors such as VS Code put buttons above each conflict: Accept Current, Accept Incoming and Accept Both. They are quick, and that is the danger. The current side is your branch and the incoming side is the one you are merging in, so clicking Accept Current on every block quietly throws away your teammate's work. The merge still succeeds, nobody sees an error, and the loss is found weeks later.

ButtonWhat survivesSafe when
Accept CurrentOnly your side (HEAD)You have read the other side and it is truly obsolete
Accept IncomingOnly the side being merged inYou have read your side and it is truly obsolete
Accept BothBoth blocks, one after the otherThe two changes are independent and the order is harmless
A quick safety check

Before you commit, run git diff --cached and also git log MERGE_HEAD -p -- path/to/file for the files you resolved. If a line that the other branch added is missing from your result, you must be able to say why.

When it merges but is still wrong

The semantic conflict

A textual conflict is something Git can see: two edits to the same lines. A semantic conflict is invisible to Git. The two sides edit different lines, so Git merges them cleanly, yet the combined program is broken. Typical cases are that one branch renames a function while the other adds a new call to the old name, or one branch changes what a function returns while the other keeps relying on the old behaviour.

Imagine one branch changed total to count money in cents, and another branch wrote a formatter that assumed dollars. They touched different lines, so the merge is silent. The program runs, which makes this worse than a crash.

python
def total_cents(items):
    return sum(price for _, price in items)

def format_total(items):
    return f"${total_cents(items):.2f}"

cart = [("pen", 250), ("book", 1200)]
print(format_total(cart))

Prices are now in cents, but the formatter still treats the number as dollars.

output
$1450.00

The shop owes the customer $14.50, not $1450.00. No marker ever appeared, so only a test or a careful reader would catch it. That is why a merge is not finished when git status goes quiet. It is finished when the tests pass.

After every merge, before you push
  1. 1Resolve and stageNo markers, nothing unmerged
  2. 2Run the test suiteOr at least build and start the app
  3. 3Read the combined diffLook for renamed or changed behaviour
  4. 4Commit and pushOnly when all of that is green
Common mistake: trusting a clean merge

Git saying Automatic merge went well only means no two edits overlapped. It says nothing about whether the program still works. Run the tests every time, even when there was no conflict at all.

Unfinished merges and unsafe starts

Edited but never added

Fixing the file in your editor is only half of resolving it. Git tracks each conflicted path as unmerged until you stage it, and that is how it knows you are done with that file. If you save the file and run git commit, Git refuses and lists the paths that are still unmerged.

output
$ git commit
error: Committing is not possible because you have unmerged files.
hint: Fix them up in the work tree, and then use 'git add/rm <file>'
fatal: Exiting because of an unresolved conflict.

$ git status
Unmerged paths:
        both modified:   app.py

The fix is simple: run git add app.py, check that git status now shows the file under changes to be committed, and then commit. The reverse also matters. If you stage a file by mistake before the conflict is truly resolved, Git will happily accept it, which is why the marker search comes first.

Merging on top of local changes

If you start a merge while you have uncommitted edits, Git has to mix two things in one working tree: your unfinished work and the merge. When the merge conflicts, nothing separates your edits from the merge's changes. git merge --abort then tries to rebuild the pre-merge state, and it may not be able to restore your uncommitted changes. Git itself refuses to merge when those edits touch files the merge needs, but edits to other files go through, and that is the messy case.

Start of mergeIf it conflictsRecovery
Clean working treeOnly merge changes in the treegit merge --abort returns you to exactly where you were
Uncommitted editsYour edits and merge changes are mixed togetherAbort may not restore them, and you cannot tell the two apart
Common mistake: merging with a dirty tree

Run git status before git merge. If it lists changes, commit them, or set them aside with git stash push -m "before merge", and only then start the merge.

Recovering when it goes wrong

Fix forward, do not rewrite shared history

Suppose a bad merge has already been pushed and teammates have pulled it. The tempting move is git push --force after resetting your branch. That rewrites history that other people are standing on: their next pull collides with yours, and commits they made on top can vanish from the remote. Fixing forward keeps everyone's history intact.

Which undo do I use?

To undo a merge that others have, use git revert -m 1 <merge-commit>. The -m 1 tells Git to keep the first parent, the branch you merged into, as the mainline. If the merge was mostly right and only needs a correction, an ordinary new commit that fixes the file is even simpler.

The panic ladder

When you are not sure what state you are in, climb these steps in order. Each is stronger than the one before, so stop at the first one that works.

Panic recovery ladder
  1. 1git merge --abortMerge is still in progress: go back to before it began
  2. 2git reset --hard ORIG_HEADMerge was already committed locally: move the branch back
  3. 3git reflogSomething is missing: find the commit and bring it back
CommandUse it whenCareful, because
git merge --abortThe merge is stopped on conflictsOnly safe from a clean start
git reset --hard ORIG_HEADThe merge is committed but not pushedIt also throws away uncommitted edits
git reflogA commit seems lostIt shows only your local history of moves

Git saves the tip of your branch in ORIG_HEAD just before a merge, so it points at the commit you were on beforehand. If even that is not enough, the reflog records every place HEAD has been for a while, including commits no branch points at any more.

bash
git reflog
# a1b2c3d HEAD@{0}: merge feature: Merge made by the 'ort' strategy.
# 9f8e7d6 HEAD@{1}: commit: Add discount rule
git branch rescue 9f8e7d6

Put a branch on the commit you want back, and nothing can lose it again.

Common mistake: force pushing to get tidy

A red history on the remote feels embarrassing, but it is harmless. A forced push can delete a teammate's commits. Revert or fix forward, and keep --force for branches that only you use.

When you do not know what it should do

Sometimes both sides are valid code and you cannot tell which behaviour is wanted. Guessing is how semantic conflicts are born. Check git log or git blame to find who wrote the other change, then ask them. They know the intent, and a two-minute chat is cheaper than a bad guess that nobody notices.

Remember

Search for markers, read before accepting, stage every file, run the tests, keep your tree clean before merging, never force push over a mistake, and ask the author when intent is unclear.

Part 12 · Merge Conflicts Cheat Sheet

Spot the conflict and resolve it

Keep this cheat sheet handy the next time Git stops halfway through a merge. It starts with how to recognise a conflict, then gives the loop that clears it, and the later pages cover finishing, bailing out, shortcuts, prevention and recovery.

1. Spot it: three signs

A conflict shows up in three places, and any one of them is enough to know you are mid-merge. Git prints a CONFLICT line when the merge command runs, git status lists the affected files under Unmerged paths, and each affected file contains conflict markers.

WhereWhat you seeWhat it means
Merge outputCONFLICT (content): Merge conflict in app.pyGit could not combine both edits on its own
git statusUnmerged paths: then both modified: app.pyThe file is waiting for you to decide
Inside the file<<<<<<<, ======= and >>>>>>> linesYour side sits above =======, the incoming side below it
bash
git merge feature
git status
grep -n '<<<<<<<' app.py

Three quick checks: the merge message, the status list, and a search for leftover markers

output
Auto-merging app.py
CONFLICT (content): Merge conflict in app.py
Automatic merge failed; fix conflicts and then commit the result.
On branch main
You have unmerged paths.

Unmerged paths:
  (use "git add <file>..." to mark resolution)
	both modified:   app.py

12:<<<<<<< HEAD

2. The resolve loop

Resolving is the same short routine for every file: open it, decide what the final text should be, remove all three marker lines, save, and then tell Git the file is done with git add. Staging is the signal that the file is resolved. Repeat until no unmerged paths are left.

Resolve loop, once per unmerged file
  1. 1Edit the filekeep the final text you want
  2. 2Delete the markersall three marker lines go
  3. 3git add <file>marks it resolved
  4. 4More unmerged paths?repeat for the next file
Common mistake: markers left in the file

If you save with a <<<<<<< line still in the file and run git add, Git happily accepts it and the markers end up in your commit. Search for <<<<<<< before every git add.

Finish, bail out, or take a whole side

3. Finish the merge or the rebase

Once every file is staged, the operation is still open and needs one last command. For a merge, a plain git commit records the result, and git merge --continue does the same thing with a clearer name. A rebase works one commit at a time, so after each resolved commit you run git rebase --continue and Git moves on to the next one, possibly stopping at another conflict.

OperationFinish withRepeats?
Mergegit commit or git merge --continueNo, one commit finishes it
Rebasegit rebase --continueYes, once per conflicting commit

4. Bail out

If the conflict is bigger than you expected, you can walk away. git merge --abort and git rebase --abort both put the branch back to where it was before the operation began. Aborting is safe when you started from a clean working tree. If you had uncommitted edits when you began, git merge --abort may not be able to restore them, so commit or stash before you merge. During a rebase only, git rebase --skip drops the commit that is currently conflicting and continues with the rest.

Stuck in the middle of a merge or rebase?
Common mistake: aborting with unsaved work

Running git merge --abort while you hold uncommitted edits can lose them. Start merges from a clean git status.

5. Take a whole side

Sometimes one version is simply right and there is nothing to blend. git checkout --ours <file> keeps your version and git checkout --theirs <file> keeps the incoming one. Stage the file afterwards like any other resolution. The two words swap meaning in a rebase, because Git replays your commits on top of the other branch: ours is then the branch you are rebasing onto, and theirs is your own commit being replayed.

CommandDuring mergeDuring rebase
--oursYour current branchThe branch you rebase onto
--theirsThe branch being merged inYour commit being replayed
bash
git checkout --theirs config.yml
git add config.yml

Keep the incoming config.yml completely, then mark it resolved

Tools, listing, prevention and the safety net

6. A visual tool, and showing the base

git mergetool opens each unmerged file in the merge tool you have configured and shows the two sides next to the result. When it closes, Git stages the file for you. To make plain markers easier to understand, set merge.conflictStyle to zdiff3. It adds a middle section with the original text from the common ancestor, so you can see what each side changed.

bash
git mergetool
git config --global merge.conflictStyle zdiff3

Open the visual tool, and turn on base display for every future conflict

7. List the unmerged files

In a big merge, the status output can be long. This command prints only the paths that still have conflicts, one per line, which is handy as a checklist that shrinks as you run git add.

bash
git diff --name-only --diff-filter=U

U means unmerged

output
app.py
docs/readme.md

8. Prevent conflicts

Most conflicts come from two people editing the same lines far apart in time. Shrinking that gap is the cheapest fix.

HabitWhy it helps
Small commitsEach change touches fewer lines, so overlaps are smaller and easier to read
Sync with main dailyYou see other people's changes while they are still fresh in everyone's mind
Short branchesLess time for the main branch to drift away from yours
Shared formatterEveryone formats code the same way, so style-only edits never collide

9. Safety net: the reflog recovers anything you lose

Git records every place your branch tip has pointed, even after a bad resolution, a wrong --theirs or a hard reset. git reflog lists those positions, newest first. Find the entry from just before things went wrong and move back to it. Anything that was committed can be recovered this way. Edits that were never committed or staged are not in the reflog, which is another reason to commit often.

bash
git reflog
git reset --hard HEAD@{2}

Look at the history of where HEAD has been, then return to the state two moves ago

output
a1b2c3d HEAD@{0}: reset: moving to HEAD~1
9f8e7d6 HEAD@{1}: merge feature: Merge made by the 'ort' strategy.
4c5d6e7 HEAD@{2}: commit: Add login form
Remember

Spot it, edit and git add each file, finish with commit or --continue, abort from a clean tree if it goes wrong, and let git reflog rescue what you lose.

Part 13 · Check yourself

Quiz

Try to answer each question before opening the answer. They ask you to predict what Git will do and to spot what went wrong, so reason from how a three-way merge works rather than from remembered commands.

On main, line 1 of app.py was changed. On feature, only line 10 of the same file was changed, and nothing else. You run git merge feature from main. Does Git report a conflict?
  • No. The two edits are in different hunks, so each hunk was changed by only one side and Git takes it automatically.
  • A conflict needs both sides to change the same region differently since the merge base, not just both touching the file.
  • Git prints a merge summary and creates the merge commit, so there is nothing to resolve.
# base:     line1 ... line10
# main:     LINE1 ... line10   (changed line 1)
# feature:  line1 ... LINE10   (changed line 10)
You fixed every block in config.js in your editor and removed the markers. You run git commit and Git refuses, saying you have unmerged files. What did you forget, and what do you run?
  • Editing the file does not tell Git the conflict is resolved, so the path is still listed under Unmerged paths.
  • Run git add config.js, then git commit or git merge --continue.
  • Run git status afterwards to confirm the path moved to Changes to be committed.
You are in the middle of git rebase main on your feature branch and hit a conflict in app.py. You want to keep your own commit's version of the file. Which command do you use, and why is it easy to get wrong?
  • Use git checkout --theirs app.py, then git add app.py and git rebase --continue.
  • During a rebase the sides are swapped: ours is the upstream you are replaying onto, and theirs is your own commit being replayed.
  • In a normal merge the same intent would need --ours, so check which operation is in progress before picking a flag.
This block appears in a file after you set merge.conflictStyle to diff3. What did each side change, and what should the final line be if you want both changes?
  • The base section shows the original, def connect(host):. Ours added a port parameter, and theirs added a timeout parameter.
  • Because neither side undid the other's change, the sensible result combines them: def connect(host, port, timeout):.
  • Delete all four marker lines, including the base separator, then run the tests, since a clean-looking file can still be logically wrong.
<<<<<<< HEAD
def connect(host, port):
||||||| base
def connect(host):
=======
def connect(host, timeout):
>>>>>>> feature
The merge commit is finished and already pushed to the shared remote, and now you realise it broke the build. Why is git reset --hard ORIG_HEAD the wrong tool, and what should you use?
  • git merge --abort no longer works because the merge is finished, and reset --hard would rewrite history that teammates already have.
  • Use git revert -m 1 <merge-commit>, which adds a new commit undoing the merge and keeps the first parent's line.
  • Reserve reset --hard ORIG_HEAD for a finished merge that has not been pushed.

Summary

  • A conflict happens only when both sides changed the same region differently since the merge base, or one edited what the other deleted.
  • Read the markers as ours above =======, theirs below, and use diff3 or zdiff3 to see the base as well.
  • Resolve each block by keeping ours, keeping theirs or combining both, delete every marker, run the tests, then git add the file.
  • Finish with git commit or git merge --continue, and bail out of an unfinished merge with git merge --abort.
  • During a rebase, ours and theirs swap, you finish with git rebase --continue, and you never rebase commits others already have.
  • Undo a finished merge with reset --hard ORIG_HEAD if unpushed or revert -m 1 if pushed, and use git reflog as the last safety net.
  • Prevent conflicts with small commits, daily pulls, short-lived branches and a shared formatter.