在Git上找到分支点?


453

我有一个包含分支机构master和A以及两者之间大量合并活动的存储库。当基于master创建分支A时,如何在存储库中找到提交?

我的存储库基本上如下所示:

-- X -- A -- B -- C -- D -- F  (master) 
          \     /   \     /
           \   /     \   /
             G -- H -- I -- J  (branch A)

我正在寻找修订版A,但git merge-base (--all)找不到。


Answers:


497

我在寻找相同的东西,但我发现了这个问题。谢谢您的询问!

但是,我发现我在这里看到的答案似乎并不能完全满足您所要求的(或我一直在寻找的)答案,它们似乎是在G提交而不是A提交。

因此,我创建了以下树(按时间顺序分配的字母),以便进行测试:

A - B - D - F - G   <- "master" branch (at G)
     \   \     /
      C - E --'     <- "topic" branch (still at E)

这看起来与您的有所不同,因为我想确保我(参考此图,不是您的图)是B,但不是A(也不是D或E)。这是附加在SHA前缀和提交消息上的字母(可以从这里克隆我的repo ,如果任何人都感兴趣的话):

G: a9546a2 merge from topic back to master
F: e7c863d commit on master after master was merged to topic
E: 648ca35 merging master onto topic
D: 37ad159 post-branch commit on master
C: 132ee2a first commit on topic branch
B: 6aafd7f second commit on master before branching
A: 4112403 initial commit on master

所以,目标:二分找到B。经过一番修补,我发现了以下三种方法:


1.在视觉上,使用gitk:

您应该在视觉上看到像这样的树(从主视图看):

gitk screen capture from master

或此处(从主题查看):

gitk screen capture from topic

在这两种情况下,我都选择了B图形中的提交。单击它之后,它的完整SHA就会显示在图形下方的文本输入字段中。


2.外观,但从终端:

git log --graph --oneline --all

(编辑/旁注:添加--decorate也可能很有趣;它添加了分支名称,标记等的指示。由于下面的输出不能反映其用法,因此不将其添加到上面的命令行中。)

显示(假设git config --global color.ui auto):

output of git log --graph --oneline --all

或者,以纯文本形式:

* a9546a2从主题合并回主目录
| \  
| * 648ca35将母版合并到主题中
| | \  
| * | 132ee2a在主题分支上的第一次提交
* | | 将master合并到主题后,e7c863d在master上提交
| | /  
| / |   
* | 在主节点上的37ad159分支后提交
| /  
* 6aafd7f在分支之前对主机进行第二次提交
* 4112403主控上的初始提交

无论哪种情况,我们都将6aafd7f提交视为最低的公共点,即B在我的图形中还是A在您的图形中。


3.带壳魔术:

您无需在问题中指定是要上述内容,还是要通过一个命令获取一个修订版本的命令,而没有其他要求。好吧,这是后者:

diff -u <(git rev-list --first-parent topic) \
             <(git rev-list --first-parent master) | \
     sed -ne 's/^ //p' | head -1
6aafd7ff98017c816033df18395c5c1e7829960d

您也可以将其放入〜/ .gitconfig中(请注意:尾部的破折号很重要;感谢Brian引起注意)

[alias]
    oldest-ancestor = !zsh -c 'diff -u <(git rev-list --first-parent "${1:-master}") <(git rev-list --first-parent "${2:-HEAD}") | sed -ne \"s/^ //p\" | head -1' -

这可以通过以下命令行(用引号引起来)完成:

git config --global alias.oldest-ancestor '!zsh -c '\''diff -u <(git rev-list --first-parent "${1:-master}") <(git rev-list --first-parent "${2:-HEAD}") | sed -ne "s/^ //p" | head -1'\'' -'

注:zsh可以很容易地一直bash,但sh不会工作-在<()语法不存在香草sh。(再次感谢@conny,在此页面上对另一个答案的评论中让我意识到了这一点!)

注意:以上的替代版本:

由于liori用于指出在比较相同分支当上述可以落下,并想出替代的diff形式其去除从所述混合物中的sed形式,并且使这种“安全”的(即,它返回一个结果(即,最新提交),即使您将master与master进行比较):

作为.git-config行:

[alias]
    oldest-ancestor = !zsh -c 'diff --old-line-format='' --new-line-format='' <(git rev-list --first-parent "${1:-master}") <(git rev-list --first-parent "${2:-HEAD}") | head -1' -

从外壳:

git config --global alias.oldest-ancestor '!zsh -c '\''diff --old-line-format='' --new-line-format='' <(git rev-list --first-parent "${1:-master}") <(git rev-list --first-parent "${2:-HEAD}") | head -1'\'' -'

因此,在我的测试树中(暂时无法使用,很抱歉;它又回来了),现在可以同时在master和topic上使用(分别提供提交G和B)。liori,再次感谢您提供其他表格。


因此,这就是我[和liori]提出的。它似乎为我工作。它还允许使用其他可能方便使用的别名:

git config --global alias.branchdiff '!sh -c "git diff `git oldest-ancestor`.."'
git config --global alias.branchlog '!sh -c "git log `git oldest-ancestor`.."'

快乐的混蛋!


5
感谢lindes,shell选项非常适合需要查找长期运行的维护分支的分支点的情况。当您寻找过去可能要提交一千次的修订版时,视觉选项确实不会削减它。* 8')
Mark Booth 2012年

4
在第三个方法中,您依赖于上下文将显示第一行不变的行。在某些情况下或者如果您碰巧有稍微不同的要求(例如,我只需要一个历史记录是--first-parent),并且在有时可能使用相同方法的脚本中使用此方法,则不会发生这种情况两侧都有分支)。我发现使用diff'if-then-else'模式更安全,并且从输出中删除更改/删除的行,而不是依靠足够大的上下文diff --old-line-format='' --new-line-format='' <(git rev-list …) <(git rev-list …)|head -1
liori 2012年

3
git log -n1 --format=format:%H $(git log --reverse --format=format:%H master..topic | head -1)~也能发挥作用,我认为
托比亚斯·舒尔特

4
@JakubNarębski:您是否有办法为该树git merge-base --fork-point ...提供提交B(commit 6aafd7f)?当您发布该消息时,我正忙于其他内容,听起来不错,但我终于尝试了一下,但没有使它正常工作……我得到的是最新的消息,或者只是无声的失败(没有错误)消息,但退出状态1),试图论证一样git co master; git merge-base --fork-point topicgit co topic; git merge-base --fork-point mastergit merge-base --fork-point topic master(对于结帐)等有什么我做错了或丢失?
林德斯2014年

25
@JakubNarębski@lindes --fork-point基于reflog,因此仅当您在本地进行更改时,它才起作用。即使这样,reflog条目也可能已过期。这很有用,但一点都不可靠。
Daniel Lubarov 2015年

131

您可能正在寻找git merge-base

git merge-base在两次提交之间找到最佳的共同祖先,以用于三向合并。如果另一个共同祖先是前一个共同祖先,则它比另一个共同祖先更好。没有更好的共同祖先的共同祖先就是最好的共同祖先,即合并基础。请注意,一对提交可以有多个合并基础。


10
另请注意--allgit merge-base” 的选项
JakubNarębski,2009年

10
这并不能回答原始问题,但是大多数人会问一个更简单的问题,这就是答案:)
FelipeC 2012年

6
他说他不需要git merge-base的结果
Tom Tanner

9
@TomTanner:我只是查看了问题历史记录,对原始问题进行了编辑,以便git merge-base在发布答案大约五个小时后添加该笔记(可能是针对我的回答)。不过,我将保留此答案,因为它对于通过搜索找到此问题的其他人可能仍然有用。
Greg Hewgill

我相信您可以使用merge-base --all的结果,然后使用merge-base --is-ancestor在返回提交列表中进行筛选,以从列表中找到最老的公共祖先,但这不是最简单的方法。
Matt_G

42

我已经习惯git rev-list了这种事情。例如,(请注意3个点)

$ git rev-list --boundary branch-a...master | grep "^-" | cut -c2-

将吐出分支点。现在,它并不完美;由于您已将母版合并到分支A中两次,因此将拆分出几个可能的分支点(基本上是原始分支点,然后是母版合并到分支A中的每个点)。但是,它至少应该缩小可能性。

我已经将该命令添加为别名~/.gitconfig

[alias]
    diverges = !sh -c 'git rev-list --boundary $1...$2 | grep "^-" | cut -c2-'

所以我可以这样称呼它:

$ git diverges branch-a master

注意:这似乎是分支上的第一次提交,而不是共同的祖先。(即,根据原始问题中的图形给出G而不是给出A。)我有一个答案,该答案A将在当前发布。
lindes 2011年

@lindes:在我尝试过的每种情况下,它都会给共同的祖先。你有没有的例子吗?
mipadi

是。在我的答案中(您可以克隆到仓库的链接;git checkout topic然后使用topic代替branch-a它运行),它列出648ca357b946939654da12aaf2dc072763f3caee37ad15952db5c065679d7fc31838369712f0b338- 37ad159和和648ca35都在当前分支的祖先(后者是的当前HEAD topic)中,但是分支发生之前也没有意义。你有什么不同吗?
林德斯

@lindes:我无法克隆您的存储库(可能是权限问题?)。
mipadi

哎呀,对不起!谢谢你让我知道。我忘了运行git update-server-info。现在走应该很好。:)
lindes 2011年

31

如果您喜欢简洁的命令,

git rev-list $(git rev-list --first-parent ^ branch_name master | tail -n1)^^! 

这是一个解释。

以下命令为您提供了创建branch_name之后在master中进行的所有提交的列表

git rev-list --first-parent ^ branch_name主 

由于您只关心最早的那些提交,因此需要输出的最后一行:

git rev-list ^ branch_name --first-parent主| 尾-n1

根据定义,最早提交而不是“ branch_name”的祖先的父级 “ branch_name”中,并且在“ master”中,因为它是“ master”中某个事物的祖先。因此,您在两个分支中都拥有最早的提交。

命令

git rev-list提交^^!

只是显示父提交参考的一种方法。你可以用

git log -1提交^

管他呢。

PS:我不同意祖先顺序无关紧要的说法。这取决于您想要什么。例如,在这种情况下

_C1___C2_________主
  \ \ _XXXXX_分支A(X表示主节点和A之间的任意交叉)
   \ _____ / B支

最好将C2作为“分支”提交输出。这是开发人员从“ master”分支出来的时候。当他分支时,分支“ B”甚至没有合并到他的分支中!这就是本文中提供的解决方案。

如果您想要的是最后一个提交C,使得从原点到分支“ A”上的最后一个提交的所有路径都经过C,那么您想忽略祖先顺序。这纯粹是拓扑结构,可让您有了一个想法,因为当您同时有两个版本的代码时。那时您将使用基于合并的方法,并且在我的示例中它将返回C1。


3
到目前为止,这是最干净的答案,让我们将其投票至最高。建议的编辑:git rev-list commit^^!可以简化为git rev-parse commit^
罗素·戴维斯

这应该是答案!
2014年

2
这个答案很好,我只是替换为git rev-list --first-parent ^branch_name mastergit rev-list --first-parent branch_name ^master因为如果master分支为0,则比另一个分支(可快速转发到它)提前提交,则不会创建任何输出。对于我的解决方案,如果master严格位于前面(即分支已完全合并),则不会创建任何输出,这就是我想要的。
MichaelSchmeißer2014年

3
除非我完全缺少某些东西,否则这将无法工作。示例分支中的两个方向都有合并。听起来您尝试将其考虑在内,但我相信这将导致您的答案失败。git rev-list --first-parent ^topic master只会将您带回到最后一次合并mastertopic(如果存在)的第一次提交。
2015年

1
@jerry你是正确的,这个答案是垃圾;例如,如果刚刚发生了后合并(将master合并为主题),并且master之后没有新提交,则第一个git rev-list --first-parent命令完全不输出任何内容。
TamaMcGlinn

19

鉴于该线程中的许多答案都没有给出问题所要求的答案,因此这里是每个解决方案结果的摘要,以及我用来复制问题中给定存储库的脚本。

日志

创建具有给定结构的存储库,我们得到以下内容的git日志:

$ git --no-pager log --graph --oneline --all --decorate
* b80b645 (HEAD, branch_A) J - Work in branch_A branch
| *   3bd4054 (master) F - Merge branch_A into branch master
| |\  
| |/  
|/|   
* |   a06711b I - Merge master into branch_A
|\ \  
* | | bcad6a3 H - Work in branch_A
| | * b46632a D - Work in branch master
| |/  
| *   413851d C - Merge branch_A into branch master
| |\  
| |/  
|/|   
* | 6e343aa G - Work in branch_A
| * 89655bb B - Work in branch master
|/  
* 74c6405 (tag: branch_A_tag) A - Work in branch master
* 7a1c939 X - Work in branch master

我唯一的补充是标记,它使我们可以清楚地了解创建分支的点以及因此希望查找的提交。

有效的解决方案

唯一有效的解决方案是lindes提供的解决方案正确返回A

$ diff -u <(git rev-list --first-parent branch_A) \
          <(git rev-list --first-parent master) | \
      sed -ne 's/^ //p' | head -1
74c6405d17e319bd0c07c690ed876d65d89618d5

正如Charles Bailey指出的那样,这种解决方案非常脆弱。

如果你branch_Amaster,然后合并masterbranch_A中间没有提交,然后lindes'解决方案只为您提供最新的第一divergance

这意味着对于我的工作流程,我认为我将不得不坚持对长期运行的分支的分支点进行标记,因为我无法保证以后可以可靠地找到它们。

这实际上归结为git缺乏hg所谓的分支机构。博客jhw 在他的文章《为什么我比Git更喜欢Mercurial》和他的后续文章More On Mercurial vs. Git(带有Graphs!)中将这些世系家庭联系起来。我建议人们阅读它们,看看为什么一些信奉宗教的信徒错过了命名的分支机构git

无效的解决方案

mipadi提供的解决方案返回两个答案,I并且C

$ git rev-list --boundary branch_A...master | grep ^- | cut -c2-
a06711b55cf7275e8c3c843748daaa0aa75aef54
413851dfecab2718a3692a4bba13b50b81e36afc

Greg Hewgill提供的解决方案返回I

$ git merge-base master branch_A
a06711b55cf7275e8c3c843748daaa0aa75aef54
$ git merge-base --all master branch_A
a06711b55cf7275e8c3c843748daaa0aa75aef54

Karl提供的解决方案返回X

$ diff -u <(git log --pretty=oneline branch_A) \
          <(git log --pretty=oneline master) | \
       tail -1 | cut -c 2-42
7a1c939ec325515acfccb79040b2e4e1c3e7bbe5

剧本

mkdir $1
cd $1
git init
git commit --allow-empty -m "X - Work in branch master"
git commit --allow-empty -m "A - Work in branch master"
git branch branch_A
git tag branch_A_tag     -m "Tag branch point of branch_A"
git commit --allow-empty -m "B - Work in branch master"
git checkout branch_A
git commit --allow-empty -m "G - Work in branch_A"
git checkout master
git merge branch_A       -m "C - Merge branch_A into branch master"
git checkout branch_A
git commit --allow-empty -m "H - Work in branch_A"
git merge master         -m "I - Merge master into branch_A"
git checkout master
git commit --allow-empty -m "D - Work in branch master"
git merge branch_A       -m "F - Merge branch_A into branch master"
git checkout branch_A
git commit --allow-empty -m "J - Work in branch_A branch"

我怀疑git版本对此有什么影响,但是:

$ git --version
git version 1.7.1

感谢Charles Bailey向我展示了一种更紧凑的方式编写示例存储库脚本。


2
Karl的解决方案很容易解决:diff -u <(git rev-list branch_A) <(git rev-list master) | tail -2 | head -1。感谢您提供创建存储库的说明:)
FelipeC 2012年

我认为您的意思是“ Karl提供的解决方案的清理变体返回X”。原来效果很好,只是丑陋:-)
Karl

不,您的原件不能正常工作。当然,这种变体的效果更差。但是添加选项--topo-order可以使您的版本工作:)
FelipeC 2012年

@felipec-请参阅我对Charles Bailey答案的最终评论。las,我们的聊天室(以及所有的旧注释)现在已被删除。我会在有空的时候尝试更新我的答案。
Mark Booth 2012年

有趣。我有点假设拓扑是默认的。愚蠢的我:-)
2012年

10

通常,这是不可能的。在分支历史中,分支和合并在分支命名分支之前发生,并且两个命名分支的中间分支看起来相同。

在git中,分支只是历史部分提示的当前名称。他们确实没有很强的身份。

这通常不是什么大问题,因为两个提交的merge-base(请参阅Greg Hewgill的答案)通常更有用,给出两个分支共享的最新提交。

在分支机构的历史记录中某个时刻已完全集成分支机构的情况下,依靠提交的父级顺序的解决方案显然行不通。

git commit --allow-empty -m root # actual branch commit
git checkout -b branch_A
git commit --allow-empty -m  "branch_A commit"
git checkout master
git commit --allow-empty -m "More work on master"
git merge -m "Merge branch_A into master" branch_A # identified as branch point
git checkout branch_A
git merge --ff-only master
git commit --allow-empty -m "More work on branch_A"
git checkout master
git commit --allow-empty -m "More work on master"

如果已经进行了合并合并而其父级相反,则该技术也会失败(例如,使用临时分支将测试合并到master中,然后快速转发到feature分支中以进一步构建)。

git commit --allow-empty -m root # actual branch point
git checkout -b branch_A
git commit --allow-empty -m  "branch_A commit"
git checkout master
git commit --allow-empty -m "More work on master"
git merge -m "Merge branch_A into master" branch_A # identified as branch point
git checkout branch_A
git commit --allow-empty -m "More work on branch_A"

git checkout -b tmp-branch master
git merge -m "Merge branch_A into tmp-branch (master copy)" branch_A
git checkout branch_A
git merge --ff-only tmp-branch
git branch -d tmp-branch

git checkout master
git commit --allow-empty -m "More work on master"


3
谢谢Charles,您已经说服了我,如果我想知道分支最初的分叉点,我将不得不对其进行标记。我真的希望它git具有hg与命名分支相同的名称,它将使管理长期维护的分支变得如此容易。
Mark Booth

1
“在git中,分支只是历史部分提示的当前名称。它们的确没有很强的身份。”这是一件令人恐惧的事情,使我确信我需要更好地了解Git分支-谢谢(+ 1)
dumbledad

在分支历史中,分支和合并在分支命名分支之前发生,并且两个命名分支的中间分支看起来相同。是的 +1。
user1071847

5

怎么样

git log --pretty=oneline master > 1
git log --pretty=oneline branch_A > 2

git rev-parse `diff 1 2 | tail -1 | cut -c 3-42`^

这可行。这确实很麻烦,但这是我发现的唯一看起来确实可以完成工作的东西。
GaryO 2010年

1
等同于Git别名:(diverges = !bash -c 'git rev-parse $(diff <(git log --pretty=oneline ${1}) <(git log --pretty=oneline ${2}) | tail -1 | cut -c 3-42)^'无临时文件)
conny 2010年

@conny:哦,哇-我从来没有见过<(foo)语法...,它非常有用,谢谢!(仅供参考,也可以在zsh中使用。)
lindes

这似乎给了我分支上的第一次承诺,而不是共同的祖先。(即,根据原始问题中的图表,它给出的是G而不是A。)不过,我认为我已经找到了答案,该答案将在当前发布。
lindes 2011年

除了执行'git log --pretty = oneline'之外,您还可以执行'git rev-list',然后也可以跳过剪切,而且,这会给父提交带来分歧点,所以尾巴-2 | 头1.所以:diff -u <(git rev-list branch_A) <(git rev-list master) | tail -2 | head -1
FelipeC

5

当然,我缺少一些东西,但是IMO,上述所有问题都是由于我们一直试图找到历史上的分支点而引起的,并且由于可用的合并组合而导致了各种各样的问题。

相反,我采用了不同的方法,因为两个分支共享很多历史记录,而分支之前的所有历史记录都完全相同,因此100%相同,因此,我的提议不是从前开始,而是从第一个开始提交),寻找两个分支中的第一个区别。分支点将简单地是找到的第一个差异的父级。

在实践中:

#!/bin/bash
diff <( git rev-list "${1:-master}" --reverse --topo-order ) \
     <( git rev-list "${2:-HEAD}" --reverse --topo-order) \
--unified=1 | sed -ne 's/^ //p' | head -1

它解决了我所有的常见情况。当然有边境的不包括在内,但是... ciao :-)


diff <(git rev-list“ $ {1:-master}” --first-parent)<(git rev-list“ $ {2:-HEAD}” --first-parent)-U1 | 尾巴-1
亚历山大·伯德

我发现它更快(2-100倍):comm --nocheck-order -1 -2 <(git rev-list --reverse --topo-order topic) <(git rev-list --reverse --topo-order master) | head -1
Andrei Neculau


4

经过大量的研究和讨论,很明显没有万能的子弹可以在所有情况下都起作用,至少在当前版本的Git中不行。

这就是为什么我写了几个补丁来添加tail分支概念的原因。每次创建分支时,也会创建指向原始点的指针,tail ref。每当分支重新建立基础时,此引用都会更新。

要找出devel分支的分支点,您所要做的就是使用 devel@{tail},就是这样。

https://github.com/felipec/git/commits/fc/tail


1
可能是唯一稳定的解决方案。您是否看到这可能会混入git中?我没有看到拉取请求。
2014年

@AlexanderKlimetschek我没有发送补丁,我也不认为这些补丁会被接受。但是,我尝试了另一种方法:“ update-branch”钩子,其功能非常相似。默认情况下,这种方式Git不会执行任何操作,但是您可以启用该挂钩以更新tai​​l分支。虽然您不会有devel @ {tail},但是使用tails / devel并不是很糟糕。
FelipeC 2014年

3

这是我先前的回答先前的回答的改进版本。它依靠来自合并的提交消息来查找分支的首次创建位置。

它适用于此处提到的所有存储库,我什至还解决了邮件列表产生的一些棘手问题。我也为此编写了测试

find_merge ()
{
    local selection extra
    test "$2" && extra=" into $2"
    git rev-list --min-parents=2 --grep="Merge branch '$1'$extra" --topo-order ${3:---all} | tail -1
}

branch_point ()
{
    local first_merge second_merge merge
    first_merge=$(find_merge $1 "" "$1 $2")
    second_merge=$(find_merge $2 $1 $first_merge)
    merge=${second_merge:-$first_merge}

    if [ "$merge" ]; then
        git merge-base $merge^1 $merge^2
    else
        git merge-base $1 $2
    fi
}

3

以下命令将显示提交A的SHA1

git merge-base --fork-point A


如果父子分支之间没有中间合并,则不会这样做。
nawfal

2

我似乎很高兴

git rev-list branch...master

您得到的最后一行是分支上的第一次提交,因此,只需获得该分支的父代即可。所以

git rev-list -1 `git rev-list branch...master | tail -1`^

似乎对我有用,不需要diff等(这很有帮助,因为我们没有该版本的diff)

更正:如果您在master分支上,则此方法不起作用,但是我在脚本中执行此操作,因此问题不大


2

要从分支点查找提交,可以使用它。

git log --ancestry-path master..topicbranch

在给定的示例中,此命令对我不起作用。请您提供什么作为提交范围的参数?
JesperRønn-Jensen2014年

2

有时这实际上是不可能的(某些情况下您可能会幸运地拥有更多数据),并且此处的解决方案将行不通。

Git不保留引用历史记录(包括分支)。它仅存储每个分支(头)的当前位置。这意味着随着时间的流逝,您可能会丢失git中的某些分支历史记录。例如,无论何时分支,它都会立即丢失哪个分支是原始分支。分支机构所做的全部是:

git checkout branch1    # refs/branch1 -> commit1
git checkout -b branch2 # branch2 -> commit1

您可能假设第一个提交到的是分支。情况往往如此,但并非总是如此。完成上述操作后,没有什么可以阻止您首先提交任何一个分支。另外,不能保证git时间戳是可靠的。直到您承诺两者都真正在结构上成为分支。

虽然在图中,我们倾向于在概念上对提交进行编号,但是当提交树分支时,git没有真正稳定的序列概念。在这种情况下,您可以假设数字(指示顺序)是由时间戳确定的(将所有时间戳设置为相同时,看看git UI如何处理事物很有趣)。

这是人类在概念上的期望:

After branch:
       C1 (B1)
      /
    -
      \
       C1 (B2)
After first commit:
       C1 (B1)
      /
    - 
      \
       C1 - C2 (B2)

这是您实际得到的:

After branch:
    - C1 (B1) (B2)
After first commit (human):
    - C1 (B1)
        \
         C2 (B2)
After first commit (real):
    - C1 (B1) - C2 (B2)

您将假设B1是原始分支,但实际上它实际上可能是一个死分支(有人签出-b但从未提交)。直到您都承诺在git中获得合法的分支结构后,您才能:

Either:
      / - C2 (B1)
    -- C1
      \ - C3 (B2)
Or:
      / - C3 (B1)
    -- C1
      \ - C2 (B2)

您始终知道C1早于C2和C3,但您永远无法可靠地知道C2早于C3还是C3早于C2(因为您可以将工作站上的时间设置为任何时间)。B1和B2也会产生误导,因为您不知道哪个分支先出现。在许多情况下,您都可以做出非常好且通常准确的猜测。这有点像比赛跑道。一般情况下,所有事物都与赛车相同,那么您可以假设一辆落后一圈的赛车开始了落后一圈的比赛。我们也有非常可靠的约定,例如master几乎总是代表寿命最长的分支,尽管可悲的是,我看到的情况甚至不是这样。

此处给出的示例是历史保存示例:

Human:
    - X - A - B - C - D - F (B1)
           \     / \     /
            G - H ----- I - J (B2)
Real:
            B ----- C - D - F (B1)
           /       / \     /
    - X - A       /   \   /
           \     /     \ /
            G - H ----- I - J (B2)

这里的真实也令人误解,因为我们人类从左到右阅读,从根到叶(参考)。Git不会那样做。git在(A <-B或B-> A)中执行(A-> B)。它从ref读取到根。引用可以在任何地方,但往往是叶子,至少对于活动分支而言。ref指向一个提交,并且提交仅包含其父母(而不是其孩子)的赞。当提交是合并提交时,它将有多个父级。第一父级始终是合并到其中的原始提交。其他父项始终是已合并到原始提交中的提交。

Paths:
    F->(D->(C->(B->(A->X)),(H->(G->(A->X))))),(I->(H->(G->(A->X))),(C->(B->(A->X)),(H->(G->(A->X)))))
    J->(I->(H->(G->(A->X))),(C->(B->(A->X)),(H->(G->(A->X)))))

这不是一个非常有效的表示,而是git可以从每个ref(B1和B2)获取的所有路径的表达。

Git的内部存储看起来更像这样(不是作为父级的A出现两次):

    F->D,I | D->C | C->B,H | B->A | A->X | J->I | I->H,C | H->G | G->A

如果您转储原始的git commit,您将看到零个或多个父字段。如果为零,则表示没有父级,并且提交是一个根(实际上可以有多个根)。如果存在,则表示没有合并,而且不是根提交。如果有多个,则表示该提交是合并的结果,而第一个之后的所有父级都是合并提交。

Paths simplified:
    F->(D->C),I | J->I | I->H,C | C->(B->A),H | H->(G->A) | A->X
Paths first parents only:
    F->(D->(C->(B->(A->X)))) | F->D->C->B->A->X
    J->(I->(H->(G->(A->X))) | J->I->H->G->A->X
Or:
    F->D->C | J->I | I->H | C->B->A | H->G->A | A->X
Paths first parents only simplified:
    F->D->C->B->A | J->I->->G->A | A->X
Topological:
    - X - A - B - C - D - F (B1)
           \
            G - H - I - J (B2)

当两者都命中A时,它们的链将是相同的,然后它们的链将完全不同。第一个提交与另外两个提交的共同点是共同祖先,并且从那里分开。在这里,术语“提交”,“分支”和“引用”之间可能会有一些混淆。实际上,您可以合并一个提交。这就是合并的真正作用。ref只是指向提交,分支仅是文件夹.git / refs / heads中的ref,文件夹的位置决定了ref是分支而不是诸如标签之类的东西。

您失去历史记录的地方是合并将根据情况执行以下两项操作之一。

考虑:

      / - B (B1)
    - A
      \ - C (B2)

在这种情况下,在任一方向上的合并都将创建一个新提交,其中第一个父级为当前签出分支所指向的提交,第二个父级为合并到当前分支中分支的尖端处的提交。由于两个分支的共同祖先必须合并,因此它必须创建一个新的提交。

      / - B - D (B1)
    - A      /
      \ --- C (B2)

此时,D(B1)现在具有来自两个分支(本身和B2)的两组更改。但是,第二个分支没有对B1的更改。如果将更改从B1合并到B2,以便它们被同步化,那么您可能会期望像这样(可以使用--no-ff强制git merge这样做):

Expected:
      / - B - D (B1)
    - A      / \
      \ --- C - E (B2)
Reality:
      / - B - D (B1) (B2)
    - A      /
      \ --- C

即使B1包含其他提交,您也将得到它。只要B2中没有B1没有的更改,两个分支就会合并。它执行快速前进,就像重新设置一样(重新设置也会消耗或线性化历史记录),除了重新设置不同,因为只有一个分支具有更改集,所以不必将一个分支的更改集应用到另一个分支上。

From:
      / - B - D - E (B1)
    - A      /
      \ --- C (B2)
To:
      / - B - D - E (B1) (B2)
    - A      /
      \ --- C

如果您停止在B1上的工作,那么从长远来看,可以很好地保留历史。通常只有B1(可能是主节点)会前进,因此B2在B2历史记录中的位置成功代表了将其合并到B1中的点。这就是git希望您做的,从A分支到B,然后您可以根据需要将A合并到B中,只要累积更改即可,但是将B合并回A时,不希望您继续使用B并进一步。如果您在将分支快速合并到分支中之后继续进行分支工作,则每次都删除B的先前历史记录。在每次快进提交到源然后提交到分支之后,您实际上每次都在创建一个新分支。

         0   1   2   3   4 (B1)
        /-\ /-\ /-\ /-\ /
    ----   -   -   -   -
        \-/ \-/ \-/ \-/ \
         5   6   7   8   9 (B2)

1到3和5到8是结构分支,如果您遵循4或9的历史记录,则会显示出来。git中没有办法知道这个未命名和未引用的结构分支中,哪个属于命名分支和引用分支。结构的末端。您可能会根据此图假定0到4属于B1,4到9属于B2,但是除了4和9之外,我们不知道哪个分支属于哪个分支,我以一种给出的幻想。0可能属于B2,而5可能属于B1。在这种情况下,有16个不同的可能性,每个结构分支都可以属于每个命名分支。

有很多git策略可以解决此问题。您可以强制git merge永远不要快进,并始终创建一个merge分支。保留分支历史记录的一种可怕方法是根据您选择的约定使用标签和/或分支(确实建议使用标签)。我真的不建议在要合并的分支中使用虚拟的空提交。一个非常常见的约定是,除非您想真正关闭分支,否则不要合并到集成分支。人们应该尝试坚持这种做法,否则您将在拥有分支机构这一点上工作。但是,在现实世界中,理想并不总是可行的,这意味着在每种情况下正确地做事都不可行。如果你是


“ Git不会保留引用历史记录”,它会保存,但默认情况下不会保存很长时间。请参阅man git-reflog以及有关日期的部分:“ master@{one.week.ago}的意思是“ master以前在此本地存储库中指向一周前的位置”。或在讨论<refname>@{<date>}man gitrevisions。而core.reflogExpireman git-config
Patrick Mevzek

2

这个问题不是一个解决方案,但是我认为值得一提的是当我拥有一个长期存在的分支机构时所使用的方法:

在创建分支的同时,还创建了一个具有相同名称但带有-init后缀的标记,例如feature-branchfeature-branch-init

(奇怪的是,这是一个很难回答的问题!)


1
考虑设计“分支”概念的愚蠢思想,没有概念在何时何地创建……加上其他建议的解决方案的巨大复杂性-人们试图以这种方式过分智能事情,我想我更喜欢您的解决方案。每次创建分支时,只有它会让人需要记住这一点-git用户经常这样做。另外-我在某处读到“标签”具有“沉重”的惩罚。不过,我认为那是我会做的。
Motti Shneor '19

1

一种简单的方法,可以轻松查看分支点,git log --graph即使用选项--first-parent

例如,取回购接受的答案中

$ git log --all --oneline --decorate --graph

*   a9546a2 (HEAD -> master, origin/master, origin/HEAD) merge from topic back to master
|\  
| *   648ca35 (origin/topic) merging master onto topic
| |\  
| * | 132ee2a first commit on topic branch
* | | e7c863d commit on master after master was merged to topic
| |/  
|/|   
* | 37ad159 post-branch commit on master
|/  
* 6aafd7f second commit on master before branching
* 4112403 initial commit on master

现在添加 --first-parent

$ git log --all --oneline --decorate --graph --first-parent

* a9546a2 (HEAD -> master, origin/master, origin/HEAD) merge from topic back to master
| * 648ca35 (origin/topic) merging master onto topic
| * 132ee2a first commit on topic branch
* | e7c863d commit on master after master was merged to topic
* | 37ad159 post-branch commit on master
|/  
* 6aafd7f second commit on master before branching
* 4112403 initial commit on master

这样更容易!

请注意,如果仓库中有很多分支,您将要指定要比较的2个分支,而不是使用--all

$ git log --decorate --oneline --graph --first-parent master origin/topic

0

问题似乎是要在一侧的两个分支之间找到最新的单提交剪切,而在另一侧找到最早的共同祖先(可能是回购的初始提交)。这符合我对“分支”点的直觉。

记住,用普通的git shell命令计算起来根本不容易,因为git rev-list-我们最强大的工具-无法让我们限制到达提交的路径。我们最接近的是git rev-list --boundary,它可以为我们提供所有“阻塞方式”的提交。(注意:git rev-list --ancestry-path很有趣,但在这里我不做介绍。)

这是脚本:https : //gist.github.com/abortz/d464c88923c520b79e3d。它相对简单,但是由于循环的原因,它的复杂程度足以保证其要旨。

请注意,这里提出的大多数其他解决方案可能无法在所有情况下都基于一个简单的原因:在线性git rev-list --first-parent化历史记录中不可靠,因为可以使用任何一种顺序进行合并。

git rev-list --topo-order另一方面,对于按地形顺序进行提交非常有用-但进行差异比较脆弱:给定图有多种可能的地形排序,因此您要取决于排序的某种稳定性。就是说,strongk7的解决方案在大多数情况下可能效果很好。但是,由于不得不遍历整个回购记录的历史,所以我的速度较慢...两次。:-)


0

以下实现了svn log --stop-on-copy的 git等效项,还可以用于查找分支源。

方法

  1. 前往所有分支机构
  2. 分别为目标分支和其他分支收集mergeBase
  3. git.log并迭代
  4. 停在mergeBase列表中出现的第一次提交时

就像所有河流都流向大海一样,所有分支机构都需要掌握,因此我们发现了看似无关的分支机构之间的合并基础。当我们从分支头经过祖先返回时,由于理论上它应该是该分支的起点,因此可以在第一个潜在的合并基础处停止。

笔记

  • 我还没有尝试过兄弟姐妹和表兄弟之间的分支合并的方法。
  • 我知道必须有更好的解决方案。

详细信息:https : //stackoverflow.com/a/35353202/9950


-1

您可以使用以下命令在branch_a中返回最早的提交,而从master无法访问该提交:

git rev-list branch_a ^master | tail -1

也许有一个额外的完整性检查是的父提交从主竟可达...


1
这行不通。如果branch_a一次合并到master中,然后继续,则该合并中的提交将被视为master的一部分,因此它们不会显示在^ master中。
FelipeC 2012年

-2

您可以检查分支A的引用日志,以查找从其创建的提交以及该分支指向的提交的完整历史记录。Reflog在中.git/logs


2
我认为这通常不会奏效,因为可以删除reflog。而且我也不认为(?)reflog也不会被推送,因此这仅在单回购情况下有效。
GaryO

-2

我相信我已经找到一种处理这里提到的所有极端情况的方法:

branch=branch_A
merge=$(git rev-list --min-parents=2 --grep="Merge.*$branch" --all | tail -1)
git merge-base $merge^1 $merge^2

查尔斯·贝利(Charles Bailey)完全正确,即基于祖先顺序的解决方案仅具有有限的价值。最终,您需要某种“此提交来自分支X”的记录,但该记录已经存在;默认情况下,“ git merge”将使用诸如“将分支'branch_A'合并到master”之类的提交消息,这告诉您第二个父级(commit ^ 2)的所有提交均来自'branch_A',并已合并到第一个父级(commit ^ 1),即“主”。

有了这些信息,您可以找到'branch_A'的第一个合并(这是在'branch_A'真正存在的时候),并找到合并的基础,这将是分支点:)

我已经尝试过Mark Booth和Charles Bailey的存储库,并且该解决方案有效。怎么可能 唯一不起作用的方法是,如果您手动更改了用于合并的默认提交消息,则分支信息确实会丢失。

为了有用:

[alias]
    branch-point = !sh -c 'merge=$(git rev-list --min-parents=2 --grep="Merge.*$1" --all | tail -1) && git merge-base $merge^1 $merge^2'

那你就可以git branch-point branch_A '。

请享用 ;)


4
依赖合并消息比假设父订单脆弱得多。这也不只是一种假设的情况。我经常使用git merge -m什么我已经合并了,而不是potentionally短暂分支的名称(如“合并主线变成XYZ功能重构”)。假设我-m在我的示例中对我的帮助较少?这个问题不能完全解决,因为我可以使用一个或两个临时分支进行相同的历史记录,并且无法分辨出差异。
CB Bailey 2012年

1
@CharlesBailey那是你的问题。您不应从提交消息中删除这些行,而应将其余消息添加到原始消息的下面。如今,“ git merge”会自动打开一个编辑器,供您添加所需的内容,而对于git的旧版本,您可以执行“ git merge --edit”。无论哪种方式,如果您确实想要的话,都可以使用提交钩子为每个提交添加一个“在分支'foo'上提交的”。但是,此解决方案适用于大多数人
FelipeC

2
没有为我工作。branch_A是从已经有很多合并的master分支出来的。这个逻辑没有给我确切的提交哈希值,其中创建了branch_A。
Venkat Kotra 2015年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.