Working with branches

This part has a small setup stage. Run 04_setup.sh if you want to follow along.

Branching is one of the topics where git shines. Git allows for lightweight branching and therefore for easy experiments. We will not go into discussion about branching strategies. This is something your team has to decide and find out. However, if you want to read up, have a look at this article by Martin Fowler about Patterns for Managing Source Code Branches.

Creating a branch

All you need to do to create a branch in git is use the git branch branchName command. It will create a second ref entry, named like your branch name and put the current commit hash into. If you use git branch without providing it with a branch name it will list your branches.

$ ls -l
total 0
-rw-rw-r-- 1 martin martin 0 Jul 17 00:19 file1
$ git branch
* main
$ git status --short

We are on a main branch and one file is present. This file is tracked and not changed as the git status output tells us.

We will now do three things, look at the content of HEAD, look at the content of .git/refs/heads and the content of these files before and after we create a branch.

$ cat .git/HEAD
ref: refs/heads/main
$ ls .git/refs/heads
main
$ cat .git/refs/heads/main
66eecc4254266d583817205576bab67ff44b91ee
$ git log --oneline
66eecc4 initial commit

We see our HEAD points to refs/heads/main and this points to the first commit. Nothing new. Now we create a branch.

$ git branch mybranch
$ cat .git/HEAD
ref: refs/heads/main
$ ls .git/refs/heads
main
mybranch
$ cat .git/refs/heads/main
66eecc4254266d583817205576bab67ff44b91ee
$ cat .git/refs/heads/mybranch
66eecc4254266d583817205576bab67ff44b91ee

Git created a second file in .git/refs/heads which is called mybranch just as the branch name. The content is the same as for the main file and the HEAD still points to the main branch ref. Git basically creates a new pointer to the value of the current HEAD. In our case to the tip of the main branch.

Switching Branches

Now we want to switch to our newly created branch. We have to options git checkout and git switch. We will use the newer git switch.

Again, let's look at the value of the HEAD before and after.

$ cat .git/HEAD
ref: refs/heads/main
$ git switch mybranch
$ cat .git/HEAD
ref: refs/heads/mybranch

Git just points HEAD to the new ref. If we now commit, we will update .git/refs/heads/mybranch and not the ref for main. That's it. That is branching with git.

Of course there are switches for git switch to speed up things.

command
git switch -c branchName create branch and switch to it
git switch - switch back to the branch you came from

Renaming a branch

If a branch name is only a file name it surely can be renamed. Use git branch -m newName to rename the branch you are on. Be aware, renaming is a completely local operation. If you are collaboration no one will know you renamed the branch in your repository clone.

Conflicts

Before we talk about moving content from one branch to another. Let's quickly talk about conflicts. If a file is changed in more then one branch, chances are hight there will be a structural conflict. Git is not very clever in resolving those conflicts automatically. It will prompt you for help. A conflict looks like this:

base line
<<<<<<< HEAD
added on main
=======
added on mybranch
>>>>>>> mybranch

From <<<<<<< HEAD to ====== is what we have on the branch we are applying this change to. There is even a special diff format:

diff --cc file1
index 5c32171,e6bb0bc..0000000
--- a/file1
+++ b/file1
@@@ -1,2 -1,2 +1,6 @@@
  base line
++<<<<<<< HEAD
 +added on main
++=======
+ added on mybranch
++>>>>>>> mybranch

Compared to the normal diff, this has two columns for change markers.

ReReRe

Git can help you resolving conflicts which may happen more then once. This can be the case if you perform a rebase our must merge one source into multiple branches. It is called rerere and stands for "Reusing Recorded Resolution". You can activate it with git config rerere.enabled true. Now git will record your resolutions and will try to use them if it encounters the same conflict again. Of course there is a lot more to it. Check out the documentation.

Moving content between branches

There are multiple ways moving content from one branch to another. We will look at cherry-pick, merge and rebase.

We can group them into two broad categories.

  1. Applying the diff of a commit. This creates a new commit, with a new tree but possibly the same blobs. Cherry-pick does that for one commit. Rebase for a whole branch.
  2. Keeping the commits intact. Merge does this. If needed merge creates a merge commit with a new diff to show the differences between both branches.

Cherry pick

To perform a cherry pick we need a commit to pick. Cherry-picks work best if the commit is atomic - meaning it should only do one logical change.

commit graph describing a cherry pick as taking the diff of a commit and apply it on top of another commit on a different branch

We will now create a cherry pickable commit on the main branch and pick it into mybranch. But first, assume we already did some work on mybranch, which is not on main.

$ git switch mybranch
echo 'some work on mybranch' > work
$ git add .
git commit -am 'add work on mybranch'
[mybranch b5d81b3] add work on mybranch
 1 file changed, 1 insertion(+)
 create mode 100644 work
$ git switch main
echo 'important config' > config
$ git add .
$ git status --short
A  config
git commit -am 'add configuration fix'
[main fe5ae35] add configuration fix
 1 file changed, 1 insertion(+)
 create mode 100644 config
$ git log --oneline
fe5ae35 add configuration fix
66eecc4 initial commit

We now have the commit fe5ae35d5b4e2ef782cd51652966b076f4f20a6c ready and want to pick it into mybranch. We either can copy the commit hash, type the first few digits or can use the help of git rev-list to calculate it in the command directly. git rev-list returns the commit hashes of all the commits leading to the supplied endpoint, e.g. git rev-list HEAD will print all the commits reachable from HEAD to the initial commit. If supplied with the -n 1 switch it will only print the first commit.

$ git switch mybranch
$ git rev-list main -n 1
fe5ae35d5b4e2ef782cd51652966b076f4f20a6c
git cherry-pick $(git rev-list main -n 1)
[mybranch e433fd3] add configuration fix
 Date: Thu Jul 17 00:19:24 2025 +0200
 1 file changed, 1 insertion(+)
 create mode 100644 config
$ git log --oneline
e433fd3 add configuration fix
b5d81b3 add work on mybranch
66eecc4 initial commit
$ git diff e433fd3f7044a00e98850187eeb19fdf0dfbe1c7..fe5ae35d5b4e2ef782cd51652966b076f4f20a6c
diff --git a/work b/work
deleted file mode 100644
index 4efabd5..0000000
--- a/work
+++ /dev/null
@@ -1 +0,0 @@
-some work on mybranch

As we can see, we created a new commit on mybranch. This commit has a unique commit hash, the parent commit and the tree in the commit differ from the one on main.

Merges

Now let's perform a simple merge. By default git does either a ort 3-way merge or a fast-forward merge if possible. We will first have a look at the ort strategy which replaced the recursive strategy recently as default.

Merging is done with the git merge command and can be, if needed, aborted with git merge --abort.

ORT Strategy

The ort strategy takes two heads and resolves them with a 3-way merge algorithm. It can take various arguments, including arguments like ignore-space-change and similar whitespace aware modifiers. You can read it up here: Merge Strategies. This strategy is uses when the two heads have diverged, meaning each head points to commits the other one is not aware of.

Lucky for us, we have such a case. We will now merge the mybranch into main, look at the commit and at its diff.

$ git switch main
$ git merge mybranch -m 'merge mybranch'
Merge made by the 'ort' strategy.
 work | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 work
$ git show
commit eebc1321afa3f47fcb249180e296ee620dd423c8
Merge: fe5ae35 e433fd3
Author: maschmi <maschmi@maschmi.net>
Date:   Thu Jul 17 00:19:24 2025 +0200

    merge mybranch

$ git cat-file -p eebc1321afa3f47fcb249180e296ee620dd423c8
tree 20e5c2a953d3f198e78d0c7f38004f6356340262
parent fe5ae35d5b4e2ef782cd51652966b076f4f20a6c
parent e433fd3f7044a00e98850187eeb19fdf0dfbe1c7
author maschmi <maschmi@maschmi.net> 1752704364 +0200
committer maschmi <maschmi@maschmi.net> 1752704364 +0200

merge mybranch

As we can see we created a merge commit. And interestingly this commit has two parents and according to git show has not changed anything - no diff present. A merge commit is a special commit. It has two parents. The first parent is always the branch you are merging into, the second is the source branch. git show does not know which parent to compare to, to create a diff. There are multiple options to show the diff, we will use the -m\' flag. For more information runman git show`.

$ git show -m
commit eebc1321afa3f47fcb249180e296ee620dd423c8 (from fe5ae35d5b4e2ef782cd51652966b076f4f20a6c)
Merge: fe5ae35 e433fd3
Author: maschmi <maschmi@maschmi.net>
Date:   Thu Jul 17 00:19:24 2025 +0200

    merge mybranch

diff --git a/work b/work
new file mode 100644
index 0000000..4efabd5
--- /dev/null
+++ b/work
@@ -0,0 +1 @@
+some work on mybranch

Now we see the change introduced by the merge commit.

graphical representation of the merge commit and the branch history

Fast Forward

A very easy way for git, and for us, is to do a fast forward merge. This only works, if the target branch we want to merge into, does not have any commits which are not already in the source branch we want to merge.

$ git switch -c fastforward
$ touch fast.forward
$ git add .
git commit -am 'add fast.forward'
[fastforward a24a54b] add fast.forward
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 fast.forward
$ git switch main
$ git merge fastforward
Updating eebc132..a24a54b
Fast-forward
 fast.forward | 0
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 fast.forward
$ git log --oneline
a24a54b add fast.forward
eebc132 merge mybranch
fe5ae35 add configuration fix
e433fd3 add configuration fix
66eecc4 initial commit
b5d81b3 add work on mybranch

We see no merge commit. It was not necessary as the main branch had nothing on it the fastforward not yet contained.

graphical representation of a fast forward merge

Reverting merge commits

graphical representation of reverting a merge commit, see text for details

Reverting merge commits can be tricky. The best way to revert is to reset the target branch to the commit before the merge commit. We can do a git revert for a merge commit and git will remove the changes introduced by it. However, git will keep the commits from the source branch in the history and will not apply the again when we merge the same branch a second time.

Show me the whole history

Before we go to the rebase, let's have a look at a rather helpful log format. We can tell git log with the --all switch to show the log for all branches. When we combine this with --oneline and `--graph' for graph, we get a nice graph of the history. However, it sometimes is a bit confusing to read.

git log --all --graph --oneline
* a24a54b (HEAD -> main, fastforward) add fast.forward
*   eebc132 merge mybranch
|\  
| * e433fd3 (mybranch) add configuration fix
| * b5d81b3 add work on mybranch
* | fe5ae35 add configuration fix
|/  
* 66eecc4 initial commit

We can read it from the top:

Rebase

graphical representation of a rebase

A rebase works a bit differently compared to a merge. The first difference is: You rebase your current branch on top of the source branch. The second difference is: It will change the whole history of your current branch, every commit is changed. Remember the cherry-pick? Git applied the diff of the commit on your target branch, creating a new tree and a new commit. Rebase does this for all commits on your current branch. Starting with the oldest one. After the rebase you will have a linear history. But it will also be new and tell a different story.

Exercise

Jump over to the branches exercise to get familiar with merging, conflict resolution and rebasing.