如何查找所有未合并的提交,按创建它们的分支进行分组,这些提交都在主分支中?

18

我需要从未合并的分支创建一些代码审查。

在寻找解决方案时,请不要考虑本地分支的问题,因为它将在服务器上运行;只会有一个 origin 远程库,我将始终在其他命令之前运行 git fetch origin 命令,并且当涉及到分支时,我们将参考 origin/branch-name

如果设置很简单,每个从主干派生的分支都可以继续自己的方式,我们可以直接运行:

git rev-list origin/branch-name --not origin/master --no-merges

对于每个未合并的分支,将生成的提交添加到每个分支的每个代码审查中。

问题在于当两三个分支之间有合并且仍在某些分支上继续工作时。正如我所说,对于每个分支,我都想编写程序创建代码审查,我不希望在多个审查中包含一个提交。

主要问题在于找到每个提交的原始分支。
或者更简单地说... 找到所有未合并的提交,按它们很可能被创建的分支进行分组。

让我们关注一个简单的例子:

      *    b4 - branch2's head
   *  |    a4 - branch1's head
   |  *    b3
   *  |    merge branch2 into branch1
*  |\ |    m3 - master's head
|  * \|    a3
|  |  |
|  |  *    b2
|  *  |    merge master into branch1
* /|  |    m2
|/ |  *    merge branch1 into branch2
|  * /|    a2
|  |/ |
|  |  *    b1
|  | /
|  |/
| /|
|/ |
|  *       a1
* /        m1
|/
|
*          start

我希望获得的是:

  • 分支1:a1,a2,a3,a4
  • 分支2:b1,b2,b3,b4

目前我找到的最佳解决方案是运行:

git show-branch --topo-order --topics origin/master origin/branch1 origin/branch2

并解析结果:

* [master] m3
 ! [branch1] a4
  ! [branch2] b4
---
  + [branch2] b4
  + [branch2^] b3
 +  [branch1] a4
 ++ [branch2~2] b2
 -- [branch2~3] Merge branch 'branch1' into branch2
 ++ [branch2~4] b1
 +  [branch1~2] a3
 +  [branch1~4] a2
 ++ [branch1~5] a1
*++ [branch2~5] m1

输出的解释如下:

  1. n 行是所分析的 n 个分支
  2. 一行用 ---- 分隔
  3. 每个提交记录都有一行,如果该提交记录在第 n 个分支上,则在第 n 个缩进字符上显示加号(或者在合并提交记录的情况下显示减号)。
  4. 最后一行是所有分支所分叉于的共同祖先(合并基)

对于第三点,提交记录名称的解析始于分支名称。从我所看到的情况来看,此分支与创建提交记录的分支相对应,可能是通过推广由第一父级到达的路径而创建的。

由于我不关心合并提交,因此我将忽略它们。

然后我将解析每个分支-路径-提交记录,以获取它们的哈希值,并使用 rev-parse 处理此情况。

我应该如何处理这种情况?


4个回答

18

可以使用--mirror来克隆仓库,这将创建一个裸仓库,可用作原始仓库的镜像,并且可以使用git remote update --prune进行更新,在此之后应删除所有标签以启用此功能。

我是这样实现的:
1. 获取未合并到主分支的分支列表

git branch --no-merged master

2. 对于每个分支,获取该分支中而不在主分支中的修订版本列表。

git rev-list branch1 --not master --no-merges
如果列表为空,则从分支列表中删除该分支。
对于每个版本,确定原始分支。
git name-rev --name-only revisionHash1

并匹配正则表达式^([^\~\^]*)([\~\^].*)?$。第一个模式是分支名称,第二个是分支的相对路径。
如果找到的分支名称不等于初始分支,请从列表中删除修订版本。

最后,我得到了一个分支列表,对于每个分支,还有一系列提交记录的列表。


经过更多的bash研究,可以使用一行代码完成所有操作:

git rev-list --all --not master --no-merges | xargs -L1 git name-rev | grep -oE '[0-9a-f]{40}\s[^\~\^]*'

结果是以某种形式输出

hash branch

可以读取、解析、排序、分组或进行其他操作。


4
如果我理解你的问题空间,你可以使用--sha1-name,然后通过git-what-branch运行提交,以列出你感兴趣的内容。使用以下命令:

git show-branch --topo-order --topics --sha1-name origin/master origin/branch1 origin/branch2

git-what-branch:发现提交位于哪个分支上,或者如何到达命名分支。这是来自Seth RobertsonPerl脚本。之后将报告格式化以适应你的需求。

git-what-branch 已经是一个外部工具。我对构建自己的非常简单的工具/命令很感兴趣。 - Alex P
1
那就fork它,然后添加你想要的东西。;-) 或者,使用它所提供的功能,并将你需要的内容移植到你自己的工具中。 - bubba

4
这个问题没有正确答案,因为它过于不明确。
Git历史实际上是一个有向无环图(DAG),通常无法确定两个任意节点之间的语义关系,除非节点被充分标记。除非您可以保证示例图中的提交消息遵循可靠的机器可解析模式,否则提交未充分标记——在没有其他上下文(例如,保证开发人员遵循某些最佳实践)的情况下,无法自动识别您感兴趣的提交。
以下是我的一个例子。您说提交与相关联,但仅通过查看示例图的节点无法确定这一点。可能曾经您的示例库历史记录如下:
      *    merge branch1 into branch2 - branch2's head
      |\
     _|/
    / *    b1
   |  |
   |  |
  _|_/
 / |
|  *       a1
* /        m1
|/
|
*          start - master's head

请注意,上面的图中甚至还不存在branch1。上述图可能是由以下事件序列引起的:
  1. 在共享仓库中,branch2start 处被创建。
  2. 用户#1 在本地的 branch2 分支上创建了 a1
  3. 同时,用户#2 在本地的 branch2 分支上创建了 m1b1
  4. 用户#1 推送他/她的本地 branch2 分支到共享仓库,导致共享仓库中的 branch2 引用指向 a1
  5. 用户#2 尝试将他/她的本地 branch2 分支推送到共享仓库,但由于非快进错误而失败(branch2 当前指向 a1,无法快进到 b1)。
  6. 用户#2 运行 git pull,将 a1 合并到 b1 中。
  7. 出于某种莫名其妙的原因,用户#2 运行 git commit --amend -m "merge branch1 into branch2"
  8. 用户#2 推送,共享仓库的历史记录最终看起来像上面的 DAG。

过了一段时间后,用户#1 从 a1 创建了分支 branch1,并创建了 a2。与此同时,用户#2 将 m1 快进合并到了 master 分支,导致以下提交历史:

      *    merge a1 into b1 - branch2's head
   *  |\   a2 - branch1's head
   | _|/
   |/ *    b1
   |  |
   |  |
  _|_/
 / |
|  *       a1
* /        m1 - master's head
|/
|
*          start

假设这个事件序列在技术上是可能的(虽然不太可能),那么人类甚至Git如何告诉您哪些提交“属于”哪个分支?

解析合并提交消息

如果您能保证用户不更改合并提交消息(始终接受Git默认值),并且Git从来没有也永远不会更改默认合并提交消息格式,那么合并提交的提交消息可以用作提示,表示 a1branch1上开始。 您将需要编写一个脚本来解析提交消息-没有简单的Git一行命令可以为您完成此操作。

如果合并总是有意的

或者,如果您的开发人员遵循最佳实践(每次合并都是有意的,并且旨在引入具有不同名称的分支,从而得到一个没有 git pull创建的那些愚蠢的合并提交 的存储库),并且您不感兴趣已经完成的子分支的提交,则您感兴趣的提交位于第一父路径上。 如果您知道哪个分支是正在分析的分支的父分支,则可以执行以下操作:

git rev-list --first-parent --no-merges parent-branch-ref..branch-ref

该命令列出了可从branch-ref到达的提交的SHA1标识符,但排除了可从parent-branch-ref到达的提交以及从子分支合并的提交。
在您上面的示例图中,假设父级顺序由您的注释确定,而不是由进入合并提交的行的顺序确定,则git rev-list --first-parent --no-merges master..branch1将按顺序打印提交a4、a3、a2和a1的SHA1标识符(如果要相反的顺序,请使用--reverse),而git rev-list --first-parent --no-merges master..branch2将再次按顺序打印提交b4、b3、b2和b1的SHA1标识符。
如果分支具有明确的父/子关系
如果您的开发人员未遵循最佳实践,并且您的分支充斥着由git pull(或等效操作)创建的那些愚蠢的合并,但您具有明确的父/子分支关系,则编写执行以下算法的脚本可能适用于您:
  1. Find all commits reachable from the branch of interest excluding all commits from its parent branch, its parent's parent branch, its parent's parent's branch, etc., and save the results. For example:

    git rev-list master..branch1 >commit-list
    
  2. Do the same for all child, grandchild, etc. branches of the branch of interest. For example, assuming branch2 is considered to be a child of branch1:

    git rev-list ^master ^branch1 branch2 >commits-to-filter-out
    
  3. Filter out the results of step #2 from the results of step #1. For example:

    grep -Fv -f commits-to-filter-out commit-list
    
这种方法的问题在于一旦子分支合并到其父分支中,即使在子分支上继续开发,那些提交也被认为是父分支的一部分。尽管从语义上讲这是有道理的,但它并不能产生你所说的想要的结果。
一些最佳实践:
以下是一些最佳实践,可以使未来解决这个特定问题更容易。大多数情况下,如果不是全部情况,都可以通过在共享存储库中巧妙使用钩子来强制执行这些最佳实践。
  1. 每个分支只能有一个任务,禁止多个任务。
  2. 绝不允许在子分支合并到父分支后继续开发。合并意味着任务已完成,没有其他的事情需要做了。预期问题的答案如下:
    • 问:如果我在子分支中发现了一个错误怎么办?答:从父分支开始新建一个分支。不要在子分支上继续开发。
    • 问:如果新功能还没有完成怎么办?答:那你为什么要合并分支?也许你合并了一个完整的子任务;如果是这样,剩余的子任务应该在其自己的分支上进行。不要在子分支上继续开发。
  3. 禁止使用 git pull
  4. 除非所有子分支都已合并到父分支中,否则不得将子分支合并到其父分支中。
  5. 如果分支没有任何子分支,请考虑在合并之前将其重新基于父分支进行变基,使用--no-ff选项。如果它有子分支,你仍然可以进行变基,但请保留子分支的--no-ff合并(这比应该更棘手)。
  6. 经常将父分支合并到子分支中,以便更容易解决合并冲突。
  7. 避免直接将祖父分支合并到其孙分支中——先将其合并到子分支中,然后再将子分支合并到孙分支中。
如果你的所有开发者都遵循这些规则,那么只需简单地:
git rev-list --first-parent --no-merges parent-branch..child-branch

您只需要查看该分支上所做的提交,减去其子分支上所做的提交。


这是一个想法,但可悲的是,我与之合作的开发人员并没有遵循最佳实践。 - Alex P
@AlexandruPătrănescu:很遗憾,Git没有绝对可靠的方法来自动确定合并是否从不同的“真实”分支(应该被排除的提交)带来了提交,而不是由git pull或等效操作创建的愚蠢分支(应包括提交)。这需要人类判断,因此您必须手动硬编码异常(例如,通过使用git notes注释提交)。 - Richard Hansen

3
我建议按照你描述的方法进行操作。但是我会先对git log --format="%H:%P:%s" ^origin/master origin/branch1 origin/branch2的输出进行处理,以便更好地遍历树形结构。
  1. 从输出中构建正确的树形结构,并标记父子节点。
  2. 从头部开始遍历(使用 git rev-parse 获取它们的SHA)。将每个提交与其来自的头部名称和距离进行标记。
    • 对于非第一父步骤(合并的另一部分),将距离增加100。
    • 如果遇到合并提交,请检查它关于哪个分支合并到了哪里的信息。在跟随两个父链接时使用此信息:如果您要转到的分支的解析名称与当前HEAD不匹配,则将距离添加10000。
    • 对于双方的父级:您现在知道它们的名称。将它们所有的第一父级子项添加到字典中:commit -> known-name
  3. 取出已知命名提交的字典,并从下向上遍历树(向子项而非父项移动)。从合并到分支的距离中减去10000。在此过程中,不要到达您不是第一父级的提交,并在到达分支点(有两个子项的提交)或其中一个你的分支头部时停止。

现在对于每个提交,您将有一个距离值列表(可能为负数),用于指示该提交最可能创建在哪个分支上。对于每个提交,距离最小的分支是该提交最可能创建的分支。

如果您有时间,可以遍历整个历史记录,然后减去主分支的历史记录 - 如果您的分支以前被合并到主分支中,则这可能会得出稍微更好的结果。


我忍不住写了一个Python脚本来实现我的描述。但是有一个变化:每次正常步骤,距离不会增加,而是减少。这样做的效果是在合并点之后生存时间较长的分支更受青睐,我个人更喜欢这种方法。这里是代码: https://gist.github.com/Chronial/5275577

使用方法:只需运行 git-annotate-log.py ^origin/master origin/branch1 origin/branch2 检查结果的质量(将输出带有注释的git日志树形结构)。


感谢@Chronial为这个问题所做的努力。虽然不完全符合我的需求,但赏金归你,因为你确实帮助了我解决了赏金所设定的问题。 - Alex P
谢谢,但请注意:我的Python脚本与name-rev的功能相同,只是以更复杂的方式实现,可以适用于更多情况。例如:在feature1上有8个提交 - 在第4个提交时,您分支出feature2并在该分支上进行了1个提交。您的方法将把前4个提交从feature1分配给feature2 - Chronial

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接