Branching and merging
Last updated on 2024-07-29 | Edit this page
Overview
Questions
- How can I or my team work on multiple features in parallel?
- How to combine the changes of parallel tracks of work?
- How can I permanently reference a point in history, like a software version?
Objectives
- Be able to create and merge branches.
- Know the difference between a branch and a tag.
Motivation for branches
In the previous section we tracked a guacamole recipe with Git.
Up until now our repository had only one branch with one commit coming after the other:
- Commits are depicted here as little boxes with abbreviated hashes.
- Here the branch
main
points to a commit. - “HEAD” is the current position (remember the recording head of tape recorders?).
- When we talk about branches, we often mean all parent commits, not only the commit pointed to.
Now we want to do this:
(Source: https://twitter.com/jay_gee/status/703360688618536960)
Software development is often not linear:
- We typically need at least one version of the code to “work” (to compile, to give expected results, …).
- At the same time we work on new features, often several features concurrently. Often they are unfinished.
- We need to be able to separate different lines of work really well.
The strength of version control is that it permits the researcher to isolate different tracks of work, which can later be merged to create a composite version that contains all changes:
- We see branching points and merging points.
- Main line development is often called
main
(ormaster
in older conventions). - Other than this convention there is nothing special about
main
, it is just a branch. - Commits form a directed acyclic graph (we have left out the arrows to avoid confusion about the time arrow).
A group of commits that create a single narrative are called a branch. There are different branching strategies, but it is useful to think that a branch tells the story of a feature, e.g. “fast sequence extraction” or “Python interface” or “fixing bug in matrix inversion algorithm”.
Let us inspect the project history using the git graph
alias:
OUTPUT
* dd4472c (HEAD -> main) we should not forget to enjoy
* 2bb9bb4 add half an onion
* 2d79e7e adding ingredients and instructions
- We have three commits and only one development line (branch) and
this branch is called
main
. - Commits are states characterized by a 40-character hash (checksum).
-
git graph
print abbreviations of these checksums. - Branches are pointers that point to a commit.
- Branch
main
points to commitdd4472c8093b7bbcdaa15e3066da6ca77fcabadd
. -
HEAD
is another pointer, it points to where we are right now (currentlymain
)
On which branch are we?
To see where we are (where HEAD points to) use
git branch
:
OUTPUT
* main
- This command shows where we are, it does not create a branch.
- There is only
main
and we are onmain
(star represents theHEAD
).
In the following we will learn how to create branches, how to switch between them, how to merge branches, and how to remove them afterwards.
Creating and working with branches
Let’s create a branch called experiment
where we add
cilantro to ingredients.txt
.
BASH
$ git branch experiment main # create branch called "experiment" from main
# pointing to the present commit
$ git switch experiment # switch to branch "experiment"
$ git branch # list all local branches and show on which branch we are
- Verify that you are on the
experiment
branch (note thatgit graph
also makes it clear what branch you are on:HEAD -> branchname
):
OUTPUT
* experiment
main
- Then add 2 tbsp cilantro on top of the
ingredients.txt
:
* 2 tbsp cilantro
* 2 avocados
* 1 lime
* 2 tsp salt
* 1/2 onion
- Stage this and commit it with the message “let us try with some cilantro”.
- Then reduce the amount of cilantro to 1 tbsp, stage and commit again with “maybe little bit less cilantro”.
We have created two new commits:
OUTPUT
* 6feb49d (HEAD -> experiment) maybe little bit less cilantro
* 7cf6d8c let us try with some cilantro
* dd4472c (main) we should not forget to enjoy
* 2bb9bb4 add half an onion
* 2d79e7e adding ingredients and instructions
- The branch
experiment
is two commits ahead ofmain
. - We commit our changes to this branch.
Interlude: The multipurpose “checkout” command
Older versions of git used git checkout
for the actions
now handled by both restore
and switch
.
git checkout
can still be found in a lot of documentation,
Git tools, and scripts. Depending on the context
git checkout
can do very different actions:
- Switch to a branch:
- Bring the working tree to a specific state (commit):
- Set a file/path to a specific state (throws away all unstaged/uncommitted changes):
This is unfortunate from the user’s point of view but the way Git is
implemented it makes sense. Picture git checkout
as an
operation that brings the working tree to a specific state. The state
can be a commit or a branch (pointing to a commit).
In Git 2.23 (2019-08-16) and later this is much nicer:
Exercise: create and commit to branches
In this exercise, you will create two new branches, make new commits to each branch. We will use this in the next section, to practice merging.
- Change to the branch
main
. - Create another branch called
less-salt
- Note! Makes sure you are on main branch when you create the
less-salt branch. A safer way would be to explicitly specify that you
want to branch from the main branch, e.g.:
git branch less-salt main
- Note! Makes sure you are on main branch when you create the
less-salt branch. A safer way would be to explicitly specify that you
want to branch from the main branch, e.g.:
- On this new branch reduce the amount of salt in your recipe.
- Commit your changes to this
less-salt
branch.
Use the same commands as we used above.
We now have three branches (in this case HEAD
points to
less-salt
):
OUTPUT
experiment
* less-salt
main
OUTPUT
* bf59be6 (HEAD -> less-salt) reduce amount of salt
| * 6feb49d (experiment) maybe little bit less cilantro
| * 7cf6d8c let us try with some cilantro
|/
* dd4472c (main) we should not forget to enjoy
* 2bb9bb4 add half an onion
* 2d79e7e adding ingredients and instructions
Here is a graphical representation of what we have created:
- Now switch to
main
. - Add and commit the following
README.md
tomain
:
Now you should have this situation:
OUTPUT
* 40fbb90 (HEAD -> main) draft a readme
| * bf59be6 (less-salt) reduce amount of salt
|/
| * 6feb49d (experiment) maybe little bit less cilantro
| * 7cf6d8c let us try with some cilantro
|/
* dd4472c we should not forget to enjoy
* 2bb9bb4 add half an onion
* 2d79e7e adding ingredients and instructions
Merging branches
It turned out that our experiment with cilantro was a good idea. Our
goal now is to merge experiment
into main
.
First we make sure we are on the branch we wish to merge into:
OUTPUT
experiment
less-salt
* main
Then we merge experiment
into main
:
We can verify the result in the terminal:
OUTPUT
* c43b24c (HEAD -> main) Merge branch 'experiment'
|\
| * 6feb49d (experiment) maybe little bit less cilantro
| * 7cf6d8c let us try with some cilantro
* | 40fbb90 draft a readme
|/
| * bf59be6 (less-salt) reduce amount of salt
|/
* dd4472c we should not forget to enjoy
* 2bb9bb4 add half an onion
* 2d79e7e adding ingredients and instructions
What happens internally when you merge two branches is that Git creates a new commit, attempts to incorporate changes from both branches and records the state of all files in the new commit. While a regular commit has one parent, a merge commit has two (or more) parents.
To view the branches that are merged into the current branch we can use the command:
OUTPUT
experiment
* main
We are also happy with the work on the less-salt
branch.
Let us merge that one, too, into main
:
We can verify the result in the terminal:
OUTPUT
* 4f00317 (HEAD -> main) Merge branch 'less-salt'
|\
| * bf59be6 (less-salt) reduce amount of salt
* | c43b24c Merge branch 'experiment'
|\ \
| * | 6feb49d (experiment) maybe little bit less cilantro
| * | 7cf6d8c let us try with some cilantro
| |/
* | 40fbb90 draft a readme
|/
* dd4472c we should not forget to enjoy
* 2bb9bb4 add half an onion
* 2d79e7e adding ingredients and instructions
Observe how Git nicely merged the changed amount of salt and the new ingredient in the same file without us merging it manually:
OUTPUT
* 1 tbsp cilantro
* 2 avocados
* 1 lime
* 1 tsp salt
* 1/2 onion
If the same file is changed in both branches, Git attempts to incorporate both changes into the merged file. If the changes overlap then the user has to manually settle merge conflicts (we will do that later).
Deleting branches safely
Both feature branches are merged:
OUTPUT
experiment
less-salt
* main
This means we can delete the branches:
OUTPUT
Deleted branch experiment (was 6feb49d).
Deleted branch less-salt (was bf59be6).
This is the result:
Compare in the terminal:
OUTPUT
* 4f00317 (HEAD -> main) Merge branch 'less-salt'
|\
| * bf59be6 reduce amount of salt
* | c43b24c Merge branch 'experiment'
|\ \
| * | 6feb49d maybe little bit less cilantro
| * | 7cf6d8c let us try with some cilantro
| |/
* | 40fbb90 draft a readme
|/
* dd4472c we should not forget to enjoy
* 2bb9bb4 add half an onion
* 2d79e7e adding ingredients and instructions
As you see only the pointers disappeared, not the commits.
Git will not let you delete a branch which has not been reintegrated
unless you insist using git branch -D
. Even then your
commits will not be lost but you may have a hard time finding them as
there is no branch pointing to them.
Exercise: encounter a fast-forward merge
- Create a new branch from
main
and switch to it. - Create a couple of commits on the new branch (for instance edit
README.md
):
- Now switch to
main
. - Merge the new branch to
main
. - Examine the result with
git graph
. - Have you expected the result? Discuss what you see.
The following exercises are advanced, absolutely no problem to postpone them to a few months later. If you give them a go, keep in mind that you might run into conflicts, which we will learn to resolve in the next section.
(Optional) Exercise: Moving commits to another branch
Sometimes it happens that we commit to the wrong branch, e.g. to
main
instead of a feature branch. This can easily be
fixed:
1. Make a couple of commits to main
, then realize these
should have been on a new feature branch.
2. Create a new branch from main
, and rewind
main
back using
git reset --hard <hash>
.
3. Inspect the situation with git graph
. Problem
solved!
(Optional) Exercise: Rebasing
As an alternative to merging branches, one can also rebase
branches. Rebasing means that the new commits are replayed on
top of another branch (instead of creating an explicit merge
commit).
Note that rebasing changes history and should not be done on
public commits!
1. Create a new branch, and make a couple of commits on it.
2. Switch back to main
, and make a couple of commits on
it.
3. Inspect the situation with git graph
.
4. Now rebase the new branch on top of main
by first
switching to the new branch, and then
git rebase main
.
5. Inspect again the situation with git graph
. Notice that
the commit hashes have changed - think about why!
(Optional) Exercise: Squashing commits
Sometimes you may want to squash incomplete commits,
particularly before merging or rebasing with another branch (typically
main
) to get a cleaner history.
Note that squashing changes history and should not be done on
public commits!
1. Create two small but related commits on a new feature
branch, and inspect with git graph
.
2. Do a soft reset with git reset --soft HEAD~2
.
This rewinds the current branch by two commits, but keeps all changes
and stages them.
3. Inspect the situation with git graph
,
git status
and git diff --staged
.
4. Commit again with a commit message describing the changes.
5. What do you think happens if you instead do
git reset --soft <hash>
?
Summary
Let us pause for a moment and recapitulate what we have just learned:
BASH
$ git branch # see where we are
$ git branch <name> # create branch <name>
$ git switch <name> # switch to branch <name>
$ git merge <name> # merge branch <name> (to current branch)
$ git branch -d <name> # delete merged branch <name>
$ git branch -D <name> # delete unmerged branch <name>
Since the following command combo is so frequent:
There is a shortcut for it:
Typical workflows
With this there are two typical workflows:
BASH
$ git switch -c new-feature # create branch, switch to it
$ git commit # work, work, work, ...
# test
# feature is ready
$ git switch main # switch to main
$ git merge new-feature # merge work to main
$ git branch -d new-feature # remove branch
Sometimes you have a wild idea which does not work. Or you want some throw-away branch for debugging:
BASH
$ git switch -c wild-idea
# work, work, work, ...
# realize it was a bad idea
$ git switch main
$ git branch -D wild-idea # it is gone, off to a new idea
# -D because we never merged back
No problem: we worked on a branch, branch is deleted,
main
is clean.
Test your understanding
- Which of the following combos (one or more) creates a new branch and
makes a commit to it?
$ git branch new-branch $ git add file.txt $ git commit
$ git add file.txt $ git branch new-branch $ git switch new-branch $ git commit
$ git switch -c new-branch $ git add file.txt $ git commit
$ git switch new-branch $ git add file.txt $ git commit
- What is a detached
HEAD
? - What are orphaned commits?
- Both 2 and 3 would do the job. Note that in 2 we first stage the
file, and then create the branch and commit to it. In 1 we create the
branch but do not switch to it, while in 4 we don’t give the
-c
flag togit switch
to create the new branch. - When you check out a branch name, HEAD will point to the most recent
commit of that branch. You can however check out a particular
hash. This will bring your working directory back in time to that
commit, and your HEAD will be pointing to that commit but it will not be
attached to any branch. If you want to make commits in that state, you
should instead create a new branch:
git switch -c test-branch <hash>
. - An orphaned commit is a commit that does not belong to any branch,
and therefore doesn’t have any parent commits. This could happen if you
make a commit in a detached HEAD state. Commits rarely vanish in Git,
and you could still find the orphaned commit using
git reflog
.
Key Points
- A branch is a division unit of work, to be merged with other units of work.
- A tag is a pointer to a moment in the history of a project.