1

In pre-merge-commit, I need the hash of the merge head to verify a few things about the commits that are about to be merged to the current branch.

However, it seems that neither the reference MERGE_HEAD nor the file .git/MERGE_HEAD exist at the time when the hook is running.

How can I get the merge head revision in my hook?

Piotr Siupa
  • 3,929
  • 2
  • 29
  • 65
  • If the file has been removed, the answer is "you can't": that's the only place the information was stored and until the merge commit itself exists, that's the only place Git could have extracted them from. It seems a bit odd that Git should remove this before the hook finishes though. – torek Sep 07 '22 at 21:44
  • 1
    @torek It seems that the file is not so much removed as not created in the first place. I did some tests and the file seems to always be present if the merge is interrupted in any way: the flag `--no-commit`, a merge conflict or a non-0 exit code of a hook. It is there even if the commit is stopped by a rejected message which is definitely checked after the `pre-merge-commit` has run. – Piotr Siupa Sep 08 '22 at 10:44
  • Aha, that makes more sense. The merge code has the `git commit` action built in to it, so it won't need to make the file if it goes to make the commit on its own. It's ugly but I suppose you could have your pre-merge-commit hook reject the merge to force a separate commit in which the file exists. You might consider modifying Git to provide the missing information *to* the pre-merge-commit hook (by adding the file, or adding it as an argument) and see if you can get the Git developers to take that as a change to the next Git release. – torek Sep 08 '22 at 16:13
  • @torek Heh, I've looked into the code and it seems that the file is always saved during preparation but it is done after the hook is called. (https://github.com/git/git/blob/79f2338b3746d23454308648b2491e5beba4beff/builtin/merge.c#L893) Maybe it would be enough to just move this line a little higher? I would maybe made a pull request but getting enough know-how on the project to do that properly would take me the whole weekend so I'll pass. Too bad they've disabled GitHub issues :-( – Piotr Siupa Sep 09 '22 at 19:11
  • Yep, moving it up a bit would probably work fine. Note that there's a whole process involved in submitting changes. I used GitGitGadget, which makes parts of this less painful (and other parts more painful). – torek Sep 09 '22 at 19:12

3 Answers3

1

I have to do the exact same task, and I have the same issue: no MERGE_HEAD/MERGE_MSG when the pre-merge-commit hook is triggered.

Meaning no way to know the source commit/branch being merged to HEAD.

The only workaround so far is to use instead the prepare-commit-msg hook which can use MERGE_HEAD/MERGE_MSG (I use the latter to extract the source branch name, as torek explained in another answer)

Of course, there are drawbacks:

  • the biggest one is that this "Committing-Workflow Hook" takes place after possible conflicts were resolved. If your hook exits 1 at this point, all the resolution would be lost.
    (the result is still in the index, but if you have to git merge --abort as a result of the hook denying the merge... said index would be cleared)
  • it is triggered for every commit, merge or not, so you need to test the case where MERGE_HEAD does not exist, and exit 0.
  • it will slightly slow down the execution of every commit (instead of just a merge commit), because it needs to spawn the prepare-commit-msg hook. This is mainly visible on Windows.
VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • I don't see that topic discussed in the [Git mailing list](https://public-inbox.org/git/?q=MERGE_HEAD+pre-merge-commit) at the moment (Dec. 2022, post Git 2.39) – VonC Dec 15 '22 at 20:03
  • I have walked this road before and I recommend to turn back. In my case the only thing lost was a commit message which you had to retype in some cases and it still was too annoying. – Piotr Siupa Dec 16 '22 at 06:44
1

Internally Git has a function write_merge_heads that finds the correct object and writes it to the file .git/MERGE_HEAD. Unfortunately, this function is called after the pre-merge-hook was already executed. (It's called regardless if the hook succeeds or fails.)

If you're willing to have a tiny bit of extra typing each merge, there is a really simple fix. Just exit from the pre-merge-commit hook with non-0 code if the MERGE_HEAD is not present. After the merge is resumed, MERGE_HEAD will be there.

Here is a template for such pre-merge-commit hook:

#!/bin/sh

if [ ! -f .git/MERGE_HEAD ]
then
    printf 'Cannot find the MERGE_HEAD!\n' 1>&2
    printf 'Git tends to not pass this information for the first call of the `pre-merge-commit` hook.\n' 1>&2
    printf 'Please, run `git merge --continue` or just `git commit` and it should work this time.\n' 1>&2
    exit 1
fi

# The actual hook's body...
Piotr Siupa
  • 3,929
  • 2
  • 29
  • 65
  • Upvoted, but knowing my users, I could not rely on that: everything must be automatic, in one go. – VonC Dec 16 '22 at 22:17
1

How can I get the merge head revision in my hook?

By using the environment variable GIT_REFLOG_ACTION, which, in case of a merge, will be something like "merge mybranch".

So "${GIT_REFLOG_ACTION#* }" is enough to extract the branch name, even tough its content is supposed to be human-readable".


The following illustrates the use of "${GIT_REFLOG_ACTION#* }", while adding information on the nature of the pre-merge-commit hook.

I thought a failed status would mean the merge is cancelled, but no:
exit 1 in that hook leaves the merge "in progress".

If you need to actually abort the merge, the hook has to call itself (in background) and wait one second (for the main hook process to exit, and for Git to deny the merge.
That second call should execute git merge --abort

#!/bin/bash

root="$(pwd)"

src="${GIT_REFLOG_ACTION#* }"
dst="$(git rev-parse --abbrev-ref HEAD)"

if [[ "${abort}" == 1 ]]; then
    echo "Abort merge between '${src}' and '${dst}'"
    git merge --abort </dev/null
    cmd /d /c "exit"
    exit 0
fi

echo "------"
echo "PRE-MERGE-COMMIT (${GIT_REFLOG_ACTION})"
echo "------"

# Should not happen, but test it just in case
if [[ "${src}" == "" ]]; then
    echo "no merge in progress for this commit"
    exit 1
fi

echo "Merge from '${src}' to '${dst}'"
# if you decide to prevent the merge:
export abort=1
"${0}" >&2 </dev/null &
exit 1

"${0}" >&2 </dev/null & is the self-call. The pre-merge-commit exits with status 1, git acknowledges the merge is denied (but leaves it pending), and then, one second later, the same pre-merge-commit script (called in background) aborts the merge for good.

The cmd /d /c "exit" is used when the hook is triggered from a Windows CMD session in order to force the CMD prompt to appear.
Otherwise the echo "Abort merge between '${src}' and '${dst}'" is displayed... and you would need to type enter to find the prompt again (because that echo is done from a background process).

VonC
  • 1,262,500
  • 529
  • 4,410
  • 5,250
  • According to git-scm.com/book/en/v2/Git-Internals-Environment-Variables, GIT_REFLOG_ACTION is supposed to be a human-readable text. Can we really assume it can be reliably parsed and it won't change format between Git releases? – Piotr Siupa Dec 16 '22 at 20:42
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/250480/discussion-between-vonc-and-piotr-siupa). – VonC Dec 16 '22 at 20:44