小编典典

如何在预提交挂钩中正确 git stash/pop 以获得干净的工作树进行测试?

all

我正在尝试使用裸运行的单元测试进行预提交挂钩,并且我想确保我的工作目录是干净的。编译需要很长时间,所以我想尽可能地利用重用已编译的二进制文件。我的脚本遵循我在网上看到的示例:

# Stash changes
git stash -q --keep-index

# Run tests
...

# Restore changes
git stash pop -q

但这会导致问题。这是重现:

  1. 添加// Step 1a.java

  2. git add .

  3. 添加// Step 2a.java

  4. git commit

  5. git stash -q --keep-index# 存储更改

  6. 运行测试
  7. git stash pop -q# 恢复更改

在这一点上,我遇到了问题。git stash pop -q显然有冲突,而我a.java

// Step 1
<<<<<<< Updated upstream
=======
// Step 2
>>>>>>> Stashed changes

有没有办法让它干净地弹出?


阅读 140

收藏
2022-03-01

共1个答案

小编典典

有——但让我们以稍微迂回的方式到达那里。(另外,请参阅下面的警告:存储代码中有一个错误,我认为这是非常罕见的,但显然更多的人遇到了。2021 年 12 月添加的新警告:git stash已用 C 重写,并且有一系列全新的错误. 我曾经温和地建议git stash避免; 现在我敦促每个人尽可能避免它。)

git stash push(默认操作git stash; 请注意,这是git stash save在 2015 年拼写的,当时我写了这个答案的第一个版本)做出了一个至少有两个父母的提交。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 applyor git stash pop,这样做的方法是使用--index(不是--keep-index,那只是为了创建存储时间,告诉存储脚本“敲打工作目录”)。

只是使用--index仍然会失败,因为--keep-index将索引更改重新应用于工作目录。因此,您必须首先摆脱所有这些更改……为此,您只需要 (re)run git reset --hard,就像之前的 stash 脚本本身所做的那样。(可能你也想要-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 pop --index -q抱怨一点(或者,更糟糕的是,恢复旧的存储)。git stash push因此,您应该通过测试来保护“恢复”步骤,以查看“保存”步骤是否实际隐藏了任何内容。

当它什么都不做时,初始git stash --keep-index -q只是安静地退出(状态为 0),所以我们需要处理两种情况:在保存之前或之后不存在存储;并且,在保存之前存在一些存储,并且保存什么也没做,所以旧的现有存储仍然是存储堆栈的顶部。

我认为最简单的方法是使用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 时已经修复。相反,现在还有许多其他错误。毫无疑问,它们最终会被修复,但根据您使用的 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在此之后运行时,index-commit ( 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提交进行比较,以便在将特殊工作目录提交作为存储包的一部分之前计算要记录在特殊临时索引中的文件集。因为foo.txt相对于 没有变化HEAD,它对特殊临时索引失败git add。然后使用 index-commit 的版本进行特殊工作树提交foo.txt。修复非常简单,但没有人将它放入官方 git [还? ]。

并不是说我想鼓励人们修改他们的 git 版本,但这是修复。)

2022-03-01