Jason Codes

gup: A friendlier git pull --rebase

Posted (updated )

By now most git users would have heard about rebasing your local commits on top of the remote branch HEAD before you git push them rather than merging to prevent the proliferation of useless same branch merge commits like "Merge remote branch 'origin/topic' into topic".

If one is using git pull, the rebasing can be accomplished by using git pull --rebase instead. This essentially changes git pull from doing git fetch && git merge $TRACKING_BRANCH to git fetch && git rebase $TRACKING_BRANCH.

There's still the inconvenience of having to stash any uncommitted changes before a rebase. If you don't, you'll get messages like "refusing to pull with rebase: your working tree is not up-to-date". This results in a fetch, stash, rebase, pop dance which gets tiring. I think we can do better.

Update 2011-01-11: Another thing to watch out for when using git pull --rebase is merge commits. You cannot preserve merges when rebasing using git pull as it does not let you pass in the --preserve-merges option. This means you could end up losing valuable merge commits. Glen Maddern has a great post on Rebasing Merge Commits in Git over on the Envato Notes blog which covers this in more detail. The good news is that my gup script already handles rebasing merge commits by passing the -p (--preserve-merges) option to git rebase.

Update 2011-09-16: My gup function has had a number of tweaks since I first posted. I removed the quiet flag from git stash pop as late versions of git seem to silence the error when pop fails. The other significant change is that gup will now explicitly fast-forward if it can rather than rebasing. The rest of the changes are minor (e.g. refactoring for style).

Update 2011-09-16: I now prefer git-up when available as it has nicer output and it also has an option to show if one needs to bundle. I still use the gup function as it's handy on foreign systems where I don't have my normal Ruby setup and I like the command name better :).

function gup
{
  # subshell for `set -e` and `trap`
  (
    set -e # fail immediately if there's a problem


    # use `git-up` if installed
    if type git-up > /dev/null 2>&1
    then
      exec git-up
    fi


    # fetch upstream changes
    git fetch


    BRANCH=$(git symbolic-ref -q HEAD)
    BRANCH=${BRANCH##refs/heads/}
    BRANCH=${BRANCH:-HEAD}


    if [ -z "$(git config branch.$BRANCH.remote)" -o -z "$(git config branch.$BRANCH.merge)" ]
    then
      echo "\"$BRANCH\" is not a tracking branch." >&2
      exit 1
    fi


    # create a temp file for capturing command output
    TEMPFILE="`mktemp -t gup.XXXXXX`"
    trap '{ rm -f "$TEMPFILE"; }' EXIT


    # if we're behind upstream, we need to update
    if git status | grep "# Your branch" > "$TEMPFILE"
    then


      # extract tracking branch from message
      UPSTREAM=$(cat "$TEMPFILE" | cut -d "'" -f 2)
      if [ -z "$UPSTREAM" ]
      then
        echo Could not detect upstream branch >&2
        exit 1
      fi


      # can we fast-forward?
      CAN_FF=1
      grep -q "can be fast-forwarded" "$TEMPFILE" || CAN_FF=0


      # stash any uncommitted changes
      git stash | tee "$TEMPFILE"
      [ "${PIPESTATUS[0]}" -eq 0 ] || exit 1


      # take note if anything was stashed
      HAVE_STASH=0
      grep -q "No local changes" "$TEMPFILE" || HAVE_STASH=1


      if [ "$CAN_FF" -ne 0 ]
      then
        # if nothing has changed locally, just fast foward.
        git merge --ff "$UPSTREAM"
      else
        # rebase our changes on top of upstream, but keep any merges
        git rebase -p "$UPSTREAM"
      fi


      # restore any stashed changes
      if [ "$HAVE_STASH" -ne 0 ]
      then
        git stash pop
      fi


    fi


  )
}

Throw the following into your shell's startup script. I keep the script in my dotfiles as it's much easier to bring it along to new machines. Alternatively you could remove the function wrapper and save it as a standalone script in ~/bin.

Once setup you can pull changes for the current tracking branch, rebase any unpushed commits on top of any new ones from upstream, all while preserving anything you have uncommitted (via git stash) with a single command: gup.