ton
ton

Reputation: 1624

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

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 ?

Upvotes: 31

Views: 10259

Answers (5)

mani
mani

Reputation: 307

as documented in prepare-commit-msg:

It takes one to three parameters. The first is the name of the file that contains the commit log message. The second is the source of the commit message, and can be: message; ...

or commit, followed by a commit object name (if a -c, -C or --amend option was given)

so i've used

#!/bin/bash
[ "$2" = "commit" -a "$3" = "HEAD" ] && echo "ignore amend or do something"

Upvotes: -1

kunicmarko20
kunicmarko20

Reputation: 2180

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

Upvotes: 20

Roger Dueck
Roger Dueck

Reputation: 625

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

Upvotes: 1

jsharp
jsharp

Reputation: 603

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.

Upvotes: 3

torek
torek

Reputation: 490068

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.

Upvotes: 9

Related Questions