XClose

Research Software Engineering Summer School

Home
Menu

Branches

NOTE: using bash/git commands is not fully supported on jupyterlite yet (due to single thread/process restriction), and the cells below might error out on the browser (jupyterlite) version of this notebook

Branches are incredibly important to why git is cool and powerful.

They are an easy and cheap way of making a second version of your software, which you work on in parallel, and pull in your changes when you are ready.

Setting up somewhere to work

In [1]:
%%bash
rm -rf learning_git/git_example # Just in case it's left over from a previous class; you won't need this
mkdir -p learning_git/git_example
cd learning_git/git_example

We need to move this Jupyter notebook's current directory as well.

In [2]:
import os
top_dir = os.getcwd()
top_dir
Out[2]:
'/home/runner/work/rsd-summerschool/rsd-summerschool/ch00git'
In [3]:
git_dir = os.path.join(top_dir, 'learning_git')
git_dir
Out[3]:
'/home/runner/work/rsd-summerschool/rsd-summerschool/ch00git/learning_git'
In [4]:
working_dir=os.path.join(git_dir, 'git_example')
In [5]:
os.chdir(working_dir)

Configuring Git with your name and email

We should quickly configure Git to know our name and email address:

In [6]:
%%bash
git config --global user.name "Lancelot the Brave"
git config --global user.email "l.brave@spamalot.uk"

Additionally, it's also a good idea to define what's the name of the default branch when we create a repository:

In [7]:
%%bash
git config --global init.defaultBranch main

Historically, the default branch was named master. Nowadays, the community and most of the hosting sites have changed the default (read about this change in GitHub and Gitlab.

Initialising the repository

Now, we will tell Git to track the content of this folder as a git "repository".

In [8]:
%%bash
pwd # Note where we are standing-- MAKE SURE YOU INITIALISE THE RIGHT FOLDER
git init
/home/runner/work/rsd-summerschool/rsd-summerschool/ch00git/learning_git/git_example
Initialized empty Git repository in /home/runner/work/rsd-summerschool/rsd-summerschool/ch00git/learning_git/git_example/.git/

Adding a new remote to your repository

In [9]:
%%bash
git remote add origin git@github.com:UCL/github-example.git

Working on the main branch

Let's quickly add, commit, and push a file to the main branch of our repository for comparisons later.

In [10]:
%%writefile Wales.md
Mountains In Wales
==================

* Tryfan
* Yr Wyddfa
Writing Wales.md
In [11]:
%%bash
ls
Wales.md
In [12]:
%%bash
git add Wales.md
git commit -m "Add wales"
[main (root-commit) 2e01052] Add wales
 1 file changed, 5 insertions(+)
 create mode 100644 Wales.md
In [13]:
%%bash
git push -uf origin main # You shouldn't need the extra `f` switch. We use it here to force the push and rewrite that repository.
      #You should copy the instructions from YOUR repository.
git@github.com: Permission denied (publickey).
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
---------------------------------------------------------------------------
CalledProcessError                        Traceback (most recent call last)
Cell In[13], line 1
----> 1 get_ipython().run_cell_magic('bash', '', "git push -uf origin main # You shouldn't need the extra `f` switch. We use it here to force the push and rewrite that repository.\n      #You should copy the instructions from YOUR repository.\n")

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/site-packages/IPython/core/interactiveshell.py:2541, in InteractiveShell.run_cell_magic(self, magic_name, line, cell)
   2539 with self.builtin_trap:
   2540     args = (magic_arg_s, cell)
-> 2541     result = fn(*args, **kwargs)
   2543 # The code below prevents the output from being displayed
   2544 # when using magics with decorator @output_can_be_silenced
   2545 # when the last Python token in the expression is a ';'.
   2546 if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False):

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/site-packages/IPython/core/magics/script.py:155, in ScriptMagics._make_script_magic.<locals>.named_script_magic(line, cell)
    153 else:
    154     line = script
--> 155 return self.shebang(line, cell)

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/site-packages/IPython/core/magics/script.py:315, in ScriptMagics.shebang(self, line, cell)
    310 if args.raise_error and p.returncode != 0:
    311     # If we get here and p.returncode is still None, we must have
    312     # killed it but not yet seen its return code. We don't wait for it,
    313     # in case it's stuck in uninterruptible sleep. -9 = SIGKILL
    314     rc = p.returncode or -9
--> 315     raise CalledProcessError(rc, cell)

CalledProcessError: Command 'b"git push -uf origin main # You shouldn't need the extra `f` switch. We use it here to force the push and rewrite that repository.\n      #You should copy the instructions from YOUR repository.\n"' returned non-zero exit status 128.

Now we can look at what branches exists and what branch we are on.

In [14]:
%%bash
git branch # Tell me what branches exist
* main

And create and switch/checkout to a new branch:

In [15]:
%%bash
git switch -c experiment # Make a new branch (use instead `checkout -b` if you have a version of git older than 2.23)
Switched to a new branch 'experiment'
In [16]:
%%bash
git branch
* experiment
  main

Let's overwrite, add, and commit Wales.md on our new branch.

In [17]:
%%writefile Wales.md
Mountains In Wales
==================

* Pen y Fan
* Tryfan
* Snowdon
* Glyder Fawr
* Fan y Big
* Cadair Idris
Overwriting Wales.md
In [18]:
%%bash
git add Wales.md
git commit -m "Add Cadair Idris"
[experiment 36ead50] Add Cadair Idris
 1 file changed, 5 insertions(+), 1 deletion(-)

And compare the file on main and experiment.

In [19]:
%%bash
git switch main # Switch to an existing branch (use `checkout` if you are using git older than 2.23)
Switched to branch 'main'
In [20]:
%%bash
cat Wales.md
Mountains In Wales
==================

* Tryfan
* Yr Wyddfa
In [21]:
%%bash
git switch experiment
Switched to branch 'experiment'
In [22]:
cat Wales.md
Mountains In Wales
==================

* Pen y Fan
* Tryfan
* Snowdon
* Glyder Fawr
* Fan y Big
* Cadair Idris

The file exists in 2 different branches with different content!

Publishing branches

To let the server know there's a new branch use:

In [23]:
%%bash
git push -u origin experiment
git@github.com: Permission denied (publickey).
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
---------------------------------------------------------------------------
CalledProcessError                        Traceback (most recent call last)
Cell In[23], line 1
----> 1 get_ipython().run_cell_magic('bash', '', 'git push -u origin experiment\n')

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/site-packages/IPython/core/interactiveshell.py:2541, in InteractiveShell.run_cell_magic(self, magic_name, line, cell)
   2539 with self.builtin_trap:
   2540     args = (magic_arg_s, cell)
-> 2541     result = fn(*args, **kwargs)
   2543 # The code below prevents the output from being displayed
   2544 # when using magics with decorator @output_can_be_silenced
   2545 # when the last Python token in the expression is a ';'.
   2546 if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False):

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/site-packages/IPython/core/magics/script.py:155, in ScriptMagics._make_script_magic.<locals>.named_script_magic(line, cell)
    153 else:
    154     line = script
--> 155 return self.shebang(line, cell)

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/site-packages/IPython/core/magics/script.py:315, in ScriptMagics.shebang(self, line, cell)
    310 if args.raise_error and p.returncode != 0:
    311     # If we get here and p.returncode is still None, we must have
    312     # killed it but not yet seen its return code. We don't wait for it,
    313     # in case it's stuck in uninterruptible sleep. -9 = SIGKILL
    314     rc = p.returncode or -9
--> 315     raise CalledProcessError(rc, cell)

CalledProcessError: Command 'b'git push -u origin experiment\n'' returned non-zero exit status 128.

We use --set-upstream origin (Abbreviation -u) to tell git that this branch should be pushed to and pulled from origin per default.

If you are following along, you should be able to see your branch in the list of branches in GitHub.

Once you've used git push -u once, you can push new changes to the branch with just a git push.

If others checkout your repository, they will be able to do git switch experiment to see your branch content, and collaborate with you in the branch.

In [24]:
%%bash
git branch -r

Local branches can be, but do not have to be, connected to remote branches They are said to "track" remote branches. push -u sets up the tracking relationship. You can see the remote branch for each of your local branches if you ask for "verbose" output from git branch:

In [25]:
%%bash
git branch -vv
* experiment 36ead50 Add Cadair Idris
  main       2e01052 Add wales

Find out what is on a branch

In addition to using git diff to compare to the state of a branch, you can use git log to look at lists of commits which are in a branch and haven't been merged yet.

In [26]:
%%bash
git log main..experiment
commit 36ead50686fe5d6bf92ce065b6dbbfd4612860db
Author: Lancelot the Brave <l.brave@spamalot.uk>
Date:   Tue Jan 14 01:32:40 2025 +0000

    Add Cadair Idris

Git uses various symbols to refer to sets of commits. The double dot A..B means "ancestor of B and not ancestor of A"

So in a purely linear sequence, it does what you'd expect.

In [27]:
%%bash
git log --graph --oneline HEAD~9..HEAD~5
fatal: ambiguous argument 'HEAD~9..HEAD~5': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'
---------------------------------------------------------------------------
CalledProcessError                        Traceback (most recent call last)
Cell In[27], line 1
----> 1 get_ipython().run_cell_magic('bash', '', 'git log --graph --oneline HEAD~9..HEAD~5\n')

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/site-packages/IPython/core/interactiveshell.py:2541, in InteractiveShell.run_cell_magic(self, magic_name, line, cell)
   2539 with self.builtin_trap:
   2540     args = (magic_arg_s, cell)
-> 2541     result = fn(*args, **kwargs)
   2543 # The code below prevents the output from being displayed
   2544 # when using magics with decorator @output_can_be_silenced
   2545 # when the last Python token in the expression is a ';'.
   2546 if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False):

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/site-packages/IPython/core/magics/script.py:155, in ScriptMagics._make_script_magic.<locals>.named_script_magic(line, cell)
    153 else:
    154     line = script
--> 155 return self.shebang(line, cell)

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/site-packages/IPython/core/magics/script.py:315, in ScriptMagics.shebang(self, line, cell)
    310 if args.raise_error and p.returncode != 0:
    311     # If we get here and p.returncode is still None, we must have
    312     # killed it but not yet seen its return code. We don't wait for it,
    313     # in case it's stuck in uninterruptible sleep. -9 = SIGKILL
    314     rc = p.returncode or -9
--> 315     raise CalledProcessError(rc, cell)

CalledProcessError: Command 'b'git log --graph --oneline HEAD~9..HEAD~5\n'' returned non-zero exit status 128.

But in cases where a history has branches, the definition in terms of ancestors is important.

In [28]:
%%bash
git log --graph --oneline HEAD~5..HEAD
fatal: ambiguous argument 'HEAD~5..HEAD': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'
---------------------------------------------------------------------------
CalledProcessError                        Traceback (most recent call last)
Cell In[28], line 1
----> 1 get_ipython().run_cell_magic('bash', '', 'git log --graph --oneline HEAD~5..HEAD\n')

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/site-packages/IPython/core/interactiveshell.py:2541, in InteractiveShell.run_cell_magic(self, magic_name, line, cell)
   2539 with self.builtin_trap:
   2540     args = (magic_arg_s, cell)
-> 2541     result = fn(*args, **kwargs)
   2543 # The code below prevents the output from being displayed
   2544 # when using magics with decorator @output_can_be_silenced
   2545 # when the last Python token in the expression is a ';'.
   2546 if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False):

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/site-packages/IPython/core/magics/script.py:155, in ScriptMagics._make_script_magic.<locals>.named_script_magic(line, cell)
    153 else:
    154     line = script
--> 155 return self.shebang(line, cell)

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/site-packages/IPython/core/magics/script.py:315, in ScriptMagics.shebang(self, line, cell)
    310 if args.raise_error and p.returncode != 0:
    311     # If we get here and p.returncode is still None, we must have
    312     # killed it but not yet seen its return code. We don't wait for it,
    313     # in case it's stuck in uninterruptible sleep. -9 = SIGKILL
    314     rc = p.returncode or -9
--> 315     raise CalledProcessError(rc, cell)

CalledProcessError: Command 'b'git log --graph --oneline HEAD~5..HEAD\n'' returned non-zero exit status 128.

If there are changes on both sides, like this:

In [29]:
%%bash
git switch main
Switched to branch 'main'
In [30]:
%%writefile Scotland.md
Mountains In Scotland
==================

* Ben Eighe
* Cairngorm
* Aonach Eagach
Writing Scotland.md
In [31]:
%%bash
git diff Scotland.md
In [32]:
%%bash
git add Scotland.md
git commit -m "Commit Aonach onto main branch"
[main 4d38f5c] Commit Aonach onto main branch
 1 file changed, 6 insertions(+)
 create mode 100644 Scotland.md

Then this notation is useful to show the content of what's on what branch:

In [33]:
%%bash
git log --left-right --oneline main...experiment
< 4d38f5c Commit Aonach onto main branch
> 36ead50 Add Cadair Idris

Three dots means "everything which is not a common ancestor" of the two commits, i.e. the differences between them.

Merging branches

We can merge branches, and just as we would pull in remote changes, there may or may not be conflicts.

In [34]:
%%bash
git branch
git merge experiment
  experiment
* main
Merge made by the 'ort' strategy.
 Wales.md | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)
In [35]:
%%bash
git log --graph --oneline HEAD~3..HEAD
fatal: ambiguous argument 'HEAD~3..HEAD': unknown revision or path not in the working tree.
Use '--' to separate paths from revisions, like this:
'git <command> [<revision>...] -- [<file>...]'
---------------------------------------------------------------------------
CalledProcessError                        Traceback (most recent call last)
Cell In[35], line 1
----> 1 get_ipython().run_cell_magic('bash', '', 'git log --graph --oneline HEAD~3..HEAD\n')

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/site-packages/IPython/core/interactiveshell.py:2541, in InteractiveShell.run_cell_magic(self, magic_name, line, cell)
   2539 with self.builtin_trap:
   2540     args = (magic_arg_s, cell)
-> 2541     result = fn(*args, **kwargs)
   2543 # The code below prevents the output from being displayed
   2544 # when using magics with decorator @output_can_be_silenced
   2545 # when the last Python token in the expression is a ';'.
   2546 if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False):

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/site-packages/IPython/core/magics/script.py:155, in ScriptMagics._make_script_magic.<locals>.named_script_magic(line, cell)
    153 else:
    154     line = script
--> 155 return self.shebang(line, cell)

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/site-packages/IPython/core/magics/script.py:315, in ScriptMagics.shebang(self, line, cell)
    310 if args.raise_error and p.returncode != 0:
    311     # If we get here and p.returncode is still None, we must have
    312     # killed it but not yet seen its return code. We don't wait for it,
    313     # in case it's stuck in uninterruptible sleep. -9 = SIGKILL
    314     rc = p.returncode or -9
--> 315     raise CalledProcessError(rc, cell)

CalledProcessError: Command 'b'git log --graph --oneline HEAD~3..HEAD\n'' returned non-zero exit status 128.

Cleaning up after a branch

In [36]:
%%bash
git branch
  experiment
* main
In [37]:
%%bash
git branch -d experiment
Deleted branch experiment (was 36ead50).
In [38]:
%%bash
git branch
* main
In [39]:
%%bash
git branch --remote
In [40]:
%%bash
git push --delete origin experiment 
# Remove remote branch 
# - also can use github interface
git@github.com: Permission denied (publickey).
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
---------------------------------------------------------------------------
CalledProcessError                        Traceback (most recent call last)
Cell In[40], line 1
----> 1 get_ipython().run_cell_magic('bash', '', 'git push --delete origin experiment \n# Remove remote branch \n# - also can use github interface\n')

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/site-packages/IPython/core/interactiveshell.py:2541, in InteractiveShell.run_cell_magic(self, magic_name, line, cell)
   2539 with self.builtin_trap:
   2540     args = (magic_arg_s, cell)
-> 2541     result = fn(*args, **kwargs)
   2543 # The code below prevents the output from being displayed
   2544 # when using magics with decorator @output_can_be_silenced
   2545 # when the last Python token in the expression is a ';'.
   2546 if getattr(fn, magic.MAGIC_OUTPUT_CAN_BE_SILENCED, False):

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/site-packages/IPython/core/magics/script.py:155, in ScriptMagics._make_script_magic.<locals>.named_script_magic(line, cell)
    153 else:
    154     line = script
--> 155 return self.shebang(line, cell)

File /opt/hostedtoolcache/Python/3.12.8/x64/lib/python3.12/site-packages/IPython/core/magics/script.py:315, in ScriptMagics.shebang(self, line, cell)
    310 if args.raise_error and p.returncode != 0:
    311     # If we get here and p.returncode is still None, we must have
    312     # killed it but not yet seen its return code. We don't wait for it,
    313     # in case it's stuck in uninterruptible sleep. -9 = SIGKILL
    314     rc = p.returncode or -9
--> 315     raise CalledProcessError(rc, cell)

CalledProcessError: Command 'b'git push --delete origin experiment \n# Remove remote branch \n# - also can use github interface\n'' returned non-zero exit status 128.
In [41]:
%%bash
git branch --remote

A good branch strategy

  • A develop or main branch: for general new code - (the cutting edge version of your software)
  • feature branches: for specific new ideas. Normally branched out from main.
  • release branches: when you share code with users. A particular moment of the develop process that it's considered stable.
    • Useful for including security and bug patches once it's been released.
  • A production branch: code used for active work. Normally it's the same than the latest release.

Grab changes from a branch

Make some changes on one branch, switch back to another, and use:

git checkout <branch> <path>

to quickly grab a file from one branch into another. This will create a copy of the file as it exists in <branch> into your current branch, overwriting it if it already existed. For example, if you have been experimenting in a new branch but want to undo all your changes to a particular file (that is, restore the file to its version in the main branch), you can do that with:

git checkout main test_file

Using git checkout with a path takes the content of files. To grab the content of a specific commit from another branch, and apply it as a patch to your branch, use:

git cherry-pick <commit>