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

后端 未结 5 1277
陌清茗
陌清茗 2021-02-19 07:09

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

5条回答
  •  清酒与你
    2021-02-19 07:17

    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.

提交回复
热议问题