如何将本地存储库中的分支合并到另一个本地存储库中的分支

How can I merge a branch in a local repository to a branch that is in another local repository

假设我有 2 个本地存储库。

Local Repository A //Cloned from production remote repository.

Local Repository B //Cloned from development remote repository.

我从本地存储库 A 创建了自己的功能分支

FeatureA //In Local Repository A

从本地存储库A完成FeatureA后,我想将这个FeatureA分支合并到一个名为

的分支中
Developer //In Local Repository B 

注意,FeatureA 和 Developer 这两个分支如何位于不同的本地存储库中。

如何将 FeatureA 分支合并到 Developer 分支?

Git实际上并没有合并b运行ches。事实上,就像 Git 中的大多数事情一样,b运行ches 在这里根本不重要:只有 commits 事情。 Git 合并 提交 .

这对您来说意味着,只有当所有 提交 相关时,您才能获得正确的结果。 (他们可能是,但我们看不到。 可以找到。)

关于提交有一些重要的事情需要了解(这不是一个完整的列表,但这里对于合并很重要):

  • 它们是通过哈希 ID 找到的。
  • 散列 ID 大、难看且 random-looking(实际上 运行dom,但非常不可预测,人类无法使用)。
  • 提交通过哈希 ID 引用其他较早的提交。这对于 提交本身 来说很好,毕竟它们是由计算机程序(或一系列计算机程序:我们称这些程序为 git)读取的,但对倒霉的人应该负责这些程序和计算机。

由于这些特殊的要点,Git 对人类做出了很大的让步:它将让我们使用 b运行ch names 来查找提交.不仅如此,它还有一个专为人类设计的特殊功能。当我们:

  1. 首先将 Git 指向某个特定的 b运行ch,然后
  2. 直接 Git 进行 new 提交,

Git 将 更新我们“在” 上的 b运行ch 名称,以便该名称现在指的是 new commit,而不是指它刚刚表示的任何特定提交。

对于普通的,single-parent commit,我们可以画出这样的情况。我们首先用 single-letter pseudo-IDs 替换真实提交的真实哈希 ID,single-letter pseudo-IDs 我们 选择用我们脆弱的人类大脑工作。然后我们用一个箭头从它出来绘制每个提交,向后指向一个较早的提交。这将 latest 提交放在右边:

... <-F <-G <-H

所以这里 H 代表 最新 提交的哈希 ID。在内部表示 of 提交 H、Git 的某处已经保存了早期提交 G 的真实哈希 ID。我们将其绘制为从 H 的表示中出来的箭头,指向 G.

的表示

当然,G本身也是一个commit,所以它也存储了一个previous-commit哈希ID:G指向FF 同样指向一些较早的提交,依此类推。这将永远重复,或者更确切地说,直到我们回到有史以来的第一次提交:在此公式中提交 A。 (因此我们的存储库只有微不足道的八次提交。)

出于 Whosebug 的目的,由于惰性 and/or 字体问题,我倾向于停止绘制 commit-to-commit 箭头 作为 箭头,而是这样做:

A--B--...--G--H

但实际上从提交到提交的每个连接都只有一种方式:从 之后的 提交,例如 H,到更早的提交。这是因为提交一旦完成,就完全是 100% read-only。 in 提交不能更改。1

当我们给这些图加上b运行ch的名字时,它们的工作原理就变得更加清晰了。假设我们有两个名字,maindevelop,*两个名字都指向提交 H,像这样:

...--G--H   <-- main, develop

这意味着包括 H 在内的所有提交都在 both b运行ches 上。我们现在必须选择 one b运行ch 为“开”,使用 git checkoutgit switch:3

git switch main

为了记住我们使用的是哪个 b运行ch,我们添加特殊名称 HEAD,像这样全部大写,仅附加到其中一个 b运行通道名称:

...--G--H   <-- main (HEAD), develop

这表明我们正在使用提交H通过名称main .

结帐或切换命令的工作方式(非常粗略):

  • 从工作区中移除——Git 调用 工作树——从某些 other 提交,如果/根据需要;
  • 用我们刚刚切换 .
  • 的提交中的所有文件填充此工作区

我们马上就会看到这个,但现在,让我们切换到 develop,或者甚至创建一个新的 b运行ch 名称 feature


1需要此 read-only 属性 才能使散列方案起作用。 提交的哈希 ID 只是存储在该提交中的所有位的加密校验和。如果您从 Git 数据库中取出一个提交,将其转换为普通数据,然后以某种方式修改该数据并将其放回 Git 数据库,您得到的是 新和d提交 具有 新的和不同的哈希 ID。旧提交在旧 ID 下保持不变。

Git 验证,在 object-extraction 时间,来自数据库 out 的所有位仍然校验和为原始值。如果他们不这样做,Git 宣布数据库损坏,并停止运行。由于文件内容也使用相同的散列技巧存储,因此我们可以确保 none 的文件曾被损坏。一旦它们 进入 存储库,它们将永远存在 2 并且永远无法更改。

2从技术上讲,可以从存储库数据库中删除提交,但这很棘手,我们不会在此处介绍。

3这里checkoutswitch的用法没有区别。通过将一个命令 checkout 拆分为两个单独的命令 switchrestore,最终清除了 git checkout 的某些历史错误,并且学习新的命令是有意义的只要您不被迫使用缺少新版本的 Git 的旧版本。 (虽然我现在已经使用 Git 超过 15 年了,所以我在这里有一些老习惯,有时 not-so-good 习惯。如果我使用 git checkout,那是习惯,或者因为有人给了我可能要更新 Git 1.7 版本。)


在 b运行ch

上进行新提交

如果我们现在切换到现有的 b运行ch develop,我们得到:

...--G--H   <-- main, develop (HEAD)

为此,Git 需要删除来自提交 H 的所有文件,而是放入来自提交 H 的所有文件。这种remove-and-replace-with-sameness显然是愚蠢的,所以Git 针对这种特殊情况跳过这一步4 Git 这次 根本不删除或替换任何文件。因此,如果我们开始进行更改但忘记切换到不同的 b运行ch(或创建一个新的 b运行ch;见下文),通常在您发现错误后立即进行更改是安全的。

无论如何,既然我们在 develop,让我们以通常的方式进行新的提交。我会跳过很多重要的细节——特别是,我不会提到 Git 的 index 又名 staging area——并且假设你知道这里要知道的一切;5 但是,我们这样做了,我们现在有 Git 进行新的提交,我们将其称为 I.:

          I
         /
...--G--H

新提交 I 指向现有提交 H。但是现在特殊的魔术发生了:Git 将新提交的哈希 ID 写入当前 b运行ch name,即具有 HEAD附上。所以为了完成我们的绘图——看看为什么我把 I 单独放在一行上——我们画这个:

          I   <-- develop (HEAD)
         /
...--G--H   <-- main

请注意名称 develop 现在如何指向 new 提交。所有其他 b运行ch 名称均未更改:只有名称 develop 已移动。

如果我们进行第二次新提交 J,我们得到:

          I--J   <-- develop (HEAD)
         /
...--G--H   <-- main

通过 H 的提交都在 both b运行ches,而新提交 I-J 仅在 develop .

如果我们现在 运行 git checkout maingit switch main,我们得到:

          I--J   <-- develop
         /
...--G--H   <-- main (HEAD)

这一次,Git 确实必须删除一些文件——那些特定于提交 J 的文件——并用正确的文件替换它们以提交 H。所以 Git 这样做了,如果我们现在检查我们的文件,我们会看到我们有来自提交 H.6

的文件

现在我们通过名称 main 重新提交 H,让我们创建一个 new b运行ch 名称 。我们必须为这个新名称选择一些提交到 point-to,通常的选择是“我们现在正在进行的提交”,即提交 H:

git switch -c feature    # or git checkout -b feature

这使我们处于这种状态:

          I--J   <-- develop
         /
...--G--H   <-- feature (HEAD), main

如果我们现在进行两次 更多 提交,我们得到:

          I--J   <-- develop
         /
...--G--H   <-- main
         \
          K--L   <-- feature (HEAD)

我们现在处于合并有意义的状态。


4这种跳过的实现方式比这里描述的更聪明,但是对于“不切换提交的切换 b运行ches”,效果是始终允许更改。在更复杂的情况下,你会得到奇怪的效果;请参阅 Checkout another branch when there are uncommitted changes on the current branch 了解详细信息。

5有很多东西要知道!有关详细信息,请参阅其他答案或 Git 教程。

6这是脚注 4 中提到的复杂情况的来源。我们忘记提交的文件,或故意没有提交,可以在Git的索引又名staging-area中的工作树and/or中进行。但是,如果我们确保在 IJ 中提交所有内容,我们将处于“干净”状态,如 git status 所报告的那样,在结帐之前和之后——除非事情得到确实复杂,但我们不去那里。


合并就是合并工作

让我们再画一遍,但改变一些名字,切换运行四处走动,完全删除名称 main(它挡住了路):

          I--J   <-- br1 (HEAD)
         /
...--G--H
         \
          K--L   <-- br2

我们现在通过名称 br1 使用提交 J。我们可以 运行:

git merge <hash-of-L>

或:

git merge br2

让 Git 找到提交 L 并进行合并工作。或者,如果我们还没有准备好合并提交 L,但想合并提交 K,我们可以 运行:

git merge <hash-of-K>

也就是说,我们会 运行 git log br2 并查看提交 L,然后查看提交 K。它有一些丑陋的大哈希 ID,b789abc... 或其他什么,我们用鼠标抓住它,cut-and-paste 样式,并生成如下命令:

git merge b789abc

(缩写哈希 ID 也有效,因此您可以重新输入前 4、7 或 15 个字符并停止,但这里很容易出错:为此我总是使用 cut-and-paste)。

我们通常不会费心像这样合并一定数量的提交,但在一些复杂的情况下——例如,如果我们有:

          o--o--...--o   <-- br1 (HEAD)
         /
...--o--*
         \
          o--...--(thousands of commits)--...--o   <-- br2

我们可能希望将合并分成更小的块,在 br2 的很长的一行中的某处选择一些提交首先合并:

          o--...--o---M   <-- br1 (HEAD)
         /           /
...--o--*           /
         \         /
          o--...--o--(hundreds of commits)--...--o   <-- br2

合并了 500 个提交,我们将原来的 1400 个提交减少到 900 个要合并;我们可以再做 500 个,只剩下 400 个要合并,等等

在任何情况下,无论 many 如何提交,我们正在合并,合并操作的工作方式相同。鉴于:

          I--J   <-- br1 (HEAD)
         /
...--G--H
         \
          K--L   <-- br2

git merge br2、Git:

  • 找到当前提交J(很简单:Git使用HEAD);
  • 找到另一个提交 L(这很简单:Git 使用我们给它的 br2);和
  • 找到合并基础提交,提交H:这更难。

Git发现通过an algorithm合并基础提交,但我们可以将at描述为最佳共同祖先,在这种情况下,只需提交 H.7

Git 现在在每个提交中使用快照——我们没有正确描述这一点,但每个提交都包含每个文件的完整快照——来弄清楚“我们的 b运行ch" br2,通过比较提交 H 与提交 J,并通过比较找出“他们的 b运行ch” 上的“他们改变了什么”提交 H 与提交 L。这里对 diff 的三个提交是:

  • 合并基础,在两个git diff命令的左边;
  • 我们当前的或 HEAD 提交,在“我们的”差异的右侧;
  • 我们选择合并的提交,在“他们的”差异的右侧。

两个差异的输出决定了要合并的更改集。

合并算法现在组合这些更改,将组合更改应用于合并基础提交中的快照——提交H 在这里——因此 保留我们的更改 添加他们的更改 ,这就是我们想要的。

成功合并这两组更改并将它们应用到合并基础后,Git 现在根据结果进行新提交。8 新提交是一个 merge commit,它在一个方面很特别:它不是指向单个父节点,而是指向 两个 父节点,就像这样:

          I--J
         /    \
...--G--H      M   <-- br1 (HEAD)
         \    /
          K--L   <-- br2

新合并提交 Mfirst 父级是我们在 运行 git merge 时使用的提交,即提交 Jother 父级是我们在命令行中命名的提交,在本例中为提交 L。合并提交 M 作为所有文件的快照,具有 combining-and-applying-to-H's-snapshot.

的结果

7正如维基百科文章所指出的,DAG 中不一定有一个唯一的 LCA 节点。对于这些情况,合并算法变得更加棘手;我们不会在这里介绍这些。

8如果Git合并变化失败,故意中途停止合并,留下我们清理一团糟。我们也不会在这里介绍这种情况。


这对您意味着什么

为了 git merge 完成它的工作,你给它的提交必须:

  • 所有都在一个存储库中;和
  • 相关,就最佳共享提交而言:H 在我们上面的示例中。

当您只有一个存储库时,第一个条件很容易满足——所有提交都在 the(单个)存储库中——第二个通常是这种情况,因为我们通常通过从一些现有的 b运行ch 上的某个起点增长它来制作 new b运行ch。该共享 起点 提交是 a 共同起点,因此 a 共享提交。如果从那以后有合并,可能会有一些更好共享ommit,但除此之外这是 the 共享提交:

 ...--*--*--*--o--o   <-- br1 (HEAD)
             \
              o----o   <-- br2

加星标的提交 * 在两个 b运行 上,因此最右边的提交作为合并基础。或者:

 ...--*--*--*--o--o--M1--o--o   <-- br1 (HEAD)
             \      /
              *----*----o----o   <-- br2

同样,所有加星标的提交都在两个 b运行ches 上。合并提交 M1 的额外父级将 br2 加入 br1,所以再一次,最右边的星号提交作为合并基础。一旦我们合并 M2 我们有:

 ...--*--*--*--o--o--M1--o--o--M2   <-- br1 (HEAD)
             \      /         /
              *----*----*----*   <-- br2

请注意如何合并“添加”所有其他 b运行ch 的提交到 br1

当你有两个独立的存储库时,提交是否相关?现在我们进入任何分布式版本控制系统的复杂性之一,例如 Git.

当您克隆一个Git存储库时,您实际上是在复制提交。某个存储库 Rgit clone 制作了一些克隆 C,但是 C 拥有所有来自 R.9 的提交 在 Git 中,克隆会复制提交,但 不会复制 b运行ches,10 这在某种意义上很奇怪——例如,Mercurial 也复制了 b运行ches——但对你来说重要的是情况是 commits 被复制了。

现在, 提交被复制到 C 后,有人可以在 R[=609] 中进行更多提交=], and/or 其他人可以在 C 中进行更多提交。但是,如果它们都遵循相同的标准程序——从它们的提交开始,然后仅仅添加——这些提交将全部“加入过去”,恰好与我们使用 单个 存储库的方式相同。

在这种情况下,您所要做的就是:

  • 克隆 RCthird Git 存储库, 那么
  • 添加到该存储库 这两个存储库的 other 中的所有提交,这些提交还没有在你的第三个存储库中克隆。

第二步——“添加我们没有的提交”——似乎是一件大事。在某些方面,它是……但是我们已经因为克隆而不得不这么做了。也就是说,假设 Rcentral everyone 克隆了一些“真实来源”存储库。你让你的克隆 Cyou。 Alice 克隆 Calice,Bob 克隆 Cbob,等等。

在某些时候,某人 进行了新的提交,并最终——以某种方式——将他们的提交放入 Rcentral 。现在 每个拥有克隆 的人都必须将 那些 新提交到他们的克隆中,如果他们想看到并使用它们的话。所以我们有 git fetch.

我们运行git获取<em>name</em>。 Git 调用我们这里使用的 name remote。当您克隆 Rcentral 时,您的 Git 在您的克隆 C 中添加一个标准远程名称,origin。你的Git将Rcentral的URL存储在这个标准名称下,从现在开始,你可以运行:

git fetch origin

让你的 Git 调用 Rcentral Git。他们将列出他们的提交(通过哈希 ID)和他们的 b运行ch 名称(通过名称),并且您的 Git 将确定这些提交中是否有任何提交对您来说是 新的,如果有,就去获取。然后,您的 Git 将通过获取他们的 b运行ch 名称来设置 remote-tracking 名称mainfeature 等等,并在它们前面加上 origin/origin 部分来自 remote。这些名称“track”11 b运行ch names over on origin, 所以它们是 remote-tracking origin.

的名称

您可以添加更多 Git 存储库作为额外的远程。 也就是说,使用:

git remote add repo-xyz <url>

您添加第二个遥控器,使用名称 repo-xyz,以存储给定的 url。现在你可以 运行:

git fetch repo-xyz

你的Git会在你刚刚保存的URL调用Git,询问他们他们的b运行ch 名称和提交哈希 ID,并带来他们拥有的任何提交,而你没有。然后,您的 Git 将在您的克隆 C 中创建 remote-tracking 形式的名称 repo-xyz/*。如果他们有 main,您将有一个 repo-xyz/main。如果他们有一个develop,你就会有一个repo-xyz/develop

这些 remote-tracking 名称中的每一个都将准确记住一个提交哈希 ID,就像 C 中的每个 b运行ch 名称一样Rcentral,或者这个添加的远程只记住一个提交哈希 ID。因为 git fetch 读取了它们的 current 状态,哟r remote-tracking 名字现在会记住他们 b运行ch 状态,因为你 运行 git fetch.

所以,有了 运行 git fetch origin git fetch repo-xyz,你现在有:

  • 你所有的提交,加上
  • 所有提交 origin 都有你没有的,加上
  • 所有提交 repo-xyz 有你没有的,加上
  • remote-tracking 名称 origin/*repo-xyz/* 以记住 b运行ch 名称并提交来自 originrepo-xyz 的哈希 ID。
定位特定提交的

Remote-tracking 名称与定位特定提交的 b运行ch 名称一样有效。因此,您可以将 remote-tracking 名称传递给 git merge。他们 在这里工作的唯一一件事是你不能“使用” remote-tracking 名称。那是因为它不是 b运行ch 名称,Git 只会让你将 HEAD 附加到 b运行ch 名称。如果你想要一个 b运行ch 名称指向某个现有 remote-tracking 名称所在的提交,你可以使用 git checkoutgit switch 来执行此操作:

git switch -c update-some-abc-branch repo-xyz/abc-branch

因为你有两个遥控器(originrepo-xyz),你可能会运行陷入你无法制造的烦恼一个名称,例如 main,您在使用 both origin/main and repo-xyz/main 时使用的名称。您可能需要使用一些时髦的不匹配的 b运行ch 名称,就像我在上面所做的那样。这很好用:不需要在每个存储库中使用 相同的 名称。12

这为您提供了您需要的所有信息:

  • 您的 存储库 C 中创建 b运行ch 名称,以便在“打开”那些 b运行切;
  • 运行 git mergecommits 由你的 b运行ch 名称识别 and/or 你的 remote-tracking名字。

只要你记得 Git 真正关心的是 提交和它们的哈希 ID,以及 你的 b运行ch 名称只是为了让你找到你选择的提交,你会没事的。


9制作省略某些提交的克隆是可能的,但我们会考虑我们 做的通常情况那。

10Git 使用 remote-tracking names。复制 b运行ch 名称是可能的,但会导致混淆。 remote-tracking 名称技术反而会导致……混乱。我不确定这里是否有那么大的改进。但就是这样。

11Git 严重滥用这个动词 track。不过,这又是事实。 Git 称这些东西为 remote-tracking b运行ch names,但它们实际上并不是 b运行ch 名称,所以我使用 remote-tracking 名称 因为它们是名称并且它们确实跟踪其他(远程)名称。

12这就是强制每个人都使用相同的 b运行ch 名称的 Mercurial 方法非常有用的地方。这也是强制每个人都使用相同的 b运行ch 名称的 Mercurial 方法非常有害的地方。这完全取决于您需要完成什么。

我发布这个答案以备将来参考。

我错误地认为存在两个不同的远程存储库(一个用于生产,一个用于开发)。

原来,这个项目只有一个远程仓库。

这是我应该做的。

  1. 从远程存储库,将其克隆到我的本地计算机中一次。

  2. 然后检查我本地机器上的 Production 分支。

  3. 从我刚刚签出的 Production 分支创建我自己的分支。

  4. 完成功能后,将我的分支合并到 Developer 分支。