Diverging Branches
Last updated on 2024-09-24 | Edit this page
Overview
Questions
- What are branches?
- How do we use branches in git effectively?
- How can I check out other peoples branches whilst working on my own?
- How do I keep my development branch up-to-date with
main
?
Objectives
- How branches can be used to fix bugs or develop features in isolation.
- Switching branches, stashing and restoring.
- How to keep a development branch up-to-date.
- Differences between and when to use merge and rebase.
- Git worktrees instead of branches.
- Tracking multiple origins
Diverging Branches
As you and your collaborator(s) work on your repository you may find
that changes others have made get merged into the main
before you have finished your work. This has in fact just happened, the
work to add a Zero Division exception has been merged via a Pull
Request, but the work to address the Square Root function hasn’t and is
in effect behind the main
branch. The following is a
representation of the current state, albeit from a single developer.
In this example the main
branch now includes the commits
3-8c52dce
and 5-2315fa0
from the
ns-rse/1-zero-division
branch as well as the commit
7-bc43901
which was made when the
ns-rse/1-zero-division
branch was merged in. The
ns-rse/2-square-root
branch does not contain these
commits.
In this particular example that is not necessarily a problem, the two
features/issues are completely independent and it would be possible to
merge the ns-rse/2-square-root
branch into
main
without any merge conflicts because neither have
modified the same files in the same location.
That will not always the case though, sometimes merge conflicts might
arise if the second branch is changing some of the same files as the
first branch. Another scenario might be that whilst work was being done
on adding a new feature branch a critical bug was fixed that the new
feature depends on and the changes now in main
need
incorporating in the feature branch.
There are two approaches to solving this merging
(git merge
) and rebasing (git rebase
).
Merging
The syntax of git merge
is
Where <ref>
is one of a commit, branch name or tag
(both of which are references to commits). There is an option for how
the merge is made known as fast-forward
. Fast-forward is
the default action unless annotated tags are being merged that is in the
incorrect hierarchy. To explicitly enabled this behaviour
(--ff
) and the branch pointer, that is where the current
branch diverged from the the main
branch) is updated to
point to the most recent commit on the main
branch.
Typically though the main
branch contains work from
someone else’s branch and we want to incorporate those changes in the
another branch.
3. Switch back to main
Check the contents of README.md
(there is no such file
as the it exists on branch1
).
5. Merge branch1
into main
Switch back to main
and merge branch1
(this
is equivalent to merging a Pull Request). The file
README.md
now exists on the main
branch.
6. Merge main
, which now contains
README.md
, into branch2
Switch to branch2
which has now diverged as it contains
changes of its own and main
contains the changes
made on branch1
. We want to merge the changes on
main
and “fast-forward” if possible.
BASH
git switch branch2
git merge --ff main # Merge changes merged into main from branch1 into branch2
git logp
* d914fee - (HEAD -> branch2) Merge branch 'main' into branch2 (2024-03-01 12:02:08 +0000) <Neil
|\
| * 7817070 - (main, branch1) Adding a README.md (2024-03-01 11:57:35 +0000) <Neil Shephard>
* | a14a643 - Adding a LICENSE (2024-03-01 12:00:39 +0000) <Neil Shephard>
|/
* 1bd6bb8 - Initial commit (2024-03-01 11:57:06 +0000) <Neil Shephard>
7. Merge branch2
into main
We now have the changes from branch1
included in
branch2
by virtue of having merged main
. If we
switch back to main
we can merge the changes from
branch2
.
BASH
git switch main
git merge branch2
git logp
* d914fee - (HEAD -> main, branch2) Merge branch 'main' into branch2 (2024-03-01 12:02:08 +0000) <Neil Shephard>
|\
| * 7817070 - (branch1) Adding a README.md (2024-03-01 11:57:35 +0000) <Neil Shephard>
* | a14a643 - Adding a LICENSE (2024-03-01 12:00:39 +0000) <Neil Shephard>
|/
* 1bd6bb8 - Initial commit (2024-03-01 11:57:06 +0000) <Neil Shephard>
8. Delete branch1
and branch2
As we’re done with branch1
and branch2
we
can delete them.
BASH
# Delete the two branches
git branch -d branch{1,2}
git logp
* d914fee - (HEAD -> main) Merge branch 'main' into branch2 (2024-03-01 12:02:08 +0000) <Neil Shephard>
|\
| * 7817070 - Adding a README.md (2024-03-01 11:57:35 +0000) <Neil Shephard>
* | a14a643 - Adding a LICENSE (2024-03-01 12:00:39 +0000) <Neil Shephard>
|/
* 1bd6bb8 - Initial commit (2024-03-01 11:57:06 +0000) <Neil Shephard>
Having used git merge
we couldn’t perform a simple
fast-forward because the history of main
now contained
changes that were made on branch1
and so a separate commit
(d914fee
) was made to merge the main
branch
into main
(commits are denoted by *
and so you
can see the commits were made on separate branches). We can see from the
graph that README.md
was added from a separate
branch1
and LICENSE
was added from
branch2
, although after deleting the branches they are no
longer shown by name in the git log --graph
output.
Rebasing
Rebasing moves the point at which the branch diverged from its
original position to another, in this case the HEAD
of the
main
branch. You are changing the base
commit,
hence the name git rebase
.
git rebase
takes a different approach to bringing
branches up-to-date and in effects moves the point at which a branch
diverged from main
rather than merging the changes in.
5. Merge branch1
into main
(equivalent to
making a Pull Request)
Switch back to main
and merge branch1
(this
is equivalent to merging a Pull Request). The file
README.md
now exists on the main
branch.
6. Rebase branch2
onto main
so it includes
the README.md
and the point of divergence is updated
Switch to branch2
which has now diverged as it contains
changes of its own and main
contains the changes
made on branch1
. We want to rebase branch2
onto main
so that it appears as if branch2
forked after the changes from branch1
were
merged.
BASH
git switch branch2
git rebase main # Rebase branch2 onto main
git logp
* 12f5202 - (HEAD -> branch2) Adding a LICENSE (2024-03-01 12:19:12 +0000) <Neil Shephard>
* 4e8e933 - (main, branch1) Adding README.md (2024-03-01 12:18:37 +0000) <Neil Shephard>
* 2459609 - Initial commit (2024-03-01 12:18:37 +0000) <Neil Shephard>
7. Merge branch2
into main
We now have the changes from branch1
included in
branch2
by virtue of having rebased onto main
after the changes in branch1
were merged in. If we
switch back to main
we can merge the changes from
branch2
.
BASH
git switch main
git merge branch2
git logp
* 12f5202 - (HEAD -> main, branch2) docs: Adding a LICENSE (2024-03-01 12:19:12 +0000) <Neil Shephard>
* 4e8e933 - (branch1) docs: Adding README.md (2024-03-01 12:18:37 +0000) <Neil Shephard>
* 2459609 - Initial commit (2024-03-01 12:18:37 +0000) <Neil Shephard>
8. Delete branch1
and branch2
As we’re done with branch1
and branch2
we
can delete them.
BASH
git branch -d branch{1,2}
git logp
* 12f5202 - (HEAD -> main) Adding a LICENSE (2024-03-01 12:19:12 +0000) <Neil Shephard>
* 4e8e933 - Adding README.md (2024-03-01 12:18:37 +0000) <Neil Shephard>
* 2459609 - Initial commit (2024-03-01 12:18:37 +0000) <Neil Shephard>
As you can see the history of the main
branch is now
linear.
Challenge 1: Diverging Branches
In your pairs bring the square-root
branch up-to-date
and incorporate the changes that have been merged into main
from the zero-division
branch and then create a Pull
Request to merge the updated square-root
changes into
main
on GitHub, review it and merge it.
The person who has been working on the square-root
issue/branch will be at the helm for this, but work together to come up
with a solution. You can use either of the two strategies
git merge
or git rebase
to do this.
The first thing to do is make sure main
is up-to-date
and has the changes that have been merged from the
zero-division
branch locally.
Then you can switch branches to the square-root
branch
and merge the main branch in.
You can now push the changes that are on the square-root
branch to GitHub and make a Pull Request for approval
The first thing to do is make sure main
is up-to-date
and has the changes that have been merged from the
zero-division
branch locally.
Then you can switch branches to the square-root
branch
and merge and rebase onto main.
You can now push the changes that are on the square-root
branch to GitHub and make a Pull Request for approval
Oh no I’ve got a merge conflict
Both the git merge
and git rebase
strategies in the worked examples and the python-maths
repositories you worked through in the challenge were fairly painless
because none of the changes that were made touched the same files. In
real-life things are often likely to be a bit more messy and when you
want to update your diverged branch you will often find that files you
have been working on have been modified and merged into
main
by others. This results in a “merge conflict” where
Git can not determine which lines are required and therefore requires
manual intervention.
If you have undertaken the Git & GitHub Through GitKraken - From Zero to Hero! course you will have encountered merge conflicts when working through the “Python Calculator” exercise and have some idea of how to resolve them. We will however now go through resolving the issue when updating diverged branches.
2. Create branch1
and add a README.md
Again we add a README.md
but this time we make two
commits to it, adding an extra line.
4. Create branch2
and add a README.md
We now set ourselves up for a conflict by creating a
README.md
on branch2
, knowing full well that
such a file already exists on branch1
. We put different
text into it.
5. Merge branch1
into main
Merge branch1
into main
. The
README.md
has the text from branch1
. As we are
done with this branch we can delete it now.
6. Switch to branch2
and add another line to
README.md
Switch back to branch2
and add another line to
README.md
, stage and commit it. The history now shows that
we have two commits on this branch after the “Initial commit”.
BASH
# Switch to branch2 add more to `README.md` and rebase
git switch branch2
echo "Lets add another commit to make things messier" >> README.md
git add README.md
git commit -m "Bulking out README.md with more information"
git logp
* bce21bd - (HEAD -> branch2) Bulking out README.md with more information (2024-03-01 13:26:01 +0000) <Neil Shephard>
* 29b2e32 - This repo needs a README.md (2024-03-01 13:23:16 +0000) <Neil Shephard>
* 57e68aa - Initial commit (2024-03-01 13:20:14 +0000) <Neil Shephard>
6. Rebase branch2
onto main
We now want to update branch2
by rebasing onto
main
so that we have the new changes from main
(i.e. those merged from branch1
). In this instance though
we know both branch1
and branch2
have
modified the file README.md
and so we expect to get a
conflict and sure enough we do.
BASH
git rebase main
Auto-merging README.md
CONFLICT (add/add): Merge conflict in README.md
error: could not apply fcfe2db... This repo needs a README.md
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
Recorded preimage for 'README.md'
Could not apply fcfe2db... This repo needs a README.md
Oh dear we have, as expected, encountered the dreaded “merge
conflict” as both branch1
and branch2
made
changes to README.md
. Lets take a look at what the file now
looks like.
BASH
cat README.md
<<<<<<< HEAD
# Just a test
Lets add another line in a separate commit
=======
# Just a test
But we're creating a merge conlict
>>>>>>> 29b2e32 (This repo needs a README.md)
Here HEAD
refers to the branch that is being merged in
(main
) which contains the changes we made on
branch1
and merged into main
. The text that
this refers to is delimited by <<<<<<<
and =======
and is # Just a test
and
Lets add another line in a separate commit
. The commit
(fcfe2db
) on branch2
which added two
lines (although technically its 4 since we also included blank lines)
then follows and is delimited by =======
and
>>>>>>>
and includes the message.
We are given some useful information as to what we could do and there are three options.
Resolve all conflicts manually, mark them as resolved with "git add/rm <conflicted files>", then run "git rebase --continue".
You can instead skip this commit: run "git rebase --skip".
To abort and get back to the state before "git rebase", run "git rebase --abort".
These are really useful messages telling us how we can proceed. In
this instance we want to take option 1, so we should open the
README.md
and edit it to leave it in the state we want the
file to be in.
7. Resolve the conflict
You can use the nano
editor to open the file with
nano README.md
. Edit it to look like this
You can use Ctrl+k
to remove a whole line at once. Save
the file and return to the command prompt (in nano
this is
Ctrl+O
then Ctrl+X
).
Callout
nano
is a
simple text editor found on most GNU/Linux and OSX systems that is quick
and easy to use. A useful bookmark to help whilst developing the muscle
memory for the commands is the nano
shortcuts cheatsheet.
It is possible that your system may use a different editor than
nano
by default, e.g. vim
. It does not matter
which text editor you use to edit and save the files and if you are
comfortable using this then that is not a problem.
8. Add the conflicted file and continue with rebase
You can now continue with the advice and add the conflicted files back to Git and continue with the rebase.
BASH
git add README.md
git rebase --continue
Recorded resolution for 'README.md'.
[detached HEAD d041adb] This repo needs a README.md
1 file changed, 4 insertions(+)
Auto-merging README.md
CONFLICT (content): Merge conflict in README.md
error: could not apply 84a1592... Bulking out README.md with more information
hint: Resolve all conflicts manually, mark them as resolved with
hint: "git add/rm <conflicted_files>", then run "git rebase --continue".
hint: You can instead skip this commit: run "git rebase --skip".
hint: To abort and get back to the state before "git rebase", run "git rebase --abort".
error: could not parse conflict hunks in 'README.md'
Could not apply 84a1592... Bulking out README.md with more information
Hang on, we just resolved the merge conflict why are we being told
there is another? Well the first conflict with commit
fcfe2db
was resolved and we are told as much in the line
Recorded resolution for 'README.md'
, however there is now a
conflict between that and commit 84a1592
. We get the same
advice so lets take a look at the state of README.md
BASH
# Just a test
Lets add another line in a separate commit
But we're creating a merge conflict
<<<<<<< HEAD
>>>>>>> fcfe2db (This repo needs a README.md)
=======
Lets add another commit to make things messier
>>>>>>> 84a1592 (Bulking out README.md with more information)
Here we can see its the second line that we added to
README.md
under branch2
that read
Lets add another commit to make things messier
that is
causing the problem. Its not in the main
branch on which we
are rebasing so Git doesn’t know whether it should be and we have to
manually resolve this. Edit the file so that it looks like the
following.
9. Add the conflicted file and continue with the second stage of the rebase
Then add the conflicted file and continue with the rebase.
BASH
git add README.md
git rebase --continue
[detached HEAD 0ccfe91] Bulking out README.md with more information
1 file changed, 1 insertion(+), 1 deletion(-)
Successfully rebased and updated refs/heads/branch2.
We are told that the rebase has been successful and
branch2
now contains all commits from main
(which includes those merged from branch1
). If we look at
the contents of README.md
it contains all of the lines we
added to both branches as that is how we chose to resolve the conflicts
manually.
BASH
cat README.md
# Just a test
Lets add another line in a separate commit
But we're creating a merge conflict
Lets add another commit to make things messier
The history/graph is linear now and shows that branch2
is two commits ahead of main
.
BASH
git logp
* 0ccfe91 - (HEAD -> branch2) Bulking out README.md with more information (2024-03-01 14:00:57 +0000) <Neil Shephard>
* d041adb - This repo needs a README.md (2024-03-01 13:59:31 +0000) <Neil Shephard>
* 64905e8 - (main) Ooops, missed a line from the README.md (2024-03-01 13:56:35 +0000) <Neil Shephard>
* e68485d - Adding README.md (2024-03-01 13:55:50 +0000) <Neil Shephard>
* dec5385 - Initial commit (2024-03-01 13:55:50 +0000) <Neil Shephard>
Callout
You may be wondering why when performing git rebase
it
mentions git merge
. This is because a rebase will
sequentially merge all commits from the branch you are rebasing onto, in
this case main
, into the HEAD
of your current
checked out branch (branch2
).
Repeating yourself
You had to resolve two merge conflicts here, if the history you are merging has a lot of commits you may end up solving the same merge conflict repeatedly. There is a way to avoid this though.
Callout
Bringing a diverged branch up-to-date can get very messy and confusing if there is a large amount of divergence. The best strategy to avoid this complication is two fold.
- Break work down into small chunks and regularly merge them into
main
. - If this can not be avoided or lots of others are making changes you
should
git merge
orgit rebase
your feature branch ontomain
frequently.
You may encounter this situation and find that you are repeatedly
resolving the same conflict as you want the finer grained control over
git rebase
and one option is to
git rebase --abort
and use git merge
instead
as you only have to resolve the conflicts once, although there may be a
lot of them. One disadvantage of this is it makes it look like the
commits stem from you and so many people prefer the rebase strategy.
Help is at hand though if you find you are repeatedly being asked to resolve the same conflict as you progress through a rebase in the form of rerere which stands for “reuse recorded resolution” and causes Git to remember how it has resolved merge conflicts at a given point and the next time it is encountered it will use the solution from the first instance.
You can enable this in your global configuration, which is covered in greater detail in the next episode, with the following.
If you only wish to use this strategy on some repositories you can apply it to your local configuration from within the working directory.
You can of course enable globally and disable locally as local configuration variables take precedence over global.
Challenge 2: Merge Conflicts
You have now merged both the Zero Division and Square Root features
into your main
branch. In order to gain experience of
resolving merge conflicts the branch
origin/ns-rse/merge-conflict
exists with some of these
changes already in place.
In your pairs work through the tasks of resolving these conflicts.
- Create a new branch
resolve-merge-conflict
. - Merge the
origin/ns-rse/merge-conflict
branch intoresolve-merge-conflict
. - Look at the file you are told there are conflicts with and resolve
them, you should remove the conflict delimiters
(
<<<<<<< HEAD
/=======
/>>>>>>> origin/ns-rse/merge-conflict
) and select just one of the changes to retain.
The first thing to do is make sure main
is up-to-date
and has the changes that have been merged from the
zero-division
branch locally.
BASH
cd ~/work/git/hub/ns-rse/python-maths
git switch main
git pull
git switch -c resolve-merge-conflict
git merge origin/ns-rse/merge-conflict
You should, hopefully, see some merge conflicts being reported.
BASH
Auto-merging tests/test_arithmetic.py
CONFLICT (content): Merge conflict in tests/test_arithmetic.py
Recorded preimage for 'tests/test_arithmetic.py'
Automatic merge failed; fix conflicts and then commit the result.
…and if we look at the tests/test_arithmetic.py
it shows
the following conflicts.
PYTHON
<<<<<<< HEAD
def test_divide_zero_division_exception() -> None:
"""Test that a ZeroDivisionError is raised by the divide() function."""
with pytest.raises(ZeroDivisionError):
arithmetic.divide(2, 0)
||||||| cdd8fcc
=======
def test_divide_zero_division_exception() -> None:
"""Test that a ZeroDivisionError is raised by the divide() function."""
with pytest.raises(ZeroDivisionError):
arithmetic.divide(10, 0)
>>>>>>> origin/ns-rse/merge-conflict
The ns-rse/merge-conflict
uses
arithmetic.divide(10, 0)
whilst the function added in the
earlier task uses arithmetic.divide(2, 0)
. Select one to
use (it doesn’t matter which) and tidy up so it looks like the
following.
Merge or Rebase
Arguments rage online between experienced users as to whether you
should git merge
or git rebase
it can often be
a matter of preference and you should agree within your team which
strategy to use and stick with it.
However it is worth noting that if you git merge
your
changes from the main
branch into your feature branch when
you come to merge your feature branch into main
via a Pull
Request then the git diff
will show all changes for commits
that have been merged into main
since your feature branch
was made and not just the changes you have made in your feature branch
(i.e. the commits that have already been merged into main
also appear in your pull request). This can make reviewing pull requests
considerably harder and is a good case for using git rebase
to keep your feature branches up-to-date when you know they have
diverged.
Key Points
- Branches can become outdated as work progresses
- Branches can be brought up-to-date with either
git merge
orgit rebase
.