9

我希望能够使用索引的当前状态为我的项目运行测试,忽略未提交的工作更改(我稍后计划将其添加到预提交挂钩)。但是,我无法弄清楚如何以永远不会导致合并冲突的方式删除然后恢复非索引更改。我需要这个,因为它是由脚本运行的,所以它不应该在完成后改变存储库状态。

git stash --include-untracked --keep-indexgit stash pop接近,但在许多情况下,它会导致合并冲突,即使两个命令之间没有进行任何更改。

例如:

mkdir project; cd project; git init .;

# setup the initial project with file.rb and test.rb
cat > file.rb <<EOF
def func()
  return 42
end
EOF

cat > test.rb <<EOF
#!/usr/bin/env ruby
load './file.rb'
if (func() == 42)
  puts "Tests passed"
  exit 0
else
  puts "Tests failed"
  exit 1
end
EOF

chmod +x test.rb
git add .
git commit -m "Initial commit"

# now change file.rb and add the change
cat > file.rb <<EOF
def func()
  return 10 + 32
end
EOF
git add file.rb

# now make a breaking change to func, and don't add the change
cat > file.rb <<EOF
def func()
  return 20 + 32 # not 42 anymore...
end 
EOF

从这里我想针对索引的当前状态运行测试,并恢复未提交的更改。预期结果是测试通过,因为重大更改未添加到索引中。

以下命令不起作用:

git commit --include-untracked --keep-index
./test.rb
git stash pop

问题发生在git stash pop- 发生合并冲突。

我能想到的唯一其他解决方案是进行临时提交,然后存储剩余的更改,然后使用 回滚提交git reset --soft HEAD~,然后弹出存储。然而,这都很麻烦,而且我不确定在预提交挂钩中运行有多安全。

这个问题有更好的解决方案吗?

4

3 回答 3

9

和你一样,我跑

git stash --keep-index --include-untracked

然后我可以运行测试等等。

下一部分很棘手。这些是我尝试过的一些事情:

  • git stash pop可能会因冲突而失败,这是不可接受的。
  • git stash pop --index可能会因冲突而失败,这是不可接受的。
  • git checkout stash -- .应用所有跟踪的更改(良好),但也将它们暂存(不可接受),并且不会从存储中恢复未跟踪的文件(不可接受)。藏匿处仍然存在(很好——我可以git stash drop)。
  • git merge --squash --strategy-option=theirs stash可能会因冲突而失败,这是不可接受的,即使没有冲突,它也不会从存储中恢复未跟踪的文件(不可接受)。
  • git stash && git stash pop stash@{1} && git stash pop(尝试以相反的顺序应用变更集)可能会因冲突而失败,这是不可接受的。

但是我发现了一组命令可以满足我们的要求:

# Stash what we actually want to commit
git stash
# Unstash the original dirty tree including any untracked files
git stash pop stash@{1}
# Replace the current index with that from the stash which contains only what we want to commit
git read-tree stash
# Drop the temporary stash of what we want to commit (we have it all in working tree now)
git stash drop

为了减少输出,并浓缩为一行:

git stash --quiet && git stash pop --quiet stash@{1} && git read-tree stash && git stash drop --quiet

据我所知,唯一不能恢复的是在索引中添加然后从工作树中删除的文件(它们最终会被添加并呈现)和在索引中重命名的文件和然后从工作树中删除(相同的结果)。出于这个原因,我们需要在初始存储之前查找与这两种情况匹配的文件,git status -z | egrep -z '^[AR]D' | cut -z -c 4- | tr '\0' '\n'然后在恢复后循环并删除它们。

git stash --keep-index --include-untracked显然,如果工作树有任何未跟踪的文件或未暂存的更改,您应该只运行初始。要检查这一点,您可以git status --porcelain | egrep --silent '^(\?\?|.[DM])'在脚本中使用测试。

我相信这比现有的答案要好——它不需要任何中间变量(除了树是否脏,以及在恢复存储后需要删除哪些文件的记录),具有更少的命令和不需要为了安全而关闭垃圾收集。有中间藏匿处,但我认为这正是它们的用途。

这是我当前的预提交钩子,它完成了提到的所有事情:

#!/bin/sh

# Do we need to tidy up the working tree before tests?
# A --quiet option here doesn't actually suppress the output, hence redirection.
git commit --dry-run >/dev/null
ret=$?
if [ $ret -ne 0 ]; then
    # Nothing to commit, perhaps. Bail with success.
    exit 0
elif git status --porcelain | egrep --silent '^(\?\?|.[DM])'; then
    # There are unstaged changes or untracked files
    dirty=true

    # Remember files which were added or renamed and then deleted, since the
    # stash and read-tree won't restore these
    #
    # We're using -z here to get around the difficulty of parsing
    # - renames (-> appears in the string)
    # - files with spaces or doublequotes (which are doublequoted, but not when
    #   untracked for unknown reasons)
    # We're not trying to store the string with NULs in it in a variable,
    # because you can't do that in a shell script.
    todelete="$(git status -z | egrep -z '^[AR]D' | cut -z -c 4- | tr '\0' '\n')"
else
    dirty=false
fi

if $dirty; then
    # Tidy up the working tree
    git stash --quiet --keep-index --include-untracked
    ret=$?

    # Abort if this failed
    if [ $ret -ne 0 ]; then
        exit $ret
    fi
fi

# Run tests, remember outcome
make precommit
ret=$?

if $dirty; then
    # Restore the working tree and index
    git stash --quiet && git stash pop --quiet stash@{1} && git read-tree stash && git stash drop --quiet
    restore_ret=$?

    # Delete any files which had unstaged deletions
    if [ -n "$todelete" ]; then
        echo "$todelete" | while read file; do
            rm "$file"
        done

        # Abort if this failed
        if [ $restore_ret -ne 0 ]; then
            exit $restore_ret
        fi
    fi
fi

# Exit with the exit status of the tests
exit $ret

欢迎任何改进。

于 2014-11-01T00:56:34.577 回答
4
$ git config gc.auto 0   # safety play
$ INDEX=`git write-tree`
$ git add -f .
$ WORKTREE=`git write-tree`
$ git read-tree $INDEX
$ git checkout-index -af
$ git clean -dfx
$ # your tests here
$ git read-tree $WORKTREE
$ git checkout-index -af
$ git clean -dfx
$ git read-tree $INDEX
$ git config --unset gc.auto
$ # you're back.

git clean-x的联机帮助页相当省略地建议了这个解决方案

于 2012-12-15T17:55:03.123 回答
0

这是迄今为止我想出的最好的解决方案。它适用于 git 钩子pre-commitcommit-msg. 当有以下情况时,我已经对其进行了测试:

  • 只有阶段性的变化
  • 只有未分阶段的更改
  • 分阶段和非分阶段的变化
  • 没有变化

在所有三种情况下,它都能正常工作。最大的缺点是它非常hacky,创建和删除临时提交和存储。

#!/bin/bash

git commit -m "FOR_COMMIT_MSG_HOOK" -n
commit_status=$?
# git stash save always returns 0, even if it
# failed creating a stash. So compare the new and old git stash list
# output to see if a stash was created
current_stash_count="$(git stash list)"
git stash save -q -u "FOR_COMMIT_MSG_HOOK"
new_stash_count="$(git stash list)"

echo "##### Running tests #####"
# put testing code here


if [[ $commit_status -eq 0 ]]; then
    git reset --soft 'HEAD~'
fi

if [[ "$current_stash_count" != "$new_stash_count" ]]; then
  git stash pop -q
fi

exit $result
于 2012-12-16T00:48:27.570 回答