Gittin' efficient with Git Push

At work we undertake two university placement rounds each year. It's always fun to get to teach people things you know and get exciting new ideas and energy into the team from fresh faces.

One of the other benefits of hosting juniors is that they often ask questions you've never thought about or just taken the answer at face value. One such question during onboard related to the semantics about git push. So let's take a look at the current behaviour, and then how we might fix it.


The Problem

Let's say our student is being on-boarded to an existing project and they are being asked to work on an existing branch upstream. That's pretty easy, we get them to do a git pull on the relevant branch.

Now assuming that the developer does not have a local copy of this branch git goes ahead and sets all that up for us making an association between the upstream branch and the local copy it is currently creating.

What happens however if the student being on-boarded is asked to work on a new feature or bugfix branch and nothing exists in the upstream yet?

They create the new branch, perhaps with git flow, and start working. After their first commit they decide to push their changes upstream. But unlike when we pull a branch git doesn't automatically create an upstream version of our branch in the same way that it does with a local one if pulling.

When asked about this behaviour my response was the following:

Just copy that last command in the terminal with the set-upstream bit and it will do it for you.

While that is true and it does work it doesn't really answer the question and that bothered me, so why does that happen?


The Git Push Spec

push.default
Defines the action git push should take if no refspec is explicitly given. Different values are well-suited for specific workflows; for instance, in a purely central workflow (i.e. the fetch source is equal to the push destination), upstream is probably what you want. Possible values are:

nothing - do not push anything (error out) unless a refspec is explicitly given. This is primarily meant for people who want to avoid mistakes by always being explicit.

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.

upstream - push the current branch back to the branch whose changes are usually integrated into the current branch (which is called @{upstream}). This mode only makes sense if you are pushing to the same repository you would normally pull from (i.e. central workflow).

simple - in centralized workflow, work like upstream with an added safety to refuse to push if the upstream branch's name is different from the local one.

As this branch is new and no refspec has been given the default git implementation (which is simple) says that for added safety we refuse to push up the branch without an upstream. It allows beginners to make sure they truly understand what they are doing before they make their change. That's nice and all but after a long time with git it feels inefficient so how can we change this to be useful and what are the caveats?


The Solution

As usual the actual solution here depends very much on your work flow and how you like to use git. I propose a few of the following after testing how they work.

Change push.default

The simplest method here is to change the push.default config parameter to something other than simple. Both current and upstream work but there are a few gotchas to each.

git config push.default current

When using the above command git will happily create / update a branch upstream for us that matches the name of our current local branch. In cases where you do trunk based development, or your developers largely work on their own branches (like we do with git flow) then this is almost universally going to be a great choice.

What happens if we routinely rename our branches though? I work with Jakob, and while Jakob is a good guy (for the sake of this post) he creates terrible branch names. For example imagine the branch name below:

feature/scott-is-the-worst

As this branch is is an outright lie, when I work on this locally I want it to read something else. So on checking it out I set up tracking and change it's name.

git checkout -t -b feature/scott-is-the-best origin/feature/scott-is-the-worst

Now my branch is appropriately named I can work on it and finish whatever needs to be done. With my push.default set to current what happens when I push now? git creates a new upstream branch notifying everyone that I am awesome, but leaves poor Jakob's branch telling everyone I am the worst to be stale. In this case I think he has every right to call me the worst, messing up our nice branching strategy.

Perhaps I could fix this with push.default set to upstream instead?

git config push.default upstream

If we replay the same steps above (for brevity I won't repeat them, expect for the Jakob being mean bit) then we expect I have a renamed branch on my local machine with changes ready to push upstream. What happens now? As the name might suggest we update Jakob's poorly named branch with my changes despite the naming being different. This happens because when we checked out the branch locally and changed it's name git set up tracking of the upstream so it knows which branch it needs to update. Nice!

Now I am done fixing Jakob's branch I need to do some new work. I go and create a new branch of my own to work on.

git flow feature start jakob-is-mostly-alright

I commit all my changes and want to push this upstream. So I do a push only to find that git doesn't know what to do. Like with push.default set to simple, upstream has no tracking set up between this local branch and our origin so it doesn't push up the code and asks me what I want to do.

Git Aliases

Depending on how you want to work you might find that keeping push.default set to simple or upstream provides the safest method for making sure the correct branch always receives the correct updates.

That's totally reasonable but it still doesn't help us with the initial push. Thankfully we can create git aliases which provides short hand on the command line to run multiple or verbose git commands. I tend to use a version floating around the internet called pushup.

git config alias.pushup \!"git push --set-upstream origin 'git symbolic-ref --short HEAD'"

What Should I Do?

My current work flow is to use push.default current because it suits my use case. If you're nervous about changing the defaults you could try push.default upstream with the alias set up for some quality of life on the command line. What's great about the latter is you'll always know whether your branch is upstream or not because it will cause an error the first time. If you're nervous about sending your code upstream until it's not embarrassing this might be a good method to double check that.


A Word on Git Aliases

In almost every online tutorial for git the writer will append the --global flag to their git config commands. As this might indicate that changes your configuration for all the git repositories on your machine. While that's great shorthand I like to configure each repository manually each time. It means I don't accidentally inherit a setting (like push.default current) which might cause issues on a new project I am part of.

The default git settings are extremely conservative so always be aware of overriding them at the global level. With that being said some things (like aliases) can be super handy in your global config.


Enjoy the power of typing less with aliases or a push configuration that better matches your workflow!