1

我希望我的pre-commit钩子在允许执行提交之前编译程序并运行所有自动测试。问题是通常我的工作副本在我提交时并不干净。它们不是我不想提交的暂存文件,甚至不是未跟踪的文件。有时我什至明确指定只提交几个与当前暂存的文件无关的文件。

当然,我只想编译和测试将要提交的更改,而忽略其他更改。它有3个步骤:

  1. 删除所有不会提交的更改。
  2. 运行测试。
  3. 将所有更改恢复到第一步之前的状态。

第一步可以通过运行来实现git stash push --include-untracked --keep-index。存储条目也将有助于第三步。但是,当我提交未暂存的文件的明确列表时,我不知道该怎么做。

(第二步并不是问题的一部分。)

第 3 步理论上可以使用命令完成,git stash pop --index但是如果某个文件被暂存,然后在没有再次暂存的情况下进行了更多更改,则此命令似乎容易发生冲突。

此脚本创建一个存储库,其中包含一些涵盖各种极端情况的文件和更改:

#!/usr/bin/env sh

set -e -x

git init test-repo
cd test-repo
git config user.email "you@example.com"
git config user.name "Your Name"

echo foo >old-file-unchanged
echo foo >old-file-changed-staged
echo foo >old-file-changed-unstaged
echo foo >old-file-changed-both
git add .
git commit -m 'previous commit'

echo bar >old-file-changed-staged
echo bar >old-file-changed-both
echo bar >new-file-staged
echo bar >new-file-both
git add .
echo baz >old-file-changed-unstaged
echo baz >old-file-changed-both
echo baz >new-file-both
echo baz >untracked-file
4

1 回答 1

2

您实际上非常接近正确的解决方案。

(在这个答案中,我将使用“缓存”这个词而不是“阶段”,因为后者与“存储”太相似了。)

事实上,即使您要提交未缓存的文件,使用 stash 的技巧也会起作用。这是因为 Git 在运行钩子期间会更改缓存,因此它始终包含正确的文件。您可以通过将命令添加git status到您的pre-commit钩子来检查它。

所以你可以使用git stash push --include-untracked --keep-index.

恢复存储时的冲突问题也很容易解决。您已经在存储中备份了所有更改,因此没有丢失任何东西的风险。只需删除所有当前更改并将存储应用到一个干净的状态。

这可以分两步完成。该命令git reset --hard将删除所有跟踪的文件。该命令git clean -d --force将删除所有未跟踪的文件。

之后,您可以在git stash pop --index没有任何冲突风险的情况下运行。


一个简单的钩子看起来像这样:

#!/bin/sh

set -e

git stash push --include-untracked --keep-index --quiet --message='Backed up state for the pre-commit hook (if you can see it, something went wrong)'

#TODO Tests go here

git reset --hard --quiet
git clean -d --force --quiet
git stash pop --index --quiet

exit $tests_result

让我们分解一下。

set -e确保脚本在出现错误时立即停止,因此不会造成任何进一步的损害。备份所有更改的存储条目在开始时完成,因此如果出现错误,您可以手动控制并恢复所有内容。

git stash push --include-untracked --keep-index --quiet --message='...'实现两个目的。它会为所有当前更改创建备份,并从工作目录中删除所有非暂存更改。该标志--include-untracked确保未跟踪的文件也被备份和删除。该标志--keep-index取消从工作目录中删除缓存的更改(但它们仍包含在存储中)。

#TODO Tests go here是你测试的地方。确保不要在此处退出脚本。在执行此操作之前,您仍然需要恢复隐藏的更改。不要以错误代码退出,而是将其值设置为变量tests_result

git reset --hard --quiet从工作目录中删除所有跟踪的更改。该标志--hard确保缓存中没有任何内容,并且所有文件都被删除。

git clean -d --force --quiet从工作目录中删除所有未跟踪的文件。该标志-d告诉 Git 递归删除目录。该标志--force告诉 Git 你知道你在做什么,它真的应该删除所有这些文件。

git stash pop --index --quiet恢复保存在最新存储中的所有更改并将其删除。该标志--index告诉它确保它没有混淆哪些文件被缓存,哪些文件没有被缓存。


这种方法的缺点

这种方法只是半稳健的,对于简单的用例应该足够了。但是,它们是相当多的极端情况,可能会在实际使用中破坏某些东西。

git stash push拒绝使用只添加了 flag 的文件--intent-to-add。我不确定为什么会这样,而且我还没有找到解决它的方法。您可以通过添加不带标志的文件或至少将其添加为空文件并仅保留未缓存的内容来绕过该问题。

Git 只跟踪文件,不跟踪目录。但是,该命令git clean可以删除目录。结果,脚本将删除空目录(除非它们被忽略)。

自上次提交以来添加的文件.gitignore将被删除。我认为这是一个功能,但如果你想阻止它,你可以通过颠倒git resetand的顺序git clean。请注意,这仅在.gitignore包含在当前提交中时才有效。

git stash push如果没有更改,则不会创建新的存储,但它仍然返回 0。要处理不更改的提交,例如使用更改消息,--amend您需要添加一些代码来检查存储是否真的创建并仅在它创建时才弹出它。

Git stash 似乎删除了有关当前合并的信息,因此在合并提交上使用此代码会破坏它。为了防止这种情况,您需要备份文件.git/MERGE_*并在弹出存储后恢复它们。


强大的解决方案

我已经设法解决了这种方法的大部分问题(使代码在此过程中更长)。

剩下的唯一问题是删除空目录和被忽略的文件(如上所述)。我不认为这些问题严重到需要花时间试图绕过它们。(不过,这是可行的。)

#!/bin/sh

backup_dir='./pre-commit-hook-backup'
if [ -e "$backup_dir" ]
then
    printf '"%s" already exists!\n' "$backup_dir" 1>&2
    exit 1
fi

intent_to_add_list_file="$backup_dir/intent-to-add"
remove_intent_to_add() {
    git diff --name-only --diff-filter=A | tr '\n' '\0' >"$intent_to_add_list_file"
    xargs -0 -r -- git reset --quiet -- <"$intent_to_add_list_file"
}
readd_intent_to_add() {
    xargs -0 -r -- git add --intent-to-add --force -- <"$intent_to_add_list_file"
}

backup_merge_info() {
    echo 'If you can see this, tests in the `pre-commit` hook went wrong. You need to fix this manually.' >"$backup_dir/README"
    find ./.git -name 'MERGE_*' -exec cp {} "$backup_dir" \;
}
restore_merge_info() {
    find "$backup_dir" -name 'MERGE_*' -exec mv {} ./.git \;
}

create_stash() {
    git stash push --include-untracked --keep-index --quiet --message='Backed up state for the pre-commit hook (if you can see it, something went wrong)'
}
restore_stash() {
    git reset --hard --quiet
    git clean -d --force --quiet
    git stash pop --index --quiet
}

run_tests() (
    set +e
    printf 'TODO: Put your tests here.\n' 1>&2
    echo $?
)

set -e
mkdir "$backup_dir"
remove_intent_to_add
backup_merge_info
create_stash
tests_result=$(run_tests)
restore_stash
restore_merge_info
readd_intent_to_add
rm -r "$backup_dir"
exit "$tests_result"
于 2021-09-08T21:52:43.600 回答