在 filter-branch 之后删除无用的合并(那些没有任何 'non-mainline' 提交的合并)

Remove useless merges (those without any 'non-mainline' commits) after filter-branch

我已经执行了 git filter-branch --index-filter 'git rm --cached --ignore-unmatched badfiles/ badfiles2/' --prune-empty(根据 here)删除了一堆文件,准备将剩余的文件移动到另一个存储库。 --prune-empty 摆脱了任何由此产生的空提交,但它不会对合并起作用,这是有道理的。

现在这个特定回购的历史看起来很丑陋,有一堆合并实际上没有添加任何东西,还有一些合并只是其他合并的合并,实际上没有添加任何更改(在重写的历史中; 他们可能 'useful' 在 filter-branch 之前)。

考虑这个带注释的片段(用 git log --graph --oneline --shortstat 生成):

*   575e3b5 Merge pull request #68 from chris/feature # KEEP THIS MERGE!
|\  
| * 5dbc3f1 Actual feature changes
| |  2 files changed, 2 insertions(+), 2 deletions(-)
| * 35abc98 Cleanup/prep
|/  
|    2 files changed, 22 insertions(+), 16 deletions(-)
*   c3b3d86 Merge pull request #46 from org/topic_branch-mods # USELESS-C
|\  
* \   892de05 Merge pull request #47 from org/topic_branch # USELESS-B
|\ \  
| |/  
|/|   
| *   e738d4b Merge branch 'master' into topic_branch # USELESS-A
| |\  
| |/  
|/|   
* | 4182dac CommitMsg #40 #SQUASHED-PR
| |  2 files changed, 15 insertions(+), 6 deletions(-)
* | 3b42762 CommitMsg
|/  
|    2 files changed, 29 insertions(+), 14 deletions(-)
* c4e62ba CommitMsg
|  2 files changed, 39 insertions(+), 16 deletions(-)
* c2bb13f CommitMsg
   4 files changed, 241 insertions(+)

我想将其缩短为(显然使用不同的 ID):

*   575e3b5 Merge pull request #68 from chris/feature # KEEP THIS MERGE!
|\  
| * 5dbc3f1 Actual feature changes
| |  2 files changed, 2 insertions(+), 2 deletions(-)
| * 35abc98 Cleanup/prep
|/  
|    2 files changed, 22 insertions(+), 16 deletions(-) 
* 4182dac CommitMsg #40 #SQUASHED-PR
|  2 files changed, 15 insertions(+), 6 deletions(-)
* 3b42762 CommitMsg
|  2 files changed, 29 insertions(+), 14 deletions(-)
* c4e62ba CommitMsg
|  2 files changed, 39 insertions(+), 16 deletions(-)
* c2bb13f CommitMsg
   4 files changed, 241 insertions(+)

所以我想摆脱 'USELESS' 合并,它们都是“empty”合并(没有合并更改),但我想保留与 also-'empty' 关联的 history/grouping KEEP 合并在顶部,将这些提交组合成一个 'changeset'.

或者看传统的简化侧向历史中的另一个例子:

A -- B -- C -- D   ==>  A -- B --- D'
 \----\--/   /                \-E-/
       \----E 

我已经尝试过删除 'empty' 合并(如 this)的解决方案,但是那些删除 all 空合并的解决方案,我想保留 'useful' 示例中显示的空合并...

据我所知,'useless' 空合并不包含任何未一直到历史记录中的 left/top 的提交。有没有办法彻底过滤掉那些? 我想我什至不知道如何 describe/define 那些...

请注意,给出的示例有意简单化。对于它的价值,在历史的后期,这个 repo 看起来像这样,我想修剪所有这些:

*   3d37e42 Merge pull request #239 from jim/topic-dev
|\  
| *   05eaf9e Merge pull request #7 from org/master
| |\  
| |/  
|/|  
* |   1576482 Merge pull request #193 from john/master
|\ \  
| * \   187100e Merge branch 'master' of github.com:org/repo into master
| |\ \  
| * \ \   067cc55 Merge branch 'master' of github.com:org/repo into master
| |\ \ \  
| * \ \ \   a69e3d2 Merge branch 'master' of github.com:org/repo into master
| |\ \ \ \  
| | |/ / /  
* | | | |   0ce6813 Merge pull request #212 from jim/feature
|\ \ \ \ \  
| | |_|_|/  
| |/| | |   
| * | | |   0f5352e Merge pull request #5 from org/master
| |\ \ \ \  
| |/ / / /  

这是问题的核心:

I guess I don't really even know how to describe/define those...

Git 的核心是一个图形操作程序,旨在构建 DAG(有向无环图),其中图形中的每个节点都是一个提交。每个提交都携带源快照作为一种数据有效负载这一事实与此过程无关。 (这当然与 Git 最终变得 有用 高度相关。)

您想使用现有的(post-过滤)DAG 并构建一个不同的 DAG。您需要定义一种算法,用于将不需要的 DAG 转换为需要的 DAG。你不一定要用git filter-branch来实现转换,但是如果你打算这样做,你将不得不进一步细化这个转换成一个与"so-far"知识一起工作的算法:它可以查看 filter-branch 提议复制的提交的当前提交哈希 ID。那是在 $GIT_COMMIT 中。它可以读取该提交(使用 Git 管道命令),并且可以使用 shell 函数 map 从其他已复制的提交中找到映射,如 the git filter-branch documentation 中所述.

我也不太清楚如何定义"useful merge"。不过,我认为最明显的算法是不(至少不直接)适合 filter-branch 的算法:它是一种迭代松弛算法,您从完整的图形开始并反复提取合并节点,将它们的父节点连接到它们的父节点子节点,只要这些节点没有用。 (定义 无用 仍然取决于您。)最后,您有一个要保留的节点列表和要删除的节点列表。 该列表对您为 filter-branch 编写的过滤器很有用:您现在可以 运行 git filter-branch 使用 --commit-filter 运行s git commit-tree 像往常一样,或提供的 skip_commit 功能如文档中所述。 "keep" 或 "skip" 取决于您使用松弛算法生成的列表。

好的,我不认为这很完美,但它确实解决了这个特殊情况下的问题;在某些情况下,它 相当 尽可能多地清理,但如果有人感兴趣,这是一个步骤:

git filter-branch --commit-filter '
if ! git rev-parse --verify "$GIT_COMMIT^2" 1>/dev/null 2>&1 ||
  [ "$(git log --no-merges "$GIT_COMMIT^2" "^$GIT_COMMIT^1" --oneline | wc -l)" -gt 0 ];
then
  #echo take $GIT_COMMIT >&2
  # Pick one:
  git_commit_non_empty_tree "$@" # Drop empty commits
  #git commit-tree "$@" # Keep empty commits
else
  #echo "breakup $GIT_COMMIT ($*)" >&2
  skip_commit "" "" "" # (quietly) only keep the first parent
fi' -f HEAD

如果 1) 提交没有第二个 parent(git rev-parse returns 如果引用的提交 ($GIT_COMMIT^2) 不存在则出错)或 2) 第二个 parent ($GIT_COMMIT^2) 包含第一个 parent ($GIT_COMMIT^1) 不包含的提交(参见 here),保留提交(如果它是 not-empty;如果你想保留空的,请使用 git commit-tree); 如果第二个parent存在并且没有添加任何有用的东西,我们跳过提交,并且有意只通过第一个parent-I 不确定这是 'legit',但它从历史记录中删除了第二个 parent,它在我的案例中有效...(请参阅下面的注意事项)

来自bottom-up:

*   575e3b5 Merge pull request #68 from chris/feature # KEEP THIS MERGE!
|\  
| * 5dbc3f1 Actual feature changes
| |  2 files changed, 2 insertions(+), 2 deletions(-)
| * 35abc98 Cleanup/prep
|/  
|    2 files changed, 22 insertions(+), 16 deletions(-)
*   c3b3d86 Merge pull request #46 from org/topic_branch-mods # USELESS-C
|\  
* \   892de05 Merge pull request #47 from org/topic_branch # USELESS-B
|\ \  
| |/  
|/|   
| *   e738d4b Merge branch 'master' into topic_branch # USELESS-A
| |\  
| |/  
|/|   
* | 4182dac CommitMsg #40 #SQUASHED-PR
| |  2 files changed, 15 insertions(+), 6 deletions(-)
* | 3b42762 CommitMsg
|/  
|    2 files changed, 29 insertions(+), 14 deletions(-)
* c4e62ba CommitMsg
|  2 files changed, 39 insertions(+), 16 deletions(-)
* c2bb13f CommitMsg
   4 files changed, 241 insertions(+)

它保留了 SQUASHED-PR 之前的所有内容(请注意,提交 ID 4182dac 和 parent 会保留,因为它们的历史没有改变)。它决定 USELESS-A 应该留在 b/c 它的第二个 parent (4182dac) 包含提交它的第一个 parent (c4e62ba) 做了 not 包含,but 然后它查看 USELESS-B,其第二个 parent(包括 USELESS-A) 没有 添加任何有用的东西,所以它删除了它(同样,包括 USELESS-A)。然后USELESS-C就没用了,就掉了,KEEP第二个parent有'something useful',所以保留了下来。所以你结束于:

*   63b4d39 Merge pull request #68 from chris/feature # KEEP THIS MERGE!
|\  
| * 9a5570d Actual feature changes
| |  2 files changed, 2 insertions(+), 2 deletions(-)
| * a251317 Cleanup/prep
|/  
|    2 files changed, 22 insertions(+), 16 deletions(-) 
* 4182dac CommitMsg #40 #SQUASHED-PR
|  2 files changed, 15 insertions(+), 6 deletions(-)
* 3b42762 CommitMsg
|  2 files changed, 29 insertions(+), 14 deletions(-)
* c4e62ba CommitMsg
|  2 files changed, 39 insertions(+), 16 deletions(-)
* c2bb13f CommitMsg
   4 files changed, 241 insertions(+)

重要注意事项

  • 这只适用于只有两个分支的简单历史,因为我们 明确地 通过 "" "" "" 在这种情况下离开 "" "",否则将包含在 "$@" 中。如果你有多个 parents(或者更确切地说,如果你的 commit 有多个 parents),你必须调整它以解决这个问题;应该不会太难,但我现在不是为了假设而修复它 - 你可能想选择特定的 parents 来删除,idk.
  • 如果 USELESS-A 在合并到 USELESS-B 之前有一个 'useful' 提交(那么可以说这不会是无用的),USELESS-A不是得到pruned/dropped,所以你可能还是有些丑。
  • 可能在其他情况下这不起作用或可以改进。如果您发现任何建议,请在评论中添加建议(像往常一样)!