MP

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:

meme -- Git CLI Users Magit Users -- Look what they need to mimic a fraction of our power

Created: 2022-01-18

Tags: emacs, git, magit