Notes of Maks Nemisj

Experiments with JavaScript

GIT. Learn by doing: Chapter 4

This chapter is going to be part II of explanations about the merge. The reason why I explain merge in so many details because we will need it to understand how the D from DVCS works in Git. D stands for Distributed, and this is where an understanding of ‘merge’ is of significant importance.

Fast-Forward

In the last chapter, we have seen that doing a merge creates a new commit and brings two history lines into one. But what about a “fast-forward” merge, which doesn’t create a separate commit? What is it, and why do we need it?

Fast-forward merge is not a real merge, but merely a movement of a branch label to a new commit. It’s important to note that fast-forward makes only sense when working with branch labels. Such a merge does not change Git history since it does not create any commits. Additionally, such a merge is not possible to trace back, like who did it, when this happened and why.

Often fast-forward occurs when working with a feature branch, and there are no changes in the master branch. As soon as you try to merge a feature branch back to master, Git will apply a fast-forward merge. It will move the ‘master’ label to the commit where the feature branch is on. Look at these two pictures below, one before the merge and one after.

fast-forward stricture before

After I try to merge the feature branch into master, I get the following:

Fortunately, editors of nowadays, like VisualCode are giving us “advice” on not doing ‘fast-forward’ and instead create a commit.

The option “Create a new commit even if fast-forward is possible” is precisely doing this. Creating a commit even for a fast-forward merge might be very convenient. This gives you the ability to trace back when this merge happened, why, and by whom. Though, to do it or not is a matter of taste.

It might look not essential to know about fast-forward, though, as you will see, this information is also needed when we will start studying remotes and distributed property of the Git.

This is going to be the last picture for the fast-forward paragraph. Look at the Git history after I’ve merged with the option “Create a new commit even if fast-forward is possible”. As you can see, very clear what happens, by whom, and when.

Have you noticed that I’ve done this merge one day later than other commits? Cool, right? With fast-forward, that wouldn’t be possible to see 😉

Squash merge

So far, we’ve seen that as soon as we merge two history lines into one, there appears a merge commit, except for fast-forward of course. However, it is possible to join two distinct branches without a merge point. Reasoning why you need it, I will explain later, but for now necessary to remember that it can be done pretty easily.

Such a merge won’t leave any traces of a merge action unless you specify it in a commit message. From outside, the merge commit will look like an ordinary commit, meaning you won’t be able to tell whether it has been merged. Git calls such merge a squashWhat Git is doing is taking all the commits from the branch you want to merge, squashes them into one, and applies that commit on top of the current branch.

Let’s take as an example history from the picture above and merge feature-branch again into master, but using a squash method. The result is:

History of squashed merge
History of squashed merge

As you can see, the commit message states “Merge branch ‘feature’“. Besides that, there is nothing else that can give us a clue that it was a merge. As soon as you change the message into something else, the reader of the history will never know it was a merge. While normal merge commit has parents of both history lines, squashed merge commit has parent only of the active line.

There is another crucial difference between these two merge techniques. Let’s have a look at how the changes are reflected in Git history when using normal merge and squashed merge.

First, look at the annotation (git blame) of the file after the normal merge.

The blame of zork file
The blame of “zork” file

I know it’s not sexy, but it has all the essential information we need. On the right pane, you see the content of the zork file, and on the left pane, the commit (hash, author, and date) in which the appropriate line was changed. For example, line 2 was modified in commit 43b9aa3a by Maks Nemisj. Line 3 was modified in 8c989ba8 commit. Such a detailed overview gives us the ability to trace down any change to the file per line, which means that after merge we can see that every line was changed in a different commit.

Now, let’s have a look at the annotation when squash merge is applied:

The blame of "zork" file - squashed
The blame of “zork” file – squashed

On that picture above, you can notice that both lines have been changed in commit 98425574. This commit is a merge commit itself, you can see it in the history log above.

There are a couple of reasons for doing a squashed merge:

  • You don’t want your merge to appear in the main Git history. For example, when working with feature branches and is not interested in progress, but only in a feature result.
  • Other creative ways of working with git

Already up to date

“Already up 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 this.

This time, the task is going to be more complicated. After the wrong merge has happened, there are other commits applied on top of it, and we won’t be able “just reset the branch label to new merge”, like in the chapter before.

Task 1: Bootstrap the repository

Before we begin, I will tell you the story for the next repo. A long time ago, I decided to keep a list of my favorite books in a Git repository. For sci-fi books, I have created a separate branch called ‘sci-fi’. For other books, I’ve used ‘master’ branch. After a couple of beers, I’ve decided to merge my sci-fi branch into the master branch and unnoticeably made a mistake – I’ve lost some books from the list. I should never drink and merge. Then, I kept adding books to the master branch. Today, I’ve noticed my mistake and will fix it together with you.

curl -o- https://raw.githubusercontent.com/nemisj/git-learn/master/merge-with-conflict.sh | bash
wget -qO- https://raw.githubusercontent.com/nemisj/git-learn/master/merge-with-conflict.sh | bash
Invoke-WebRequest https://raw.githubusercontent.com/nemisj/git-learn/master/merge-with-conflict.ps1 -UseBasicParsing | Invoke-Expression

Task 2: Locate merge points

Now, you’re up and running, let’s look at our history and define the strategy we are going to use to solve the issue.

"merge-with-conflict" history
“merge-with-conflict” history

I propose to merge both lines again using squash, but this time resolve conflicts correctly. If we will do squashed merge, Git will not see it as a merge, but only as “standalone” commit. Then apply newly created commit to the master branch.

To do the first merge, we have to define merge points. These are going to be two dots before the merge. In my case, these are c2f446ae and 5e59b07c

Task 3: checkout and merge –squash

This time, we are going to merge both commits without branch labels. Exactly, as you’ve already done before with git checkout ${hash} and git merge ${hash}. Then switch to the master and merge new commit hash into the master. Execute in your terminal ( do not forget to replace all the hashes with the ones you have):

git checkout c2f446ae
git merge --squash 5e59b07c
// resolve conflicts at this stage, but without mistake
git commit --no-edit -a

When you use --no-edit, commit message will include the information about to be merged commits.

This is what I have so far:

First merge with squash

Task 4: Merge into master

Momentarily we should have correctly resolved the books file, meaning we can apply it to the master. There are a couple of possible ways to do it, but for now, we will use merge with squash. Let’s remember the new hash, in my case, it is 7735a0a0 and merge it into the master. Execute in your terminal ( do not forget about correct hashes):

git checkout master
git merge --squash 7735a0a0

As soon as you will merge, there is going to be a merge conflict again. That’s okay, we can resolve it. This time, you will have duplicated information in the file because changed lines are very close to each other, and Git is unable to see what it should add and where.

Diff of the second merge

You should be able to resolve this one quickly. As soon as you have done this, the last thing is to commit this merge. Don’t forget to describe the message correctly, something like “Fixing merge commit 86b9a72b with correct resolution”.

That’s it for today. In the next chapter, I will start explaining what are “The remotes” and things about “push” and “pull”.

, , ,

One thought on “GIT. Learn by doing: Chapter 4

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.