Branches

Suppose you are working on a calculator program. You start working on a new multiplication feature, and you have already committed and pushed several times. The code is still WIP (Work In Progress, far from done); it errors when you try to run it, for example. Then someone discovers a bug, and you want to fix it right away. But now you have a problem: you can't work on the bug because you are in the middle of working on multiplication and the code is still WIP.

To prevent this situation, you can make a new branch when you start working on multiplication, and then put all your commits to the new multiplication branch. Commits on the new branch don't affect the rest of your project, and you can start fixing the bug at any time.

Next we go through this process in practice.

The setup

We will work on the following code.

Create calculator.py with this content
import sys

first_number = int(sys.argv[1])
operation = sys.argv[2]
second_number = int(sys.argv[3])

if operation == "+":
    print(first_number + second_number)
elif operation == "-":
    print(first_number + second_number)

In case you are not familiar with Python code, this program is meant to be called from command line, and it uses 3 command line arguments to do a calculation. Like this, for example (use py instead of python3 on Windows):

$ python3 calculator.py 1 + 2
3

$ python3 calculator.py 7 - 3
10

The subtraction doesn't work; this is the bug that we will fix later. Let's commit the code:

$ git status
On branch main
Your branch is up to date with 'origin/main'.

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        calculator.py

nothing added to commit but untracked files present (use "git add" to track)

$ git add calculator.py

$ git status
On branch main
Your branch is up to date with 'origin/main'.

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   calculator.py


$ git commit -m "create calculator.py"
[main 9e639ca] create calculator.py
 1 file changed, 10 insertions(+)
 create mode 100644 calculator.py

$ git status
On branch main
Your branch is ahead of 'origin/main' by 1 commit.
  (use "git push" to publish your local commits)

nothing to commit, working tree clean

Branch basics

When I work with branches, I constantly use the following command to check what's going on, similarly to git status:

$ git log --oneline --graph --all
* 9e639ca (HEAD -> main) create calculator.py
* 7b47314 (origin/main, origin/HEAD) add better description to README
* 50ec14e Initial commit

We already saw --oneline last time. I will explain --graph and --all soon.

By default, there is only one branch. In new GitHub repos, it's called main. GitHub's name for the default branch used to be master, and many projects still have a branch named master instead of a main branch.

Seeing main next to 9e639ca means that 9e639ca is the latest commit on main. On the next line, origin means GitHub and origin/main is GitHub's main branch, so 7b47314 next to it means that the latest commit we pushed is 7b47314.

There is also a notion of current branch, which is often called HEAD. Above we see (HEAD -> main), which means that now the current branch is main.

Let's make a new branch:

$ git checkout -b multiplication
Switched to a new branch 'multiplication'

$ git status
On branch multiplication
nothing to commit, working tree clean

$ git log --oneline --graph --all
* 9e639ca (HEAD -> multiplication, main) create calculator.py
* 7b47314 (origin/main, origin/HEAD) add better description to README
* 50ec14e Initial commit

Here multiplication is a branch name; you can name a branch however you want. The latest commit on the multiplication branch is 9e639ca, same as on main.

We can go back to the main branch with git checkout, but without -b; the -b means that a new branch is created.

$ git checkout main
Switched to branch 'main'
Your branch is ahead of 'origin/main' by 1 commit.
  (use "git push" to publish your local commits)

$ git status
On branch main
Your branch is ahead of 'origin/main' by 1 commit.
  (use "git push" to publish your local commits)

nothing to commit, working tree clean

$ git log --oneline --graph --all
* 9e639ca (HEAD -> main, multiplication) create calculator.py
* 7b47314 (origin/main, origin/HEAD) add better description to README
* 50ec14e Initial commit

$ git checkout multiplication
Switched to branch 'multiplication'

$ git status
On branch multiplication
nothing to commit, working tree clean

$ git log --oneline --graph --all
* 9e639ca (HEAD -> multiplication, main) create calculator.py
* 7b47314 (origin/main, origin/HEAD) add better description to README
* 50ec14e Initial commit

On the multiplication branch, let's create the multiplication code:

Add to end of calculator.py
elif operation == "*":
    print(first_number * second_unmber)

The new multiplication code is buggy, but let's try it and commit it anyway. We need to quote * because it is a special character on the terminal; it is good to know how that works, but I won't explain it here because I want to focus on git.

$ python3 calculator.py 2 "*" 3
Traceback (most recent call last):
  File "/tmp/tmp62hfy1wf/working_dir/reponame/calculator.py", line 12, in <module>
    print(first_number * second_unmber)
NameError: name 'second_unmber' is not defined

$ git add calculator.py

$ git diff --staged
diff --git a/calculator.py b/calculator.py
index 4b57a5f..eabc67c 100644
--- a/calculator.py
+++ b/calculator.py
@@ -8,3 +8,5 @@ if operation == "+":
     print(first_number + second_number)
 elif operation == "-":
     print(first_number + second_number)
+elif operation == "*":
+    print(first_number * second_unmber)

$ git commit -m "multiplication code, not working yet"
[multiplication 2f8a77c] multiplication code, not working yet
 1 file changed, 2 insertions(+)

$ git log --oneline --graph --all
* 2f8a77c (HEAD -> multiplication) multiplication code, not working yet
* 9e639ca (main) create calculator.py
* 7b47314 (origin/main, origin/HEAD) add better description to README
* 50ec14e Initial commit

Note how the main branch was left behind when we committed; our latest commit is only on the multiplication branch, because we did git checkout multiplication before committing.

Let's leave the multiplication branch for now...

$ git checkout main
Switched to branch 'main'
Your branch is ahead of 'origin/main' by 1 commit.
  (use "git push" to publish your local commits)

...and fix the subtraction bug by changing just the last line:

Replace last line of calculator.py with this
    print(first_number - second_number)

Let's commit the fix:

$ git add calculator.py

$ git diff --staged
diff --git a/calculator.py b/calculator.py
index 4b57a5f..28264b8 100644
--- a/calculator.py
+++ b/calculator.py
@@ -7,4 +7,4 @@ second_number = int(sys.argv[3])
 if operation == "+":
     print(first_number + second_number)
 elif operation == "-":
-    print(first_number + second_number)
+    print(first_number - second_number)

$ git commit -m "fix subtraction bug"
[main d60059c] fix subtraction bug
 1 file changed, 1 insertion(+), 1 deletion(-)

$ git status
On branch main
Your branch is ahead of 'origin/main' by 2 commits.
  (use "git push" to publish your local commits)

nothing to commit, working tree clean

$ git log --oneline --graph --all
* d60059c (HEAD -> main) fix subtraction bug
| * 2f8a77c (multiplication) multiplication code, not working yet
|/  
* 9e639ca create calculator.py
* 7b47314 (origin/main, origin/HEAD) add better description to README
* 50ec14e Initial commit

Now you can see what the --graph does: it shows how commits are based on other commits, drawing a Y-shaped graph at left in this case.

Without --all, git hides some branches. For example, if you are on main, you won't see the multiplication branch:

$ git status
On branch main
Your branch is ahead of 'origin/main' by 2 commits.
  (use "git push" to publish your local commits)

nothing to commit, working tree clean

$ git log --oneline --graph
* d60059c (HEAD -> main) fix subtraction bug
* 9e639ca create calculator.py
* 7b47314 (origin/main, origin/HEAD) add better description to README
* 50ec14e Initial commit

I guess this might be useful if you have many unrelated branches and you want to ignore them, but I always use --all.

Git lola

As you can see, git log --oneline --graph --all is a very useful command, but it's a bit long to type. Run this:

$ git config --global alias.lola "log --oneline --graph --all"

Now git lola does the same as git log --oneline --graph --all:

$ git lola
* d60059c (HEAD -> main) fix subtraction bug
| * 2f8a77c (multiplication) multiplication code, not working yet
|/  
* 9e639ca create calculator.py
* 7b47314 (origin/main, origin/HEAD) add better description to README
* 50ec14e Initial commit

You can name the alias however you want, but I named it lola for consistency with this blog post, which I believe is where the command comes from. The commands in the blog post are longer than here because back in 2010, --oneline didn't exist and you needed a combination of several other options instead. Also, it seems like colored output wasn't turned on by default.

Think about "is based on" instead of "is newer than"

Let's go back to the multiplication branch.

$ git checkout multiplication
Switched to branch 'multiplication'

$ git lola
* d60059c (main) fix subtraction bug
| * 2f8a77c (HEAD -> multiplication) multiplication code, not working yet
|/  
* 9e639ca create calculator.py
* 7b47314 (origin/main, origin/HEAD) add better description to README
* 50ec14e Initial commit

Now let's fix the multiplication bug by changing the last line:

Replace last line of calculator.py with this
    print(first_number * second_number)

Let's try it out and then commit the result:

$ python3 calculator.py 2 "*" 3
6

$ git add calculator.py

$ git diff --staged
diff --git a/calculator.py b/calculator.py
index eabc67c..56a6f0e 100644
--- a/calculator.py
+++ b/calculator.py
@@ -9,4 +9,4 @@ if operation == "+":
 elif operation == "-":
     print(first_number + second_number)
 elif operation == "*":
-    print(first_number * second_unmber)
+    print(first_number * second_number)

$ git commit -m "fix multiplication bug"
[multiplication 102c641] fix multiplication bug
 1 file changed, 1 insertion(+), 1 deletion(-)

$ git lola
* 102c641 (HEAD -> multiplication) fix multiplication bug
* 2f8a77c multiplication code, not working yet
| * d60059c (main) fix subtraction bug
|/  
* 9e639ca create calculator.py
* 7b47314 (origin/main, origin/HEAD) add better description to README
* 50ec14e Initial commit

Now the subtraction fix d60059c shows up below the first multiplication commit 2f8a77c, even though the multiplication commit is older. In other words, the latest commit is not always first in the output of our --graph command.

If you really want to, you can ask git to sort by commit time by adding --date-order:

$ git lola --date-order
* 102c641 (HEAD -> multiplication) fix multiplication bug
| * d60059c (main) fix subtraction bug
* | 2f8a77c multiplication code, not working yet
|/  
* 9e639ca create calculator.py
* 7b47314 (origin/main, origin/HEAD) add better description to README
* 50ec14e Initial commit

As you can see, it just looks messier this way, which is probably why this is not the default. I don't use --date-order because I don't actually care about which of two commits on different branches is newer and which is older; I just want to see what commits each branch contains, and how d60059c and 2f8a77c are both based on 9e639ca, for example.

Pushing a branch

Run git push to upload the current branch to GitHub. For example, we are currently on the multiplication branch, so git push will push that branch to GitHub.

$ git status
On branch multiplication
nothing to commit, working tree clean

$ git push
fatal: The current branch multiplication has no upstream branch.
To push the current branch and set the remote as upstream, use

    git push --set-upstream origin multiplication

This error message is confusing, but just copy/paste the command it suggests and run it.

$ git push --set-upstream origin multiplication
Username for 'https://github.com': username
Password for 'https://username@github.com':
Enumerating objects: 12, done.
Counting objects: 100% (12/12), done.
Delta compression using up to 2 threads
Compressing objects: 100% (8/8), done.
Writing objects: 100% (8/8), 5.76 KiB | 5.76 MiB/s, done.
Total 8 (delta 5), reused 0 (delta 0)
remote: Resolving deltas: 100% (5/5), completed with 4 local objects.
remote:
remote: Create a pull request for 'multiplication' on GitHub by visiting:
remote:      https://github.com/username/reponame/pull/new/multiplication
remote:
To https://github.com/username/reponame
 * [new branch]      multiplication -> multiplication
Branch 'multiplication' set up to track remote branch 'multiplication' from 'origin'.

Now Git will remember that you already ran the command it suggested, and git push without anything else after it will work next time. This is branch-specific though, so you need to do the --set-upstream thing once for each branch.

In GitHub, there should be a menu where you can choose a branch and it says main by default. You should now see a multiplication branch in that menu, and if you click it and then open calculator.py, you should see the multiplication code.

The git push output contains a link for creating a pull request. We will use it later.

Merges and merge conflicts

Now we have two versions of the calculator program: the code on main can subtract correctly, and the code on multiplication can multiply.

$ git lola
* 102c641 (HEAD -> multiplication, origin/multiplication) fix multiplication bug
* 2f8a77c multiplication code, not working yet
| * d60059c (main) fix subtraction bug
|/  
* 9e639ca create calculator.py
* 7b47314 (origin/main, origin/HEAD) add better description to README
* 50ec14e Initial commit

Next we want to combine the changes from the multiplication branch into the main branch so that on main, subtraction and multiplication both work, and we no longer need the multiplication branch. This is called merging the multiplication branch into main.

Before merging, you need to go to the branch where you want the result to be:

$ git checkout main
Switched to branch 'main'
Your branch is ahead of 'origin/main' by 2 commits.
  (use "git push" to publish your local commits)

Now we can merge.

$ git merge multiplication
Auto-merging calculator.py
CONFLICT (content): Merge conflict in calculator.py
Automatic merge failed; fix conflicts and then commit the result.

$ git status
On branch main
Your branch is ahead of 'origin/main' by 2 commits.
  (use "git push" to publish your local commits)

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:   calculator.py

no changes added to commit (use "git add" and/or "git commit -a")

Git tried to combine the changes (Auto-merging calculator.py), but it couldn't do it (Automatic merge failed) because the two branches contain conflicting changes (CONFLICT). In these cases, git needs your help.

Open calculator.py in your editor. You should see this:

Content of calculator.py
import sys

first_number = int(sys.argv[1])
operation = sys.argv[2]
second_number = int(sys.argv[3])

if operation == "+":
    print(first_number + second_number)
elif operation == "-":
<<<<<<< HEAD
    print(first_number - second_number)
=======
    print(first_number + second_number)
elif operation == "*":
    print(first_number * second_number)
>>>>>>> multiplication

The code on main (current branch, aka HEAD) is between <<<<<<< and =======. That's the correct subtraction code. The code on multiplication branch is between ======= and >>>>>>>. Now you have to combine it all together: you need to include the * code and make sure that the subtraction code actually subtracts. Like this:

Change the content of calculator.py to this
import sys

first_number = int(sys.argv[1])
operation = sys.argv[2]
second_number = int(sys.argv[3])

if operation == "+":
    print(first_number + second_number)
elif operation == "-":
    print(first_number - second_number)
elif operation == "*":
    print(first_number * second_number)

Let's check whether we combined it correctly:

$ python3 calculator.py 7 - 3
4

$ python3 calculator.py 2 "*" 3
6

Let's ask git status what we should do next:

$ git status
On branch main
Your branch is ahead of 'origin/main' by 2 commits.
  (use "git push" to publish your local commits)

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:   calculator.py

no changes added to commit (use "git add" and/or "git commit -a")

The relevant part is (use "git add <file>..." to mark resolution), which means that once the conflicts are fixed, we should git add the file. Let's do that:

$ git add calculator.py

$ git status
On branch main
Your branch is ahead of 'origin/main' by 2 commits.
  (use "git push" to publish your local commits)

All conflicts fixed but you are still merging.
  (use "git commit" to conclude merge)

Changes to be committed:
        modified:   calculator.py

As git status says, the next step is to commit the result. Do not use -m with merge commits such as this one. Instead, write just git commit, without anything else after it. It will open an editor that already contains a commit message for you; just close the editor by pressing Ctrl+X and you are done.

$ git commit
hint: Waiting for your editor to close the file...
[main 9b66474] Merge branch 'multiplication'

$ git lola
*   9b66474 (HEAD -> main) Merge branch 'multiplication'
|\  
| * 102c641 (origin/multiplication, multiplication) fix multiplication bug
| * 2f8a77c multiplication code, not working yet
* | d60059c fix subtraction bug
|/  
* 9e639ca create calculator.py
* 7b47314 (origin/main, origin/HEAD) add better description to README
* 50ec14e Initial commit

The default commit message, Merge branch 'multiplication', is very recognizable: when people see it in git logs, they immediately know what that commit does.

Finally, we can push all this to GitHub:

$ git push
Enumerating objects: 12, done.
Counting objects: 100% (12/12), done.
Delta compression using up to 2 threads
Compressing objects: 100% (8/8), done.
Writing objects: 100% (8/8), 5.76 KiB | 5.76 MiB/s, done.
Total 8 (delta 5), reused 0 (delta 0)
To https://github.com/username/reponame
   7b47314..9b66474  main -> main

$ git lola
*   9b66474 (HEAD -> main, origin/main, origin/HEAD) Merge branch 'multiplication'
|\  
| * 102c641 (origin/multiplication, multiplication) fix multiplication bug
| * 2f8a77c multiplication code, not working yet
* | d60059c fix subtraction bug
|/  
* 9e639ca create calculator.py
* 7b47314 add better description to README
* 50ec14e Initial commit

Deleting a branch

Because we have now merged multiplication into main, we no longer need the multiplication branch and we can delete it.

Let's start with git branch -D:

$ git lola
*   9b66474 (HEAD -> main, origin/main, origin/HEAD) Merge branch 'multiplication'
|\  
| * 102c641 (origin/multiplication, multiplication) fix multiplication bug
| * 2f8a77c multiplication code, not working yet
* | d60059c fix subtraction bug
|/  
* 9e639ca create calculator.py
* 7b47314 add better description to README
* 50ec14e Initial commit

$ git branch -D multiplication
Deleted branch multiplication (was 102c641).

$ git lola
*   9b66474 (HEAD -> main, origin/main, origin/HEAD) Merge branch 'multiplication'
|\  
| * 102c641 (origin/multiplication) fix multiplication bug
| * 2f8a77c multiplication code, not working yet
* | d60059c fix subtraction bug
|/  
* 9e639ca create calculator.py
* 7b47314 add better description to README
* 50ec14e Initial commit

We can see that origin/multiplication wasn't deleted, so the branch is still on GitHub. Let's delete it from GitHub too:

$ git push --delete origin multiplication
To https://github.com/username/reponame
 - [deleted]         multiplication

$ git lola
*   9b66474 (HEAD -> main, origin/main, origin/HEAD) Merge branch 'multiplication'
|\  
| * 102c641 fix multiplication bug
| * 2f8a77c multiplication code, not working yet
* | d60059c fix subtraction bug
|/  
* 9e639ca create calculator.py
* 7b47314 add better description to README
* 50ec14e Initial commit

For whatever reason, you need to specify origin in the git push command. As usual, origin means GitHub.

It is also possible to delete a branch by clicking something in GitHub, but then it will keep showing up in git lola because Git on your computer won't know that it is gone. To fix this, you can run git fetch --prune and then look at git lola again.