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.
docker run -it gitforpragmatists/01-basics-03-branch-and-merge
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 switch
1
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
.
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.
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.
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
Want to stay up to date with Git for Pragmatists? 📬
Sign up for our mailing list!