有——但让我们以稍微迂回的方式到达那里。 (另外,请参阅下面的警告:存储代码中有一个错误,我认为这是非常罕见的,但显然更多的人遇到了。新警告,于 2021 年 12 月添加:git stash 已用 C 重写并具有全新的一堆错误。我曾经温和地建议避免使用git stash;现在我敦促每个人尽可能避免使用它。)
git stash push(git stash 的默认操作;请注意,当我编写此答案的第一个版本时,这在 2015 年拼写为 git stash save)进行至少有两个父级的提交(请参阅this answer关于藏匿的更基本的问题)。 stash 提交是工作树状态,第二个父提交 stash^2 是存储时的索引状态。
存储完成后(假设没有-p 选项),脚本(git stash 是一个shell 脚本)使用git reset --hard 清除更改。
当您使用--keep-index 时,脚本不会以任何方式更改已保存的存储。相反,在git reset --hard 操作之后,脚本使用额外的git read-tree --reset -u 来清除工作目录更改,将它们替换为存储的“索引”部分。
换句话说,它几乎就像在做:
git reset --hard stash^2
除了git reset 也会移动分支——根本不是你想要的,因此使用read-tree 方法。
这是您的代码返回的地方。您现在 # Run tests 处理索引提交的内容。
假设一切顺利,我假设您希望将索引恢复到执行 git stash 时的状态,并将工作树也恢复到其状态。
使用git stash apply 或git stash pop,这样做的方法是使用--index(不是--keep-index,这只是用于创建存储的时间,告诉存储脚本“敲击工作目录”) .
尽管使用--index 仍然会失败,因为--keep-index 将索引更改重新应用到工作目录。因此,您必须首先摆脱所有这些更改……为此,您只需(重新)运行git reset --hard,就像之前存储脚本本身所做的那样。 (可能你也想要-q。)
所以,这是最后一个# Restore changes 步骤:
# Restore changes
git reset --hard -q
git stash pop --index -q
(我将它们分开为:
git stash apply --index -q && git stash drop -q
我自己,只是为了清楚起见,但 pop 会做同样的事情)。
如以下评论中所述,如果最初的 git stash push 步骤没有发现要保存的更改,则最终的 git stash pop --index -q 会有点抱怨(或者,更糟糕的是,会恢复 old 存储)。因此,您应该通过测试来保护“恢复”步骤,以查看“保存”步骤是否实际隐藏了任何内容。
最初的git stash --keep-index -q 在什么都不做的时候只是安静地退出(状态为0),所以我们需要处理两种情况:在保存之前或之后不存在stash;并且,在保存之前存在一些存储,并且保存没有执行任何操作,因此旧的现有存储仍然是存储堆栈的顶部。
我认为最简单的方法是使用git rev-parse 找出refs/stash 的名称,如果有的话。所以我们应该让脚本读起来更像这样:
#! /bin/sh
# script to run tests on what is to be committed
# First, stash index and work dir, keeping only the
# to-be-committed changes in the working directory.
old_stash=$(git rev-parse -q --verify refs/stash)
git stash push -q --keep-index
new_stash=$(git rev-parse -q --verify refs/stash)
# If there were no changes (e.g., `--amend` or `--allow-empty`)
# then nothing was stashed, and we should skip everything,
# including the tests themselves. (Presumably the tests passed
# on the previous commit, so there is no need to re-run them.)
if [ "$old_stash" = "$new_stash" ]; then
echo "pre-commit script: no changes to test"
sleep 1 # XXX hack, editor may erase message
exit 0
fi
# Run tests
status=...
# Restore changes
git reset --hard -q && git stash apply --index -q && git stash drop -q
# Exit with status from test-run: nonzero prevents commit
exit $status
警告:git stash 中的小错误
(注意:我相信这个错误在转换为 C 时已修复。相反,现在有许多 other 错误。毫无疑问,它们最终会被修复,但取决于您使用的 Git 版本正在使用,git stash 可能有各种严重程度不同的错误。)
git stash 写入其"stash bag" 的方式存在一个小错误。索引状态存储是正确的,但假设您执行以下操作:
cp foo.txt /tmp/save # save original version
sed -i '' -e '1s/^/inserted/' foo.txt # insert a change
git add foo.txt # record it in the index
cp /tmp/save foo.txt # then undo the change
当您在此之后运行 git stash push 时,索引提交 (refs/stash^2) 将在 foo.txt 中插入文本。工作树提交 (refs/stash) 应该 具有 foo.txt 的版本,没有额外插入的内容。但是,如果您查看它,您会发现它的(索引修改)版本是错误的。
上面的脚本使用--keep-index 将工作树设置为索引,这一切都很好,并且为运行测试做了正确的事情。运行测试后,它使用git reset --hard 回到HEAD 提交状态(仍然非常好)......然后它使用git stash apply --index 恢复索引(有效)和工作目录。
这就是问题所在。索引是(正确地)从存储索引提交中恢复的,但是工作目录是从存储工作目录提交中恢复的。此工作目录提交具有索引中的foo.txt 版本。换句话说,取消更改的最后一步——cp /tmp/save foo.txt——尚未完成!
(stash 脚本中的错误是因为脚本将工作树状态与HEAD 提交进行比较,以便在制作特殊工作目录之前计算要记录在特殊临时索引中的文件集提交 stash-bag 的一部分。由于foo.txt 相对于HEAD 未更改,因此无法将git add 提交到特殊临时索引。然后使用索引提交的版本进行特殊工作树提交foo.txt. 修复非常简单,但没有人将它放入官方 git [还没有?]。
不是我想鼓励人们修改他们的 git 版本,而是here's the fix。)