Reputation: 3577
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:
$(git branch 2> /dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/')
$(git symbolic-ref HEAD)
$(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
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