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 a
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.
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
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
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
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.