1.4 Merging Branches#

In this section, we’ll see how to integrate work from the feature branch back into the main branch.

Types of Merges#

The two most common merge strategies are fast-forward and three-way merges:

  • Fast-forward merge: This merge strategy is used when the target branch can be updated to point to the commit of the source branch without needing to create a new commit.

  • Three-way merge: A three-way merge is used when the two branches have diverged and a new commit is required to reconcile the differences.

Fast-forward merge#

Let’s first understand visually how a fast-forward merge of the feature branch into main would look like by comparing the current state of the repository with the state after the merge.

This is how the repository looks like now:

flowchart RL C2((C2)) --> C1((C1)) C3((C3)) --> C2 main --> C2 feature --> C3 HEAD{HEAD} --> feature

How can we incorporate the changes from feature into main? If we look at the commit history, we can see that main is behind feature by one commit. To update main with the changes from feature, we can simply move the main pointer to the same commit as feature. This is possible whenever you can update a branch to match another by advancing its pointer through the commit history until it reaches the same commit. This process is known as a ‘fast-forward’ because it involves moving the pointer forward to the commit of the other branch without creating a new merge commit.

So after the fast-forward merge, the repository would look like this:

flowchart RL C2((C2)) --> C1((C1)) C3((C3)) --> C2 main --> C3 feature --> C3 HEAD{HEAD} --> main

What to notice

  • Since main was behind feature by one commit, the main pointer was simply moved to the same commit as feature and now it points to C3.

  • feature still points to C3.

  • HEAD now points to main since in order to do the merge we had to switch first to main.

Now that we understand what a fast-forward merge is, let’s see how to do it in Git. To perform a fast-forward merge, first switch to the branch you want to merge into, and then merge the other branch into it using git merge <branch-name>. For example, to merge feature into main:

git switch main
Switched to branch 'main'
git merge feature
Updating d30b4a0..4c34d2d
Fast-forward
 file1.txt | 1 +
 1 file changed, 1 insertion(+)

What to notice

  • Git tell us that there was one file changed, file1.txt, and that there was 1 insertions (which we know is the third line).

git status
On branch main
nothing to commit, working tree clean

After the fast-forward merge, there are no changes to commit, the working tree is clean.

git log
commit 4c34d2de5a3d346a271ea3c30477f0049ddc88cc (HEAD -> main, feature)
Author: msdp-book <msdp.book@gmail.com>
Date:   Fri Mar 15 19:16:24 2024 +0100

    Add third line to file1.txt

commit d30b4a05bd3582bd7ec537c09ed0c764cc605e1e
Author: msdp-book <msdp.book@gmail.com>
Date:   Fri Mar 15 19:16:22 2024 +0100

    Add second line to file1.txt

commit 2007711caa39b5ad6bee678519eb0b98e71bd1a3
Author: msdp-book <msdp.book@gmail.com>
Date:   Fri Mar 15 19:16:17 2024 +0100

    Create file1.txt with the first line

We see that all the changes that we discussed in the diagram have been made. The main branch now points to the same commit as feature, and the feature branch still points to the same commit. The HEAD pointer is now pointing to main.

Impact of switching branches on the working directory#

We will now explore what happens to the working directory when we switch branches. To see this, let’s first add a new file and make changes to an existing file on the feature branch, and then switch to the main branch.

git switch feature
touch file2.txt
echo "This is first line." >> file2.txt
echo "This is the fourth line." >> file1.txt
git add .
git commit -m "Add file2 and modify file1"
Switched to branch 'feature'
[feature ae47e9c] Add file2 and modify file1
 2 files changed, 2 insertions(+)
 create mode 100644 file2.txt
ls
cat file1.txt
file1.txt  file2.txt
This is the first line.
This is the second line.
This is the third line.
This is the fourth line.
git switch main
Switched to branch 'main'
ls
cat file1.txt
file1.txt
This is the first line.
This is the second line.
This is the third line.

What to notice

  • We see that the file file2.txt is no longer in the working directory after switching to main.

  • The changes made to file1.txt on the feature branch are removed after switching to main.

Git updates the files in your working directory to reflect the state of the files in the new branch. This means that any files unique to the previous branch but not present in the new one may be removed, and any files unique to the new branch may be added or modified.

Switching branches with uncommitted changes#

Git prevents loss of work by disallowing branch switching with uncommitted changes. To see this in action, let’s make changes to file1.txt on the feature branch and then try to switch to main without committing the changes.

git switch feature
echo "This is the fifth line." >> file1.txt
git switch main
Switched to branch 'feature'
error: Your local changes to the following files would be overwritten by checkout:
	file1.txt
Please commit your changes or stash them before you switch branches.
Aborting

What to notice

  • Git does not allow branch switching with uncommitted changes to protect against data loss.

Modifying an staged file#

Changes made after staging are not automatically included in the staged snapshot. To include changes in the staged snapshot, the file must be staged again.

git add file1.txt
echo "This is the sixth line." >> file1.txt
git status
On branch feature
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
	modified:   file1.txt

Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
	modified:   file1.txt

What to notice

  • The file file1.txt is staged and has uncommitted changes at the same time.

We can also see the status in a more compact form using the -s option:

git status -s
MM file1.txt

The first character in the output of git status -s shows the status of the staged snapshot, and the second character shows the status of the working directory.

What to notice

  • As we saw before, with git status, file1.txt has both staged and not staged modifications.

Let’s now stage the latest changes to file1.txt

git add file1.txt
git status -s
M  file1.txt

now all modifications has been staged and we can commit them:

git commit -m "Add fifth and sixth lines to file1.txt"
[feature 73aa479] Add fifth and sixth lines to file1.txt
 1 file changed, 2 insertions(+)

Viewing all commits#

When working with multiple branches in Git, you might find yourself on a branch that is behind others in terms of commits. For instance, if you’re on the main branch that hasn’t been updated with the latest commits from a feature branch, using the git log command while on main will only show the commit history up to the point main is currently at. This means you won’t see the commits that are on feature or any other branch until they are merged into main.

This is because git log displays by default only the commit history of that branch, starting from the current commit and tracing back through the history of commits that lead to the current state of the branch.

Let’s see this in action by using the git log command while on the main branch.

git switch main
git log
Switched to branch 'main'
commit 4c34d2de5a3d346a271ea3c30477f0049ddc88cc (HEAD -> main)
Author: msdp-book <msdp.book@gmail.com>
Date:   Fri Mar 15 19:16:24 2024 +0100

    Add third line to file1.txt

commit d30b4a05bd3582bd7ec537c09ed0c764cc605e1e
Author: msdp-book <msdp.book@gmail.com>
Date:   Fri Mar 15 19:16:22 2024 +0100

    Add second line to file1.txt

commit 2007711caa39b5ad6bee678519eb0b98e71bd1a3
Author: msdp-book <msdp.book@gmail.com>
Date:   Fri Mar 15 19:16:17 2024 +0100

    Create file1.txt with the first line

Now compare this with the output of git log --all:

git switch main
git log --all
Already on 'main'
commit 73aa47900ddb4b5cf6a6fcc222bb6e8f73bc16f4 (feature)
Author: msdp-book <msdp.book@gmail.com>
Date:   Fri Mar 15 19:16:33 2024 +0100

    Add fifth and sixth lines to file1.txt

commit ae47e9cc31a286967d519a32f7f089776ee1223e
Author: msdp-book <msdp.book@gmail.com>
Date:   Fri Mar 15 19:16:31 2024 +0100

    Add file2 and modify file1

commit 4c34d2de5a3d346a271ea3c30477f0049ddc88cc (HEAD -> main)
Author: msdp-book <msdp.book@gmail.com>
Date:   Fri Mar 15 19:16:24 2024 +0100

    Add third line to file1.txt

commit d30b4a05bd3582bd7ec537c09ed0c764cc605e1e
Author: msdp-book <msdp.book@gmail.com>
Date:   Fri Mar 15 19:16:22 2024 +0100

    Add second line to file1.txt

commit 2007711caa39b5ad6bee678519eb0b98e71bd1a3
Author: msdp-book <msdp.book@gmail.com>
Date:   Fri Mar 15 19:16:17 2024 +0100

    Create file1.txt with the first line

What to notice

  • Commits from feature are visible with git log --all, showing the comprehensive history across branches.

Checking out a commit and the detached HEAD State#

We have seen that we can move the HEAD pointer to a specific branch by using git switch <branch-name> but we can also move the HEAD pointer to a specific commit by using git checkout <commit-hash>. We can also checkout a commit relative to the current position of the branch pointer by using git checkout <branch>~<number>. This is useful for exploring the history of a repository.

git checkout feature~2
Note: switching to 'feature~2'.

You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.

If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:

  git switch -c <new-branch-name>

Or undo this operation with:

  git switch -

Turn off this advice by setting config variable advice.detachedHead to false

HEAD is now at 4c34d2d Add third line to file1.txt
git status
HEAD detached at 4c34d2d
nothing to commit, working tree clean
git log --all
commit 73aa47900ddb4b5cf6a6fcc222bb6e8f73bc16f4 (feature)
Author: msdp-book <msdp.book@gmail.com>
Date:   Fri Mar 15 19:16:33 2024 +0100

    Add fifth and sixth lines to file1.txt

commit ae47e9cc31a286967d519a32f7f089776ee1223e
Author: msdp-book <msdp.book@gmail.com>
Date:   Fri Mar 15 19:16:31 2024 +0100

    Add file2 and modify file1

commit 4c34d2de5a3d346a271ea3c30477f0049ddc88cc (HEAD, main)
Author: msdp-book <msdp.book@gmail.com>
Date:   Fri Mar 15 19:16:24 2024 +0100

    Add third line to file1.txt

commit d30b4a05bd3582bd7ec537c09ed0c764cc605e1e
Author: msdp-book <msdp.book@gmail.com>
Date:   Fri Mar 15 19:16:22 2024 +0100

    Add second line to file1.txt

commit 2007711caa39b5ad6bee678519eb0b98e71bd1a3
Author: msdp-book <msdp.book@gmail.com>
Date:   Fri Mar 15 19:16:17 2024 +0100

    Create file1.txt with the first line

What to notice

  • We moved the HEAD pointer two commits back from the current position of the feature branch.

  • By coincidence, the commit we checked out is the same as the commit main is pointing.

  • HEAD is not pointing to main is pointing to the commit directly, note that there is no arrow from HEAD to main, as we used to see before.

When you check out a commit, you are in a detached HEAD state. This means that the HEAD pointer is no longer pointing to a branch, but to a specific commit. This is useful for inspecting the state of the repository at a specific point in time, but it is not recommended to make changes in this state. If you make changes and commit them, they will be based on the commit you checked out, not on the tip of any branch. This means that if you switch back to a branch, the changes you made in the detached HEAD state will not be part of the branch. To exit a detached HEAD state and retain your changes, you can create a new branch from the current commit:

git switch -c new-branch-name
Switched to a new branch 'new-branch-name'

This command creates and switch to the new branch (in one command) starting from the current commit, effectively bringing your HEAD back to a non-detached state and ensuring that any commits made in the detached HEAD state are not lost.

git status
On branch new-branch-name
nothing to commit, working tree clean

After creating and switching to the new branch we are not in a detached HEAD state anymore, and the changes we made are now part of the new branch.

Deleting a branch#

Finally, once your feature it’s merged, you usually don’t want to keep the feature branch around. To delete a branch, first you need to ensure that you are not currently on the branch you want to delete by switching to another branch or commit. Then use the git branch -d <branch-name> command. For example, to delete the new-branch-name branch:

git switch main
git branch -d new-branch-name
Switched to branch 'main'
Deleted branch new-branch-name (was 4c34d2d).

The -d option deletes the branch safely, meaning it will prevent you from deleting the branch that contained unmerged changes. If you are sure you want to delete the branch even if it contains unmerged changes, you can use the -D option, which forces the deletion.