19 04 2020
GIT. Learn by doing: Chapter 3
Merge commits, not branches
In this chapter, we are going to look closer to the merge process. After you’ve learned, that branch is only a label, time to have a look at what merge is.
Let’s start with some theory and slowly move to practice. First, I want to introduce you to the five rules of the merge:
- The first rule of the merge is:
The merge is about merging two commits, not branches - The second rule of the merge is:
The merge is about joining two commits, not branches ( this is not a typo, it’s just very essential ) - The third rule of the merge is:
It’s not necessary to merge the last two commits ( those without children ), but any commit can be merged with any commit - The fourth rule of the merge is:
Merging two commits produces a new commit, even when you merge the same commits again - The fifth rule of the merge is:
History of both commits should have diverged and they must have a common parent
As you can see, my rules again are based on the commits and not branches. You should look at the merge process as just joining two commits together, which produces a new commit. As soon as you make this idea yours, you won’t be afraid anymore to have merge conflicts, cause then you can re-merge and try again. This brings us to the first important conclusion. Try to visualize the merge, like on the left picture and not on the right one:
I think most of the rules from above are pretty easy to follow except for the last one: “Both commits must have a common parent, and their history should have diverged”. Picture for better visualization:
Imagine I would like to merge commits ee92080
and 52d8110
together. Both commits are sitting in one line, meaning they are do not have diverged history. Such a merge won’t result in a new commit and won’t be registered as a merge.
The split point ee92080
is the commit where history has diverged and any commit from one ‘branch’ ( commits 31f021f
and 52d8110
) can be merged with another ‘branch’ ( commits 8cd0b84
and 8106bf4
)
Doing such merge results in a ‘fast-forward’ merge, which is actually not a merge, cause it doesn’t change the history of the repository at all. I will tell you about such a merge in the next chapter.
As soon as history does diverge and you perform a merge, then Git is doing the following actions. First, Git is traversing all the commits back to the parent, looking at the common parent of both commits. As soon as such parent found, it performs a 3-way merge.
Though, both commits should have a common parent. Maybe you don’t know, but commits should not necessary have common parent and common tree, but this is also a story for the next chapter.
Three-way merge is…
In a 3-way merge are always 3 commits which are involved: parent commit and two commits which you want to merge. Parent commit often called “base” . An example of why we need a three-way merge:
Example:
Say you’ve made some changes to a file by removing a line at the beginning. Then you “jumped back” in time, checked out the previous commit, and changed the same file again by adding a line at the end. Of course, both times you’ve committed your changes, which created a diverged history.
Now you would like to merge your both commits. But how would Git know what to do with the differences? Should the merged version include both lines, first and the last one, or should it remove them?
With a three-way merge, Git compares each of the commits against the original copy. So it can see that you removed the first line in one commit and that you add the last line in another commit. Then Git can use that information to produce the merged version of your file.
Below is the “diff” of both files. File a
( on the left ) is with added line at the end. File b
(on the right) has the line removed at the beginning.
As you can see, now it’s challenging to define what the result should be. By having the third file as a base, we know which line to add and which to remove.
Enough of the theory, let’s start with some exercises.
Merge. Re-merge. Fix. Merge
Task 1: Bootstrap the repository
I think you’ve already guessed that we are going to start merging commits and not branches. For this time, I will keep ‘branch’ labels in the repository so that it’s more visible in the Git graph. Bootstrap the repository (choose the one you need):
curl -o- https://raw.githubusercontent.com/nemisj/git-learn/master/three-way-merge.sh | bash
wget -qO- https://raw.githubusercontent.com/nemisj/git-learn/master/three-way-merge.sh | bash
Invoke-WebRequest https://raw.githubusercontent.com/nemisj/git-learn/master/three-way-merge.ps1 -UseBasicParsing | Invoke-Expression
Graph of the created repository:
As I already said before, we start at the commit “two (start): Change a to x”. Please remember the hash of it. For me it is 30c13547
.
I advise you to look closely at the changes which are made before you start typing the exercises. This repository has approximately the changes I’ve described in an example of a three-way merge. Also note that all of the commits have prefix “one” or “two” to denote on which branch the commit was done, just to make it more readable.
Task 2: Merge
The first thing we are going to test is how merge works with commits. While we are on a commit “two (start): Change a to x” (30c13547
), let’s merge commit “one: Added d” (6d1a64a7
) to it. Execute in terminal ( do not forget to replace hash with the one you have ):
git merge 6d1a64a7 --no-edit
git branch first-merge
Command 1 will create a merge with the default merge message. Command 2 will place a label on a newly created merge commit.
Typically, you don’t have to do the second step, when the commit you’re on has a branch label. Git will automatically reset this branch label to the new commit. However, for our test case, and readability, we create it our self.
The result I have is:
Also please note, that it does not matter which commit is currently checked out and which you specify with merge
command. Even if I would checkout commit “one: Added d” (6d1a64a7
) and would merge commit “two (start): Change a to x” (30c13547
), the result would be the same. This is precisely why I proposed you to visualize merge as on the left picture above.
Task 3: Re-merge
This time we are going to see whether it is possible to merge the same commits again. First, checkout the commit “two (start): Change a to x”. I’m doing it through a command line client, but you can use your Git client.
Let’s merge again, but this time create another branch label, making it “second-merge”. Execute in your terminal ( reminder, don’t forget to change hash 6d1a64a7
to yours) :
git merge 6d1a64a7 --no-edit
git branch second-merge
Result I have:
As you can see, merging multiple times the same commits is pretty valid and always creates a new merge commit. It has the same message, the identical author, the same parents, but different hash.
What does this give us? Well, the same “time machine” which was before. Don’t be afraid to merge, anytime you can put your branch label on a new merge commit. When could it go wrong? For example, when you have merge conflicts, and you resolved it incorrectly. This is exactly what we are going to try next.
Task 4: Merge. Fix. Re-Merge
This time task becomes a bit more complicated – merge two commits that have conflicts. To do that, let’s checkout the two
branch and merge one
into it. For Git, when you will execute command git merge one
, it will resolve the commit hash of the one
first, and then will do a commit. Which means not much different then what we have done so far. The only difference is that it will also reset the two
branch label to the new commit. If you would do it another way round: checkout the one
and merge two
into it, merge commit would be the same, and Git would put one
on a new commit.
This time I will use Git client and will checkout and merge using ui and not cli.
And of course this will fail.
Now, you can resolve conflicts the way you like and commit them. My rep graph now:
Imagine this happening at work, you starting to work and see that your merge is not good. How can you solve it? The most simplistic solution so far would be:
- put the
one
branch label on the previous commit where it was before the merge. This is commit – “one: Added head” (058371ac
) - Perform merge again and resolve correctly
If you want to use command line Git to put a branch label on a different commit, then you can use git reset --hard ${commit-hash}
. This command will reset your current branch label to a new ${commit-hash}. Otherwise, use your git client if it allows you to do that. Not all git clients have such a menu option directly.
An example how I’ve done it. This time a short video:
That is it for today. Next time I will continue with merge theory and practice and will teach you more complicated solutions to resolving conflicts. I hope after this chapter, you will be braver in resolving merge conflicts. Just don’t forget. Everything is a commit, and if you screw up, you have “a time machine”.
Last but not least
Two more things I would like to tell you before finishing.
A couple of paragraphs above I told about a common parent which is used when merged. This one you always can find by using git merge-base Executing it when doing merge in one of the tasks before the merge, and it would always show the commit hash of the common ancestor.
The second one is about branch labels which I’ve created for better visualization: “first-merge” and “second-merge”. If we wouldn’t create them and jump directly to the start commit, the old merge commit would still be there, only not visible. It would be the dangling one. Still, remember that word? Good!
GIT: Learn by doing. Chapter 2 GIT. Learn by doing: Chapter 4
[…] to date” you will see if you try to merge a branch again. If you have already read my chapter “Merge commits, not branches” then you should know one possible solution on how to fix […]