您实际上非常接近正确的解决方案。
(在这个答案中,我将使用“缓存”这个词而不是“阶段”,因为后者与“存储”太相似了。)
事实上,即使您要提交未缓存的文件,使用 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 reset
and的顺序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"