-->

How to detect commit --amend by pre-commit hook ?

2020-07-03 05:06发布

问题:

When I do commit --amend, it is unsafe commit if the commit already has been pushed to remote repository.

I want to detect unsafe commit --amend by pre-commit hook and abort.

But pre-commit hook has no arguments. I don't know how to detect --amend.

What should I do ?

回答1:

Following @Roger Dueck's answer, ended up doing:

#./.git/hooks/prepare-commit-msg

IS_AMEND=$(ps -ocommand= -p $PPID | grep -e '--amend');

if [ -n "$IS_AMEND" ]; then
  return;
fi


回答2:

TL;DR version: there's a script below (kind of in the middle) that enforces a particular work-flow that may work for you, or may not. It doesn't exactly prevent particular git commit --amends (plus you can always use --no-verify to skip the script), and it does prevent (or at least warn about) other git commits, which may or may not be what you want.

To make it error-out instead of warning, change WARNING to ERROR and change sleep 5 to exit 1.

EDIT: erroring-out is not a good idea, because you can't tell, in this git hook, that this is an "amend" commit, so this will fail (you have to add --no-verify) if you're simply adding a new commit to a branch that has an upstream and is at the upstream's head.


It's not necessarily unsafe, because git commit --amend does not actually change any commits in your repo, it just adds a new, different commit and re-points the branch tip there. For instance, if your branch looks like this:

A - B - C - D      <-- master, origin/master
          \
            E - F  <-- HEAD=branch, origin/branch

then what a successful git commit --amend does is this:

A - B - C - D      <-- master, origin/master
          \
            E - F  <-- origin/branch
              \
                G  <-- HEAD=branch

You still have commit F, and commit G is the "amended" version of F. However, it's true that G is not a "fast forward" of F and you probably should not git push -f origin branch in this case.

A similar cases occurs if you're already in that kind of situation, i.e., after that successful git commit --amend (done without or in spite of the script below):

A - B - C - D       <-- master, origin/master
          \
            E - F   <-- origin/branch
              \
                G   <-- HEAD=branch

If you now git commit (even without --amend), you'll add a new commit, e.g., G connects to H; but again, attempting to push H is a non-fast-forward.

You can't specifically test for --amend, but you can check whether there is an "upstream", and if so, whether the current HEAD is an ancestor of that upstream. Here's a slightly cheesy pre-commit hook that does this (with a warning-and-sleep rather than an error-exit).

#!/bin/sh

# If initial commit, don't object
git rev-parse -q --verify HEAD >/dev/null || exit 0

# Are we on a branch?  If not, don't object
branch=$(git symbolic-ref -q --short HEAD) || exit 0

# Does the branch have an upstream?  If not, don't object
upstream=$(git rev-parse -q --verify @{upstream}) || exit 0

# If HEAD is contained within upstream, object.
if git merge-base --is-ancestor HEAD $upstream; then
    echo "WARNING: if amending, note that commit is present in upstream"
    sleep 5:
fi
exit 0

The basic problem here is that this situation occurs all the time even without using git commit --amend. Let's say you start with the same setup as above, but commit F does not exist yet:

A - B - C - D      <-- master, origin/master
          \
            E      <-- HEAD=branch, origin/branch

Now you, in your copy of the repo, decide to work on branch. You fix a bug and git commit:

A - B - C - D      <-- master, origin/master
          \
            E      <-- origin/branch
              \
                F  <-- HEAD=branch

You're now ahead of origin and git push origin branch would do the right thing. But while you were fixing one bug, Joe fixes a different bug in his copy of the repo, and pushes his version to origin/branch, beating you to the push step. So you run git fetch to update and you now have this:

A - B - C - D      <-- master, origin/master
          \
            E - J  <-- origin/branch
              \
                F  <-- HEAD=branch

(where J is Joe's commit). This is a perfectly normal state, and it would be nice to be able to git commit to add another fix (for, say, a third bug) and then either merge or rebase to include Joe's fix too. The example pre-commit hook will object.

If you always rebase-or-merge first, then add your third fix, the script won't object. Let's look at what happens when we get into the F-and-J situation above and use git merge (or a git pull that does a merge):

A - B - C - D             <-- master, origin/master
          \
            E - J         <-- origin/branch
              \   \
                F - M     <-- HEAD=branch

You are now at commit M, the merge, which is "ahead of" J. So the script's @{upstream} finds commit J and checks whether the HEAD commit (M) is an ancestor of J. It's not, and additional new commits are allowed, so your "fix third bug" commit N gives you this:

A - B - C - D             <-- master, origin/master
          \
            E - J         <-- origin/branch
              \   \
                F - M - N <-- HEAD=branch

Alternatively you can git rebase onto J, so that before you go to fix the third bug you have:

A - B - C - D          <-- master, origin/master
          \
            E - J      <-- origin/branch
              \  \
              (F) F'   <-- HEAD=branch

(here F' is the cherry-picked commit F; I put parentheses around F to indicate that, while it's still in your repo, it no longer has any branch label pointing to it, so it's mostly invisible.) Now the pre-commit hook script won't object, again.



回答3:

A quick way to detect a "pure" amend in the pre-commit hook:

if git diff --cached --quiet ; then
  echo "This is a pure amend"
else
  echo "This is a commit with changes"
fi

By "pure" I mean you're only rewriting the commit message and not any of the changes in the commit. If there are any changes in your index when you call git commit --amend, you're rewriting more than the commit message and this will behave as if you're doing a conventional git commit.



回答4:

Following @perror's answer, I came up with the following:

parent=$(/bin/ps -o ppid -p $PPID | tail -1)
if [ -n "$parent" ]; then
    amended=$(/bin/ps -o command -p $parent | grep -e '--amend')
    if [ -n "$amended" ]; then
        echo "This is an 'amend'"
    fi  
fi


回答5:

Another way to check if this is an --amend case, in standard shell:

git_command=$(ps -ocommand= -p $PPID)
if [ -z "${git_command##git\ commit*--amend*}" ]; then
    echo "The original command was a: git commit --amend"
    exit 0
fi


标签: git githooks