Git: a preliminary trick to allow only merge, not direct commits to the master

I am having the problem of creating a pre-receive binding on a remote git branch, doing what I want.

What is the problem?

Direct fixing of the main branch is not allowed. Only merging with the main branch is allowed.

Decision

My solution so far is to check if there are changes in the push from the user where the master is affected. But the problem is that I cannot tell if the change is a direct commit or a merge.

 #!/bin/sh while read oldrefid newrefid refname do if [ "$refname" = "refs/heads/master" ]; then echo $(git merge-base $oldrefid $newrefid) echo "---- Direct commit to master branch is not allowed ----" echo "Changes only with a merge from another branch" exit 1 fi done 

Does anyone have an idea how to check if a merge is a merge?

Thanks!

+7
git git-commit git-merge githooks
source share
2 answers

What you get in the pre-hook is the previous and new tip of the branch, so you will need to check the list of added commits and see if any of them are combined:

 nonmerges=$(git rev-list --no-merges --first-parent $oldrefid..$newrefid | wc -l) [ "$nonmerges" -eq 0 ] && exit 0 

--first-parent restricts the output to commit from the main line, i.e. commits that were merged into (reachable via second / third / ... parent) are skipped.

Perhaps the difficulty: smoothing fast-forwarding (which is difficult to distinguish from a series of regular commits).

+4
source share

Here is a short answer: look at the value obtained:

 git rev-list --count --max-parents=1 $oldrefid..$newrefid 

You want it to be zero. Read on for an explanation (and reservations).


Your loop has the correct outline:

  • read all ref updates;
  • for those who are updating the branches you are interested in (or other links), do some checking.

The trick is doing a check. Consider the other two pieces of information that you receive, namely the old and new SHA-1 identifiers, and that in these hooks, one (but not both) of these two SHA-1 identifiers can be all 0 (this means that ref is created or deleted).

To insist that this change is not creation or deletion, your test must ensure that none of the SHA-1 is null. (If you agree that you want to delete only the deletion, you can simply check that the new SHA-1 is not all-zero. But if creation can happen, it is only so if somehow the master branch is finally deleted, for example, someone entering the server receiving clicks and manually deleting it - you still need to make sure that the old SHA-1 is not completely zero for the final test. Obviously, this type of deletion is possible, the question arises whether you write code to handle the case.)

In any case, the most typical push simply updates the link. Please note that any new commits have already been written to the repository (they will be collected in the garbage if you refuse to click), so your task at this point:

  • find and check any commits that refer to the name that he will no longer call (these are commits that will be deleted using persistent pressing); and
  • find and check any commits that this link will indicate now that it was not used for designation (these are new commits that will be added by clicking, whether fast forward or not: remember that a new push can delete one or more commits while adding one or more).

To find these two sets of commits, you should use git rev-list , since its job is to create the SHA-1 list specified by some expression. The two expressions you want here are "all commit identifiers that can be found from one revision that cannot yet find any other identifier." In terms of git rev-list this is git rev-list $r1 ^$r2 , 1 or equivalent, git rev-list $r2..$r1 , for the two revision specifications $r1 and $r2 . Two revspecs are, of course, only the old and new identifiers for the proposed push .

The order of these two identifiers determines which of the git rev-list commit lists: those that will be deleted — this set is empty for the fast-forward operation — and those that will be added.


In this particular case, your goal is not to create these lists of commits themselves (although this worked), but to select something from these lists.

You might want to prevent delegations from committing (for example, to allow fast forward, even if the push user has indicated a force flag). In this case, it is enough to simply verify that the "be deleted" list is empty. You can do this by making sure that the list is actually empty, or - it’s easier in the shell script -having git rev-list count them for you and verify that the resulting number is zero.

You definitely want to prevent add-ons that are not merges, but allow add-ons. In this case, adding --max-parents=1 (which can also be written --no-merges ) tells git rev-list to suppress commits that have two or more parents, i.e. Mergers. By adding --count , you get the number of commits that satisfy this “not merging because zero or one parent” limit. If this count is zero, then any added commits should merge by definition.

Consequently:

 n=$(git rev-list --count --max-parents=1 $oldrefid..$newrefid) if [ $n -gt 0 ]; then echo "disallowed: push adds $n non-merge commit(s)" 1>&2 exit 1 fi 

for example, will be enough to enforce this particular restriction.


1 Almost, but not quite equivalent, you can write git rev-list $r1 --not $r2 : the difference is that the --not effect --not delayed, so if you have to add another revision identifier $r3 --not will be apply to r3 . That is, git rev-list A ^BC means yes-A, not-B, yes-C , but A --not BC means yes-A, not-B, not-C . Note that in the rev-list B..A means A ^B , i.e. Exactly B inverted.

+6
source share

All Articles