【问题标题】:Finding a branch point with Git?用 Git 寻找分支点?
【发布时间】:2010-12-04 09:01:41
【问题描述】:

我有一个包含 master 和 A 分支的存储库,并且两者之间有很多合并活动。基于master创建分支A时,如何在我的存储库中找到提交?

我的存储库基本上是这样的:

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

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

【问题讨论】:

标签: git branch


【解决方案1】:

我一直在寻找同样的东西,我发现了这个问题。谢谢你的提问!

但是,我发现我在这里看到的答案似乎并没有相当给出您要求的(或我正在寻找的)答案——他们似乎给出了G提交,而不是 A 提交。

所以,我创建了以下树(按时间顺序分配的字母),所以我可以测试一下:

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

这看起来和你的有点不同,因为我想确保我得到(参考这张图表,而不是你的)B,而不是 A(而不是 D 或 E)。以下是附加到 SHA 前缀和提交消息的字母(我的 repo 可以从 here 克隆,如果这对任何人都感兴趣的话):

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:

你应该可以看到这样的树(从主视图):

或这里(从主题来看):

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


2。视觉上,但从终端:

git log --graph --oneline --all

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

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

或者,直接文本:

* a9546a2 从主题合并回主 |\ | * 648ca35 合并主到主题 | |\ | * | 132ee2a 第一次提交主题分支 * | |在 master 合并到 topic 后在 master 上提交 e7c863d | |/ |/| * | 37ad159 在 master 上的分支后提交 |/ * 6aafd7f 分支前在 master 上的第二次提交 * 4112403 在 master 上的初始提交

在任何一种情况下,我们都将 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起作用——&lt;() 语法在原版sh 中不存在。 (再次感谢@conny,让我在对此页面上另一个答案的评论中意识到这一点!)

注意:上述的替代版本:

感谢lioripointing out,在比较相同的分支时,上述内容可能会失败,并提出另一种差异形式,从混合中删除 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`.."'

祝你好运!

【讨论】:

  • 感谢 lindes,shell 选项非常适合您想要找到长期运行的维护分支的分支点的情况。当您正在寻找过去可能有一千次提交的修订时,视觉选项确实不会削减它。 *8')
  • 在您的第三种方法中,您依赖于上下文将显示第一行未更改的行。这不会在某些边缘情况下发生,或者如果您碰巧有稍微不同的要求(例如,我只需要一个历史记录是 --first-parent,我在有时可能使用相同的脚本中使用此方法两侧的分支)。我发现使用diff 的 if-then-else 模式并从其输出中删除更改/删除的行而不是指望有足够大的上下文会更安全。作者:diff --old-line-format='' --new-line-format='' &lt;(git rev-list …) &lt;(git rev-list …)|head -1
  • git log -n1 --format=format:%H $(git log --reverse --format=format:%H master..topic | head -1)~ 也可以,我认为
  • @JakubNarębski:你有办法让git merge-base --fork-point ... 为这棵树提交 B(提交 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(用于结帐)等参数。我做错了什么或遗漏了什么?
  • @JakubNarębski @lindes --fork-point 基于 reflog,因此只有在本地进行更改时它才会起作用。即使这样,reflog 条目也可能已经过期。它很有用,但根本不可靠。
【解决方案2】:

您可能正在寻找git merge-base:

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

【讨论】:

  • 还要注意“git merge-base”的--all选项
  • 这并没有回答最初的问题,但大多数人会问一个更简单的问题,这就是答案:)
  • 他说他不想要 git merge-base 的结果
  • @TomTanner:我刚刚查看了问题历史记录,并且在我的答案发布五小时后(可能是为了回应我的答案),对原始问题进行了编辑,以包含关于 git merge-base 的注释。不过,我将保留此答案,因为它可能对通过搜索找到此问题的其他人仍然有用。
  • @sourcedelica - 您在错误的答案中发布了有用的建议。你想要this。谢谢!
【解决方案3】:

我用git rev-list 来处理这类事情。例如,(注意 3 个点)

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

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

我已将该命令添加到我在 ~/.gitconfig 中的别名中:

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

所以我可以这样称呼它:

$ git diverges branch-a master

【讨论】:

  • 注意:这似乎给出了分支上的第一次提交,而不是共同的祖先。 (即,根据原始问题中的图表,它给出G 而不是A。)我有一个得到A 的答案,我将立即发布。
  • @lindes:在我尝试过的每种情况下,它都给出了共同的祖先。你有没有的例子吗?
  • 是的。在my answer(它有一个可以克隆的repo的链接;git checkout topic然后用topic代替branch-a运行它),它列出了648ca357b946939654da12aaf2dc072763f3caee37ad15952db5c065679d7fc31838369712f0b338——两者都是37ad159648ca35 是当前分支的祖先(后者是 topic 的当前 HEAD),但分支发生之前的点也不是。你有什么不一样的吗?
  • @lindes:我无法克隆你的仓库(可能是权限问题?)。
  • 糟糕,对不起!谢谢你让我知道。我忘了运行 git update-server-info。现在去应该不错。 :)
【解决方案4】:

如果你喜欢简洁的命令,

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

这里有一个解释。

以下命令为您提供 master 中在创建 branch_name 后发生的所有提交的列表

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

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

git rev-list ^branch_name --first-parent master | tail -n1

根据定义,不是“branch_name”祖先的最早提交的父是in“branch_name”,并且在“master”中,因为它是“master. "所以你得到了两个分支中最早的提交。

命令

git rev-list commit^^!

只是显示父提交引用的一种方式。你可以使用

git log -1 提交^

或其他。

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

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

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

如果您想要的是最后一次提交 C,使得从起源到分支“A”上的最后一次提交的所有路径都通过 C,那么您想忽略祖先顺序。这纯粹是拓扑的,让您了解从何时开始同时运行两个版本的代码。那时您将使用基于合并的方法,在我的示例中它将返回 C1。

【讨论】:

  • 这是迄今为止最干净的答案,让我们把它投票给顶部。建议编辑:git rev-list commit^^! 可以简化为 git rev-parse commit^
  • 这应该是答案!
  • 这个答案很好,我只是用git rev-list --first-parent branch_name ^master 替换了git rev-list --first-parent ^branch_name master,因为如果主分支在另一个分支之前有0 次提交(可快速转发到它),则不会创建任何输出。使用我的解决方案,如果 master 严格领先(即分支已完全合并),则不会创建输出,这就是我想要的。
  • 这行不通,除非我完全错过了一些东西。示例分支中有两个方向的合并。听起来你试图考虑到这一点,但我相信这会导致你的答案失败。 git rev-list --first-parent ^topic master 只会带你回到最后一次从 master 合并到 topic 后的第一次提交(如果它甚至存在的话)。
  • @jerry 你是对的,这个答案是垃圾;例如,在刚刚发生反向合并的情况下(将 master 合并到 topic 中),并且 master 之后没有新的提交,第一个 git rev-list --first-parent 命令根本不输出任何内容。
【解决方案5】:

目的:此答案测试此线程中提供的各种答案。

测试库

-- X -- A -- B -- C -- D -- F  (master) 
          \     /   \     /
           \   /     \   /
             G -- H -- I -- J  (branch A)
$ 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_A 合并到 master,然后将 master 合并到 branch_A 而不干预提交,那么 lindes 的解决方案只会为您提供最近的第一个分歧

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

这真的归结为gits 缺少hg 所称的命名分支。博主 jhw 在他的文章 Why I Like Mercurial More Than Git 和他的后续文章 More On Mercurial vs. Git (with Graphs!) 中称这些血统 vs. 家庭。我建议人们阅读它们以了解为什么一些善变的皈依者会怀念在git 中没有命名的分支

不正确的解决方案

mipadi提供的解决方案返回两个答案,IC

$ 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 向我展示了一种更紧凑的方式来编写示例存储库的脚本。

【讨论】:

  • Karl 的解决方案很容易解决:diff -u &lt;(git rev-list branch_A) &lt;(git rev-list master) | tail -2 | head -1。感谢您提供创建 repo 的说明 :)
  • 我认为您的意思是“Karl 提供的解决方案的清理变体返回 X”。原版效果很好,只是丑陋:-)
  • 不,您的原件不能正常工作。诚然,这种变化的效果甚至更差。但是添加选项 --topo-order 会使您的版本工作:)
  • @felipec - 请参阅我对Charles Bailey 答案的最终评论。唉,我们的chat(以及所有旧的 cmets)现在已被删除。当我有时间时,我会尝试更新我的答案。
  • 有趣。我有点假设拓扑是默认的。傻我:-)
【解决方案6】:

一般来说,这是不可能的。在分支历史中,在一个命名分支被分支之前的分支合并和两个命名分支的中间分支看起来是一样的。

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

这通常不是一个大问题,因为两个提交的合并基础(请参阅 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,然后快速转发到功能分支以进一步构建),这种技术也会失败。

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"

【讨论】:

  • 谢谢Charles,你说服了我,如果我想知道分支最初分歧的点,我将不得不标记它。我真的希望git 有一个等同于hg 的命名分支,这将使管理长期维护分支so 更容易。
  • “在 git 中,分支只是历史片段提示的当前名称。它们并没有真正的强身份”这句话说起来很吓人,让我相信我需要更好地理解 Git 分支 - 谢谢 (+1)
  • 在分支历史中,在一个命名分支被分支之前的分支合并,两个命名分支的中间分支看起来是一样的。 是的。 +1。
【解决方案7】:

这样的事情怎么样

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`^

【讨论】:

  • 这行得通。这真的很麻烦,但这是我发现的唯一似乎真正能完成这项工作的东西。
  • 等效的 Git 别名:diverges = !bash -c 'git rev-parse $(diff &lt;(git log --pretty=oneline ${1}) &lt;(git log --pretty=oneline ${2}) | tail -1 | cut -c 3-42)^'(没有临时文件)
  • @conny:哦,哇——我从来没有见过
  • 这似乎给了我分支上的第一个提交,而不是共同的祖先。 (即,根据原始问题中的图表,它给出了G 而不是A。)不过,我想我已经找到了答案,我将立即发布。
  • 而不是'git log --pretty=oneline'你可以只做'git rev-list',然后你也可以跳过剪辑,此外,这给了父提交的点分歧,所以只是尾 -2 |头 1. 所以:diff -u &lt;(git rev-list branch_A) &lt;(git rev-list master) | tail -2 | head -1
【解决方案8】:

当然我遗漏了一些东西,但是 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
  • 我发现这更快(2-100x):comm --nocheck-order -1 -2 &lt;(git rev-list --reverse --topo-order topic) &lt;(git rev-list --reverse --topo-order master) | head -1
【解决方案9】:

一种更容易查看git log --graph 中的分支点的简单方法是使用选项--first-parent

例如,从accepted answer 中取repo

$ 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

这样更容易!

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

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

【讨论】:

    【解决方案10】:

    我最近也需要解决这个问题,最后为此编写了一个 Ruby 脚本:https://github.com/vaneyckt/git-find-branching-point

    【讨论】:

    • 它不工作,砂砾在“unpack_object_header_gently”中失败并且没有维护。
    【解决方案11】:

    我似乎得到了一些快乐

    git rev-list branch...master
    

    你得到的最后一行是分支上的第一次提交,所以这是获取其父级的问题。所以

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

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

    更正:如果您在 master 分支上,这不起作用,但我在脚本中执行此操作,所以这不是问题

    【讨论】:

      【解决方案12】:

      经过大量研究和讨论,很明显没有灵丹妙药可以在所有情况下都有效,至少在当前版本的 Git 中不行。

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

      要找出devel分支的分支点,只要使用devel@{tail}就可以了。

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

      【讨论】:

      • 可能是唯一稳定的解决方案。你看看这是否可以进入 git?我没有看到拉取请求。
      • @AlexanderKlimetschek 我没有发送补丁,我认为这些补丁不会被接受。然而,我尝试了一种不同的方法:一个“update-branch”钩子,它做的事情非常相似。默认情况下,这种方式 Git 不会做任何事情,但您可以启用挂钩来更新尾部分支。虽然你不会有 devel@{tail},但使用 tails/devel 也不错。
      【解决方案13】:

      这是我之前的回答 previous answer 的改进版本。它依赖于来自合并的提交消息来查找第一次创建分支的位置。

      它适用于这里提到的所有存储库,我什至解决了spawned on the mailing list 的一些棘手问题。我也为此wrote tests

      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
      }
      

      【讨论】:

        【解决方案14】:

        以下命令将显示 Commit A 的 SHA1

        git merge-base --fork-point A

        【讨论】:

        • 如果父分支和子分支之间有中间合并,则不会。
        • 原始海报指出这不起作用,他正在寻找其他东西。
        【解决方案15】:

        有时它实际上是不可能的(除了一些例外,您可能很幸运地拥有额外的数据)并且这里的解决方案不起作用。

        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 是原始分支,但实际上它可能只是一个死分支(有人执行了 checkout -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 不会那样做。我们在头脑中做 (A->B) 的地方 git 做 (AA)。它从 ref 读取它到 root。 Refs 可以在任何地方,但往往是叶子,至少对于活跃的分支。一个 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 提交,您将看到零个或多个父字段。如果为零,则表示没有父级,并且提交是根(实际上可以有多个根)。如果有,则表示没有合并,也不是根提交。如果有多个,则意味着提交是合并的结果,并且第一个之后的所有父级都是合并提交。

        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 时,它们的链条将相同,在此之前,它们的链条将完全不同。第一个提交,另外两个提交的共同点是共同的祖先,它们从哪里分道扬镳。术语 commit、branch 和 ref 之间可能存在一些混淆。您实际上可以合并提交。这就是合并的真正作用。一个 ref 只是指向一个提交,而一个分支只不过是文件夹 .git/refs/heads 中的一个 ref,文件夹位置决定了一个 ref 是一个分支,而不是诸如标签之类的其他东西。

        你失去历史的地方是合并将根据情况做两件事之一。

        考虑:

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

        在这种情况下,任一方向的合并都会创建一个新提交,其中第一个父级作为当前签出分支指向的提交,第二个父级作为您合并到当前分支的分支尖端的提交.它必须创建一个新的提交,因为两个分支自它们的共同祖先以来都发生了变化,必须合并。

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

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

        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(可能是 master)会前进,因此 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 种不同的可能性,每个结构分支都可以属于哪个命名分支。这是假设这些结构分支都不是来自已删除的分支,或者是从 master 拉取时将分支合并到自身中的结果(两个 repos 上的相同分支名称实际上是两个分支,单独的存储库就像分支所有分支) .

        有许多 git 策略可以解决这个问题。您可以强制 git merge 永远不要快进并始终创建合并分支。保存分支历史的一种可怕方法是根据您选择的某些约定使用标签和/或分支(确实推荐使用标签)。我真的不建议在您要合并的分支中使用虚拟的空提交。一个非常常见的约定是在您想要真正关闭您的分支之前不要合并到集成分支中。这是人们应该尝试遵守的做法,否则您将围绕拥有分支机构的点工作。然而,在现实世界中,理想并不总是具有实际意义,做正确的事并不适用于所有情况。如果您在一个分支上所做的事情是孤立的,那么您可能会遇到这样一种情况,即当多个开发人员在做一件事情时,他们需要快速分享他们的更改(理想情况下,您可能真的想在一个分支上工作,但是并非所有情况都适合任何一方,通常两个人在一个分支上工作是你想要避免的)。

        【讨论】:

        • "Git 不会保留 ref 历史记录" 它会,但不是默认情况下,也不会长时间保留。请参阅man git-reflog 和有关日期的部分:“master@{one.week.ago} 表示“master 曾经在此本地存储库中指向一周前的位置”。或man gitrevisions 中关于&lt;refname&gt;@{&lt;date&gt;} 的讨论。和core.reflogExpireman git-config
        【解决方案16】:

        这个问题的解决方案不太好,但我认为当我有一个长期存在的分支时,我认为值得注意的是我使用的方法:

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

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

        【讨论】:

        • 考虑到设计一个“分支”概念的愚蠢到令人难以置信的愚蠢,而没有任何关于何时何地创建它的概念......再加上其他建议解决方案的巨大复杂性——人们试图摆脱-smart this way-to-to-smart thing,我想我更喜欢你的解决方案。只有在每次创建分支时都需要记住这样做的负担——这是 git 用户经常做的事情。另外-我在某处读到“标签”会受到“沉重”的惩罚。不过,我认为我会这样做。
        • 有没有办法自动化这个?告诉 git 自动为你做这件事的方法?我认为这绝对是最好的方法
        【解决方案17】:

        似乎使用 reflog 解决了这个问题,git reflog &lt;branchname&gt; 显示了分支的所有提交,包括分支创建。

        这是来自在合并回 master 之前有 2 次提交的分支。

        git reflog june-browser-updates
        b898b15 (origin/june-browser-updates, june-browser-updates) june-browser-updates@{0}: commit: sorted cve.csv
        467ae0e june-browser-updates@{1}: commit: browser updates and cve additions
        d6a37fb june-browser-updates@{2}: branch: Created from HEAD
        

        【讨论】:

        • git reflog &lt;branchname&gt; | tail -n 1 | cut -f1 -d' ' 将为您提供分支来自的父级的短哈希
        【解决方案18】:

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

        git log --ancestry-path master..topicbranch
        

        【讨论】:

        • 这个命令在给定的例子中对我不起作用。请问您将提供什么作为提交范围的参数?
        【解决方案19】:

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

        请记住,使用普通的 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 的solution 可能大部分时间都运行良好。但是,由于必须遍历回购的整个历史,因此我的速度较慢...两次。 :-)

        【讨论】:

        • 你的直觉是合理的,但是没有这样的单切(即使是单根)也可以存在历史。考虑线性历史 ABCDEFG、BHIJKG、DLMN 和 IOPN 的并集:头部 G 和 N 在 D 和 I 处完全对称地发散(不考虑父级顺序)。
        【解决方案20】:

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

        接近

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

        就像所有的河流都奔向大海,所有的分支都奔向master,因此我们在看似无关的分支之间找到了merge-base。当我们从分支头穿过祖先返回时,我们可以停在第一个潜在的合并基地,因为理论上它应该是这个分支的起点。

        备注

        • 我没有尝试过这种将兄弟分支和表兄弟分支相互合并的方法。
        • 我知道一定有更好的解决方案。

        详情:https://stackoverflow.com/a/35353202/9950

        【讨论】:

          【解决方案21】:

          您可以检查分支 A 的 reflog 以查找它是从哪个提交创建的,以及该分支指向的提交的完整历史记录。 Reflogs 在.git/logs

          【讨论】:

          • 我认为这通常不起作用,因为可以修剪 reflog。而且我不认为 (?) reflogs 也不会被推送,所以这只适用于单一 repo 的情况。
          【解决方案22】:

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

          git rev-list branch_a ^master | tail -1
          

          也许还有一个额外的健全性检查,该提交的父级实际上可以从主...

          【讨论】:

          • 这不起作用。如果 branch_a 被合并到 master 一次,然后继续,则该合并上的提交将被视为 master 的一部分,因此它们不会显示在 ^master 中。
          【解决方案23】:

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

          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),即 'master'。

          有了这些信息,您可以找到“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'。

          享受;)

          【讨论】:

          • 依赖合并消息比假设父顺序脆弱。这也不仅仅是一种假设的情况。我经常使用git merge -m 来表示我已合并的什么,而不是潜在的临时分支的名称(例如“将主线更改合并到特性 x y z 重构”)。假设在我的示例中我对-m 的帮助不大?这个问题完全无法解决,因为我可以用一两个临时分支创建相同的历史记录,但无法区分。
          • @CharlesBailey 那是你的问题。您不应该从提交消息中删除这些行,您应该将消息的其余部分添加到 below 原始消息中。现在'git merge'会自动打开一个编辑器供你添加你想要的任何东西,对于旧版本的git你可以做'git merge --edit'。无论哪种方式,如果这是您真正想要的,您可以使用提交挂钩为每个提交添加“提交到分支 'foo'”。但是,此解决方案适用于大多数人
          • 对我不起作用。 branch_A 是从已经有很多合并的 master 中分叉出来的。这个逻辑没有给我创建 branch_A 的确切提交哈希。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2014-09-08
          • 1970-01-01
          • 2015-03-22
          • 2022-07-19
          • 2011-12-21
          • 2018-09-12
          相关资源
          最近更新 更多