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