2021-02-19
|~5 min read
|837 words
Many moons ago, I had an idea for a utility gpnew
to handle pushing a new branch to my origin.
That is, I wanted to avoid this situation:
% git push
fatal: The current branch test has no upstream branch.
To push the current branch and set the remote as upstream, use
git push --set-upstream origin test
gpnew
would take advantage of a second alias I’d written, gbc
(which I’d written about previously in the context of rebasing) to find the current branch I’m on:
alias gbc='git branch --show-current'
gpnew
was supposed to be simple. I started with an alias.
alias gpnew='git push --set-upstream origin $(gbc)'
By design, it was meant to evaluate the current branch and then slot it right into the space it needed to be for passing upstream.
The issue is it didn’t work!
In fact, worse than not working completely, when I ran gpnew
(even after confirming the output of gbc
independently), I’d wind up pushing my changes to the wrong branch. Specifically, it’d push them up to the previous branch, frequently develop
or main
and in either case, not where I wanted them!
After avoiding it for months I finally got sick of copying and pasting the line and dug into ways to solve my problem. While the answer was simple, along the way I learned three lessons:
HEAD
reference in git,head
Let’s start at the top. If all I cared about was the outcome and not the why, the first answer I came across today would have been sufficient. Simply revamp the gpnew
alias to git push --set-upstream origin HEAD
and call it a day.
This works because HEAD
always references where the tip of your git is, which happens to be your current branch.
This was great, and I would have gone with it, but… I wanted to understand!
In many programming languages, there’s no semantic difference between a single and double quote. Which you use is often a matter of preference. It turns out Bash is not one of those languages and there are real differences.
codeforester’s answer on Stack Overflow was the one which really clicked for me:
Here is a three-point formula for quotes in general:
Double quotes
In contexts where we want to suppress word splitting and globbing. Also in contexts where we want the literal to be treated as a string, not a regex.
Single quotes
In string literals where we want to suppress interpolation and special treatment of backslashes. In other words, situations where using double quotes would be inappropriate.
No quotes
In contexts where we are absolutely sure that there are no word splitting or globbing issues or we do want word splitting and globbing.
It was the actual example he provided of “command substitutions” (which is exactly what I’m doing) that seemed to suggest I wanted double quotes.
alias gpnew="git push --set-upstream origin $(gbc)"
All of this is good information, however, it doesn’t actually solve the problem. The real culprit in my case is that aliases are evaluated at the launch of the shell and then never reevaluated. They’re frozen.
This is why gbc
would show the correct value and gpnew
didn’t:
gbc
was an alias for a git command.gpnew
was also an alias git command, but, crucially, one where the arguments are fixed as soon as the shell launches (specifically the branch).As one example, consider the case where I’m on main
when I launch my shell and then switch to a feat-xyz
. That would mean when the alias gpnew
would be frozen as git push --set-upstream origin main
unless I reloaded the shell.
Or, perhaps even more clearly, we can see this with a simple date example:
$ date && alias datedate="echo $(date)"
Fri Feb 19 07:07:06 PST 2021
$ datedate
Fri Feb 19 07:07:06 PST 2021
$ datedate
Fri Feb 19 07:07:06 PST 2021
$ echo ${BASH_ALIASES[datedate]}
echo Fri Feb 19 07:07:06 PST 2021
Now, I may be fast with a keyboard, but it seems unlikely that I could make all of those calls within 1 second (and yet, the time is frozen).
If, instead of using an alias I write a function, however, this work more as expected desired.
$ date && function datenow() { $(echo date); }
Fri Feb 19 07:24:36 PST 2021
$ # wait a few seconds
$ datenow
Fri Feb 19 07:25:12 PST 2021
Once I understood this, I was able to convert my alias gpnew
into a function:
function gpnew(){
git push --set-upstream origin $(gbc)
}
And with that, gpnew
is now correctly deriving the value of the current branch on every invocation!
Hi there and thanks for reading! My name's Stephen. I live in Chicago with my wife, Kate, and dog, Finn. Want more? See about and get in touch!