Diverging Branches
Last updated on 2025-02-13 | Edit this page
Overview
Questions
- What are branches?
- How do we use branches in git effectively?
- How can I check out other people’s 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.
python-maths
:
ns-rse/2-square-root
is now behind the main
branch which has incorporated the changes from
ns-rse/1-zero-division
.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.
Sometimes this won’t be a problem, the two features/issues are
completely independent and have not made changes to the same file and it
would be possible to merge the branches into main
without
any merge conflicts because neither have modified the same files in the
same location.
That will not always be 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. That is in fact the situation we
have with our ns-rse/2-square-root
branch, it conflicts
with main
because of the changes we merged from the
ns-rse/1-zero-division
branch.
There are two approaches to solving this: merging
(git merge
) and rebasing (git rebase
).
Merging
python-maths
: ns-rse/2-square-root
is now
behind the main
branch which has incorporated the changes
from ns-rse/1-zero-division
.main
,
which has already had the work in ns-rse/1-zero-division
merged into it, is merged into ns-rse/2-square-root
.
Development is completed on ns-rse/2-square-root
and the
feature merged into main
.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 are in
the incorrect hierarchy and it updates the branch pointer
(HEAD
) to match the merged branch.
Typically, as in this example, the main
branch contains
work from someone else’s branch and we want to incorporate those changes
in the other 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 log --pretty="%h %ad (%cr) %x09 %an : %s"
* 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 log --pretty="%h %ad (%cr) %x09 %an : %s"
* 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 log --pretty="%h %ad (%cr) %x09 %an : %s"
* 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 branch2
(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
.
python-maths
: ns-rse/2-square-root
is now
behind the main
branch which has incorporated the changes
from ns-rse/1-zero-division
.main
which includes
ns-rse/1-zero-division
. Two more commits are made and
ns-rse/2-square-root
is then merged into
main
.git rebase
takes a different approach to bringing
branches up-to-date and in effect 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 --pretty="%h %ad (%cr) %x09 %an : %s"
* 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 --pretty="%h %ad (%cr) %x09 %an : %s"
* 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 --pretty="%h %ad (%cr) %x09 %an : %s"
* 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 should have received a message about conflicts in the
tests/test_arithmetic.py
file.
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 should have received a message about conflicts in the
tests/test_arithmetic.py
file.
BASH
git rebase main
Auto-merging pythonmaths/arithmetic.py
Auto-merging tests/test_arithmetic.py
CONFLICT (content): Merge conflict in tests/test_arithmetic.py
error: could not apply bb4294a... feature: adds square root function
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".
hint: Disable this message with "git config set advice.mergeConflict false"
Recorded preimage for 'tests/test_arithmetic.py'
Could not apply bb4294a... feature: adds square root function
Oh no I’ve got a merge conflict
Both the git merge
and git rebase
strategies in the examples we went through worked because none of the
changes that were made touched the same files. In the Challenge though
there was a conflict because both commits had added code to the end of
tests/test_arithmetic.py
and Git didn’t know which should
be kept. This situation is known as “conflict” and resolving it 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 --pretty="%h %ad (%cr) %x09 %an : %s"
* 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
).
Nano
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 it’s 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. It’s 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 --pretty="%h %ad (%cr) %x09 %an : %s"
* 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 only 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 and wonder why you keep on getting asked to resolve the same conflict. There is a way to avoid this though.
Avoid divergence if possible
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: Diverging Branches
You didn’t complete the merge/rebase in the previous challenge because of the merge conflict. In your pairs do so now.
If we look at the conflict in tests/test_arithmetic.py
we can see at the bottom the sections that are conflicting. They are
de-marked by <<<<<<<
,
======-
and >>>>>>>
and
the HEAD
and <commit-hash>
show which
branches they originate from.
The exact solution will depend on the strategy you have chosen to bring the branch up-to-date.
BASH
cat tests/test/arithmetic.py
<<<<<<< HEAD
@pytest.mark.parametrize(
("x", "target"),
[
pytest.param(4, 2, id="square root of 4"),
pytest.param(9, 3.0, id="square root of 9"),
pytest.param(25, 5.0, id="square root of 25"),
pytest.param(2, 1.4142135623730951, id="square root of 2"),
],
)
def test_square_root(x: int | float, target: int | float) -> None:
"""Test the square_root() function."""
pytest.approx(arithmetic.square_root(x), target)
||||||| 80b95c0
=======
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)
>>>>>>> main
We know that we want both of these changes so we can edit the file and remove the delimiters to leave it looking like the following.
Then you can switch branches to the square-root
branch
and merge the main branch in.
BASH
@pytest.mark.parametrize(
("x", "target"),
[
pytest.param(4, 2, id="square root of 4"),
pytest.param(9, 3.0, id="square root of 9"),
pytest.param(25, 5.0, id="square root of 25"),
pytest.param(2, 1.4142135623730951, id="square root of 2"),
],
)
def test_square_root(x: int | float, target: int | float) -> None:
"""Test the square_root() function."""
pytest.approx(arithmetic.square_root(x), target)
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)
If you check the status you see that you are in the middle of a merge
and that tests/test_arithmetic.py
has been modified by
both
.
BASH
git status
On branch ns-rse/2-square-root
Your branch is up to date with 'origin/ns-rse/2-square-root'.
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Changes to be committed:
modified: pythonmaths/arithmetic.py
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: tests/test_arithmetic.py
You now need to stage your changes and commit them.
You can now create a pull request, assign it for review and merge it.
BASH
cat tests/test/arithmetic.py
<<<<<<< HEAD
@pytest.mark.parametrize(
("x", "target"),
[
pytest.param(4, 2, id="square root of 4"),
pytest.param(9, 3.0, id="square root of 9"),
pytest.param(25, 5.0, id="square root of 25"),
pytest.param(2, 1.4142135623730951, id="square root of 2"),
],
)
def test_square_root(x: int | float, target: int | float) -> None:
"""Test the square_root() function."""
pytest.approx(arithmetic.square_root(x), target)
||||||| 80b95c0
=======
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)
>>>>>>> main
We know that we want both of these changes so we can edit the file and remove the delimiters to leave it looking like the following.
Then you can switch branches to the square-root
branch
and merge the main branch in.
BASH
@pytest.mark.parametrize(
("x", "target"),
[
pytest.param(4, 2, id="square root of 4"),
pytest.param(9, 3.0, id="square root of 9"),
pytest.param(25, 5.0, id="square root of 25"),
pytest.param(2, 1.4142135623730951, id="square root of 2"),
],
)
def test_square_root(x: int | float, target: int | float) -> None:
"""Test the square_root() function."""
pytest.approx(arithmetic.square_root(x), target)
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)
If you check the status you see that you are in the middle of a merge
and that tests/test_arithmetic.py
has been modified by
both
.
BASH
git status
On branch ns-rse/2-square-root
Your branch is up to date with 'origin/ns-rse/2-square-root'.
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Changes to be committed:
modified: pythonmaths/arithmetic.py
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified: tests/test_arithmetic.py
You now need to stage your changes and commit them.
You can now create a pull request, assign it for review and merge it.
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
.