Thursday, February 17, 2011

Keeping Local Git Branches Up To Date

Because I spend most of my time working on the master branch of the PostgreSQL git repository, I prefer to work with just a single clone.  The PostgreSQL wiki page Committing with Git describes several ways of using multiple clones and/or git-new-workdir, but my personal preference is to just use one clone.  Most of the time, I keep the main branch checked out, but every once in a while I check out one of the back-branches to look at something, or to back-patch.  (If you're unfamiliar with the PostgreSQL workflow, note that we do not merge into our official branches; we always rebase, so that there are no merge commits in our official repository.  You may or may not like this workflow, but it works for us.)

One small annoyance is that "git pull" doesn't leave my clone in the state I want.  Say I have the master branch checked out.  "git pull" will update all of my remote tracking branches, but it will only update the local branch that I currently have checked out.  This is annoying, first of all because if I later type "git log REL9_0_STABLE" I'll only get the commits since the last time I checked out and pulled that branch, rather than as I intended the latest state of the upstream, and secondly because it leads to spurious griping when I later do "git push": it complains that the old branches can't be pushed because it wouldn't be a fast-forward merge.  This is of course a little silly: since my branch tip is an ancestor of the tracking branch, it would be more reasonable to conclude that I haven't updated it than to imagine I meant to clobber the origin.

My first response to this problem was to write a little script that checked out each back-branch in turn and did git pull in it.  This is kind of annoying, though.  The older branches aren't updated all that often, and checking out a new branch takes several seconds, and we've got five of them (we used to have eight, but we're down to five).  So after playing around with it some more, I came up with this:

#!/bin/sh

git fetch || exit 1
for b in REL8_{2,3,4}_STABLE REL9_0_STABLE master; do
    revs=`git rev-list $b...origin/$b | wc -l`
    echo branch $b has $revs revisions different from origin
    if [ $revs -gt 0 ]; then
        git checkout $b || exit 1
        git rebase origin/$b || exit 1
    fi
done
git checkout master || exit 1

This is a lot faster because (1) it only runs "git fetch" once, whereas repeatedly running "git pull" rechecks the origin server every time and (2) it only checks out and rebases the branches where there's some difference between the local tracking branch and the origin.

I think it would be nicer still if there were a way to do this without actually checking out the back-branches that need to be updated.  If the given local branch tip is an ancestor of the remote tracking branch, I'd like "git pull" to move the local branch head up to the remote branch head automatically, without requiring a checkout.  I suppose I could write a script to manually update the files in .git/refs/heads, but a less magical approach would be nicer.

1 comment:

  1. I use a script with an outcome quite similar to yours, although more flexible (it does not have to list the branches you are following). It has, of course, its subtleties (i.e. it would break horribly if remote and local branches are named differently, or if you use something other than origin/), but it does the work fine – I do use merge. So, hoping the formatting will not be mangled beyond recognition:

    #!/usr/bin/ruby

    def current_branch
    `git branch` =~ /\* (.+)$/
    current = $1
    if current == '(no branch)'
    raise RuntimeError, 'Not currently following a branch - You\'d lose your location!'
    end
    end

    def origin_branches
    `git show-branch -a --topics`.map { |br|
    br =~ /\[([^\[]+)\]/ && $1
    }.map { |br|
    br =~ /^origin\/(.+)/ ? $1 : nil
    }.select {|br| br}
    end

    def merge_branch(branch)
    system('git checkout %s' % branch)
    system('git merge origin/%s' % branch)
    end

    system('git fetch')
    current = current_branch
    branches = origin_branches
    branches.each { |br| merge_branch(br) }
    system('git checkout %s' % current)


    PS: For the next three days, you can get it with the proper format at:

    http://paste.debian.net/108082/

    ReplyDelete