
2020-06-28
I’m creating this post after seeing many people suffering and manually addressing merge conflicts while integrating patches with dwm, st, or dmenu code base.
The most common issues seen:
applying patches on latest from master
using patch instead of git to patch
resolving conflict manually
In my previous git post, I went over how to create and apply patches to a git repository.
In this post, I’ll show how conflict can be solved using tools.
Note: I’m going to use git, but these techniques are not unique to git.
In this example, I’ll start by using meld diff tool. In a later section, I’ll also show different tools.
You first need to install meld.
It can be installed on Linux, windows, OS X. Most distribution package it, or head over to their website.
After it’s installed, we need to tell git to use this tool as a mergetool:
1
git config --global --replace-all merge.tool meld
You can then inspect the your ~/.gitconfig and you should see a section like this:
1
2
[merge]
tool = meld
To do some experiment, I’m creating a local git repository, with 1 file with some text:
1
2
3
4
5
6
7
8
9
10
mkdir merge-conflicts-test
cd merge-conflicts-test
git init
cat > file1 <<EOT
original line 1
original line 2
original line 3
EOT
git add file1
git commit -m "initial commit"
We have a new repository with file1 containing text:
1
2
3
original line 1
original line 2
original line 3
Lets inspect the history by running the command:
1
git log --oneline --decorate --relative-date --graph
1
* 8ca4176 (HEAD -> master) initial commit
Now lets create a condition for a conflict that is trivial to merge.
We first create a branch and add a line:
1
2
3
4
git checkout -b feature/adding-a-line
echo "line 4 from feature branch" >> file1
git add file1
git commit -m "adding some changes at the end of the file"
1
2
[feature/adding-a-line b24353e] adding some changes at the end of the file
1 file changed, 1 insertion(+)
And we can see what is in file1:
1
cat file1
1
2
3
4
original line 1
original line 2
original line 3
line 4 from feature branch
Now lets go back to master and add a change to the first line:
1
2
3
4
git checkout master
sed -i -e 's/original line 1/new change on line 1/' file1
git add file1
git commit -m "changes to line 1"
1
2
3
M file1
[master 177c8f3] changes to line 1
1 file changed, 1 insertion(+), 1 deletion(-)
And we can see what is in file1:
1
cat file1
1
2
3
new change on line 1
original line 2
original line 3
As you can see, we have 2 commits with different changes to file1.
We only have a conflict if we want to integrate changes from one branch to another.
Lets do this by merging changes from our feature branch to master:
1
2
git checkout master
git merge feature/adding-a-line
1
2
3
4
Auto-merging file1
Merge made by the 'recursive' strategy.
file1 | 1 +
1 file changed, 1 insertion(+)
And we can see what is in file1:
1
cat file1
1
2
3
4
new change on line 1
original line 2
original line 3
line 4 from feature branch
As you can see trivial conflicts are automatically merged.
The history now looks like this:
1
git log --oneline --decorate --relative-date --graph
1
2
3
4
5
6
* 8061307 (HEAD -> master) Merge branch 'feature/adding-a-line'
|\
| * b24353e (feature/adding-a-line) adding some changes at the end of the file
* | 177c8f3 changes to line 1
|/
* 8ca4176 initial commit
What you can read from this history:
HEAD points to the latest commit of our master branch, a merge commit
the commit ``changes to line 1 was done on master''
the commit ``adding some changes at the end of file'' was done on our feature branch
the common ancestor of both commits is the ``initial commit''
Now lets create a conflicts that you will have to merge manually.
We will make changes to line 3 from both our feature branch and our master branch:
1
2
3
4
git checkout -b feature/change-to-line3
sed -i -e 's/original line 3/line 3 from branch/' file1
git add file1
git commit -m "changing line 3 on our branch"
1
2
[feature/change-to-line3 2e16635] changing line 3 on our branch
1 file changed, 1 insertion(+), 1 deletion(-)
The history now looks like this:
1
git log --oneline --decorate --relative-date --graph
1
2
3
4
5
6
7
* 2e16635 (HEAD -> feature/change-to-line3) changing line 3 on our branch
* 8061307 (master) Merge branch 'feature/adding-a-line'
|\
| * b24353e (feature/adding-a-line) adding some changes at the end of the file
* | 177c8f3 changes to line 1
|/
* 8ca4176 initial commit
The commit at the top is on our new feature branch.
Now lets change line 3 on master:
1
2
3
4
git checkout master
sed -i -e 's/original line 3/another change to line 3/' file1
git add file1
git commit -m "changing line 3 again"
1
2
[master fc9a98c] changing line 3 again
1 file changed, 1 insertion(+), 1 deletion(-)
The history now looks like this:
1
git log --oneline --decorate --relative-date --graph
1
2
3
4
5
6
7
* fc9a98c (HEAD -> master) changing line 3 again
* 8061307 Merge branch 'feature/adding-a-line'
|\
| * b24353e (feature/adding-a-line) adding some changes at the end of the file
* | 177c8f3 changes to line 1
|/
* 8ca4176 initial commit
What happens if we merge our feature branch? We have a conflict that cannot be resolved automatically:
1
git merge feature/change-to-line3
1
2
3
Auto-merging file1
CONFLICT (content): Merge conflict in file1
Automatic merge failed; fix conflicts and then commit the result.
And we can see what is in file1:
1
cat file1
1
2
3
4
5
6
7
8
new change on line 1
original line 2
<<<<<<< HEAD
another change to line 3
=======
line 3 from branch
>>>>>>> feature/change-to-line3
line 4 from feature branch
In the file1, you can see changes from both branches.
You could at this point, open your text editor and manually resolve the
conflict by keeping the text we want, and removing the conflict markers
(<<<<<, >>>>>, ===
).
This is what many people do on YouTube when applying patches and trying to resolve conflicts. What you may not realize, is we have tools to do this, like meld, vimdiff, kdiff3.
With our previous configuration, all we need is to run the command:
1
git mergetool
This screen should appear:
What can you get from this screen:
The middle section is our file1 populated with the common ancestor’s value (original line 3)
The left section (called local) contains our change on our current checkout branch
The right section (called remote) contains the change we made on the branch to merge (our feature branch)
You bring the changes you want using the arrow icons.
You can take changes from both local and remote (you can take more than once )
You can edit the middle buffer
I will take both changes, save and close meld.
After all of this is done, you can commit the changes:
1
git commit
You’ll be prompted to keep or change the default commit message.
I’m going to accept the default message.
Voila! Now you know how to resolve conflicts using tools.
You can use the merge –abort option to abort an ongoing merge and then start again.
Similar tool than meld. GUI based.
You just need to configure your merge.tool git configuration to kdiff3.
Here is a screenshot of the same merge conflict above, but this time with kdiff3:
Kdiff3 adds a 4th pane, the editing pane.
The base is the common ancestor, the local and remote are like for meld, the changes to the current branch and the branch we want to merge.
Clicking on A/B/C allows you to select which change(s) you want to get. The bottom pane shows you these and also allows you to edit.
Vimdiff is similar to kdiff3 with the 4 panes, except it’s console base. You will find your way if your familiar with vim.
Just set the merge.tool git configuration to vimdiff.
Here is the merge conflict as seen with vimdiff:
Bindings:
C-w C-w
[c and ]c
:ls
dp
dg
Window numbers:
local
base
remote
file to edit
Again, the file window can be edited. This is vim, so you can use any vim commands.
In case where you have trivial and non-trivial conflicts, tools react differently:
does not detect trivial from non-trivial
apply trivial changes automatically. Good most of the time, sometime breaks.
does not detect trivial from non-trivial
The problem of resolving conflicts may not appear to be that big for an example like above, but over many files and many changes, it’s the best way to resolve them with consistency and quality.
What I’ve learned during this post? I still prefer kdiff3 over meld as a merge tool due to automatic trivial conflict resolution of kdiff3. I haven’t tried Emacs ediff too, will come when I try replacing IntelliJ with emacs lsp-mode in the future.
In my next post, I’ll demo how I organize, apply and merge patches for st using: branches, git apply, and a merge tool.
I hope you learned something :)
This is day 9 of my #100DaysToOffload. You can read more about the challenge here: https://100daystooffload.com.