Git树过滤器会在连续的提交中再次放弃更改

时间:2018-11-07 09:22:29

标签: git git-filter-branch git-rewrite-history

我们计划在源存储库中强制执行基于clang格式的样式。我们预料会有一些困难,这就是为什么我们要提供一个make目标来对当前分支执行从其与master的合并库到HEAD分支的重新格式化的原因。

作为简化示例,请考虑以下命令:

git filter-branch -f --tree-filter '
  AFFECTED_FILES=$(git diff-index --diff-filter=AM --name-only $GIT_COMMIT^);
  echo; echo AFFECTED $AFFECTED_FILES;
  for f in $AFFECTED_FILES; do
    echo formatting $f;
    echo foo >> $f;
  done
' HEAD~10..HEAD

我们在多个提交上运行树过滤器(我们仅将其限制为最后几个提交,这已经说明了问题)。我们确定受影响的文件(我们只想触摸在提交中添加或修改的文件)。为简单起见(错误更容易发现),我们在这里不使用clang格式,而只是在每个受影响的文件后附加“ foo”(只需用echo foo >> $f替换clang-format -i $f以获得实际的代码。

它确实正确应用了我们打算进行的更改。但是,在除了第一次提交之外的每个提交中,它都会丢弃我们之前所做的更改。查看提交,假设在some.txt文件中,您在diff中看到“ + foo”。在子提交中,对于some.txt,即使在子提交中根本未修改some.txt,也仅在someother.txt中,diff中会显示“ -foo”。我已经在任意测试存储库上运行了此程序,显示出相同的行为。

我也尝试了以下方法(回到实际的clang格式):

git filter-branch -f --tree-filter 'git clang-format --extensions cpp,h' -- HEAD~10..HEAD

尽管大多数提交看起来确实正确,但第一个提交将修改给定范围内任何提交所涉及的所有文件。我想避免这种情况,并且无论如何都只格式化提交所涉及的文件。

为避免撤消子提交中的更改,我缺少什么?我需要以某种方式更新索引吗?

2 个答案:

答案 0 :(得分:1)

git filter-branch中的树过滤器在每次提交时查看文件的状态,但是在一次提交中更改这些文件对树过滤器查看的下一次提交的文件状态没有影响。这意味着,如果您在git filter-branch调用中仅对一个提交进行了某些更改,则这些更改将不会传播到该提交的子级。这意味着与预重写的提交相比,这些子级的 tree 不会改变,因此看起来会撤消在其重写的父级中引入的自定义更改。

要实现所需的目标,您可能需要考虑使用一组不同的AFFECTED_FILES,例如对diff进行HEAD~10而不是仅由父提交来确保先前重写的文件仍会重新格式化。 (请注意,这并不完美,因为如果文件恢复到HEAD~10中的确切状态,那么它将再次被重新格式化,因此会被忽略,但这可能是一个非常罕见的情况则不值得编码-否则您可以在所有{<1>}操作的基部上包含差异。)

答案 1 :(得分:1)

感谢@CBBailey的快速有用的回复。有了这些信息,我想出了以下解决方案:

git filter-branch -f --tree-filter 'echo;
  PREV=$(map $(git rev-parse $GIT_COMMIT^));
  echo PREV $PREV;
  AFFECTED_FILES=$(git diff --name-only $GIT_COMMIT^..$GIT_COMMIT | egrep "\.(h|cpp)$");
  echo AFFECTED $AFFECTED_FILES;
  PREV_AFFECTED_FILES=$(bash -c "comm -23 <(git diff --name-only HEAD~10..$GIT_COMMIT^ | egrep \"\.(h|cpp)$\" | sort -u) <(echo $AFFECTED_FILES | sort -u)");
  echo PREV_AFFECTED $PREV_AFFECTED_FILES;
  for f in $PREV_AFFECTED_FILES; do
    echo "checking out $f";
    git checkout $PREV -- $f;
  done;
  for f in $AFFECTED_FILES; do
    echo formatting $f;
    clang-format -i $f;
  done
' -- HEAD~10..HEAD

除了受提交影响的文件之外,它还确定在当前提交之前在给定提交范围内受影响的所有文件(PREV_AFFECTED_FILES)。这些文件将被当前提交所触及的文件过滤掉(我们需要在bash中运行此文件,因为filter-branch使用的sh不支持使用<()进行进程替换)。我们使用由filter-branch定义的map函数(请参阅filter-branch documentation“ Filters”部分的最后一段)来确定重写的前任提交(PREV)。然后,从此提交中检出所有先前受影响的文件(这就是为什么我们需要过滤PREV_AFFECTED_FILES以使其不包含来自AFFECTED_FILES的任何文件,否则我们将覆盖所做的更改)。然后格式化当前提交中受影响的文件。使用索引过滤器可能仍然更快。但是,在给定的限制条件下,仅重新格式化已修改的文件并检出先前已修改的文件,这对于我们的用例来说足够快了。

您可以在我们的构建系统中看到最终版本(scriptinvocation)。它包含进一步的改进,例如,使用GNU Parallel来加快文件格式。