Branch and Merge

Exercise 03 Branch and Merge

Branches are a way of keeping track of a series of commits. You’ve already used a branch, the main branch! When you create a new commit the current branch will update to point at the latest commit which is called the HEAD in git.

You can just keep stacking commits onto the main branch forever but if you have multiple streams of work (or multiple people working on one repository) then you can branch from main, make your changes then merge back in.

Sometimes main is automatically released or published so branching also allows for partially completed work to be committed.

A graphic depicting a git branch and a git merge

docker run -it gitforpragmatists/01-basics-03-branch-and-merge

Branch

We can see the branches in our repository with git branch the current branch is marked with an asterisk (*)

  ex-add-cookie
  ex-add-pie
  ex-conflict-1
  ex-conflict-2
* main

You can also see the current branch by passing the --show-current

$ git branch --show-current
main

To switch between branches we can use git switch1

You can switch to an existing branch

$ git switch ex-add-cookie
Switched to branch 'ex-add-cookie'

or create a new branch from the current commit

$ git switch -c my-new-branch

Switched to a new branch 'my-new-branch'

If you’ve run both of the above you’ll see from git log that my-new-branch and ex-add-cookie are both on the same commit “Add basic cookie recipe”.

commit 46e3023faa1dca77946578a90114806cefcf2472 (HEAD -> my-new-branch, ex-add-cookie)
Author: Elliot <elliot@gitforpragmatists.xyz>
Date:   Mon Aug 22 20:03:32 2022 +0000

    Add basic cookie recipe

commit 9715cfd0584ce39e6fa12a92d16196004335b15c (main)
Author: Elliot <elliot@gitforpragmatists.xyz>
Date:   Mon Aug 22 20:03:32 2022 +0000

    Add basic cake recipe

commit 3892d41f689c60a77b24f88a16bbdaa71877bdea
Author: Elliot <elliot@gitforpragmatists.xyz>
Date:   Mon Aug 22 20:03:32 2022 +0000

    Initial commit

We can make a commit on my-new-branch to move things along.

$ do-work README.md
$ git status
On branch my-new-branch
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:   README.md

no changes added to commit (use "git add" and/or "git commit -a")
$ git add README.md
$ git commit -m "Commit to demonstrate our new branch"
[my-new-branch 69bb03a] Commit to demonstrate our new branch
 1 file changed, 1 insertion(+)
$ git log
commit 69bb03a82e4d504e93572540f1e6f6a0575e4d40 (HEAD -> my-new-branch)
Author: Git Student <test@example.com>
Date:   Sat Aug 27 14:52:07 2022 +0000

    Commit to demonstrate our new branch

commit 46e3023faa1dca77946578a90114806cefcf2472 (ex-add-cookie)
Author: Elliot <elliot@gitforpragmatists.xyz>
Date:   Mon Aug 22 20:03:32 2022 +0000

    Add basic cookie recipe

commit 9715cfd0584ce39e6fa12a92d16196004335b15c (main)
Author: Elliot <elliot@gitforpragmatists.xyz>
Date:   Mon Aug 22 20:03:32 2022 +0000

    Add basic cake recipe
...

You can see that our branch is now one commit ahead of ex-add-cookie and two ahead of main.

Merge

Branching has allowed us to create multiple streams of independent work but at some point, we will need to get that work back into main.

$ git switch main
Switched to branch 'main'
$ git merge ex-add-cookie
Updating 9715cfd..46e3023
Fast-forward
 CookieRecipe.md | 11 +++++++++++
 1 file changed, 11 insertions(+)
 create mode 100644 CookieRecipe.md

The merge is a “fast-forward” which means that the commits in ex-add-cookie had a parent that was the current HEAD of main. Reviewing the log you can see that the HEAD of main is now the same commit as the HEAD of ex-add-cookie

$ git log
commit 46e3023faa1dca77946578a90114806cefcf2472 (HEAD -> main, ex-add-cookie)
Author: Elliot <elliot@gitforpragmatists.xyz>
Date:   Mon Aug 22 20:03:32 2022 +0000

    Add basic cookie recipe

commit 9715cfd0584ce39e6fa12a92d16196004335b15c
Author: Elliot <elliot@gitforpragmatists.xyz>
Date:   Mon Aug 22 20:03:32 2022 +0000

    Add basic cake recipe

commit 3892d41f689c60a77b24f88a16bbdaa71877bdea
Author: Elliot <elliot@gitforpragmatists.xyz>
Date:   Mon Aug 22 20:03:32 2022 +0000

    Initial commit

From history’s point of view the ex-add-cookie branch may have never existed, the new commit is stacked directly onto the previous one.

If we’d also like to merge the work from ex-add-pie then we will need a merge commit. That is because the HEAD of main is no longer a parent of the commits on ex-add-pie.

$ git merge ex-add-pie
Merge made by the 'ort' strategy.
 PieRecipe.md | 13 +++++++++++++
 1 file changed, 13 insertions(+)
 create mode 100644 PieRecipe.md

When we call git merge ex-add-pie we are asked for a commit message explaining the merge. In this case the merge was simple since the changes did not conflict and the default commit message will suffice.

Reviewing the log we can see that main now has a brand new commit (our merge) as its HEAD. The two recipe commits are each now in main as the parents of the merge commit.

commit e447e4c5ccbb06b7da48fe952ba40c89cce36459 (HEAD -> main)
Merge: 46e3023 f8ea7c3
Author: Git Student <test@example.com>
Date:   Sat Aug 27 15:22:46 2022 +0000

    Merge branch 'ex-add-pie'

commit 46e3023faa1dca77946578a90114806cefcf2472 (ex-add-cookie)
Author: Elliot <elliot@gitforpragmatists.xyz>
Date:   Mon Aug 22 20:03:32 2022 +0000

    Add basic cookie recipe

commit f8ea7c3baf6551c61df7d9cc8e8462956a8c78c8 (ex-add-pie)
Author: Elliot <elliot@gitforpragmatists.xyz>
Date:   Mon Aug 22 20:03:32 2022 +0000

    Add basic pie recipe

commit 9715cfd0584ce39e6fa12a92d16196004335b15c
Author: Elliot <elliot@gitforpragmatists.xyz>
Date:   Mon Aug 22 20:03:32 2022 +0000

    Add basic cake recipe
...

What do you think would happen if you merged the branches in the other order? You can restart the docker container and try it to test if your guess is correct.

Resolving Conflicts

In the case of the pie and cookie recipes, git was able to guess how to combine our two changes into what we wanted since the two files were unrelated.

Not all merges are so simple, ex-conflict-1 and ex-conflict-2 are two branches containing changes to the same file. Whichever we merge first should merge cleanly with a merge commit but git will not be able to automatically resolve the other merge.

$ git merge ex-conflict-1
Merge made by the 'ort' strategy.
 CakeRecipe.md | 6 ++++++
 1 file changed, 6 insertions(+)
$ git merge ex-conflict-2
Auto-merging CakeRecipe.md
CONFLICT (content): Merge conflict in CakeRecipe.md
Automatic merge failed; fix conflicts and then commit the result.
$
$ git status
On branch main
You have unmerged paths.
  (fix conflicts and run "git commit")
  (use "git merge --abort" to abort the merge)

Unmerged paths:
  (use "git add <file>..." to mark resolution)
        both modified:   CakeRecipe.md

no changes added to commit (use "git add" and/or "git commit -a")

Reviewing the content of CakeRecipe.md we can see the conflict.

Basic Cake Recipe
====

1. 250g butter
2. 250g caster sugar
3. a pinch of salt
4. 3 eggs
5. a drop of vanilla


<<<<<<< HEAD
Cake Recipe Blurb
====

I often make this cake for parties and functions because it is light and fluffy.
=======
Cake Recipe Method
====

1. Combine the dry ingredients in a large bowl.
2. Make a well in the dry ingredients and crack in the eggs, stir until combined
3. Add the butter and vanilla and fold until incorporated.
>>>>>>> ex-conflict-2

The top of the file is unchanged from before then we have a conflict block of the format

<<<<<<< HEAD
...
=======
...
>>>>>>> ex-conflict-2

The marked lines show the changes added by the current branch (HEAD marking the state of the current branch commit), a separator =======, the changes added by the merging branch which for our convenience appears as the branch name ex-conflict-2.

In this case, the resolution is simple, we want both the changes so I can edit the file with nano or vim to remove the conflict block.

Basic Cake Recipe
====

1. 250g butter
2. 250g caster sugar
3. a pinch of salt
4. 3 eggs
5. a drop of vanilla



Cake Recipe Blurb
====

I often make this cake for parties and functions because it is light and fluffy.

Cake Recipe Method
====

1. Combine the dry ingredients in a large bowl.
2. Make a well in the dry ingredients and crack in the eggs, stir until combined
3. Add the butter and vanilla and fold until incorporated.

Then stage the change and conclude the merge by providing a commit message. In this case the merge was simple so I would again leave the default message but if this was a more involved merge I might add a note on how I resolved the conflict and satisfied myself that I was left in a good state.

$ git add CakeRecipe.md
$ git commit 
[main e128fad] Merge branch 'ex-conflict-2'

More complex examples of conflicts might be a function that two branches have modified the behaviour of. Neither was aware of the other making the change so the two changes need to be combined with human intelligence and new test cases may need to be added to cover a behaviour that emerges only when both changes are present.

Resolving conflicts by hand as we’ve done in this exercise is quite challenging to get correct if the merge is complicated. Merging is one of the few things I reach for visual tooling for personally. Especially if you write software, I’d encourage you to use something like meld or the tooling in your IDE to resolve merge conflicts.

Fast Forwards

Earlier when we merged our first branch ex-add-cookie we automatically benefited from a “fast-forward” merge. There are occasions where you might want to insist that your merge is fast-forward or else you don’t want to do it. For example if you want to take the latest work from a branch but only if it is not going to involve any merging.

In such a case you can specify --ff-only.

git merge --ff-only my-branch

We also saw that when you fast-forward merge from the history’s perspective the source branch may never have existed. If you want to insist on marking that a merge occurred (even if it could have been fast-forward) you can specify --no-ff.

git merge --no-ff my-branch-which-would-fast-forward

  1. Before git switch was added in Git 2.23 changing branches was performed with git checkout. I don’t recommend this because checkout is heavily overloaded and can throw away unversioned changes. [return]