So far we have been lucky. Every merge in this module has gone well because Ana and Bruno were touching different parts of the files. The luck runs out here: in this lesson the two of them are going to modify the same lines of the same function, each for a perfectly reasonable motive, and Git is going to stop halfway through the merge.
Merge conflicts have a bad reputation, and they do not deserve it. A conflict is not an error, it is not corruption and it does not mean you have done anything wrong. It is Git telling you, with complete honesty: "there are two incompatible changes to the same lines here and I have no basis for choosing; you decide". The alternative — an algorithm deciding on its own whose code survives — would be far worse.
What does cause anxiety is the feeling of being trapped: the repository in a strange state, odd symbols inside the code and the nagging doubt that you have just broken something. This lesson removes that anxiety. By the end of it you will know exactly what state your repository is in during a conflict, how to read what Git has written into the file, how to resolve it and, if it all gets too much, how to back out without leaving a trace.
Contents
- Why a conflict happens
- Provoking a real conflict
- The conflict markers
- The
diff3andzdiff3styles: seeing the ancestor as well - Finding your bearings during a conflict:
git status git diffduring a conflict: the three versions- Resolving and marking as resolved
- Shortcuts:
--oursand--theirsper file - Backing out:
--abortand--quit git mergetool: graphical tools- Special conflicts: deleted versus modified
git rerere: never resolve the same thing twice
- Why a conflict happens
Remember the three-way merge decision table from lesson 03-03. For every chunk of a file, Git compares three versions: the common ancestor's (the base), your branch's (ours) and that of the branch you are merging (theirs).
Changed in ours |
Changed in theirs |
Result |
|---|---|---|
| No | No | The base is kept |
| Yes | No | ours wins |
| No | Yes | theirs wins |
| Yes | Yes, identically | That shared version wins |
| Yes | Yes, differently | CONFLICT |
Only the last row produces a conflict: both branches changed the same thing in different ways.
And we need to be precise about what "the same thing" means, because it is the source of a very widespread fear:
- Two people touching the same file does NOT produce a conflict. If Ana edits line 10 and Bruno line 200, Git combines the two without saying a word.
- Touching the same lines does produce one, as do lines close enough together to fall into the same chunk (hunk) of the diff. Git works with a three-line margin of context, so changes separated by one or two lines can clash too.
Besides content conflicts, there are tree conflicts, which are about the existence or the location of a file rather than what is inside it:
| Type | Situation |
|---|---|
| Content | Both branches modify the same lines |
| Delete/modify | One branch deletes the file, the other modifies it |
| Add/add | Both branches create a file with the same name and different content |
| Rename/delete | One branch renames it, the other deletes it |
| Rename/rename | Both rename it, with different names |
| Directory/file | One branch creates reports/ and the other a file called reports |
Content conflicts are 90 % of cases and they are what we will look at first. Delete/modify, being the second most frequent, gets its own section.
- Provoking a real conflict
Back to the project. main is up to date with everything integrated in the previous lessons, and app.js contains this function, which renders the task list on screen:
function renderList() {
const list = document.getElementById('list');
list.innerHTML = '';
tasks.forEach(function (t) {
list.appendChild(createTaskElement(t));
});
updateCounter();
}Ana opens a branch to show the tasks sorted alphabetically:
And modifies the function:
function renderList() {
const list = document.getElementById('list');
list.innerHTML = '';
const sorted = tasks.slice().sort(function (a, b) {
return a.text.localeCompare(b.text);
});
sorted.forEach(function (t) {
list.appendChild(createTaskElement(t));
});
updateCounter();
}[feature/alphabetical-order a1e5c93] Show the tasks in alphabetical order 1 file changed, 4 insertions(+), 1 deletion(-)
Bruno, meanwhile, opens another branch from the same point to fix something that has been reported to him: when there are no tasks, the screen goes blank and the application looks broken.
function renderList() {
const list = document.getElementById('list');
list.innerHTML = '';
if (tasks.length === 0) {
list.innerHTML = '<li class="empty">No pending tasks</li>';
updateCounter();
return;
}
tasks.forEach(function (t) {
list.appendChild(createTaskElement(t));
});
updateCounter();
}[fix/empty-list-message 6d3f8b2] Show a message when the list is empty 1 file changed, 5 insertions(+)
gitGraph commit id: "3b9e7d1" branch alphabetical-order checkout alphabetical-order commit id: "a1e5c93" checkout main branch empty-list-message checkout empty-list-message commit id: "6d3f8b2"
Both changes are good and the project wants both of them. But they both insert code at exactly the same spot: between list.innerHTML = ''; and the forEach. Git has no way of knowing whether Bruno's if goes before or after Ana's sort, nor whether the forEach should walk over tasks or sorted.
Ana integrates hers first, and it goes in cleanly:
And now Bruno's:
Auto-merging app.js CONFLICT (content): Merge conflict in app.js Automatic merge failed; fix conflicts and then commit the result.
There it is. Let us dissect it.
- The conflict markers
The first thing to understand: Git has modified your file on disk. app.js no longer contains valid JavaScript, but code with annotations inside it:
function renderList() {
const list = document.getElementById('list');
list.innerHTML = '';
<<<<<<< HEAD
const sorted = tasks.slice().sort(function (a, b) {
return a.text.localeCompare(b.text);
});
sorted.forEach(function (t) {
=======
if (tasks.length === 0) {
list.innerHTML = '<li class="empty">No pending tasks</li>';
updateCounter();
return;
}
tasks.forEach(function (t) {
>>>>>>> fix/empty-list-message
list.appendChild(createTaskElement(t));
});
updateCounter();
}There are three markers by default:
| Marker | Meaning |
|---|---|
<<<<<<< HEAD |
Your branch's version starts here (ours). The label says where it comes from |
======= |
Separator: ours ends, theirs begins |
>>>>>>> fix/empty-list-message |
The other branch's version ends here (theirs), labelled with its name |
Important observations that tend to get missed:
-
Only the conflicting area carries markers. The first three lines of the function and the last three are clean: Git merged them with no trouble. A 400-line file with a 6-line conflict has 394 lines already resolved.
-
A file can have several conflict blocks, each with its own set of markers. All of them have to be resolved.
-
The markers are ordinary text. There is nothing magic about them: you can edit them, delete them or leave them (if you leave them,
git commitwill warn you, but you can force it through and put rubbish into the project, so always check). -
The label on the right is the name of the merged branch, which helps enormously when you are merging several things in a row and have lost track.
And here is the problem with the default format: you cannot see what was there before. Is the forEach over tasks in Bruno's version a change of his, or simply the original code he never touched? With this format you cannot tell, and that information is exactly what you need to resolve well. Which is what the next section is for.
- The
diff3 and zdiff3 styles: seeing the ancestor as well
diff3 and zdiff3 styles: seeing the ancestor as wellGit can show three sections instead of two, adding the common ancestor's. It is controlled by the merge.conflictStyle option:
Let us abort and redo the merge to see it:
Now app.js contains:
function renderList() {
const list = document.getElementById('list');
list.innerHTML = '';
<<<<<<< HEAD
const sorted = tasks.slice().sort(function (a, b) {
return a.text.localeCompare(b.text);
});
sorted.forEach(function (t) {
||||||| 3b9e7d1
tasks.forEach(function (t) {
=======
if (tasks.length === 0) {
list.innerHTML = '<li class="empty">No pending tasks</li>';
updateCounter();
return;
}
tasks.forEach(function (t) {
>>>>>>> fix/empty-list-message
list.appendChild(createTaskElement(t));
});
updateCounter();
}The new block, delimited by ||||||| and labelled with the common ancestor's hash, holds the original code. And now it reads completely differently:
- The ancestor had
tasks.forEach(...). - Ana replaced it with the
sortblock plussorted.forEach(...). - Bruno added the
ifin front and did not touch theforEach.
With that information the resolution is obvious: you have to keep both things, putting Bruno's if first and honouring Ana's change from tasks to sorted.
Without the ancestor you would have had to deduce it or dig through the history. With it, you see it at a glance.
zdiff3, better still
Since Git 2.35 there is one more style: zdiff3 (from zealous diff3). It does the same as diff3 but also lifts the lines common to both sides out of the conflict:
In conflicts where the two sides share lines at the start or the end of the block, the conflicting area shrinks noticeably and only what genuinely disagrees is left inside.
| Style | Sections | Available since | Recommendation |
|---|---|---|---|
merge |
2 (ours, theirs) |
Always | The default; the worst of the three |
diff3 |
3 (ours, base, theirs) |
Always | Far better; adopt it |
zdiff3 |
3, with the shared lines outside | Git 2.35 (2022) | The best if your version supports it |
This is probably the highest-return piece of advice in the whole lesson. Set it right now:
git config --global merge.conflictStyle zdiff3It fits with what you already configured in lesson 01-06 and it has no downside whatsoever.
- Finding your bearings during a conflict:
git status
git statusDuring a conflict the repository is in a special state. git status is your compass, and it changes completely:
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.js no changes added to commit (use "git add" to commit)
The key elements:
You have unmerged paths: there is a half-finished merge.Unmerged paths: a new section, distinct from "staged" and "unstaged". Conflicted files go here.both modified: the type of conflict. Others you may see:deleted by us,deleted by them,both added,added by us.- Git reminds you of the two ways out: resolve and commit, or abort.
In short format:
UU means unmerged in both columns. The full table of conflict codes:
| Code | Meaning |
|---|---|
UU |
Modified by both (both modified) |
AA |
Added by both (both added) |
DD |
Deleted by both |
AU |
Added by us, unmodified by them |
UA |
Added by them |
DU |
Deleted by us, modified by them |
UD |
Modified by us, deleted by them |
If all you want is the list of outstanding files, with no noise:
That is the command used in scripts and editor shortcuts to jump from one conflict to the next.
What is inside .git at this moment
It is worth looking once, because it demystifies the state:
MERGE_HEAD holds the commit you are merging. Its existence is what defines "being in a merge": it is why git commit knows to create a commit with two parents, and why git merge --abort knows what to undo. MERGE_MSG holds the proposed message.
And the index (the staging area) is special right now too. Normally it holds one version of each file; during a conflict it holds three, numbered by stages:
100644 8e1f5b3d7a2c9e4b6f1d8a3c5e7b2d9f4a6c8e1b 1 app.js 100644 2a7c9e4f1b6d8a3c5e2f7b9d4a1c6e8b3f5d7a2c 2 app.js 100644 9f3b7d1a5c8e2f6b4d9a1c7e3b5f8d2a6c4e9b1f 3 app.js
| Stage | Version |
|---|---|
| 1 | The common ancestor's (the base) |
| 2 | Your branch's (ours) |
| 3 | The merged branch's (theirs) |
Those three stages are what make everything that follows possible: the special diffs, --ours/--theirs and the graphical tools. When you resolve and run git add, the three stages are replaced by a single ordinary version, and that is how Git knows the file is now resolved.
git diff during a conflict: the three versions
git diff during a conflict: the three versionsA plain git diff during a conflict shows a format you have not seen before: the combined diff.
diff --cc app.js
index 2a7c9e4,9f3b7d1..0000000
--- a/app.js
+++ b/app.js
@@@ -10,7 -10,11 +10,16 @@@ function renderList()
const list = document.getElementById('list');
list.innerHTML = '';
++<<<<<<< HEAD
+ const sorted = tasks.slice().sort(function (a, b) {
+ return a.text.localeCompare(b.text);
+ });
+ sorted.forEach(function (t) {
++=======
+ if (tasks.length === 0) {
+ list.innerHTML = '<li class="empty">No pending tasks</li>';
+ updateCounter();
+ return;
+ }
+ tasks.forEach(function (t) {
++>>>>>>> fix/empty-list-message
list.appendChild(createTaskElement(t));
});
updateCounter();Notice that there are two columns of +/- markers instead of one, and that the header @@@ has three at-signs. Each column corresponds to one parent. It is a dense format; in practice the pairwise diffs get used far more.
To compare the conflicted file against each of the three versions:
# Against the common ancestor's version (stage 1)
git diff --base app.js
# Against your branch's version (stage 2)
git diff --ours app.js
# Against the merged branch's version (stage 3)
git diff --theirs app.jsYou can also pull out any of the three complete versions to look at in isolation, with the :<stage>:<path> syntax:
git show :1:app.js # the ancestor's
git show :2:app.js # yours
git show :3:app.js # the other branch'sThis is enormously useful when the conflict is large and you want to read each version in full without markers, instead of trying to decipher the mixed-up file. For example, to save Bruno's version to a separate file and consult it while you edit:
- Resolving and marking as resolved
Resolving a conflict means, quite simply, leaving the file the way it should end up. Nothing more, nothing less. There is no magic command: you edit the file, remove the markers and write the correct code.
In our case, the ||||||| block tells us the two contributions have to be combined. Ana edits app.js and leaves it like this:
function renderList() {
const list = document.getElementById('list');
list.innerHTML = '';
if (tasks.length === 0) {
list.innerHTML = '<li class="empty">No pending tasks</li>';
updateCounter();
return;
}
const sorted = tasks.slice().sort(function (a, b) {
return a.text.localeCompare(b.text);
});
sorted.forEach(function (t) {
list.appendChild(createTaskElement(t));
});
updateCounter();
}No markers. Bruno's if goes first (if there are no tasks, there is no point sorting them) and Ana's sort after it. This is code that existed on neither branch: it is the synthesis only a person could produce. That is exactly what Git was asking you for.
Before calling anything done, you have to test it. A conflict resolved without running the code is a gamble:
# Check that no forgotten marker is left behind
grep -n '^<<<<<<<\|^=======\|^>>>>>>>\|^|||||||' app.jsAnd after that, open the application or run the tests.
Now you mark it as resolved:
git add on a conflicted file has a special meaning: it replaces the three index stages with the version on disk and declares the conflict resolved. It is not "stage a change"; it is "I have decided, this is the good version".
On branch main All conflicts fixed but you are still merging. (use "git commit" to conclude merge) Changes to be committed: modified: app.js
All conflicts fixed but you are still merging: the merge is still under way (MERGE_HEAD still exists), but nothing is blocked any more. All that is left is to commit:
Git opens the editor with the message it saved in MERGE_MSG, now with an extra note:
Merge branch 'fix/empty-list-message' # Conflicts: # app.js # # 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.
Describing how you resolved it is an excellent habit, because whoever reads this a year from now will be grateful:
Merge the empty-list message Conflict in renderList(): Bruno's branch added the empty-list check and Ana's added the alphabetical ordering, both at the same spot. We keep both: first the early return when there are no tasks, then the sorting.
* c2a8f1e (HEAD -> main) Merge the empty-list message |\ | * 6d3f8b2 (fix/empty-list-message) Show a message when the list is empty * | e1f3a7b Merge branch 'feature/alphabetical-order' |\ \ | * | a1e5c93 (feature/alphabetical-order) Show the tasks in alphabetical order |/ / * / 3b9e7d1 Add CSV export of the task list |/
And this is where --cc, which we saw in lesson 03-03, comes into its own:
It shows only the lines that differ from both parents, which is to say exactly the code Ana wrote by hand while resolving. It is the way to audit a conflict resolution without reading the whole file.
- Shortcuts:
--ours and --theirs per file
--ours and --theirs per fileSometimes there is nothing to synthesise: one of the two versions is simply the right one. Typical with automatically generated files, dependency lock files, or when you know one side's work made the other's obsolete.
# Keep your branch's version, in full
git checkout --ours app.js
# Keep the merged branch's version, in full
git checkout --theirs app.jsOr with the modern commands we saw in lesson 03-02:
These commands overwrite the file on disk with stage 2 or stage 3 from the index, markers and all left out. Afterwards you still have to mark it as resolved:
Two important warnings:
-
They take the WHOLE file, not just the conflicting area. If
app.jsalso had changes from the other branch elsewhere that merged perfectly well,--oursthrows those away too. Use it only when you genuinely want one complete version. -
Do not confuse them with
-X ours/-X theirsfrom the previous lesson. These act file by file, on a conflict that has already happened; those act globally, before the conflict appears at all.
A very practical pattern when there are several files and only some of them are "generated":
# Resolve the code files by hand
vim app.js
git add app.js
# The generated ones, with the incoming version
git restore --theirs dist/bundle.js packages.lock
git add dist/bundle.js packages.lock
git commitAnd if you would rather start again on a file you have mangled while editing:
That restores the file with the original conflict markers, exactly as it was right after the failed merge.
- Backing out:
--abort and --quit
--abort and --quitIf you have lost the thread, if the conflict is far bigger than you expected or if you would simply rather do it another time, you can leave without a trace:
As if the merge had never happened. git merge --abort undoes everything: it restores the working tree and the index to the state before git merge, and it deletes MERGE_HEAD and MERGE_MSG.
It is the most reassuring operation in Git and it is worth internalising: during a conflict you are never trapped. There is always an escape key.
One precaution: if you had uncommitted changes before starting the merge, --abort may not be able to recover the exact state. That is why the advice from lesson 03-03 — merge with a clean working tree — is not a personal quirk.
There is also a strange cousin:
--quit leaves the merge state (it deletes MERGE_HEAD) but leaves the working tree and the index exactly as they are, markers and all. It is for very specific cases: you want to keep the half-finished result but you do not want Git to create a merge commit with two parents. If you do not know that you need it, you do not need it: use --abort.
| Command | Merge state | Working tree | When |
|---|---|---|---|
git merge --abort |
Cancelled | Restored to the previous state | Almost always |
git merge --quit |
Cancelled | Left as it is | Advanced cases |
git commit |
Completed | What you resolved is kept | Once you have resolved |
git mergetool: graphical tools
git mergetool: graphical toolsFor large conflicts, editing markers by hand is awkward. git mergetool opens a three- or four-panel tool: base, ours, theirs and result.
Merging:
app.js
Normal merge conflict for 'app.js':
{local}: modified file
{remote}: modified file
Hit return to start merge resolution tool (vimdiff):If you have not configured one, Git picks the first it finds installed. To pin one down:
# Meld (cross-platform, highly recommended to start with)
git config --global merge.tool meld
# VS Code
git config --global merge.tool vscode
git config --global mergetool.vscode.cmd 'code --wait --merge $REMOTE $LOCAL $BASE $MERGED'
# KDiff3
git config --global merge.tool kdiff3
# vimdiff
git config --global merge.tool vimdiffAnd an option that almost everybody ends up turning on:
Without it, git mergetool leaves *.orig files holding the conflicted version scattered everywhere, and you then have to delete them by hand (or ignore them in .gitignore).
The four variables the tool receives, worth understanding if you want to configure your own:
| Variable | Content |
|---|---|
$BASE |
The common ancestor's version (stage 1) |
$LOCAL |
Your version (stage 2, ours) |
$REMOTE |
The incoming version (stage 3, theirs) |
$MERGED |
The destination file, where you write the result |
When you save and quit the tool, git mergetool marks the file as resolved automatically (it does the git add for you) and moves on to the next conflict if there is one.
A practical note: today most modern editors ship their own conflict resolver, which detects the markers in the file and offers buttons along the lines of "accept current / accept incoming / accept both". They work on the very same marker-laden file we have seen, so everything learnt here applies unchanged. git mergetool is still useful when you want the four panels with the ancestor in plain view.
- Special conflicts: deleted versus modified
The second most frequent kind of conflict does not happen inside the file, but over its existence.
The situation: the project had a file internal-notes.md with jottings from the early days of development. Ana decided it was obsolete and deleted it on her branch:
git switch -c cleanup/delete-notes main
git rm internal-notes.md
git commit -m "Delete the internal notes, now obsolete"Bruno, who did not know that, updated it on his:
git switch -c docs/update-notes main
# … edits internal-notes.md …
git commit -am "Update the internal notes with the new workflow"Ana merges hers into main (no trouble) and then Bruno's:
CONFLICT (modify/delete): internal-notes.md deleted in HEAD and modified in docs/update-notes. Version docs/update-notes of internal-notes.md left in tree. Automatic merge failed; fix conflicts and then commit the result.
Notice two things:
- The conflict type is
modify/delete, notcontent. - There are no markers inside the file. There would be no point: the conflict is not about its content, but about whether it should exist. Git has left Bruno's version on disk, as the message says.
On branch main You have unmerged paths. Unmerged paths: (use "git add/rm <file>..." as appropriate to mark resolution) deleted by us: internal-notes.md
deleted by us: we (the branch we are standing on) deleted it, they modified it. Git cannot decide whether the deletion was right or whether Bruno's updates justify keeping it; that depends on why it was deleted, and only a person knows that.
Resolving means declaring what should happen, and there are only two options:
In both cases, afterwards:
Before deciding, it is worth looking at what those changes contained, in case Bruno wrote something that deserves keeping somewhere else:
And if the answer is "yes, it is worth keeping, but not here", you can rescue the content into another file before resolving.
The other tree conflicts
They are resolved with the same logic: git add if the file should exist with the content it has on disk, git rm if it should not exist.
Both branches created the same file with different content. Here there are markers inside (Git treats emptiness as the base), so it is resolved like an ordinary content conflict.
CONFLICT (rename/rename): Rename styles.css->css/styles.css in HEAD. Rename styles.css->assets/styles.css in branch-b
Both branches moved it somewhere different. You decide on the right location, put the file there, delete the other copies and mark everything with git add/git rm.
git rerere: never resolve the same thing twice
git rerere: never resolve the same thing twiceOne last note, so you know it exists when you need it.
rerere comes from reuse recorded resolution. If you turn it on, Git memorises how you resolved each conflict and, when the same conflict comes round again, it resolves it on its own using your earlier decision.
When does a conflict repeat itself? More often than you would think: when you merge main into your working branch every few days, when you abort a merge and retry it, when you redo a branch with rebase several times (module 5), or when you work with long-lived branches.
It is an advanced tool and it has its subtleties — it is worth reviewing whatever it resolves on its own, especially if the earlier resolution was rushed — so we only mention it here. Knowing that it exists and that it is turned on with that one line is enough for now.
Common Mistakes and Tips
Mistake 1: leaving markers in the code. Committing a file with <<<<<<< inside breaks the project spectacularly. Git warns you if it spots markers when you commit, but it is not infallible. Always check before git add:
Mistake 2: resolving by "picking a side" without thinking. The urge to take --ours and be done with it destroys the other side's work, including changes that had merged perfectly well elsewhere in the file. Read the conflict: in most cases the right answer is to keep both things, as in this lesson's example.
Mistake 3: not testing the result. A resolved conflict can compile and still be wrong: variables left unused, duplicated functions, logic that runs twice. Run the application or the tests before committing.
Mistake 4: panicking and deleting the repository. This happens more often than it should. git merge --abort leaves everything exactly as it was. There is no conflict situation you cannot walk away from.
Mistake 5: confusing --ours/--theirs during a conflict with -X ours/-X theirs. The former act on one specific file, with the merge already stopped. The latter are a global policy applied beforehand. And in a rebase, on top of that, the roles of ours and theirs are swapped compared with what you would expect (lesson 05-01).
Tip 1: turn zdiff3 on today. Seeing the common ancestor transforms the quality of your resolutions:
Tip 2: small, frequent conflicts instead of large, rare ones. The best technique for resolving conflicts is having fewer. Short-lived branches, frequent integration and bringing main into your branch every few days turn one 300-line conflict into five three-line ones.
Tip 3: if the conflict is enormous, abort and study it. Before wrestling with it, work out why it is there:
Sometimes the conclusion is that you need to talk to the other person before touching anything. That is a valid answer too.
Tip 4: document the resolution in the merge commit message. A merge commit with conflicts contains human decisions that exist nowhere else. Explaining in three lines what clashed and what was decided saves whoever comes next a lot of archaeology.
Tip 5: when the decision is about someone else's code, ask. If the conflict is in code you do not know, resolving it by eye is a gamble. A thirty-second message to whoever wrote it is cheaper than a production failure.
Exercises
Exercise 1: provoking and resolving a content conflict
Set up a test repository, provoke a conflict in which both branches modify the same line, and resolve it by combining both contributions. Do it first with the default conflict style and then with diff3, and explain what extra information the second one gives you.
Exercise 2: the delete versus modify conflict
Provoke a modify/delete conflict and resolve it both possible ways (keeping the file and keeping the deletion). Before deciding, show the content the modifying branch was contributing.
Exercise 3: rescuing the three versions
In a content conflict, without opening any editor, save the three versions of the file (base, ours and theirs) separately in /tmp and compare the base against each of the other two. Then abort the merge.
Solutions
Solution 1:
mkdir /tmp/practice-conflict && cd /tmp/practice-conflict
git init -b main
cat > greeting.js <<'END'
function greet(name) {
return "Hello " + name;
}
END
git add . && git commit -m "Add the greeting function"
# Branch A: adds an exclamation mark
git switch -c branch-a
cat > greeting.js <<'END'
function greet(name) {
return "Hello " + name + "!";
}
END
git commit -am "Add an exclamation mark"
# Branch B: adds the surname
git switch main
git switch -c branch-b
cat > greeting.js <<'END'
function greet(name, surname) {
return "Hello " + name + " " + surname;
}
END
git commit -am "Include the surname in the greeting"
# We merge
git switch main
git merge branch-a
git merge branch-bWith the default style:
function greet(name) {
<<<<<<< HEAD
return "Hello " + name + "!";
=======
}
function greet(name, surname) {
return "Hello " + name + " " + surname;
>>>>>>> branch-b
}With diff3:
<<<<<<< HEAD
function greet(name) {
return "Hello " + name + "!";
||||||| 8a1d5c3
function greet(name) {
return "Hello " + name;
=======
function greet(name, surname) {
return "Hello " + name + " " + surname;
>>>>>>> branch-bWhat diff3 adds: it makes plain that the base was "Hello " + name, that branch A only added the exclamation mark and that branch B only added the parameter and the surname. Without the base block you had to deduce that by mentally comparing the two sides, and it was easy to get wrong about what had actually changed.
A resolution that combines both contributions:
cat > greeting.js <<'END'
function greet(name, surname) {
return "Hello " + name + " " + surname + "!";
}
END
grep -c '<<<<<<<' greeting.jsgit add greeting.js
git commit -m "Merge the surname and the exclamation mark
Conflict in greet(): branch-a added the exclamation mark and
branch-b the surname parameter. We keep both changes."Solution 2:
mkdir /tmp/practice-delete && cd /tmp/practice-delete
git init -b main
echo "Project notes" > notes.md
echo "content" > other.txt
git add . && git commit -m "Base"
# The branch that deletes
git switch -c delete-notes
git rm notes.md
git commit -m "Delete the obsolete notes"
# The branch that modifies
git switch main
git switch -c update-notes
echo "Project notes - revision 2" > notes.md
git commit -am "Update the notes"
# Merge
git switch main
git merge delete-notes # fast-forward, no trouble
git merge update-notesCONFLICT (modify/delete): notes.md deleted in HEAD and modified in update-notes. Version update-notes of notes.md left in tree.
Before deciding, we look at what the branch was contributing:
Option A: keep the deletion.
Option B: keep the file (starting from git merge --abort and repeating).
The rule that settles every tree conflict: git add if the file should exist, git rm if it should not.
Solution 3:
Starting from a content conflict in progress over greeting.js:
git show :1:greeting.js > /tmp/base.js # common ancestor
git show :2:greeting.js > /tmp/ours.js # our branch
git show :3:greeting.js > /tmp/theirs.js # merged branch1,2c1,2
< function greet(name) {
< return "Hello " + name;
---
> function greet(name, surname) {
> return "Hello " + name + " " + surname;The two diffs, separately and without markers, show with complete clarity what each branch did relative to the starting point. It is the technique that rescues large conflicts: instead of reading a mixed-up file, you read the two changes one at a time.
Everything as it was.
Conclusion
Conflicts have stopped being a mystery:
- A conflict is not an error. It happens when both branches changed the same lines in different ways since the common ancestor. Two people touching the same file is not enough: they have to clash on the same lines.
- Git writes markers into the file:
<<<<<<<opens your version,=======separates and>>>>>>>closes the incoming one. Withmerge.conflictStyleset todiff3orzdiff3you also get the|||||||block holding the ancestor's content, which is the information that really lets you resolve well. git statusis the compass: theUnmerged pathssection, the conflict type (both modified,deleted by us…) and the short codes (UU,DU,AA). Internally, the index holds three stages of the file, reachable withgit show :1:,:2:and:3:, andMERGE_HEADis what defines a merge as being in progress.- Resolving means leaving the file as it should end up and marking it with
git add(orgit rmif it should not exist). There is no magic command: there is a human decision, and often the right answer is to combine both contributions. - The shortcuts
git checkout --ours/--theirs <file>(orgit restore --ours/--theirs) replace the whole file with one of the versions. Useful for generated files; dangerous if the file also had changes that merged cleanly. - You are never trapped:
git merge --abortrestores the previous state completely.--quitleaves the merge while keeping the mess, and it is for advanced cases. git mergetoolopens a multi-panel tool with base,ours,theirsand result, and marks the file as resolved when you save.- Tree conflicts (delete/modify, add/add, renames) carry no markers: you resolve them by declaring what should exist.
git rererememorises resolutions and replays them when the same conflict comes back.
What comes next
The project is in good shape: main holds the counter, the filter, the CSV export, the alphabetical ordering and the empty-list message. But Ana's repository is starting to look like a junk drawer. She has branches for features that are already integrated, branches from abandoned experiments, branches whose name nobody remembers and one simply called test.
In the last lesson of the module, Branch Management, we will tidy up: listing branches with useful information (-v, --merged, --no-merged, custom formats sorted by date), renaming them, deleting them safely (-d versus -D) and understanding exactly what it means when Git refuses to delete a branch. We will also look at naming conventions — which characters Git accepts and which prefixes people use — and we will close the module with the problem we have been dodging all lesson: all of this is happening on a single laptop.
Mastering Git: From Beginner to Advanced
Module 1: Introduction to Git
- What Is Git?
- Installing Git
- Basic Git Terminology
- The Git Data Model
- Configuring Git
- Initial Configuration
Module 2: Basic Git Operations
- Creating a Repository
- Cloning a Repository
- The Basic Git Workflow
- Staging and Committing Changes
- Inspecting Changes with git diff
- Viewing Commit History
Module 3: Branching and Merging
- Understanding Branches
- Creating and Switching Branches
- Merging Branches
- Merge Strategies
- Resolving Merge Conflicts
- Branch Management
Module 4: Working with Remote Repositories
- Understanding Remote Repositories
- Adding a Remote Repository
- Authenticating with Remote Repositories
- Fetching and Pulling Changes
- Pushing Changes
- Tracking Branches
Module 5: Advanced Git Operations
Module 6: Git Tools and Techniques
- Using Git Hooks
- Git Bisect
- Git Blame
- Git Log and Aliases
- Git Submodules
- Multiple Working Copies with git worktree
Module 7: Collaboration and Workflow Strategies
- Forks and Pull Requests
- Code Reviews with Git
- The Git Flow Workflow
- GitHub Flow
- Trunk Based Development
- Continuous Integration with Git
Module 8: Git Best Practices and Tips
- Writing Good Commit Messages
- Keeping a Clean History
- Ignoring Files with .gitignore
- File Attributes with .gitattributes
- Security Best Practices
- Performance Tips
Module 9: Troubleshooting and Debugging
- Common Git Problems
- Undoing Changes
- Resolving Divergence with the Remote
- Recovering Lost Commits
- Dealing with Corrupted Repositories
- Advanced Debugging Techniques
