hellatan
hellatan

Reputation: 3577

git push --force/-f - how to get each branch

This has to do with using git's push.default matching setting from git < 2.0. Also, forking the repo in this particular instance is not applicable. We use github at work and since it does not allow a pre-receive hook, we have to try and mitigate this via pre-push hook (somewhat of a half-measure if you ask me, but it's something).

Anyway, I'm trying to write a pre-push hook that will prevent someone from accidentally force updating our master branch (or any branch we want). However, when I tried doing a simple test of trying to get the branch name, I can only seem to get the currently checked out branch. I've used these three git "methods" to do so:

  1. $(git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/')
  2. $(git symbolic-ref HEAD)
  3. $(git rev-parse --abbrev-ref HEAD)

and all three give me the same result. Here is an example:

[dude (master)] $ git rebase -i head^
# reword a commit to get into a force-push state
[dude (master)] $ git checkout test3
[dude (test3)] $ git push 
branch 2>: test3 # output by the hook
symbolic-ref: refs/heads/test3 # output by the hook
rev-parse: test3 # output by the hook
--- pre-push: nothing to check # output by the hook
To [email protected]:dude/stuff.git
 ! [rejected]        master -> master (non-fast-forward)
error: failed to push some refs to '[email protected]:dude/stuff.git'
hint: bla bla bla

now apply a force update:

[dude (test3)] $ git push -f
branch 2>: test3 # output by the hook
symbolic-ref: refs/heads/test3 # output by the hook
rev-parse: test3 # output by the hook
--- pre-push: nothing to check # output by the hook
Counting objects: 1, done.
Writing objects: 100% (1/1), 185 bytes | 0 bytes/s, done.
Total 1 (delta 0), reused 0 (delta 0)
To [email protected]:dude/stuff.git
 + 060fa0b...763516d master -> master (forced update)

As you can see, only test3 branch is being referenced. I want to know when git is about to push the master branch instead. Does anyone know what a force-update does behind the scenes here?

Upvotes: 2

Views: 147

Answers (1)

torek
torek

Reputation: 488193

Git really does only have the notion of "one current branch", that being whatever symbolic reference (if any) is stored in HEAD. (If you're in "detached HEAD" mode then you are not on any branch at all, so the complete answer is that in git, you are "on" either no branch at all, or the one branch stored in HEAD.)

The push command, however, takes an arbitrary, user-specified, set of "refspecs". A refspec is, loosely speaking, just a source-and-destination pair of references, such as master:master or refs/heads/master:refs/heads/master. If you specify no refspecs in your push command, git uses some default, which as you have observed has changed over time (it used to be matching, now it is simple, but it is changeable with the push.default configuration and also with remote.name.push and several more items).1

Refspecs can be augmented with a leading + to set --force for that refspec only, or of course you can use --force on the push command line to set it for all refspecs. And, with git push, the "source" side of a refspec can be any arbitrary commit identifier (such as HEAD or a raw SHA-1), or empty for a "delete" operation; and the "destination" side need not be fully qualified (git will often figure out branch vs tag on its own).

I think it's much too difficult to reproduce the rules that git push uses, especially in a pre-push hook. And, unfortunately, a pre-push hook does not supply you with a copy of either the --force global setting or the per-refspec +-or-from-global force flag. It does, however, supply you with a complete list of the proposed updates, on standard input (along with the remote name and URL as arguments). Hence:

#! /bin/sh
# sample pre-push hook, not tested
case "$1" in
origin) ;;
*)
    echo "pushing to $1, not doing any checking" 1>&2
    exit 0;;  # allow the push
esac

STATUS=0

disallow() {
    echo "pre-push hook: stopping your push" 1>&2
    echo "reason: $@" 1>&2
    STATUS=1
}

# pushing to remote named "origin": do some simple tests
while read localref localsha remoteref remotesha; do
    case $remoteref in
        refs/heads/master)
            disallow "push goes to master"
            ;;
        # add more tests here as desired
    esac
done

exit $STATUS

In order to push (at all) to master on the server, one can simply copy the origin remote to another name and then git push Xorigin master:master. Or, you could use something like the script in loganfsmyth's comment to attempt to guess whether --force (or a plus sign) was given on the command line.

One additional possibility is to contact the remote (perhaps by URL, in the case of a push-by-URL, but this complicates everything) in the middle of the push, using fetch to update your information about its commit graph, and then attempt to discover whether the proposed label-move is in fact a fast forward. I am not sure if this would work at all (does git allow reading the remote repository while you're in the middle of proposing an update push? it probably does, but I don't know), and it's a bit tricky and it would still not be able to tell if the user specifically asked for a force-push, but it would solve most of the problem.


1Technically neither "matching" nor "simple" is a refspec. Instead, these are rules by which git computes the refspec(s). This is of course part of the problem: there is a lot of internal magic that gets applied between the point at which you enter the command on the command-line, and the point at which various data transfers begin. The pre-push hook is invoked after refspecs have been computed, but before your git begins sending updates to the remote.

Upvotes: 1

Related Questions