Alex headshot

AlBlue’s Blog

Macs, Modularity and More

Git Tip of the Week: Tracking Branches

2011, git, gtotw, tip

This week’s Git Tip of the Week is about configuring what happens when you pull. You can subscribe to the feed if you want to receive new instalments automatically.


Last week I wrote about the behaviour of pulling tracked branches; this week, it’s worth taking a dive in to find out what a tracked branch is.

When you initially use Git, you learn that to update items from master involves a git pull (or git fetch). Both of these reach out to the remote repository and get content that you’re interested in, with the git pull variant doing either a merge or a rebase as appropriate.

But how does Git know what to pull when you invoke git pull? Where should it pull it from? What makes a branch you have checked out locally differ from one you have pulled from a remote repository?

Remotes and Refspecs

Firstly, a (local) git repository can have many remotes. Each remote is a name of a repository on a remote end, which corresponds to a URL and a refspec. (In fact, remotes can have a second URL; one is used for fetching, whilst the other is used for pushing – this is used to permit anonymous fetches but authenticated pushes.) You need to specify, when fetching and pulling, what repository you’re talking about. For remote repositories, this will default to origin if not specified

You can specify what the refspec is when interacting with a remote repository. This is the set of branches that will be updated if you interact with that repository. This is normally of the form refs/heads/master:refs/remotes/origin/master, where refs/heads are the pointers to your local branches, and refs/remotes are the remote branches.

An optional + prefix on fetch refspecs indicates whether or not to fetch non fast-forward commits automatically. And whilst you can’t have partial wildcards (like refs/for/qa*) you can have sub paths (like refs/for/qa/*). You can also use the reference HEAD to refer to the commit that the current branch is on as a source for the refspec, which can be useful for pushes.

Tracked branches

However, each branch also has the concept of what it is tracking. As well as the branche(es) that will be affected by a fetch/pull/push, tracking says which branch is upstream of which.

Normally, branches checked out of a remote repository are automatically set up as tracking branches. If you check out EGit, you’ll get a master branch that tracks refs/remotes/egit/master (or origin if you didn’t specify a default repository identifier). Any changes you pull into your master come (by default) from EGit’s master.

However, what if you wanted to spin off another branch for experimental purposes, and keep that updated? If you do git checkout -b experimental, it diverges from your local master at that point in time. You either need to pull changes through master and then rebase, or remember where your merge point was.

Instead, you can set up your experimental branch to track another one. This means you can fetch and pull, as if you were pulling from a remote repository, and consume changes from the ongoing branch moves. This is useful if you have a long-running UAT branch which needs to be refreshed periodically from a moving target; setting it up as a tracked branch means that the only thing you need to do is git pull, and you’re up to date.

So, how do you set up a branch for tracking? Well, when you check out a branch from a remote master, it gets set up automatically. In fact, all a tracked branch is is one that’s explicitly mentioned in the .git/config file, since it lists what its remote is and where to merge from:


$ git clone upstream clone
Cloning into clone...
done.
$ cd clone
$ tail .git/config
[branch "master"]
	remote = origin
	merge = refs/heads/master

The way to read this is that master is a local branch, which tracks refs/heads/master on the remote origin. Any pulls that happen on master will result in a merge (or rebase) from refs/heads/master.

What if we wanted to set up our experimental branch? If we just do git checkout -b experimental, it won’t be tracked:


$ git checkout -b experimental
Switched to a new branch 'experimental'
$ grep branch .git/config 
[branch "master"]

We can flag it as tracked using the --track option of git checkout (or its shorter -t alias):


$ git checkout master
Switched to branch 'master'
$ git branch -d experimental
Deleted branch experimental (was 4a3fa88).
$ git checkout --track -b experimental
$ tail -3 .git/config 
[branch "experimental"]
	remote = .
	merge = refs/heads/master
$ git pull
From .
 * branch            master     -> FETCH_HEAD
Already up-to-date.

Hang on, what’s the remote = . doing in here? Well, that’s a special short hand meaning this repository, much like it means this directory in filesystem access. What we have here is experimental tracking master, and not origin/master; in other words, it’s a local branch tracking another local branch. There are times when this is useful, but what if you want to track the remote one directly instead of having to pull through a local copy?


$ git checkout master
Switched to branch 'master'
$ git branch -d experimental
Deleted branch experimental (was 4a3fa88).
$ git checkout -b experimental origin/master
Branch experimental set up to track remote branch master from origin.
Switched to a new branch 'experimental'
$ tail -3 .git/config
[branch "experimental"]
	remote = origin
	merge = refs/heads/master

Now we have a branch experimental which is tracking origin/master. When we have an update in the upstream repository, and do a pull, we see it updating both master, and also experimental:


$ git pull
remote: Counting objects: 3, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 2 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (2/2), done.
From upstream
   4a3fa88..55eb534  master     -> origin/master
Updating 4a3fa88..55eb534
Fast-forward
 0 files changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 third

The update shows origin/master being updated to the new value. The subsequent step is the updating and fast-forward of the local experimental branch. But what of the local master branch?


$ git log --oneline experimental
55eb534 Third
4a3fa88 Second
ff8536c Start
$ git log --oneline master
4a3fa88 Second
ff8536c Start

So although both experimental and master are tracking the same upstream branch, they can be updated and processed independently. This is useful when you want to advance the state of one branch (perhaps for experimentation purposes) but don’t want to change the local state of a branch.

It’s possible to add upstream tracking information to an existing local branch after the fact, in recent versions of git. If we’d checked out the experimental branch as in the first step, and didn’t want to delete/re-create it (perhaps because we’d made some local changes) then you can add it afterwards:


$ git checkout master
Your branch is behind 'origin/master' by 1 commit, and can be fast-forwarded.
$ git checkout -b experimental2
$ git checkout -b experimental2
Switched to a new branch 'experimental2'
$ git branch --set-upstream experimental2 origin/master
Branch experimental2 set up to track remote branch master from origin.
$ tail -3 .git/config 
[branch "experimental2"]
	remote = origin
	merge = refs/heads/master

So even if you have existing branches, it’s possible to wire them up to be tracking branches after the fact. You can also use this if you want to change which branch you’re tracking (say, swapping a local branch for a remote one or vice versa) by re-running the command.

It’s also worth mentioning that there is a --no-track option of git checkout, which can be used to prevent the tracking of branches upon checkout if that’s desired. This is sometimes useful if you are consuming a feature or bugfix branch and you don’t want/need to pull from it in the future.

Lastly, all of this is configured with the branch.autosetupmerge config option. If this option is false, then branches are never tracked by default. If the option is true, then branches are tracked if they are remote, and not tracked if they are local. If the option is always, then branches are always set up as tracked branches, regardless of whether they are local or remote. These effectively specify the defaults, but they can be overridden on a branch-by-branch basis using the --no-track and --track command line flags of the git checkout or git branch commands.


Come back next week for another instalment in the Git Tip of the Week series.