Git:检查分支是否严格是另一个分支的子孙分支。

4

我看到了一些关于“如何检查分支 X 是否已经rebased到 Y”的问题,但是我没有找到一个特别符合我的要求的。

我想要检查 X 是否是一个“简单地从 Y 派生出来的分支”。严格来说,只允许在 XY 之间有这种模式,而不是在 ZY 之间的模式:

换句话说,如果分支X中的每个提交都是Y的祖先(或等于Y,或者是Y的某个祖先)——而不仅仅是X的顶端提交具有Y作为祖先——那就是我们希望的。这对于帮助人们实现基于rebase的合并模式非常有用,其中X是一个特性分支,而Y是主要开发分支。(如果有人确实知道他们在做什么,并且非strict-ff分支是他们真正想要的,我们可以轻松地授予特殊许可并允许合并。但我们希望他们至少意识到他们正在这样做,但事实并非如此。)
请注意,这与“可以使用--ff-only合并”不同。如果Y是X的后代,即使X的某些祖先既不是Y的后代也不是Y的祖先,git merge --ff-only也会愉快地更新Y以指向X。

我知道我可以循环遍历X的所有祖先,并检查Y是否是每个祖先的祖先(并在到达Y本身时停止,并确保没有具有多个父级的内容),但我想知道Git中是否有内置的更好的方法。

补充:

正如@TTT指出的那样,另一个常见的情况是某人在push --force-with-lease之前pull而不是进行push,从而搞砸了rebase。 我为我们的内部“如何使用Git”文档创建了以下图形,自动化此检查将非常好:


2
这篇文章里面已经解释了为什么,不是吗? - John Kugelman
1
@tymtam,这个想法是帮助自动检查我们在仓库中想要的策略,即人们在合并之前进行变基,以保持历史记录的清洁。对于我们来说,大多数情况下,当人们没有这样做时,是因为他们没有意识到,而不是因为他们有哲学上的分歧。 - Ken Williams
1
@TTT 当然可以!我在我们内部的“如何使用Git”文档中添加了一张图片。=) - Ken Williams
1
在你需要强制推送的情况下,pull.rebase=true 仍会将你的新提交变基到旧的提交上。对于变基工作流程,我的配置偏好是 pull.ff=only,这样如果无法快进合并部分,则拉取失败。(我建议使用此配置,并建议我的团队,在真正需要显式地将变基或合并到上游分支的情况下,可以这样做。) - TTT
1
@TTT:完全同意。我已经完全停止使用git pull,而是使用git fetch后跟随我想要的操作。 - LeGEC
显示剩余6条评论
2个回答

5

检查Y..X中的所有提交是否都是Y的后代的一种方法是使用git log --boundary Y..Xgit rev-list --boundary Y..X检查该范围的边界。

从这个历史记录开始:

$ git log --graph --oneline --all
* 036a9f9 (HEAD -> X) create d.txt
* cadd199 create c.txt
| * 0680934 (Z) Merge commit '22a23fe' into Z
|/| 
| * 22a23fe create b.txt
* | 8dec744 (Y) create a.txt
|/  
* 878ac8b first commit

您将获得:

$ git log --oneline --boundary Y..X
036a9f9 (HEAD -> X) create d.txt
cadd199 create c.txt
- 8dec744 (Y) create a.txt   # <- one single boundary commit, pointing at Y

$ git log --oneline --boundary Y..Z
0680934 (Z) Merge commit '22a23fe' into Z
22a23fe create b.txt
- 8dec744 (Y) create a.txt   # <- two commits on the boundary
- 878ac8b first commit       # <-

一个可编写脚本的方式来检查你是否处于这种情况是:
# 'git rev-list' prints full hashes, boundary commits are prefixed with '-'
boundary=$(git rev-list --boundary Y..X | grep -e '^-')

want=$(git rev-parse Y)
want="-$want"

# the boundary should consist of "-<hash of Y>" only:
if [ "$boundary" = "$want" ]; then
   echo "all commits in X are descendants of Y"
fi

上述代码检查了所有提交是否在“Y”之后。你可能还会遇到以下情况:
* 036a9f9 (HEAD -> X) create d.txt
* 0680934 Merge 'origin/X' into X # <- someone created a merge commit in between
|\
| * cadd199 create c.txt
* | 22a23fe create b.txt
|/
* 8dec744 (Y) create a.txt
* 878ac8b first commit

这也会干扰变基工作流程。

如果您还想排除此选项,请使用git loggit rev-list--merges选项:

# git rev-list also has a --count option, which will output the count
# rather than the complete list of commits
merges=$(git rev-list --count --merges Y..X)
if [ "$merges" -eq 0 ]; then
   echo "all good, no merges between Y and X"
fi

关于--boundary的文档并没有很好地解释什么是“边界提交”。

我认为这个Stack Overflow回答有一个不错的定义:

边界提交是限制修订范围但不属于该范围的提交。例如,修订范围HEAD~3..HEAD包含3个提交(HEAD~2、HEAD~1和HEAD),而提交HEAD~3作为其边界提交。

更正式地说,Git通过从指定的提交开始,并通过父链接获取其他提交来处理修订范围。它在不满足选择条件的提交处停止(因此应该被排除)- 这些就是边界提交。


谢谢,看起来这个可以解决问题!它成功地区分了上面设置中的XZ。我会构建一个检查它的CI/CD作业。 - Ken Williams
更新了"合并"部分,让未来的读者更清楚(而且它看起来也适合您的需求)。 - LeGEC

2

我想你正在寻找类似于(在Bash中)的东西

[[ $(git merge-base X Y) = $(git rev-parse Y) ]] && echo yes || echo no

git merge-base X Y 可以找到两个分支的最佳公共祖先,并打印出它的完整 sha1 值。关于“最佳”,doc 中解释道:

如果一个普通祖先是另一个普通祖先的祖先,则前者比后者更好。没有更好的普通祖先的普通祖先是最佳普通祖先。

在大多数情况下,最佳公共祖先是两个分支离两个头最近的公共提交。

git rev-parse Y 打印出 Y 的头部的 sha1 值。如果最佳公共祖先与 Y 的头部相同,则满足 分支 X 中的每个提交都有 Y 作为祖先(或等于 Y 或是 Y 的祖先之一)。换句话说,Y 的提交集合是 X 的提交集合的子集。

但在实践中,远程仓库中的 Y 可能会被其他人更新,本地的 Y 可能会意外更新。测试将会说不,即使 X 真的是 Y 的后代,在 X 创建后和 Y 更新之前的这段时间内。


1
这很不错!我一开始想的是,查看 git rev-list Z...Y~1 中最后一个列出的项目是否与 git rev-parse Y 相同(在这种情况下确实有效)。类似于 [[ $(git rev-list Z...Y~1 | tail -1) = $(git rev-parse Y) ]] - matt
1
要检查祖先关系,可以使用 git merge-base--is-ancestor 选项:git merge-base --is-ancestor A D(其中 A 是可能的祖先,D 是可能的后代)。没有标准输出,需要检查返回值(或者使用 if ...,或者 ... && something 或者 ... || something)。 - LeGEC
谢谢 - 不过,在我的提交序列示例中,这似乎对于Y->ZY->X都报告为“是”,而我希望它对于Y->Z是“否”。所有X YY XZ YY Zmerge-base都报告与git rev-parse Y相同的哈希。 - Ken Williams
至于“但在实践中…”部分-实际上这应该是没问题的,因为我们的合并请求标准已经会拒绝由于竞争条件而变得无效的合并。 - Ken Williams

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