用Git找到一个分支点?

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

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

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

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

239031 次浏览

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

git merge-base查找两个提交之间要在三路合并中使用的最佳共同祖先。一个共同祖先是更好的,而另一个共同祖先是前者的祖先。没有更好的共同祖先的共同祖先是最佳共同祖先,即合并基础。注意,对于一对提交可以有多个合并基。

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

在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"

如果一个集成合并是反向的父分支,这种技术也会失败(例如,一个临时分支被用来执行一个测试合并到主分支,然后快速跳转到功能分支以进一步构建)。

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"

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

像这样的怎么样

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 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 - B - D - F - G   <- "master" branch (at G)
\   \     /
C - E --'     <- "topic" branch (still at E)

这看起来和你的有点不同,因为我想确保我得到了(指的是这张图,不是你的)B,但不是a(也不是D或E)。这里是SHA前缀和提交消息附加的字母(我的回购可以从在这里克隆,如果有人感兴趣的话):

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:

你应该能看到这样的树(从master上看):

gitk screen capture from master

或者在这里(从主题来看):

gitk screen capture from topic

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


2. 从视觉上看,但从终端来看:

# EYZ0

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

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

output of git log——graph——oneline——all

或者,直接说:

*   a9546a2 merge from topic back to master
|\
| *   648ca35 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

在任何一种情况下,我们都将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将会变成——<()语法在vanilla sh中不存在。(再次感谢你,@conny,在本页另一个答案的评论中让我意识到这一点!)

注:上述备选版本:

多亏了liori for 指出,上面的代码可以在比较相同的分支时掉下来,并提出了一个替代的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)想到的。这似乎对我有用。它还允许额外的两个别名,这可能会证明很方便:

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

git-ing快乐!

这个答案测试了这个线程中给出的各种答案。

测试库

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

正确的解决方案

唯一有效的解决方案是由林德提供的正确返回A:

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

正如查尔斯·贝利指出的那样,这个解决方案非常脆弱。

如果你branch_Amaster,然后合并masterbranch_A没有干预提交,然后林德斯的解决方案只给你最近的第一次分歧

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

这一切都归结为gits缺少hg所称的命名的分支。博主jhw在他的文章为什么我更喜欢Mercurial而不是Git和后续文章更多关于Mercurial vs. Git(有图表!)中称之为血统家庭。我建议人们阅读它们,看看为什么一些反复无常的皈依者错过了git中没有命名的分支

不正确的解决方案

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

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

格雷格Hewgill提供的解决方案返回I

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

卡尔提供的解决方案返回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

感谢查尔斯·贝利为我展示了一种更紧凑的编写示例存储库脚本的方法。

你可以使用下面的命令返回branch_a中最老的提交,master无法访问它:

git rev-list branch_a ^master | tail -1

也许有一个额外的健全性检查,提交的父对象实际上可以从master到达…

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

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'会使用一个提交消息,例如“merge branch 'branch_A' into master”,这告诉你所有来自第二个父分支(commit^2)的提交都来自'branch_A',并合并到第一个父分支(commit^1),也就是'master'。

有了这些信息,你可以找到'branch_A'的第一次合并(这是'branch_A'真正存在的时候),并找到merge-base,这将是分支点:)

我尝试了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”。

喜欢。)

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

它适用于这里提到的所有存储库,我甚至还解决了在邮件列表中生成。我也为这个编写测试

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
}

当然我遗漏了一些东西,但在我看来,以上所有的问题都是因为我们总是试图找到历史上的分支点,这导致了各种各样的问题,因为可用的合并组合。

相反,我采用了一种不同的方法,基于两个分支共享很多历史,分支之前的所有历史都是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

它解决了我所有的常规案件。当然,有些边境地区没有被覆盖,但是…你好:-)

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

如果你喜欢简洁的命令,

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

Here's an explanation.

The following command gives you the list of all commits in master that occurred after branch_name was created

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

因为你只关心那些最早的提交,所以你想要输出的最后一行:

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

The parent of the earliest commit that's not an ancestor of "branch_name" is, by definition, in "branch_name," and is in "master" since it's an ancestor of something in "master." So you've got the earliest commit that's in both branches.

The command

git rev-list commit^^!

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

git log -1 commit^

or whatever.

PS: I disagree with the argument that ancestor order is irrelevant. It depends on what you want. For example, in this case

_C1___C2_______ master
\    \_XXXXX_ branch A (the Xs denote arbitrary cross-overs between master and A)
\_____/ branch B

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

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

我似乎得到了一些快乐

git rev-list branch...master

你得到的最后一行是分支上的第一个提交,所以接下来的问题是获取它的父节点。所以

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

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

更正:如果你在主分支上,这是行不通的,但我在一个脚本中这样做,所以这不是一个问题

经过大量的研究和讨论,很明显没有什么灵丹妙药能在所有情况下都起作用,至少在当前版本的Git中不是这样。

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

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

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

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

git log --ancestry-path master..topicbranch

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

记住,使用普通的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的解决方案可能在大多数情况下工作得很好。然而,它比我的慢,因为必须遍历整个回购历史…两次。: -)

下面实现了等价于SVN log——stop-on-copy的git,也可以用来查找分支的起源。

方法

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

就像所有的河流都流向大海,所有的分支都流向主人,因此我们在看似不相关的分支之间找到了合并基地。当我们从分支头通过祖先返回时,我们可以在第一个潜在的合并基点上停下来,因为理论上它应该是这个分支的原点。

笔记

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

细节:# EYZ0

下面的命令将显示Commit A的SHA1

# EYZ0

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

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)

这里的Real也具有误导性,因为我们人类从左到右,从根到叶(参考)。Git不会这样做。当我们在头脑中做(A->B)的时候,我们会做(A<-B或B->A)。它从ref读入到root。引用可以在任何地方,但往往是叶,至少对于活动的分支是这样。引用指向一个提交,并且提交只包含对它们的父/s的like,而不是对它们的子的like。当一个提交是合并提交时,它将有多个父级。第一个父节点总是合并到的原始提交。其他父节点总是合并到原始提交中的提交。

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提交,你会看到零或多个父字段。如果为0,则表示没有父节点,提交的是根节点(实际上可以有多个根节点)。如果有一个,这意味着没有合并,它不是根提交。如果有多个,则意味着提交是合并的结果,第一个之后的所有父节点都是合并提交。

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之间可能会有一些混淆。实际上,你可以合并一个commit。这就是归并的作用。一个ref只是指向一个提交,而一个分支只不过是。git/refs/heads文件夹中的一个ref,文件夹的位置决定了一个ref是一个分支,而不是一个标签等其他东西。

你丢失历史的地方是合并会根据情况做两件事中的一件。

考虑:

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

在这种情况下,任何一个方向的合并都将创建一个新的提交,其中第一个父节点作为当前检出分支指向的提交,第二个父节点作为您合并到当前分支的分支顶端的提交。它必须创建一个新的提交,因为自它们的共同祖先以来,两个分支都发生了必须合并的更改。

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

此时D (B1)现在拥有来自两个分支(自身和B2)的两组更改。然而,第二个分支没有从B1开始的更改。如果你合并B1到B2的变化,这样它们就同步了,那么你可能会看到这样的东西(你可以强制git合并,但是使用——no-ff):

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

即使B1有额外的提交,也会得到这个结果。只要B2中没有B1中没有的变化,两个分支就会合并。它做了一个快进,就像一个rebase (rebase也吃或线性化历史),除了不像rebase只有一个分支有一个变更集,它不需要从一个分支应用一个变更集到另一个分支。

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中提取时将一个分支合并到自身的结果(两个repo上的相同分支名称实际上是两个分支,一个单独的存储库就像分支所有分支一样)。

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

这并不是一个解决问题的方法,但我认为当我有一个长寿的分支时,我使用的方法值得注意:

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

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

为了更容易看到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

这样就简单多了!

注意,如果repo有很多分支,你会想要指定你比较的两个分支,而不是使用--all:

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

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

这是来自一个分支,该分支在合并回主节点之前有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 2.36提出了一个更简单的命令:

(branch_A_tag)
|
--X--A--B--C--D--F  (master)
\   / \   /
\ /   \ /
G--H--I--J  (branch A)
vonc@vclp MINGW64 ~/git/tests/branchOrigin (branch_A)
git log -1 --decorate --oneline \
$(git rev-parse \
$(git rev-list --exclude-first-parent-only ^main branch_A| tail -1)^ \
)
80e8436 (tag: branch_A_tag) A - Work in branch main
  • git rev-list --exclude-first-parent-only ^main branch_A等于J -- I -- H -- G
  • tail -1给出G
  • git rev-parse G^给出它的第一个父标签:A或branch_A_tag

使用测试脚本:

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

这就给了你:

vonc@vclp MINGW64 ~/git/tests/branchOrigin (branch_A)
$ git log --oneline --decorate --graph --branches --all
* a55a87e (HEAD -> branch_A) J - Work in branch_A branch
| *   3769cc8 (main) F - Merge branch_A into branch main
| |\
| |/
|/|
* |   1b29fa5 I - Merge main into branch_A
|\ \
* | | e7accbd H - Work in branch_A
| | * 87a62f4 D - Work in branch main
| |/
| *   7bc79c5 C - Merge branch_A into branch main
| |\
| |/
|/|
* | 0f28c9f G - Work in branch_A
| * e897627 B - Work in branch main
|/
* 80e8436 (tag: branch_A_tag) A - Work in branch main
* 5cad19b X - Work in branch main

这是:

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

在Git 2.36 (Q2 2022)中,“&;# eyz3 &;(man)”和朋友们学习了一个选项--exclude-first-parent-only,它只沿着第一个父链向下传播UNINTERESTING位,就像--first-parent选项显示提交只沿着第一个父链缺少UNINTERESTING位一样。

参见提交9 d505b7(2022年1月11日)的张朝阳(jerry-skydio)
(由滨野朱尼奥——gitster——提交708年cbef中合并,2022年2月17日)

git-rev-list:添加——排除-first-parent-only标志

署名:Jerry Zhang

为了能够枚举用户的局部更改,知道一个分支在历史上第一次从某个集成分支分离出来是很有用的。
然而,这些局部变化可能包括任意的合并,所以在寻找散度点时,有必要忽略这个合并结构

为了做到这一点,教授“rev-list"家庭接受& # eyz1 & #,这限制排除提交的遍历仅遵循第一个父链接

-A-----E-F-G--main
\   / /
B-C-D--topic
在这个例子中,目标是返回集合# EYZ0},它表示已合并到main分支的主题分支。
git rev-list topic ^main(man)将最终不返回任何提交,因为排除main将最终遍历topic上的提交。
然而,git rev-list --exclude-first-parent-only topic ^main(man)将返回{B, C, D}

为新标志添加docs,并明确--first-parent的doc,以表明它只适用于遍历包含的提交集。

rev-list-options现在包含在它的手册页中:

# EYZ0

当查找要包含的提交时,只遵循第一个

.

. < p >此选项 能给一个更好的概述时,查看的演变 一个特定的主题分支,因为合并到一个主题中 分支倾向于只调整到更新的上游 时不时地,这个选项可以让你忽略 个人的承诺被带入你的历史 一个合并。< / p >

rev-list-options现在包含在它的手册页中:

# EYZ0

当finding提交排除(带有'{插入符号}')时,只跟随 当看到合并提交时,第一个父提交

这可以用来找到主题分支中的更改集 从它与远分支的分歧点开始 任意的合并可以是有效的主题分支更改

为什么不使用

git log master..enter_your_branch_here --oneline | tail -1

这将为您提供分支A拥有的所有master没有的提交(..函数),以及tail -1以返回最后一行输出,即找到指定分支的第一次提交(分支A)。

然后,使用那个提交的SHA

git log enter_the_sha_here^1 --oneline | head -1

它提供了在指定的提交(^1的函数)和head -1之前的所有提交,以返回第一行输出,即“one commit prior”;到分支A中最早的提交,即“分支点”。


作为一个单独的可执行命令:

for COMMIT in $(git log --format=format:%H  master..HEAD | tail -1) ; do
git log $COMMIT^1 --oneline | head -1
done

在分支A (HEAD的功能)中运行上述操作