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 --amend
s (plus you can always use --no-verify
to skip the script), and it does prevent (or at least warn about) other git commit
s, 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.