如何使用存储删除然后恢复非索引更改,而不会在git中发生合并冲突?

时间:2012-12-15 04:15:49

标签: git bash

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

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~回滚提交,然后弹出存储。然而,这既麻烦,我不确定在预提交钩子中运行是多么安全。

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

3 个答案:

答案 0 :(得分:7)

和你一样,我跑

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

欢迎任何改进。

答案 1 :(得分: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.

-x的git clean联机帮助页相当于椭圆形表示此解决方案

答案 2 :(得分:0)

这是迄今为止我发现的最佳解决方案。它适用于git hook 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