这个问题没有正确答案,因为它过于不明确。
Git历史实际上是一个有向无环图(DAG),通常无法确定两个任意节点之间的语义关系,除非节点被充分标记。除非您可以保证示例图中的提交消息遵循可靠的机器可解析模式,否则提交未充分标记——在没有其他上下文(例如,保证开发人员遵循某些最佳实践)的情况下,无法自动识别您感兴趣的提交。
以下是我的一个例子。您说提交与相关联,但仅通过查看示例图的节点无法确定这一点。可能曾经您的示例库历史记录如下:
* merge branch1 into branch2 - branch2's head
|\
_|/
/ * b1
| |
| |
_|_/
/ |
| * a1
* / m1
|/
|
* start - master's head
请注意,上面的图中甚至还不存在
branch1
。上述图可能是由以下事件序列引起的:
- 在共享仓库中,
branch2
在 start
处被创建。
- 用户#1 在本地的
branch2
分支上创建了 a1
。
- 同时,用户#2 在本地的
branch2
分支上创建了 m1
和 b1
。
- 用户#1 推送他/她的本地
branch2
分支到共享仓库,导致共享仓库中的 branch2
引用指向 a1
。
- 用户#2 尝试将他/她的本地
branch2
分支推送到共享仓库,但由于非快进错误而失败(branch2
当前指向 a1
,无法快进到 b1
)。
- 用户#2 运行
git pull
,将 a1
合并到 b1
中。
- 出于某种莫名其妙的原因,用户#2 运行
git commit --amend -m "merge branch1 into branch2"
。
- 用户#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从来没有也永远不会更改默认合并提交消息格式,那么合并提交的提交消息可以用作提示,表示 a1
在branch1
上开始。 您将需要编写一个脚本来解析提交消息-没有简单的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
(或等效操作)创建的那些愚蠢的合并,但您具有明确的父/子分支关系,则编写执行以下算法的脚本可能适用于您:
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
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
Filter out the results of step #2 from the results of step #1. For example:
grep -Fv -f commits-to-filter-out commit-list
这种方法的问题在于一旦子分支合并到其父分支中,即使在子分支上继续开发,那些提交也被认为是父分支的一部分。尽管从语义上讲这是有道理的,但它并不能产生你所说的想要的结果。
一些最佳实践:
以下是一些最佳实践,可以使未来解决这个特定问题更容易。大多数情况下,如果不是全部情况,都可以通过在共享存储库中巧妙使用钩子来强制执行这些最佳实践。
- 每个分支只能有一个任务,禁止多个任务。
- 绝不允许在子分支合并到父分支后继续开发。合并意味着任务已完成,没有其他的事情需要做了。预期问题的答案如下:
- 问:如果我在子分支中发现了一个错误怎么办?答:从父分支开始新建一个分支。不要在子分支上继续开发。
- 问:如果新功能还没有完成怎么办?答:那你为什么要合并分支?也许你合并了一个完整的子任务;如果是这样,剩余的子任务应该在其自己的分支上进行。不要在子分支上继续开发。
- 禁止使用
git pull
- 除非所有子分支都已合并到父分支中,否则不得将子分支合并到其父分支中。
- 如果分支没有任何子分支,请考虑在合并之前将其重新基于父分支进行变基,使用
--no-ff
选项。如果它有子分支,你仍然可以进行变基,但请保留子分支的--no-ff
合并(这比应该更棘手)。
- 经常将父分支合并到子分支中,以便更容易解决合并冲突。
- 避免直接将祖父分支合并到其孙分支中——先将其合并到子分支中,然后再将子分支合并到孙分支中。
如果你的所有开发者都遵循这些规则,那么只需简单地:
git rev-list --first-parent --no-merges parent-branch..child-branch
您只需要查看该分支上所做的提交,减去其子分支上所做的提交。