Skip to content

Branches

Branching in Git allows us to "diverge" development lines, so that commits need not be based on the latest version.

Scenario

Imagine you are adding a feature that will takes weeks to complete, but you suddenly need to add an urgent bug fix. How to ship the bugfix without mixing the incomplete feature?

Answer: When we work on a feature (especially a large one), start it on a new branch. Only merge it to the main branch when it is ready.

When we initalize a repo, we are on a branch called "master". When we create a commit, the commit is added to the master branch.

Info

Technically speaking, a branch is just a reference to a commit. This reference is updated when a commit is created while this branch is active.

Creating branches

  • To split the current branch into another branch, use the command git checkout -b $NEW_BRANCH. This will create the branch $NEW_BRANCH that ends at the current commit checked out.
  • Like checking out commits, we can checkout an existing branch using the git checkout $BRANCH command.
  • These commands will only create the branches locally. To make the branch appear on GitHub, you need to set the upstream of the branch to a branch on GitHub with the same name:

    $ git push -u origin $BRANCH_NAME
    

    Then the remote origin/$BRANCH_NAME is called the "upstream branch" for the local $BRANCH_NAME, and the local $BRANCH_NAME is called the "tracking branch" for the remote origin/$BRANCH_NAME.

What does an upstream branch really mean?

When we type git push or git pull without specifying a remote and branch, Git uses the upstream branch as the dstination/source to push to/pull from.

A branch is automatically created on GitHub when you try to push to it.

Try it yourself

Let's create an example repository with a few dummy commits (some output omitted):

$ git init

$ echo a > file.txt

$ git add file.txt

$ git commit -m "Initial commit"
[master (root-commit) 5eac39c] Initial commit

$ echo b > file.txt

## -a stages all tracked files.
## It is a convenient shorthand for add-and-commit
## if there are no untracked files.
$ git commit -am "Second commit"
[master 47743d3] Second commit

You can see that master is now pointing to 47743d3. Let's diverge a new branch called two at the current checkout (47743d3):

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

Then we create a new commit:

$ echo c > file.txt
$ git commit -am "Third commit"
[two 99c64ea] Third commit
 1 file changed, 1 insertion(+), 1 deletion(-)

Let's use git log to see the commits in each branch:

$ git log --oneline two
99c64ea (HEAD -> two) Third commit
47743d3 (master) Second commit
5eac39c Initial commit

You can see that master is still at 47743d3, while two is at 99c64ea.

Let's change back to master and see if the contents of file.txt are reverted:

$ cat file.txt
c

$ git checkout master
Switched to branch 'master'

$ cat file.txt
b

In fact, remote branches are also modelled as branches (which are separate branchs from the tracking branches). You can checkout the latest commit of the remote branch using git checkout origin/$BRANCH_NAME (or git checkout remotes/origin/$BRANCH_NAME to avoid confusion). However, you cannot create commits on a remote branch; you must do it on the remote-tracking branch and push it.

Hint

It is possible to create branch names with a /. For example, it is a convention for some people to use the branch name issues/1234 for a bugfix for issue number 1234. This is useful for grouping branches.

Merging branches

The most powerful feature of Git is that you can merge different branches.

  • A branch can be merged into another branch that shares a common ancestor commit,
  • In a merge, there are two branches called the "base" and the "head".
  • The new changes (since the last common ancestor) in the head branch are merged into the base branch.
  • After the merge, the base branch will contain the changes from both branches,
  • The head branch remains unchanged after the merge.
  • A "merge commit" is created on the base branch, which has two parent commits
  • To perform a merge, first checkout the base commit, then run git merge $HEAD_BRANCH

Try it yourself

Let's setup the scenario (some output omitted):

$ git init

$ echo a > file.txt

$ git add file.txt

$ git commit -am "Initial commit"
[master (root-commit) a8cea08] Initial commit

$ git checkout -b two

$ echo b > two.txt

$ git add two.txt

$ git commit -am "Second commit"
[two 952151e] Second commit

$ ls
file.txt  two.txt

$ git checkout master

$ ls
file.txt

$ echo c > three.txt

$ git add three.txt

$ git commit -am "Added three.txt"
[master 5986012] Added three.txt

$ ls
file.txt  three.txt

We can use the git log --graph --all --oneline command to get a pretty layout of the repo history on all branches:

$ git log --graph --all --oneline
* 5986012 (HEAD -> master) Added three.txt
| * 952151e (two) Second commit
|/
* a8cea08 Initial commit

Now let's merge two into master, i.e. the base branch is master, and the head branch is two.

$ git merge two
Merge made by the 'recursive' strategy.
 two.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 two.txt

This will open an editor to edit your commit message. In case you forgot, here are a few tips:

  • If your default editor is nano, use ^X-Y-<Enter> to save and exit
  • If your default editor is vi/vim, use :x<Enter> to save and exit
    • You are strongly advised to change to another editor if you didn't already know vim
  • For graphical editors, Git should continue merging after you have closed the editor.

Now let's see the state of the repository:

$ ls
file.txt  three.txt  two.txt

$ git log --graph --all --oneline
*   d2d4403 (HEAD -> master) Merge branch 'two'
|\
| * 952151e (two) Second commit
* | 5986012 Added three.txt
|/
* a8cea08 Initial commit

Wow, both three.txt and two.txt are in the same branch!

Do not delete this test repository yet. We will use it again in the "Resolving merge conflicts" section.

Resolving merge conflicts

Sometimes we have changes to the same line on multiple branches. Then reasonably, Git cannot merge them together automatically. Then we have to resolve merge conflicts.

Try it yourself

Continuing the last example. Let's create a branch four from a8cea08:

$ git checkout a8cea08

$ git checkout -b four

And create another commit that also creates two.txt, but with different contents:

$ echo four > two.txt
$ git commit -m "Commit four"
[four ef5d355] Commit four
 1 file changed, 1 insertion(+)
 create mode 100644 two.txt

Now let's try to merge two into four:

$ git merge two
CONFLICT (add/add): Merge conflict in two.txt
Auto-merging two.txt
Automatic merge failed; fix conflicts and then commit the result.

A conflict occurred in two.txt! We can check this with git status:

$ git status
On branch four
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 added:      two.txt

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

Let's look at what happens in two.txt:

$ cat two.txt
<<<<<<< HEAD
four
=======
b
>>>>>>> two

This means that the HEAD (the base branch) has the content four, while the branch two (the head branch) has the content b.

Edit the file until it becomes normal, then type git commit to continue merging.

Rebasing (optional)

Rebase changes the "base" (parent commit) of a commit.

Suppose we have commits 1111111 -> 2222222 -> 3333333. Now we want to insert another commit aaaaaaa (1111111 -> aaaaaaa) between 1111111 and 2222222. We can do this by rebasing 3333333 upon aaaaaaa. After the rebase, we will end up with 1111111 -> aaaaaaa -> 2222222 -> 3333333, where bbbbbbb is almost the same as 2222222 (except it's based on aaaaaaa), and ccccccc is almost the same as 3333333 (except it's based on bbbbbbb). Note that 2222222 and 3333333 are no longer used.

Caution

Do not rebase commits that you have already pushed! Otherwise your local history and remote history will diverge, and you will end up with very troublesome history.

These useful actions are possible with rebasing:

  • Squashing commits together
  • Changing commit messages
    • You can change the last commit message with just git commit --amend -m "New message", but you will need rebase if there are newer commits since then.
  • Changing commit content
  • Deleting a commit from the history
    • It may be possible that hackers recover your deleted commit from the git logs. If you accidentally committed a password, change your password and never use it agan.

To do these, use git rebase -i $LCA, where $LCA is the sha of the last unmodified commit. It allows you to edit a file that determines what actions to do during rebasing.

Furthermore, git pull --rebase is a handy shorthand to rebase unpushed commits upon the remote head. It uses rebase instead of merge when remote and local diverge, so there are fewer useless commits.