Set Your Upstream to the True Upstream
It's hard to miss the fact that I'm an emacs user. My wife even got me a custom-made shirt a while back that says in big block letters, "You can do THAT with emacs?!", which became her favorite phrase when I first started talking all the time about the various neat workflows that emacs makes possible.
One of emacs' "killer apps" is without a doubt Magit, a keyboard-driven, textual, highly discoverable git interface. When I first started using emacs, I used it primarily for org-mode. However, it wasn't long before I tried out Magit, having heard such great things about it, and it quickly became the main way I interacted with version control. Ultimately, it was Magit that led me to getting my emacs fully set up for doing all of my day-to-day programming: because it was so easy to visit the files shown in diffs, I very quickly felt the need to be able to effectively edit those files as part of my standard write-commit-push workflow.
One of the things you run into pretty early in Magit is that you're guided into having a different upstream remote/branch than your default push remote/branch, and that Magit makes it super easy to have pull commands automatically bring in changes from the upstream, while push commands go to the push remote. There's even a section in the manual describing the rationale for this.
At least for me, having my upstream separate from my push target is a major quality of life improvement in git. Pull operations sensibly operate on the shared main branch, status operations show me useful information about where I am relative to that branch, and push operations automatically go to my feature branch. Of course, not everyone uses emacs, and not even everyone who uses emacs uses Magit, so the question becomes how can we enable this workflow in regular, command-line git?
If you're coming from a similar place as I was, you probably always set up your branches so that you pull and push to the same target, since the command-line interface generally guides you in that direction, so commands like these all, implicitly or explicitly, set the upstream to the same branch you're pushing to:
# create mybranch locally and track origin/mybranch
git checkout --track origin/mybranch
# create mybranch locally and track origin/mybranch
git checkout -b mybranch origin/branch
# create mybranch, then set track origin/mybranch
git checkout -b mybranch
git branch --set-upstream-to origin/mybranch
# push the current branch to remote, setting the upstream
git push --set-upstream origin mybranch
If you're using git 2.0+, which is fairly likely since it was released in 2014,
doing any of the above will make all git fetch
, git pull
, and git push
operations default to fetching, pulling, and pushing to the configured upstream,
respectively. This situation is nice if you're the only person working on your
remote or if you are pushing directly to the main branch where others are also
pushing changes: in the former case, you will almost never git pull
anyway,
and in the latter case, you want your pulls to come from the same branch you're
pushing to.
However, it's probably a more common workflow to have a main branch to which
feature branches get merged via PRs, or to be working on a fork (i.e. a separate
remote from the main origin). In this case, you want to be able to easily pull
in changes from the upstream into your feature branch. However, the default of
setting your upstream to your push target makes this a bit more complicated than
it should be. For example, you need to remember to pass arguments to git pull
:
git pull origin main
You can of course set your upstream
to the actual upstream, like so:
git checkout -b mybranch
git branch --set-upstream-to origin/mybranch
# alternatively
git checkout -b mybranch origin/mybranch
Having your upstream set to the "true" upstream is a real improvement. It means
that your pulls are easier and more useful, since git pull
will just do the
right thing and pull in changes from the origin. You also get more useful
information when checking the status of your branch. For example, git status
will tell you when the upstream has new commits that you might want to pull:
Switched to branch 'mybranch'
Your branch and 'origin/main have diverged,
and have 4 and 1 different commits each, respectively.
(use "git pull" to merge the remote branch into yours)
And running git branch -vv
actually has meaningful information about how far
behind your various branches are from the upstream:
* mybranch 76aef7c7 [origin/main: ahead 1, behind 3] chore: do some chore
* mybranch2 f821e0ad [origin/main: ahead 4, behind 1] feat: add some feature
The only problem at this point is that git will fail if you try to run a git push
with no other arguments:
fatal: The upstream branch of your current branch does not match
the name of your current branch. To push to the upstream branch
on the remote, use
git push origin HEAD:dev
To push to the branch of the same name on the remote, use
git push origin HEAD
To choose either option permanently, see push.default in 'git help config'.
You would at this point need to always remember to specify git push origin mybranch
, which is honestly not so bad, but luckily it's not the end of the
story.
We can get out of this situation where there's no way to have the defaults for
git pull
and git push
behave the way we want, by setting the value of
push.default
to current
. You can do this by running:
git config --global push.default current
Or adding the following lines to your ~/.gitconfig
:
[push]
default = current
The current
option for push.default
is described in the git docs
as follows:
current
- push the current branch to update a branch with the same name on the receiving end. Works in both central and non-central workflows.
The default option in git 2.0+ is simple
, which attempts to push the branch to
its upstream branch, but refuses if the upstream name is different from the
local one. Git notes that this is the safest option for beginners, and that is
probably true, but I assume that if you have opinions about git remotes, you're
probably not a beginner.
Anyway, once you've set your push.default
to current
, you can now use git pull
and git push
to pull and push from the upstream and your own feature
branch, respectively. If you are working on a fork, you will need to do one
extra step of setting your pushRemote
to your own remote, e.g.:
git config branch.mybranch.pushRemote myremote
Realistically, it's probably easier to set this up for the entire repo rather than having to remember to do it for each branch:
git config remote.pushDefault myremote
And there you go! Once you've set push.default
to current
and potentially
set your push remote if you're working on a fork, setting up a new branch is as
simple as:
git checkout -b mybranch origin/dev
Your branch will now track upstream and push to your own feature branch and/or fork!
Of course, like many things with git, this is probably all a little more complicated than it needs to be, and it is not particularly discoverable given the default behavior. Magit guides us in the right direction and generally makes doing the right thing easy, while also being an excellent lens into discovering more about git. I always used to be a snob about using only the command-line for git interaction, but I won't lie: I've learned more about git by virtue of Magit's excellent transient (popup) API than I have from years of using the CLI. With that in mind, I hope you'll forgive me this tongue-in-cheek meme: