【问题标题】:How do I properly git stash/pop in pre-commit hooks to get a clean working tree for tests?如何在预提交挂钩中正确 git stash/pop 以获得干净的工作树进行测试?
【发布时间】:2013-12-27 02:29:17
【问题描述】:

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

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

# Run tests
...

# Restore changes
git stash pop -q

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

  1. 将// Step 1 添加到a.java
  2. git add .
  3. 将// Step 2 添加到a.java
  4. git commit
    1. git stash -q --keep-index # 存储更改
    2. 运行测试
    3. git stash pop -q # 恢复更改

此时我遇到了问题。 git stash pop -q 显然有冲突,而在 a.java 我有

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

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

【问题讨论】:

    标签: git githooks git-stash


    【解决方案1】:

    有——但让我们以稍微迂回的方式到达那里。 (另外,请参阅下面的警告:存储代码中有一个错误,我认为这是非常罕见的,但显然更多的人遇到了。新警告,于 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。)

    【讨论】:

    • 你的回答让我希望有一个很好的 git 信息图/流程图,这样我就可以了解实际发生了什么。仍在阅读并尝试摸索。
    • 有一些,但没有一个是完整的,因为它非常复杂。不过,作为使用 git stash 的替代方法,您可以考虑在您自己创建的分支上,甚至在当前分支上创建自己的两个提交。 (无论你走哪条路,最终都会得到相同的结果。:-))
    • 这就是我的感觉。我很想亲自尝试一下,但就像你说的:太复杂了。不过,多次提交对我来说是不行的。在我们的环境中,每个提交对应于一个单独的代码审查更改。多次更改会导致代码审查的爆炸式增长,尤其是在挂钩中。一般来说,每当您想要查看对更改的小修改时,我们都必须不断修改我们的最后一次更改。
    • @zilupe:git stash 中有一个长期存在的错误,您可能会在这里遇到。您是否将第 5 行更改回 git adding 之前的状态?如果是这样,你就遇到了错误。我在邮件列表中发布了一个修复程序,但它显然被忽略了......
    • @DevinRhode: git stash push 在我写上面的答案时甚至都不存在。此外,git stash 已用 C 重写,并添加了全新的错误:现在您真的应该避免使用git stash。
    【解决方案2】:

    感谢@torek 的回答,我能够编写一个脚本来处理未跟踪的文件。 (注意:由于git stash -u 的不良行为,我不想使用git stash -u)

    提到的git stash 错误保持不变,我还不确定,当 .gitignore 位于更改的文件中时,此方法是否会遇到问题。 (同样适用于@torek 的回答)

    #! /bin/sh
    # script to run tests on what is to be committed
    # Based on http://stackoverflow.com/a/20480591/1606867
    
    # Remember old stash
    old_stash=$(git rev-parse -q --verify refs/stash)
    
    # First, stash index and work dir, keeping only the
    # to-be-committed changes in the working directory.
    git stash save -q --keep-index
    changes_stash=$(git rev-parse -q --verify refs/stash)
    if [ "$old_stash" = "$changes_stash" ]
    then
        echo "pre-commit script: no changes to test"
        sleep 1 # XXX hack, editor may erase message
        exit 0
    fi
    
    #now let's stash the staged changes
    git stash save -q
    staged_stash=$(git rev-parse -q --verify refs/stash)
    if [ "$changes_stash" = "$staged_stash" ]
    then
        echo "pre-commit script: no staged changes to test"
        # re-apply changes_stash
        git reset --hard -q && git stash pop --index -q
        sleep 1 # XXX hack, editor may erase message
        exit 0
    fi
    
    # Add all untracked files and stash those as well
    # We don't want to use -u due to
    # http://blog.icefusion.co.uk/git-stash-can-delete-ignored-files-git-stash-u/
    git add .
    git stash save -q
    untracked_stash=$(git rev-parse -q --verify refs/stash)
    
    #Re-apply the staged changes
    if [ "$staged_stash" = "$untracked_stash" ]
    then
        git reset --hard -q && git stash apply --index -q stash@{0}
    else
        git reset --hard -q && git stash apply --index -q stash@{1}
    fi
    
    # Run tests
    status=...
    
    # Restore changes
    
    # Restore untracked if any
    if [ "$staged_stash" != "$untracked_stash" ]
    then
        git reset --hard -q && git stash pop --index -q
        git reset HEAD -- . -q
    fi
    
    # Restore staged changes
    git reset --hard -q && git stash pop --index -q
    
    # Restore unstaged changes
    git reset --hard -q && git stash pop --index -q
    
    # Exit with status from test-run: nonzero prevents commit
    exit $status
    

    【讨论】:

    • 我创建了一个更复杂的钩子框架,使用这个脚本作为任何感兴趣的人的基础:github.com/schamp/capn-hook
    • > 我建议不推荐使用 capn-hook,因为引入了最优秀的pre-commit。它完成了 capn-hook 应该做的所有事情,而且做得更好。去那里吧,但出于历史目的,我会保留它。
    • 我正在查看 git stash 发行说明...gist.github.com/devinrhode2/1284e8ef0052a5f086b2055e72637d8a 我想这可能在过去的 7 年里已经修复了,所以我会尝试做我想做的事情想从头开始阅读文档,然后将我的答案发回这里。
    【解决方案3】:

    根据 torek 的回答,我想出了一种方法来确保 不使用 使用 git rev-parse 来存储更改的正确行为,而不是我使用 git stash create 和 git stash store (虽然使用 git stash store不是绝对必要的)注意由于我在我的脚本中工作的环境是用php而不是bash编写的

    #!/php/php
    <?php
    $files = array();
    $stash = array();
    exec('git stash create -q', $stash);
    $do_stash = !(empty($stash) || empty($stash[0]));
    if($do_stash) {
        exec('git stash store '.$stash[0]); //store the stash (does not tree state like git stash save does)
        exec('git stash show -p | git apply --reverse'); //remove working tree changes
        exec('git diff --cached | git apply'); //re-add indexed (ready to commit) changes to working tree
    }
    //exec('git stash save -q --keep-index', $stash);
    exec('git diff-index --cached --name-only HEAD', $files );
    
    // dont redirect stderr to stdin, we will get the errors twice, redirect it to dev/null
    if ( PHP_OS == 'WINNT' )
      $redirect = ' 2> NUL';
    else
      $redirect = ' 2> /dev/null';
    $exitcode = 0;
    
    foreach( $files as $file ) {
    
      if ( !preg_match('/\.php$/i', $file ) )
        continue;
    
      exec('php -l ' . escapeshellarg( $file ) . $redirect, $output, $return );
      if ( !$return ) // php -l gives a 0 error code if everything went well
        continue;
    
      $exitcode = 1; // abort the commit
      array_shift( $output ); // first line is always blank
      array_pop( $output ); // the last line is always "Errors parsing httpdocs/test.php"
    
      echo implode("\n", $output ), "\n"; // an extra newline to make it look good
    }
    if($do_stash) {
        exec('git reset --hard -q');
        exec('git stash apply --index -q');
        exec('git stash drop -q');
    }
    exit( $exitcode );
    
    ?>
    

    php 脚本改编自这里http://blog.dotsamazing.com/2010/04/ask-git-to-check-if-your-codes-are-error-free/

    【讨论】:

      【解决方案4】:

      目前看来,这里的大多数答案至少有 5 年的历史。 git stash已经用C重写了,有一些新的bug,我真的不知道它有多可靠。

      我已经使用了几次,它似乎可以正常工作 - 我从头开始编写它,只是使用文档作为指南。尚未经过实战测试。

      # We stash and un-stash changes ourselves.
      #  - If any pre-commit/lint-staged checks fail, any auto-fixes will be lost.
      
      # Create stash
      #   index is the "staging area", so --keep-index means that anything you have already staged will be un-touched.
      # NOTE: we always create a stash - possibly even a totally empty one.
      git stash --keep-index --include-untracked --message="pre-commit auto-stash"
      uncoloredStashedStat=$(git stash show --include-untracked stash@{0})
      [[ $uncoloredStashedStat ]] && {
        echo "Stashed:"
        git diff --stat --staged stash@{0}
      }
      
      lintStagedStatus='failed'
      
      yarn lint-staged --no-stash --concurrent $pre_commit_concurrency --shell "/bin/bash"  && {
        lintStagedStatus='passed'
      }
      
      outputSuppressed=$(git add --intent-to-add "**/*.snap")
      diff=$(git diff)
      [[ $diff ]] && {
        echo "staging modifications from pre-commit scripts:"
        git diff
        git add .
      }
      
      # Pop stash
      #   We always create a stash - so we will always pop it.
      #   Popped stash should generally not cause merge conflicts,
      #   if your editor is formatting+autofixing code on save.
      [[ $uncoloredStashedStat ]] && echo "restoring stash..."
      git stash pop
      
      if test "$lintStagedStatus" != 'passed'; then
        exit 1;
      fi
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2012-07-11
        • 1970-01-01
        • 2019-12-25
        • 2021-02-10
        • 2021-09-10
        • 2022-11-05
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多