0

I have a Git pre-receive hook that performs several validations such as commit message and file size.

Now I want to also include a validation that blocks the push if there is a merge commit that touches a specific file. The reason is that we have a file that is supposed to be changed only by an automatic procedure related with release versioning, and it's very easy that people merges that file when they need to solve merge conflicts locally.

So, given the current pre-receive hook:

filename=<path-to-file>
while read OLD_SHA1 NEW_SHA1 REFNAME; do
  ...

I need to:

  1. Retrieve the list of commits being pushed
  2. For each commit, determine if the current commit refers to a merge commit
  3. If it's a merge commit, retrieve the changed files within that commit and check it includes the restricted file ($filename)

Optionally, it could also check the author if not a merge commit because it's expected that it matches a specific one (from the automated procedure). This would also protect from cases where the developer has done some wrong rebase or cherry-pick.

Can someone help on this? It would be helpful to have a complete solution (for the 3 steps) since it might be interesting for other people, I assume.

xsilmarx
  • 729
  • 5
  • 22

1 Answers1

2

It's pretty easy to detect whether there's a merge in a sequence of commits:

git rev-list --min-parents=2 $OLD..$NEW

gives them to you. (And, note that in pre- and post-receive you get the two SHA-1s in question so you're in great shape here.)

It's a bit harder to determine "changed files" because no commit stores changes. Each commit simply has a complete source tree attached. This includes merge commits. For non-merges, there's an obvious way to discover changes: compare the commit to its parent. But with a merge commit there are two (or more) parents, so which one(s) do you check? There are three obvious answers: "main line" parent, "branch" parent(s), or "all parents". I'll leave this part to you since it's a policy question.

Having decided on the policy, you then need only use git diff (or any equivalent) to compare the parent commit(s) to the merge commit. For instance, if you decide you only want to see if there is a change with respect to the "main line" (i.e., first) parent:

git diff [flags] ${sha1}^ $sha1

compares the commits. Add --name-status to the flags section and you get the file names and status-es, and you can check for M<tab><path> to see if some path was modified.

(You can gain some efficiency by using --diff-filter=M to remove all output that isn't just an M-status file, and if your filter allows only one status, as in this case, you can then use --name-only since any name coming out implies the status, so this becomes just:

git diff --name-only --diff-filter=M ${sha1}^ $sha1 | grep '^<path>$'

Make sure you have not enabled git diff's rename-detection: it's unlikely, but it's possible that the diff would decide that between the parent and the child, the file(s) you care about were renamed from other files, rather than modified in place.)

You might find some more ideas in a lightly tested pre-receive script I wrote that is available here.

torek
  • 448,244
  • 59
  • 642
  • 775