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:
- Check out a new branch called e.g.
my-feature
based on the upstream branch:
git checkout -b my-feature up/master
- Make changes, run tests, commit changes:
git commit
- Push to your fork remote and set the local branch to track the remote one:
git push -u fork
- Raise a PR in the GitHub/GitLab UI.
- Rebase your PR branch as needed:
git pull --rebase up/master && git push --force-with-lease
- PR is merged in the GitHub UI.
- 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:
- The branch you want to pull or rebase from, and eventually merge into (i.e.
origin/master
). - 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:
- Use
@{upstream}
(a.k.a.@{u}
for the branch the PR will be merged into (i.e.origin/master
). - 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.
This enables some really neat workflow optimizations (note I don't have to ever specify a remote in the normal course of affairs, which also means that I won't accidentally push to the wrong remote).
My Workflow
Commands used through a PR's lifetime:
VANILLA WORKFLOW | MY WORKFLOW |
---|---|
git switch -c my-feature -t up/master | g su my-feature |
make changes | make changes |
git commit -a | g ca |
git push -u fork then raise PR in GitHub UI | g ppr |
(time passes, need to rebase) | (time passes, need to rebase) |
git fetch --all | (automatic on cd) |
git rebase up/master | g rb |
git push --force-with-lease fork | g pf |
merge PR in the UI | merge PR in the UI |
git branch --delete my-feature | (auto-pruned by my update script) |
git push --delete fork my-feature | (auto-deleted by Refined Github) |
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.