Home > Software design >  force push to another remote branch using husky hook
force push to another remote branch using husky hook

Time:10-30

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 or git symbolic-ref --short HEAD or git 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 the in HEAD:refs/heads/sandbox or the --delete in git 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.

  • Related