I would like to run
git push origin --force CURRENT_BRANCH_NAME:sandbox
in a pre-push hook.
How do I get
CURRENT_BRANCH_NAME
as part of the above command?
I understand I can have git branch --show-current
to return the current branch name. Just not sure how to use its output with the above-mentioned git command.
CodePudding user response:
First, I'll answer the question you actually asked:
To get the current branch name, use
git branch --show-current
orgit symbolic-ref --short HEAD
orgit rev-parse --abbrev-ref HEAD
. Note that these three commands do slightly different things! See details below.In POSIX-compatible shells, to substitute the output of a command in place inside another command as an argument, use either backquotes or
$(
and)
. I prefer the$(...)
sequence as it nests properly: that is, you can put another$(...)
inside the$(...)
and it Just Works since parentheses nest. This is not true of backquotes (they can be made to work but it is trickier).
Next, I'll note that you should not bother to do this. The reason is simple: you have to pick one of the above three items and two of them are probably wrong, and this is all unnecessary. Simply use:
git push --force origin HEAD:sandbox
or, for strict correctness in the case where two of the items are probably wrong:
git push --force origin HEAD:refs/heads/sandbox
This variant uses a fully qualified reference, refs/heads/sandbox
, to refer to the branch name sandbox
in the remote Git, regardless of the result of an attempt to turn HEAD
into a Git commit hash ID.
Why this works
The git push
command pushes commits (not files, not branches, just commits and other supporting Git objects) to the other Git. Once the commits (and/or other supporting Git objects) have made it to that other Git and are ready to be used there, then git push
ends by asking (non-forced push) or commanding (forced push) the other Git to create, delete, or update some name(s) in their repository.
To achieve this sequence of events, git push
needs:
- the raw hash ID(s) of some commit(s) and/or other supporting Git objects it should send if / as needed;
- the names it should ask / command the other Git to set or delete, along with the operation to do (set vs delete); and
- the force flag (which can be a simple off/on, or one of the more complicated "with lease" style options).
Your Git—your software, sending from your repository, to the other Git that's software receiving into a target repository—gets these three items from:
- flags like
--force
or theHEAD:refs/heads/sandbox
or the--delete
ingit push --delete
, which go in the obvious positions; - for the raw hash ID(s) that should be sent, from the left side of the
src:dst
pair; - for the name to update, from the right side of this same pair.
Git calls the pair itself, with or without the optional
force flag, a refspec. So feature:sandbox
is a refspec, and HEAD:sandbox
is also a refspec. In fact, even the degenerate form:
git push origin main
for instance, uses a refspec: it's just one in which the colon is missing, so that you provide only a src
part. This is a partial refspec and in this case, Git uses the same name for the target as you supplied for the source (for git push
that is—while git fetch
also works with refspecs, its treatment of partial refspecs is different).
Since you already plan to use a full refspec, with the colon in it, the item on the left is not required to be a name. The name that your Git will send to the other Git comes from the right hand side of the refspec.
Now, there's a small hitch here: how does the sending Git (i.e., yours) know whether to ask the receiving Git to set a branch name (refs/heads/moo
), or a tag name (refs/tags/moo
), or perhaps some other kind of name (refs/for/moo
for Gerrit for instance) if you give your Git an unqualified name like moo
? The answer is that your Git and/or their Git will do their best to guess which kind of name you mean, but in general, when you are doing this—providing a full refspec, that is—it's wise to provide a full ref on the right, so that you know for sure what you intend to ask the other Git to set (or delete). This is especially true from scripts, which may operate without a human overseer.
Hence there is a general rule here
When you, as a human, are running:
git push origin feature3 v1.2
you know that feature3
is your own local branch name, and v1.2
is your own local tag name, so you know that this is really refs/heads/feature3:refs/heads/feature3
and refs/tags/v1.2:refs/tags/v1.2
. But in scripts, which are generally write-once run-many-times, it's wiser to be explicit.
Coda: the three different commands
Git has two "modes" for HEAD: attached and detached. (Note: Git calls the latter detached HEAD mode; I made up the name attached HEAD mode as the obvious counterpart, but it's not officially a Git name.) In the attached-HEAD mode, the name HEAD
is a symbolic reference to a branch name.1 In the detached mode, however, HEAD
holds a raw commit hash ID.
The command git branch --show-current
will show the name to which HEAD
is attached when you are in attached-HEAD mode, but will silently print nothing and exit with a successful status when you are in detached-HEAD mode.
The command git symbolic-ref HEAD
prints the full name of the branch to which HEAD
is attached when in attached-HEAD mode, and produces an error message, no standard output, and a nonzero exit code when in detached-HEAD mode. It prints the short version of the branch name with --short
but still produces the same no-output-but-error-instead when detached.
The command git rev-parse --abbrev-ref HEAD
prints the shortened name of the current branch when in attached-HEAD mode, and prints the word HEAD
when in detached-HEAD mode.
There's one more mode worthy of mention here, although it's quite rare: when you are on an "orphan" or "unborn" branch (Git uses both terms to refer to this state), the name HEAD
is a symbolic ref to a branch name that does not exist. In this state, git branch --show-current
and git symbolic-ref HEAD
both succeed (and print the nonexistent branch name), but git rev-parse --abbrev-rev HEAD
fails.
To test which mode you're in and get the commit hash ID:
detached=false unborn=false
branch=$(git symbolic-ref --short HEAD 2>/dev/null) || detached=true
if ! $detached; then
git rev-parse -q --verify HEAD >/dev/null || unborn=true
fi
After executing these five lines, $detached
holds whether HEAD
is detached (as a simple boolean result), $branch
holds the branch name if HEAD
is not detached, and $unborn
contains the result of testing whether there is currently a commit, or making a new commit will create the current branch. (The need for $unborn
is rare.)
1In ancient, primeval Git, this was implemented with symlinks: ln -s refs/heads/master HEAD
for instance. Git had to drop this particular shortcut when it was ported to Windows, which lacked symlinks.