Git tree-filter 在连续提交中再次丢弃更改

Git tree-filter discards changes again in consecutive commits

我们计划在源存储库中强制执行基于 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-format,而是简单地将 "foo" 附加到每个受影响的文件(将 echo foo >> $f 替换为 clang-format -i $f 即可需要获取实际代码)。

它确实正确地应用了我们想要的更改。然而,除了第一次提交,它会丢弃我们之前所做的更改。查看提交,假设在文件 some.txt 中,您在 diff 中看到“+foo”。在子提交中,对于 some.txt,您会在差异中看到“-foo”,即使 some.txt 在子提交中根本没有被修改,而只有 someother.txt。我在任意测试回购上有 运行 这个,表现出相同的行为。

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

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

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

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

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

为了实现您想要的效果,您可能需要考虑一组不同的 AFFECTED_FILES,例如针对 HEAD~10 执行 diff 而不仅仅是父提交以确保以前重写的任何文件仍会重新格式化。 (请注意,这并不完美,因为如果一个文件恢复到它在 HEAD~10 中的确切状态,那么它将开始被再次重新格式化,但这可能是一种非常罕见的情况它不值得编码 - 或者你可以包括针对所有父 filter-branch 操作基础的差异。)

感谢@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 的任何文件,否则我们会覆盖我们的更改)。然后格式化当前提交中受影响的文件。使用 index-filter 可能仍然更快。然而,在给定的限制条件下,仅重新格式化修改过的文件并检查以前修改过的文件,这对于我们的用例来说已经足够快了。

您可以在我们的构建系统中看到最终版本(script, invocation). It contains further improvements, e.g., using GNU Parallel 以加速格式化文件。