A Better Pull Request Workflow with git @{push} branches

tl;dr if you use Pull Requests and you're not using @{push}, then you're probably missing out.

The Problem

If you use GitHub or GitLab to make Pull / Merge Requests, you probably have a workflow that looks something like this:

  1. Check out a new branch called e.g. my-feature based on the upstream branch:
    git checkout -b my-feature upstream/master
  2. Make changes, run tests, commit changes:
    git commit
  3. Push to your fork remote and set the local branch to track the remote one:
    git push -u fork
  4. Raise a PR in the GitHub/GitLab UI.
  5. Rebase your PR branch as needed: git pull --rebase upstream/master && git push --force-with-lease
  6. PR is merged in the GitHub UI.
  7. Delete the remote and local branches:
    git push fork :my-feature && git branch -d my-feature

If you've used this for a while, you might notice there's a bit of a problem here, in that there are really two different upstream branches you care about for a Pull Request branch:

  1. The branch you want to pull or rebase from, and eventually merge into (i.e. origin/master).
  2. The branch you want to push to (i.e. fork/my-feature).

By setting @{upstream} to the branch you want to push to, you lose the ability to easily have the branch also track the branch it will end up being merged into.

The Solution

Luckily git has a built-in solution for this:

  1. Use @{upstream} (a.k.a. @{u} for the branch the PR will be merged into (i.e. origin/master).
  2. Use @{push} for the branch you want to push to (i.e. fork/my-feature).

I discovered these via Magit, which has a great docs section on The Two Remotes.

Configuration

You probably want to set the default push location for branches to your fork remote. I call my remotes up and fork, but you may use upstream and origin or similar. If you use different remote names, change the below commands to match.

Git Pull

When you check out a Pull Request branch, set the upstream to the branch you're going to merge into:

git checkout -b my-feature up/master

Now things like git pull --rebase will just work, as @{upstream} is set to the right branch.

Git Push

Add this to your Git Config (git config --global --edit). This will cause git push to always default to pushing to a branch with the same name on your fork remote.

# Push to the fork remote unless a pushRemote is specified.
[remote]
  pushDefault = fork
# Only push the branch I'm on (made less dangerous by remote.pushDefault).
[push]
  default = current

If you want to override the push remote for a single branch, you can set that for that branch:

# Make branch `my-upstream-pusher` push to up/my-upstream-pusher rather than fork/my-upstream-pusher.
git config branch.my-upstream-pusher.pushRemote up

This section is optional, but will make things much easier. It also makes things safer as you will now never accidentally push to the upstream tracking branch by mistake.

Git Rebase

You can now rebase on both the @{upstream} and the @{push} branch as you wish. Note that git rebase defaults to rebasing on the @{upstream}, which is normally what you want for PRs.

Git Status

This is not currently built-in, see this StackOverflow answer to integrate it as a custom alias, and to use it in your shell prompt.

$ git s  # Alias for `git status` that also shows push status.
On branch my-feature
Your branch and 'up/master' have diverged,
and have 1 and 3 different commits each, respectively.
  (use "git pull" to merge the remote branch into yours)

nothing to commit, working tree clean
Your branch is 2 commit(s) ahead and 1 commit(s) behind push branch fork/my-feature.

This makes it easy to see that this branch needs to be pulled and rebased from @{upstream}, and also pushed to @{push}.

Prune merged branches

You can delete the remote branches in the GitHub/GitLab UI. If you use Refined Github this is done for you.

See this StackOverflow answer for deleting the local branches.

Other commands

You can use @{upstream} and @{push} anywhere you would normally use a git ref, for example:

# Undo all changes since you last pushed your PR branch.
git reset --hard @{push}

# Checkout a file as it is in the upstream branch.
git checkout @{u} path/to/file

Optimising

There are plenty of optimizations to be done here, see my git config for my current settings.