3.1 API conflicts

Book can be downloaded from here

Before explaining the details you have to care for when dealing with API conflicts, let’s consider what API changes mean for merges without conflicts.

3.1.1 Example 15

Let me show you the common ancestor:

Listing 3.1:example 15 - common ancestor
1#!/usr/bin/python 
2 
3import sys 
4 
5colors = {"black": "black mirror", 
6          "white": "white noise", 
7          "blue": "blue sky"} 
8 
9def getPhrase(color): 
10    color = color.lower() 
11    if color not in colors: 
12        sys.stderr.write("There is no phrase defined for color %s\n" % color) 
13        sys.exit(1) 
14    phrase = colors[color] 
15    phrase = "%s: %s" % (color, phrase) 
16    return phrase 
17 
18for color in sys.argv[1:]: 
19    print(getPhrase(color))

We will be printing the phrases for multiple colors now, not just a single one.

On branchA the API is changed so that it can be specified whether the original color will be included in the phrase:

Listing 3.2:example 15 - branchA
1#!/usr/bin/python 
2 
3import sys 
4 
5colors = {"black": "black mirror", 
6          "white": "white noise", 
7          "blue": "blue sky"} 
8 
9def getPhrase(color, includeColor): 
10    color = color.lower() 
11    if color not in colors: 
12        sys.stderr.write("There is no phrase defined for color %s\n" % color) 
13        sys.exit(1) 
14    phrase = colors[color] 
15    if (includeColor): 
16        phrase = "%s: %s" % (color, phrase) 
17    return phrase 
18 
19for color in sys.argv[1:]: 
20    print(getPhrase(color, True))

In the real world there is probably no point in having this API change, but it’s ok. Bear with it for the sake of explanation. Let’s take a look at branchB.

Listing 3.3:example 15 - branchB
1#!/usr/bin/python 
2 
3import sys 
4 
5colors = {"black": "black mirror", 
6          "white": "white noise", 
7          "blue": "blue sky"} 
8 
9def getPhrase(color): 
10    color = color.lower() 
11    if color not in colors: 
12        sys.stderr.write("There is no phrase defined for color %s\n" % color) 
13        sys.exit(1) 
14    phrase = colors[color] 
15    phrase = "%s: %s" % (color, phrase) 
16    return phrase 
17 
18# I love color white 
19print(getPhrase("white")) 
20for color in sys.argv[1:]: 
21    print(getPhrase(color))

Now the phrase for color white will always be printed before all the colors that are used as parameters. What will happen if we tried to merge? Will we get a conflict or won’t we? What do you think?

Listing 3.4:example 15 - git merge
$ git merge example15/branchB --no-edit 
Auto-merging example.py 
Merge made by the ’recursive’ strategy. 
 example.py | 2 ++ 
 1 file changed, 2 insertions(+)

Nice! And the end result?

Listing 3.5:example 15 - final result
1#!/usr/bin/python 
2 
3import sys 
4 
5colors = {"black": "black mirror", 
6          "white": "white noise", 
7          "blue": "blue sky"} 
8 
9def getPhrase(color, includeColor): 
10    color = color.lower() 
11    if color not in colors: 
12        sys.stderr.write("There is no phrase defined for color %s\n" % color) 
13        sys.exit(1) 
14    phrase = colors[color] 
15    if (includeColor): 
16        phrase = "%s: %s" % (color, phrase) 
17    return phrase 
18 
19# I love color white 
20print(getPhrase("white")) 
21for color in sys.argv[1:]: 
22    print(getPhrase(color, True))

You do see the problem, right? Let me show you if you haven’t caught it yet:

Listing 3.6:example 15 - we have a problem
$ ./example.py blue 
Traceback (most recent call last): 
  File "./example.py", line 20, in <module> 
    print(getPhrase("white")) 
TypeError: getPhrase() takes exactly 2 arguments (1 given)

Oh, this line, added on branchB, hasn’t been adapted to the new API change coming from branchA:

Listing 3.7:example 15 - this is the problem
19# I love color white 
20print(getPhrase("white"))

And that is because, when the API was changed on branchA, the preexisting calls on that branch were adjusted.... but the calls that were added on the other branch won’t magically get those adjustments when merging. Even if there are no conflicts, API changes can be tricky to deal with when merging code.

3.1.2 Example 16

Now, what will happen when you have conflicting changes on the API? It’s the same situation we just saw, but on steroids because you will have to consider code that is modified from all the branches that are being merged, not just one.

Let’s use another example based on the example we just used. Here’s the full conflicted file:

Listing 3.8:example 16 - full conflicted file
1#!/usr/bin/python 
2 
3import sys 
4 
5RESET=chr(0x1b) + "[m" 
6colors = {"black": {"phrase": "black mirror", "fg": chr(0x1b) + "[0;7m"}, # reverse on color 
7          "white": {"phrase": "white noise", "fg": chr(0x1b) + "[1m"}, 
8          "blue": {"phrase": "blue sky", "fg": chr(0x1b) + "[1;34m"}} 
9 
10<<<<<<< HEAD 
11def getPhrase(color, showColor): 
12||||||| 46e6753 
13def getPhrase(color): 
14======= 
15def getPhrase(color, useColor): 
16>>>>>>> example16/branchB 
17    """ 
18    Get the phrase that corresponds to one color 
19 
20    Parameters 
21    ---------- 
22    color: color to get the phrase for. It can be used in any combination of uppercase/lowercase letters 
23<<<<<<< HEAD 
24    showColor: whether to display the original color name before the phrase or not 
25||||||| 46e6753 
26======= 
27    useColor: we can change the color of the phrase when writing on the output 
28>>>>>>> example16/branchB 
29    """ 
30    color = color.lower() 
31    if color not in colors: 
32        sys.stderr.write("There is no phrase defined for color %s\n" % color) 
33        sys.exit(1) 
34<<<<<<< HEAD 
35    phrase = colors[color] 
36    if showColor: 
37        phrase = "%s: %s" % (color, phrase) 
38||||||| 46e6753 
39    phrase = colors[color] 
40    phrase = "%s: %s" % (color, phrase) 
41======= 
42    phrase = colors[color]["phrase"] 
43    phrase = "%s: %s" % (color, phrase) 
44    if useColor: 
45        phrase = colors[color]["fg"] + phrase + RESET 
46>>>>>>> example16/branchB 
47    return phrase 
48 
49for color in sys.argv[1:]: 
50    print(getPhrase(color, True))

Each branch added a parameter to getPhrase()... and the name can be confusing, right? Are they the same parameter (same intent) with differing names on the two branches? That’s yet another layer of difficulty that you might have to deal with. Luckily for us, there’s docstrings for the function and on the second CB we can see what the parameter from each branch is about and we can conclude that they are definitely not the same:

Listing 3.9:example 16 - CB#2
17    """ 
18    Get the phrase that corresponds to one color 
19 
20    Parameters 
21    ---------- 
22    color: color to get the phrase for. It can be used in any combination of uppercase/lowercase letters 
23<<<<<<< HEAD 
24    showColor: whether to display the original color name before the phrase or not 
25||||||| 46e6753 
26======= 
27    useColor: we can change the color of the phrase when writing on the output 
28>>>>>>> example16/branchB 
29    """

The one in UB, showColor, is used to tell if the original color will be printed before the phrase. The one in LB, useColor, is used to change text color on the output for an added visual effect, and as usual, we need to keep both intents. On the first CB, what parameter will we add first? From my point of view, either one is fine but you might have rules to follow for that... every situation is different. I will do this to solve the first two conflicts in a single shot:

Listing 3.10:example 16 - solution to first 2 CBs
10def getPhrase(color, showColor, colorOnOutput): 
11    """ 
12    Get the phrase that corresponds to one color 
13 
14    Parameters 
15    ---------- 
16    color: color to get the phrase for. It can be used in any combination of uppercase/lowercase letters 
17    showColor: whether to display the original color name before the phrase or not 
18    colorOnOutput: we can change the color of the phrase when writing on the output 
19    """

I changed the name of the parameter coming from the other branch so that the difference between the parameters is more evident. That means that I will also have to modify the places where useColor (the original name of that parameter) is in the function. Let’s take a look at the next CB starting on line 241 :

Listing 3.11:example 16 - CB#3
24<<<<<<< HEAD 
25    phrase = colors[color] 
26    if showColor: 
27        phrase = "%s: %s" % (color, phrase) 
28||||||| 46e6753 
29    phrase = colors[color] 
30    phrase = "%s: %s" % (color, phrase) 
31======= 
32    phrase = colors[color]["phrase"] 
33    phrase = "%s: %s" % (color, phrase) 
34    if useColor: 
35        phrase = colors[color]["fg"] + phrase + RESET 
36>>>>>>> example16/branchB
dMU

Added a conditional in line 26 to control if we will include the original color name on the phrase.

dML

Modified the way we get the phrase associated to the color because the dictionary structure has changed (line 32) and added a conditional on line 34 to modify the phrase so that the output color on the terminal is changed.

Resolution

Given that LB is more different from MB than UB, I will work from LB:

Listing 3.12:example 16 - Step 1 - LB#3
31======= 
32    phrase = colors[color]["phrase"] 
33    phrase = "%s: %s" % (color, phrase) 
34    if useColor: 
35        phrase = colors[color]["fg"] + phrase + RESET 
36>>>>>>> example16/branchB

Then that is needed to be done is add the conditional to include the original color and adjust indentation:

Listing 3.13:example 16 - Step 2 - LB#3
31======= 
32    phrase = colors[color]["phrase"] 
33    if showColor: 
34        phrase = "%s: %s" % (color, phrase) 
35    if useColor: 
36        phrase = colors[color]["fg"] + phrase + RESET 
37>>>>>>> example16/branchB

And then we are done, right? Gotcha!!! Did you remember to adjust the name of the parameter that is coming from the other branch? I changed it from useColor to colorOnOutput while working on solving CBs 1 and 2, so we need to also adjust that on this CB:

Listing 3.14:example 16 - Step 2 - LB#3
31======= 
32    phrase = colors[color]["phrase"] 
33    if showColor: 
34        phrase = "%s: %s" % (color, phrase) 
35    if colorOnOutput: 
36        phrase = colors[color]["fg"] + phrase + RESET 
37>>>>>>> example16/branchB

So, the final resolution of the file would be:

Listing 3.15:example 16 - solved conflict
1#!/usr/bin/python 
2 
3import sys 
4 
5RESET=chr(0x1b) + "[m" 
6colors = {"black": {"phrase": "black mirror", "fg": chr(0x1b) + "[0;7m"}, # reverse on color 
7          "white": {"phrase": "white noise", "fg": chr(0x1b) + "[1m"}, 
8          "blue": {"phrase": "blue sky", "fg": chr(0x1b) + "[1;34m"}} 
9 
10def getPhrase(color, showColor, colorOnOutput): 
11    """ 
12    Get the phrase that corresponds to one color 
13 
14    Parameters 
15    ---------- 
16    color: color to get the phrase for. It can be used in any combination of uppercase/lowercase letters 
17    showColor: whether to display the original color name before the phrase or not 
18    colorOnOutput: we can change the color of the phrase when writing on the output 
19    """ 
20    color = color.lower() 
21    if color not in colors: 
22        sys.stderr.write("There is no phrase defined for color %s\n" % color) 
23        sys.exit(1) 
24    phrase = colors[color]["phrase"] 
25    if showColor: 
26        phrase = "%s: %s" % (color, phrase) 
27    if colorOnOutput: 
28        phrase = colors[color]["fg"] + phrase + RESET 
29    return phrase 
30 
31for color in sys.argv[1:]: 
32    print(getPhrase(color, True))

And now we are ready, right? Gotcha again!!! Did you happen to notice the call on line 32?

Listing 3.16:example 16 - call that needs to be adusted
31for color in sys.argv[1:]: 
32    print(getPhrase(color, True))

It has only 2 parameters... but we should have 3, right? Why is that? Oh, it’s one of those cases where git’s smartness is playing against us. If git notices that both branches applied the same change, it just takes it in as is from one of the branches and generates no conflict. In this case both branches added the second parameter and the value is True for both branches, so it’s the same change coming from both branches. But we need to adjust it so that both parameters get a True value and then it will be solved. All in all, the very final resolution of the conflict2 would be:

Listing 3.17:example 16 - solved conflict for real
1#!/usr/bin/python 
2 
3import sys 
4 
5RESET=chr(0x1b) + "[m" 
6colors = {"black": {"phrase": "black mirror", "fg": chr(0x1b) + "[0;7m"}, # reverse on color 
7          "white": {"phrase": "white noise", "fg": chr(0x1b) + "[1m"}, 
8          "blue": {"phrase": "blue sky", "fg": chr(0x1b) + "[1;34m"}} 
9 
10def getPhrase(color, showColor, colorOnOutput): 
11    """ 
12    Get the phrase that corresponds to one color 
13 
14    Parameters 
15    ---------- 
16    color: color to get the phrase for. It can be used in any combination of uppercase/lowercase letters 
17    showColor: whether to display the original color name before the phrase or not 
18    colorOnOutput: we can change the color of the phrase when writing on the output 
19    """ 
20    color = color.lower() 
21    if color not in colors: 
22        sys.stderr.write("There is no phrase defined for color %s\n" % color) 
23        sys.exit(1) 
24    phrase = colors[color]["phrase"] 
25    if showColor: 
26        phrase = "%s: %s" % (color, phrase) 
27    if colorOnOutput: 
28        phrase = colors[color]["fg"] + phrase + RESET 
29    return phrase 
30 
31for color in sys.argv[1:]: 
32    print(getPhrase(color, True, True))

That was some work, right?

3.1.3 Exercises

Exercise 7

From git repo, checkout revision 4284497396 and merge cdb5330a9b3 . Solve all conflicts. Solution is here.

Copyright 2020 Edmundo Carmona Antoranz