如何通过 pre-commit hook 检测 commit --amend?

27

当我执行commit --amend时,如果该提交已经推送到远程存储库,则这是一次不安全的提交。

我想通过pre-commit hook检测不安全的commit --amend,并中止提交。

但pre-commit hook没有参数。我不知道如何检测--amend。

我应该怎么办?


可能是Git提交后:跳过--amend和rebase的重复问题。 - Marchy
相关问题在这里:https://dev59.com/crTma4cB1Zd3GeqP4ElE 接受的答案建议的方法不是停止 git commit --amend,而是阻止人们执行 git push --force - pjpscriv
6个回答

18

跟随@Roger Dueck的回答,最终采取了以下措施:

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

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

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

也许这样更易读,但如果当前 shell 设置了 -e 选项,则会失败。请改用:if ps -ocommand= -p $PPID | grep -q -e '--amend'; then ...; fi - fferri
1
在我的 macOS 的 zsh 中,我不得不使用 exit 0 而不是 return - Max D. -- Stand with Ukraine
对我来说,$(ps -ocommand= -p $PPID) 返回的是 /bin/sh .husky/prepare-commit-msg .git/COMMIT_EDITMSG commit HEAD - Nickon

9
TL;DR版本:下面有一个脚本(在中间位置),它强制执行特定的工作流程,这可能适合您,也可能不适合。它并不能完全防止特定的git commit --amend(此外,您总是可以使用--no-verify跳过脚本),并且它确实会阻止(或至少警告)其他git commit,这可能是您想要的,也可能不是。
要使其出错而不是警告,请将WARNING更改为ERROR,并将sleep 5更改为exit 1。
编辑:出错不是一个好主意,因为在这个git hook中,您无法确定这是否是“修改”提交,因此如果您只是向上游分支添加新提交并处于上游分支的头部,则会失败(您必须添加--no-verify)。
它不一定是不安全的,因为git commit --amend实际上并没有更改存储库中的任何提交,它只是添加了一个新的、不同的提交并重新指向分支末端。例如,如果您的分支如下所示:
A - B - C - D      <-- master, origin/master
          \
            E - F  <-- HEAD=branch, origin/branch

那么一次成功的git commit --amend会做什么呢:

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

您仍然有提交 F,而提交 GF 的“修改”版本。然而,确实 G 不是 F 的“快进”,在这种情况下,您可能不应该 git push -f origin branch

如果你已经处于这种情况下,例如,在成功执行git commit --amend之后(在下面的脚本中未执行或执行失败):

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

如果你现在执行git commit(即使没有使用--amend),你会添加一个新的提交,例如,G连接到H;但是,尝试推送H仍然是一个非快进操作。
你无法专门测试--amend,但可以检查是否存在“上游”,如果存在,则检查当前的HEAD是否是该上游的祖先。下面是一个有点简陋的预提交钩子,它可以执行此操作(具有警告和休眠而不是错误退出)。
#!/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

基本问题在于,即使没有使用 git commit --amend,这种情况也经常发生。假设您从与上述相同的设置开始,但提交 F 还不存在:
A - B - C - D      <-- master, origin/master
          \
            E      <-- HEAD=branch, origin/branch

现在你在你的代码仓库副本中决定在分支上工作。你修复了一个错误并git commit

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

现在你已经领先于origin,而git push origin branch会做正确的事情。但是当你在修复一个错误时,Joe在他的代码库中修复了另一个错误,并将他的版本推送到origin/branch,在push步骤中击败了你。所以你运行git fetch进行更新,现在你有了这个:

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

(其中 J 是 Joe 的提交)。这是一个完全正常的状态,希望能够使用 git commit 添加另一个修复(例如第三个 bug),然后合并或变基以包括 Joe 的修复。 但是,示例 pre-commit 钩子会有所反应。

如果你总是先变基或合并,然后再添加第三个修复,那么脚本就不会有任何问题。让我们看看当我们进入上面的 F-和-J 情况,并使用 git merge(或执行合并的 git pull)时会发生什么:

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

您现在位于合并提交 M,它比提交 J要“领先”一步。因此脚本的 @{upstream} 找到提交 J 并检查 HEAD 提交(M)是否是提交 J 的祖先。但是它不是,允许添加新的额外提交,所以你的“修复第三个 bug”的提交 N 就得到了这个结果:

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

你也可以通过 git rebaseJ 分支上,这样在修复第三个问题之前,你拥有的是:

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

(这里的F'是精选的提交F;我在F周围加上括号,表示虽然它仍然在您的存储库中,但不再有任何分支标签指向它,因此它大多数时候是不可见的。)现在预提交挂钩脚本不会再抱怨了。


破碎的。假设HEAD与origin/branch相同。现在,如果我添加一个新的提交(不是修订),“git merge-base --is-ancestor HEAD $upstream”会将其视为修订:( - Bin TAN - Victor
@BinTAN-Victor:是的,请查看以 EDIT 开头的那一行。为了允许这个条件,如果两个名称标识相同的哈希 ID,则退出零。虽然如此,我总体上不建议采用这种方法:请参阅答案顶部的注释。 - torek

4
在pre-commit钩子中快速检测“纯”的修订:
if git diff --cached --quiet ; then
  echo "This is a pure amend"
else
  echo "This is a commit with changes"
fi

“pure”指的是您仅重写提交消息而不重写提交中的任何更改。如果在调用git commit --amend时,您的索引中有任何更改,则会重写提交消息以外的内容,这将表现为您正在执行常规的 git commit


1
很好,但是很大的“但是”。 - Davi Lima
1
这也可能会触发一个带有 --allow-empty 的普通提交。 - Piotr Siupa

2

在@perror的回答基础上,我得出了以下结论:

最初的回答:

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

1
我在Windows中使用Git Bash。但是它返回错误“ps:未知选项--o”。你有什么解决办法吗? - BulletRain

0

prepare-commit-msg中所述:

它需要一个至三个参数。第一个是包含提交日志消息的文件名。第二个是提交消息的来源,可以是:message;...

或commit,后跟提交对象名称(如果提供了-c、-C或--amend选项)

所以我已经使用了

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

0

检查这是否是标准 shell 中的 --amend 情况的另一种方法:

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

对我没有用,但指引了我正确的方向。请看我的回答,其中检查了父进程中的“--amend”。 - Roger Dueck
哎呀,我可能在没有注意到的情况下对上下文做出了一些假设!对此我很抱歉。但是,我很高兴你成功了(并感谢你分享你的答案!)。 - perror

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接