【问题标题】:Git tree-filter discards changes again in consecutive commitsGit 树过滤器在连续提交中再次丢弃更改
【发布时间】:2018-11-07 09:22:29
【问题描述】:

我们计划在源代码库中实施基于 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,您会在 diff 中看到“-foo”,即使在子提交中根本没有修改 some.txt,而只是修改了 someother.txt。我已经在任意测试存储库上运行它,显示相同的行为。

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

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

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

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

【问题讨论】:

  • 您可以添加您的解决方案作为您自己问题的答案。 :)

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


【解决方案1】:

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

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

【讨论】:

  • 或者——更简单,虽然计算成本更高——只需在每次提交中通过 clang-format 运行所有内容。当然,OP 通常排除了这一点,但如果应用于整个存储库,可以追溯到时间的开始,则只需执行一次......
  • @torek:同意,这种方法更适合filter-branch 所做的事情 - 并且擅长。
  • 谢谢@CBBailey。实际上,这确实修复了后续提交中的恢复。但是,这也意味着对于修改越来越多的文件的分支,该过程可能会大大减慢。重复了很多格式化工作。有没有办法访问“新父”提交哈希?然后,我可以简单地签出以前的文件而不运行另一个重新格式化(除非文件在当前提交中直接受到影响,那么我无论如何都会重新格式化)。
  • @timn:我相信map shell 函数在tree-filter 中可用,它肯定在commit-filter 中。如果你追求速度,你应该考虑使用index-filter;这不会为您提供磁盘上的文件,但如果您可以在git cat-file blob :name-in-index | filter-cmd | git hash-object -w --stdin 之类的东西中使用您的过滤器,您可以在git update-index 调用中使用输出对象ID。作为一种优化,您可以教您的过滤器命令将旧对象 id 的离线缓存保留到新对象 id,以便每个相同的输入只处理一次。
  • 再次感谢! map 成功了。请参阅我添加到问题中的解决方案以及指向实际实施的链接。
【解决方案2】:

感谢@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 不支持使用 &lt;() 进行进程替换)。我们使用 filter-branch 定义的 map 函数(参见filter-branch documentation 的 Filters 部分的最后一段)来确定重写的前置提交(PREV)。然后从该提交中检出所有先前受影响的文件(这就是为什么我们需要过滤 PREV_AFFECTED_FILES 以不包含任何来自 AFFECTED_FILES 的文件,否则我们会覆盖我们的更改)。然后格式化当前提交中受影响的文件。使用 index-filter 可能仍然更快。然而,由于给定的限制是只重新格式化修改过的文件并检查以前修改过的文件,这对于我们的用例来说已经足够快了。

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

【讨论】:

    猜你喜欢
    • 2015-09-24
    • 2011-07-03
    • 2022-07-06
    • 1970-01-01
    • 2020-06-01
    • 1970-01-01
    • 2014-06-13
    • 1970-01-01
    • 2013-12-12
    相关资源
    最近更新 更多