Chapter 5. Branching

Now that you know how to create a repository and commit to a single branch, it’s time to learn about using multiple branches. Branches allow different versions of the same content to evolve independently at the same time, while you periodically recombine the contributions from different branches in a process called “merging.” When you switch from one branch to another, Git updates your working tree to reflect the state of the repository content on the tip commit of the new branch.

A typical use for a branch is to work on a new software feature in isolation, without adding it to the main line of development of the project; these are often called “feature” or “topic” branches. You work on the feature branch while developing that feature, and switch to the master branch to work on the main project (which does not yet contain the new feature code). Periodically, you merge master into your feature branch, so you’re working on up-to-date code and notice and resolve any conflicts. When the feature is ready, you do the opposite: merge the feature branch into master, adding the new code to the main version of the project.

Another use for multiple branches is to continue maintenance on older versions of software. When you release version 1.0 of your product, it gets its own branch. Product development continues, but you may need to apply bug fixes or new features to that version even after you’ve released 2.0, for customers who are still using the older version; the 1.0 branch allows you to do that (and git cherry-pick is particularly useful in this case; see “git cherry-pick”).

The manpage gitworkflows(7) presents several branching disciplines that may be directly useful, or give you ideas on how to structure your own projects in other ways. Some of these are used in the development of Git itself.

The Default Branch, master

A new Git repository created with git init has a single branch with the default name master. There is nothing special about this name aside from being used as a default, and you can rename or delete it if you like. The master branch is conventionally used if there is only one branch in a repository, or if there are several but there is a single, clear main line of development.

If you try to use the master branch in a brand-new repository, however, you’ll get perhaps unexpected results; for example:

$ git init
Initialized empty Git repository in /u/res/zork/.git/
$ git log
fatal: bad default revision 'HEAD'

Git could be more helpful here, since getting a “fatal” error with a newly created repository strongly suggests that something is broken. The error message is technically correct, though. Git has initialized the HEAD ref to point to master, making master the current branch. However, a branch name is just a ref pointing to the latest commit on the branch—and there are no commits yet in this new, empty repository, and so there is no master branch ref yet. When you make your first commit, Git will create the master branch with it.

Making a New Branch

The usual way to make a new branch named alvin is:

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

This creates the branch alvin pointing at the current commit, and switches to it. Any existing changes to the index or working tree are preserved and will now be committed to alvin rather than to the previous branch (which is still there). Until one branch progresses, both branches point to the same commit. Until both progress independently, one branch still points to an earlier commit on the other. See Figure 5-1.

The progress of branch names
Figure 5-1. The progress of branch names

You can also specify a commit at which to start the new branch, rather than the current one, for example:

$ git checkout -b simon 9c6a1fad
Switched to a new branch 'simon'

This starts a new branch at the named commit and switches to it. If you have conflicting uncommitted changes, though, you will have to deal with them first. If you want to create the new branch but not switch to it, use git branch simon.

Switching Branches

The usual tool for switching branches is git checkout, of which the -b option given above is just a special case: switching to a branch that doesn’t yet exist is creating a new branch.

The only thing that has to happen to switch branches is to change the HEAD symref to point to the new branch name. The HEAD by definition indicates the branch that you are “on,” and switching to a branch means that you are then “on” that branch. Here, git symbolic-ref HEAD shows the symref (branch name) to which HEAD points:

$ git symbolic-ref HEAD
refs/heads/theodore
$ git checkout simon
Switched to branch 'simon'
$ git symbolic-ref HEAD
refs/heads/simon

Technically, you could update the HEAD ref directly with git update-ref, but this isn’t usually done and would be very confusing by itself; normally, you want your working tree and index to match the new branch tip when you switch branches, taking into account any uncommitted changes you may have. git checkout does all of these things, and more. Suppose you have two branches named master and commander, and you’re currently on master. To switch to commander, simply use:

$ git checkout commander
Switched to branch 'commander'

This attempts to do three things:

  1. Change the HEAD symref to point to the commander branch
  2. Reset the index to match the tip of the new branch
  3. Update the working tree index to match the index (this is called “checking out” the index, which gives the command its name)

If these succeed, then you are now on the commander branch, with an index and working tree that match the tip of that branch. The following are some possible complications.

Uncommitted Changes

Suppose you have uncommitted changes to a tracked file. There are now four versions of the file in play: the two in the tip commits of the master and commander branches, and the two in your working tree and index (one or both of which have been altered, depending on whether you have staged the changes with git add). If the committed versions in the current and destination branches are the same, then Git will preserve your altered versions when switching branches, since they represent the same sets of changes in the new branch as in the old. It reminds you of a modified file foo thus:

$ git checkout commander
M       foo
Switched to branch 'commander'

If the committed versions differ, however, or if the file does not exist at all in the destination branch, then Git warns you and refuses to switch:

$ git checkout commander
error: Your local changes to the following files would
be overwritten by checkout:
       foo
Please, commit your changes or stash them before you
can switch branches. Aborting

stash refers to the git stash command, which lets you conveniently save and restore uncommitted changes; see “git stash”.

Check Out with Merge

git checkout has a --merge (-m) option to help with this case. It performs a three-way merge between your working tree and the new branch, with the current branch as the base; it leaves you on the new branch, with the merge result in the working tree. As with any merge, you may have conflicts to resolve; see “Merge Conflicts”.

Untracked Files

Git ignores untracked files while switching branches, unless the file exists in the target branch; then it aborts, even if the versions in the working tree and destination branch are the same. You can use the --merge option to get around this without having to delete the untracked file, only to have Git restore it a moment later. The merge operation results in the same file, in this case.

Losing Your Head

If you directly check out a specific commit rather than a branch, say with a command like git checkout 520919b0, then Git gives the odd and rather dire-sounding warning that you are now in “detached HEAD mode.” Fear not, Ichabod; all will be well. “Detached HEAD`” simply means that the `HEAD ref now points directly at a commit rather than referring to a particular branch by name. Git operates normally in this mode: you can make commits, and the HEAD ref moves forward as usual. The important thing to remember is that there is no branch tracking this work, so if you switch back to a branch with git checkout branch, you will simply discard any commits you’ve made while in detached HEAD mode: the HEAD ref then points to the branch you’re on, and no ref is left marking the commit you left. Git warns you about this too, along with the commit ID you just left so that you can go back to it if you want. You can give your anonymous branch a name at any time while you’re in detached HEAD mode, with git checkout -b name.

Deleting a Branch

When you ask Git to delete a branch, it simply deletes a pointer: a branch name ref that points to the branch tip. It does not delete the content of the branch, that is, remove from the object database all commits reachable from the pointer; it couldn’t necessarily do that safely even if that were desired, since some of those commits might be part of other branches. To delete the branch simon, then:

$ git branch -d simon
Deleted branch simon (was 6273a3b0).

It may not be so simple, though; you might see this instead:

$ git branch -d simon
error: The branch 'simon' is not fully merged.
If you are sure you want to delete it, run
'git branch -D simon'.

Git is warning that you might lose history by deleting this branch. Even though it would not actually delete any commits right away, some or all of the commits on the branch would become unreachable if they are not part of some other branch as well. You could undo this mistake easily if you noticed it right away, as Git names the commit ID of the branch ref it removes, and it might be in a reflog as well; you could use git checkout -b simon 6273a3b0 to restore the branch. It would get harder if you didn’t notice until later, though, and perhaps impossible if that were after garbage collection had actually deleted the commits in question, and no one else had a copy of them.

For the branch simon to be “fully merged” into another branch, its tip commit must be an ancestor of the other branch’s tip, making the commits in simon a subset of the other branch. This makes it safe to delete simon, since all its commits will remain part of the repository history via the other branch. It must be “fully” merged, because it may have been merged several times already, but now have commits added since the last merge that are not contained in the other branch.

Git doesn’t check every other branch in the repository, though; just two:

  1. The current branch (HEAD)
  2. The upstream branch, if there is one

The “upstream branch” for simon would usually be origin/simon, referring to a branch in the repository from which this one was cloned, and with which this local simon branch coordinates via the push/pull mechanism. You can list upstream branches with git branch -vv; the upstream for each branch, if any, is listed in square brackets on the right:

$ git branch -vv
* master 8dd6fdc0 [origin/master: ahead 6] find acorns
 simon  6273a3b0 [origin/simon] sing shrilly

If simon is fully merged in the current branch, then Git deletes it with no complaint. If it is not, but it is fully merged in its upstream branch, then Git proceeds with a warning:

$ git branch -d simon
warning: deleting branch 'simon' that has been merged
to 'refs/remotes/origin/simon', but not yet merged to
HEAD.
Deleted branch simon (was 6273a3b0).

Being fully merged in its upstream indicates that the commits in simon have been pushed to the origin repository, so that even if you lose them here, they may at least be saved elsewhere.

In Figure 5-2, simon has been merged into master before, but it and master have diverged since commit 2, and so simon is not now “fully merged” into master. It is fully merged into the upstream branch origin/master, however (see Figure 5-2).

Since Git doesn’t check other branches, it may be safe to delete a branch because you know it is fully merged into another one; you can do this with the -D option as indicated, or switch to that branch first and let Git confirm the fully merged status for you.

"merged" and "fully merged"
Figure 5-2. "merged" and "fully merged"

Deleting the branch from the origin repository is not so obvious:

$ git push origin :simon

This is the general syntax for directly updating a remote ref. In this case, the local object name to the left of the colon is blank, meaning to just delete the remote ref.

When to delete a branch?

You most commonly delete a branch when it is private to you—that is, you created it in your own repository and have never pushed it elsewhere, and you have no further use for it. You can delete a branch from an upstream repository as just shown (assuming you’re allowed to), but the effect of that does not automatically spread to other people coordinating through that repository. Their git pull will not delete their corresponding remote-tracking branch (they would need to use git fetch --prune for that), and any corresponding local downstream branches they’ve created in their own repositories will not be affected in any case. The general principle at work here is that a branch indicates a set of commits that are of interest; once a branch exists in your repository, it’s up to you to decide whether you’re no longer interested. Another person’s action should not unilaterally make that decision for you.

Renaming a Branch

Renaming a local branch is simple:

$ git branch -m old new

There is no direct way to rename the corresponding branch in a remote repository, however; you must separately push the new branch and delete the old one:

$ git push -u origin new
$ git push origin :old

You will need to tell others that you’ve done this, since when they pull they will get the new branch, but they will have to manually delete the old name with git branch -d. “Renaming” a branch is not actually a Git operation per se; git branch -m is just a shortcut for the create/delete routine.