1.1 What is a Conflict?
Book can be downloaded from here
A conflict arises when there are two different directions in which the same piece of
code was modified. As long as you keep them separate (as in separate branches) there
will be no problem, but when you try to merge them, git will stop and ask you for
your help so that you can figure out what needs to be done to finish the merge
operation.
1.1.1 Example 0 - A non-conflict
Let me introduce you to one of the files that we will be using (with a few variations
along the way) as the basis for some of our examples moving forward.
Listing 1.1:Example 0 - common ancestor1#!/usr/bin/python 2 3import sys 4 5colors = {"black": "black mirror", 6 "white": "white noise", 7 "blue": "blue sky"} 8 9def getPhrase(color): 10 phrase = colors[color] 11 return phrase 12 13print(getPhrase(sys.argv[1]))
It’s a very simple python script.
Suppose that two developers start working from this file having to add a different
color each. Developer A produces this:
Listing 1.2:Example 0 - Developer A1#!/usr/bin/python 2 3import sys 4 5colors = {"black": "black mirror", 6 "white": "white noise", 7 "red": "red tide", 8 "blue": "blue sky"} 9 10def getPhrase(color): 11 phrase = colors[color] 12 return phrase 13 14print(getPhrase(sys.argv[1]))
We can see that color red was added on line 7.
And Developer B produced this:
Listing 1.3:Example 0 - Developer B1#!/usr/bin/python 2 3import sys 4 5colors = {"black": "black mirror", 6 "green": "green peas", 7 "white": "white noise", 8 "blue": "blue sky"} 9 10def getPhrase(color): 11 phrase = colors[color] 12 return phrase 13 14print(getPhrase(sys.argv[1]))
Developer B added color green on line 6.
History of the branches looks like this:
Listing 1.4:Example 0 - branch history* d3c8087 (example0/branchB) Adding green peas | * 4f281f2 (example0/branchA) Adding red tide |/ * f44c861 Get a phrase from a color
Given that the lines were added at different locations (separated by the line that
defines color white phrase), then git has no problem merging them:
Listing 1.5:Example 0 - git merge output$ git merge example0/branchB --no-edit Auto-merging example.py Merge made by the ’recursive’ strategy. example.py | 1 + 1 file changed, 1 insertion(+)
Resulting history looks like this:
Listing 1.6:Example 0 - final branch history* 74a049b Merge branch ’example0/branchB’ into example0/branchA |\ | * d3c8087 (example0/branchB) Adding green peas * | 4f281f2 Adding red tide |/ * f44c861 Get a phrase from a color
The resulting file looks like this:
Listing 1.7:Example 0 - merged code1#!/usr/bin/python 2 3import sys 4 5colors = {"black": "black mirror", 6 "green": "green peas", 7 "white": "white noise", 8 "red": "red tide", 9 "blue": "blue sky"} 10 11def getPhrase(color): 12 phrase = colors[color] 13 return phrase 14 15print(getPhrase(sys.argv[1]))
And you can see how the resulting file has both colors green and red:
Listing 1.8:Example 0 - colors section5colors = {"black": "black mirror", <-- Preexisting line 6 "green": "green peas", <-- Line from Developer B 7 "white": "white noise", <-- Preexisting line 8 "red": "red tide", <-- Line from Developer A 9 "blue": "blue sky"} <-- Preexisting line
This is also visible if you check the annotations of the
file:
Listing 1.9:Example 0 - annotations^f44c861 (Developer A 1584200950 -0600 5) colors = {"black": "black mirror", d3c80878 (Developer B 1584201394 -0600 6) "green": "green peas", ^f44c861 (Developer A 1584200950 -0600 7) "white": "white noise", 4f281f22 (Developer A 1584201248 -0600 8) "red": "red tide", ^f44c861 (Developer A 1584200950 -0600 9) "blue": "blue sky"}
There we have a successful merge. Git is able to merge the code because they
touch different sections of the file. Git can see that the two pieces of code are
separated by the line defining the white color phrase and so the merge goes
fine.
Let’s go back to our common ancestor and let’s show 2 different conflict
examples.
1.1.2 Example 1 - A simple conflict
First (again) the common ancestor, which is the same common ancestor we used
for example 0.
Listing 1.10:Example 1 - common ancestor1#!/usr/bin/python 2 3import sys 4 5colors = {"black": "black mirror", 6 "white": "white noise", 7 "blue": "blue sky"} 8 9def getPhrase(color): 10 phrase = colors[color] 11 return phrase 12 13print(getPhrase(sys.argv[1]))
Now, just like we did in our previous example, let’s have our two developers add
exactly the same colors that were added before but, instead of doing it on separate
places, they will do it at exactly the same spot.
Developer A adds color red on line 7:
Listing 1.11:example 1 - Developer A1#!/usr/bin/python 2 3import sys 4 5colors = {"black": "black mirror", 6 "white": "white noise", 7 "red": "red tide", 8 "blue": "blue sky"} 9 10def getPhrase(color): 11 phrase = colors[color] 12 return phrase 13 14print(getPhrase(sys.argv[1]))
Developer B adds color green also on line 7:
Listing 1.12:example 1 - Developer B1#!/usr/bin/python 2 3import sys 4 5colors = {"black": "black mirror", 6 "white": "white noise", 7 "green": "green peas", 8 "blue": "blue sky"} 9 10def getPhrase(color): 11 phrase = colors[color] 12 return phrase 13 14print(getPhrase(sys.argv[1]))
The new branch history would be something like this:
Listing 1.13:Example 1 - branch history* 6e7707e (example1/branchB) Adding green peas | * 3633ec9 (HEAD -> example1/branchA) Adding red tide |/ * 9f086a6 Get a phrase from a color
It’s basically the same example only that the colors were added at the same spot.
If somebody were to ask git to merge both branches, we would see something very
different from example 0:
Listing 1.14:Example 1 - git merge output$ git merge example1/branchB Auto-merging example.py CONFLICT (content): Merge conflict in example.py Automatic merge failed; fix conflicts and then commit the result
The status of the working tree at the moment:
Listing 1.15:Example 1 - git status$ git status On branch example1/branchA 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: example.py no changes added to commit (use "git add" and/or "git commit -a")
It can be seen how example.py is listed in the section called Unmerged paths.
It’s telling us it is the kind of conflict where both branches modified a file
in different conflicting directions and have produced a Content conflict.
The resulting file would look something like this:
Listing 1.16:example 1 - conflicting file1#!/usr/bin/python 2 3import sys 4 5colors = {"black": "black mirror", 6 "white": "white noise", 7<<<<<<< HEAD 8 "red": "red tide", 9======= 10 "green": "green peas", 11>>>>>>> example1/branchB 12 "blue": "blue sky"} 13 14def getPhrase(color): 15 phrase = colors[color] 16 return phrase 17 18print(getPhrase(sys.argv[1]))
git tried its best to figure out what to do but, given that the two branches do
different things at the exact same location in the code, it asks us to give it a hand so
that the code can be merged.
The conflicting portion of code is, as you might have guessed:
Listing 1.17:example 1 - conflict section7<<<<<<< HEAD 8 "red": "red tide", 9======= 10 "green": "green peas", 11>>>>>>> example1/branchB
This conflict has 2 sections. The first section is how the code looks on HEAD.
, and
we can see it at the conflict starter mark, on line 7.
Listing 1.18:example 1 - HEAD section7<<<<<<< HEAD <-- conflict section start marker 8 "red": "red tide", 9======= <-- separation between HEAD and the other branch
The second section is how the conflicting section of code looks on the other
branch that we asked to merge. Git tips us that the name of the branch is
example1/branchB on the conflict end marker, on line 11:
Listing 1.19:example 1 - HEAD section9======= <-- separation between HEAD and the other branch 10 "green": "green peas", 11>>>>>>> example1/branchB <-- conflict section end marker
The two sections are separated by a chain of equal signs.
The key question that we should try to answer at this point is: can we solve the
conflict?
You are probably thinking: But of course! Just set a line for each one of the
new colors and we are done so that we get all colors in the dictionary.
Listing 1.20:example 1 - HEAD solved conflict1#!/usr/bin/python 2 3import sys 4 5colors = {"black": "black mirror", 6 "white": "white noise", 7 "red": "red tide", 8 "green": "green peas", 9 "blue": "blue sky"} 10 11def getPhrase(color): 12 phrase = colors[color] 13 return phrase 14 15print(getPhrase(sys.argv[1]))
Congrats! You have solved your first (right?) conflict. The following steps are
required by git in order to consider that the file has been merged to wrap up the
merge operation:
1.1.3 How to wrap up a conflict?
If at this point you run git status, you will see that nothing has changed, from git’s
point of view:
Listing 1.21:git status after solving the conflict$ git status On branch example1/branchA 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: example.py no changes added to commit (use "git add" and/or "git commit -a")
Git wasn’t paying attention to you editing the file. We need to specifically take
the file as it is right now:
Listing 1.22:git add; git status$ git add example.py $ git status On branch example1/branchA All conflicts fixed but you are still merging. (use "git commit" to conclude merge) Changes to be committed: modified: example.py
When all files have been merged/added to index, a revision is finally created by
running git merge –continue. First you should get an editor with some default
content for what the revision comment will be. Edit the content to what
you find appropriate. Save and exit. When you exit the editor, the merge
revision will be created. Here it is how it looks after going out of the editor:
Listing 1.23:git merge –continue$ git merge --continue [example1/branchA 03938ed] Merge branch ’example1/branchB’ into example1/branchA
Then, we get our merge revision:
Listing 1.24:example 1 - final branch history* 03938ed (example1/branchA) Merge branch ’example1/branchB’ into example1/branchA |\ | * 6e7707e (example1/branchB) Adding green peas * | 3633ec9 Adding red tide |/ * 9f086a6 Get a phrase from a color
1.1.4 Example 2 - Another simple example
Let’s try another simple example. Using the same common ancestor as before (we
were using only 3 colors, remember?), now Developer A ends up with this:
Listing 1.25:example 2 - Developer A1#!/usr/bin/python 2 3import sys 4 5colors = {"black": "black mirror", 6 "white": "white noise", 7 "blue": "blue sky"} 8 9def getPhrase(color): 10 phrase = colors[color] 11 phrase = "%s: %s" % (color, phrase) 12 return phrase 13 14print(getPhrase(sys.argv[1]))
This is something different. The output phrase now contains the color and then
the associated phrase, separated by a colon (line 11). If we run it with blue, we get:
Listing 1.26:example 2 - running script from Developer
A$ ./example.py blue blue: blue sky
Developer B ended up with this:
Listing 1.27:example 2 - Developer B1#!/usr/bin/python 2 3import sys 4 5colors = {"black": "black mirror", 6 "white": "white noise", 7 "blue": "blue sky"} 8 9def getPhrase(color): 10 phrase = colors[color] 11 phrase = phrase.upper() 12 return phrase 13 14print(getPhrase(sys.argv[1]))
Developer B is adding an upper() call to set the phrase in uppercase (line 11).
If we run the script like we did before:
Listing 1.28:example 2 - running script from Developer
B$ ./example.py blue BLUE SKY
And history looks like this:
Listing 1.29:example 2 - branch history* 9b9d7b2 (example2/branchB) Using upper on the phrase | * c944ee5 (example2/branchA) Showing color and phrase |/ * 8c9d315 Get a phrase from a color
And we can foresee the conflict if we tried to merge this time, don’t we?
Listing 1.30:example 2 - Conflicted file1#!/usr/bin/python 2 3import sys 4 5colors = {"black": "black mirror", 6 "white": "white noise", 7 "blue": "blue sky"} 8 9def getPhrase(color): 10 phrase = colors[color] 11<<<<<<< HEAD 12 phrase = "%s: %s" % (color, phrase) 13======= 14 phrase = phrase.upper() 15>>>>>>> example2/branchB 16 return phrase 17 18print(getPhrase(sys.argv[1]))
In this case, let’s keep the line where we set the phrase to have the original color,
then we do the upper() call and it should be good:
Listing 1.31:example 2 - Resolved conflict1#!/usr/bin/python 2 3import sys 4 5colors = {"black": "black mirror", 6 "white": "white noise", 7 "blue": "blue sky"} 8 9def getPhrase(color): 10 phrase = colors[color] 11 phrase = "%s: %s" % (color, phrase) 12 phrase = phrase.upper() 13 return phrase 14 15print(getPhrase(sys.argv[1]))
And if we run the resulting script:
Listing 1.32:example 2 - running resulting
script$ ./example.py blue BLUE: BLUE SKY
1.1.5 Questions
Here are some things to think about:
Q: Could the lines be set in reverse order?
Sure! It all depends on what the requirements are from each branch and how they
should behave in the current revision. On example 1, it was just adding elements to a
dictionary so there was no meaningful difference. If they were being ordered
alphabetically then order would matter. On example 2 it’s very important the order in
which you put them. If the upper() call had been placed before re-setting the phrase
to include the original color, then the original color would end up not being
in uppercase. Is that correct? Perhaps! The requirements (not just the
conflicting code) might need to be considered to know what to do in this
case.
Q: Could it be written in a completely different way?
Sure! As long as the intent of what the different branches
meant to do is included in the conflict resolution, it will be OK.
Q: What would the conflict look like if the branches are reversed when
merging?
In example 2, working on example2/branchB, I try to merge example2/branchA.
What would be the result? It would look exactly the same only that the conflict
sections for each branch would be reversed and the name of the branch would change
on the conflict closing mark:
Listing 1.33:example 2 - inverted conflict11<<<<<<< HEAD 12 phrase = phrase.upper() 13======= 14 phrase = "%s: %s" % (color, phrase) 15>>>>>>> example2/branchA
1.1.6 Exercises
Exercise 1
Take branch exercise1/branchA from the exercises repo. There is a short list of
irregular verbs inside irregular.txt in alphabetical order. Add these two verbs:
drink and know. Then take branch example1/branchB and add these
other 2 verbs: lose and keep . Merge the two branches. Solution is here.
Exercise 2
From the exercises repo, merge branches exercise2/branchA and
exercise2/branchB. Solution is here.
Copyright 2020 Edmundo Carmona Antoranz