如何使分支B与分支A具有相同的代码?

How to make branch B have the same code as branch A?

分支 A 的代码比分支 B 少。 我想将分支 A 合并到 B 中,这样 B 将以更少的代码结束,并且基本上具有与 A 完全相同的代码。类似于撤消多次提交。问题是我必须通过 Pull Request 合并来做到这一点。我不能直接推送到B,它必须通过A(功能分支)。

Pull Request 应该是什么样子的?当我尝试将 A 合并到 B 中时,它没有检测到任何差异 - 这是为什么? 如果我翻转 Pull Request(B 到 A),它会显示 B 有但 A 没有的所有更改。

测试变基 功能分支(包括清理过的代码) B 你的开发者

第一次保存你的开发

git checkout B git add git commit -am "blabla my dev"

然后更新 A

git checkout A git pull A

然后将 B 变基到 A 之上

git checkout B git rebase A

此时您可能需要处理一些冲突

TL;DR

你想要一个新提交,其快照来自提交。然后你可以从这个做一个公关。使用普通的 Git 工具进行这个新提交是很棘手的,但是使用旁路进行它很容易。不过,我会把它留到长篇。

我们需要在这里区分 pull request——一个东西GitHub add,1 over and above what Git 做的——以及 Git 自己做的。一旦我们这样做,事情就会变得更清楚一些,尽管因为这是 Git,他们可能仍然很不清楚。

Git 实际上就是 提交 。 Git 与文件无关,但提交 包含 文件。 Git 也与 branches 无关,尽管我们(和 Git)使用分支名称来查找提交。所以 Git 就是关于 提交 。这意味着我们需要确切地知道提交是什么以及为我们做了什么:

  • 每个提交都有 编号。然而,这些数字又大又丑,random-looking,用hexadecimal表示,例如e9e5ba39a78c8f5057262d49e261b42a8660d5b9。我们称这些为 哈希 ID(有时更正式地说,object ID 或 OID)。不知道将来的提交会有什么哈希 ID。但是,一旦提交,that 哈希 ID 指的是 that 提交,并且没有其他提交,任何地方,永远。 2 这允许两个不同的 Git 存储库通过比较提交编号来查看它们是否具有相同的提交。 (我们不会在这里使用 属性,但它很重要。)

  • 每个提交存储两件事:

    • 一个提交有每个文件的完整快照(虽然这些是压缩的——有时压缩得非常厉害——并且,通过用于制作提交编号,de-duplicated).

    • 一个提交也有一些关于提交本身的元数据:信息,比如谁做的,什么时候做的。在此提交数据中,每个提交都存储一个 previous 提交哈希 ID 的列表,通常只有一个元素长。单个 previous-commit 哈希 ID 是此提交的 parent

这个 my-parent-is-Frank,Frank's-is-Barb 的东西将提交粘在一起到他们的祖先链中。当我们使用普通的 git merge 时,Git 使用祖先链来确定要合并的内容。我们不想要一个正常合并在这里。同时,同样的 parent 东西是 Git 如何将提交——一个 快照 ——变成一个“改变”:找出“我”发生了什么变化,如果我的 parent 是提交 feedcab(不能是 frank,那个 non-hexadecimal 字母太多)我提交 ee1f00d,Git 比较 这两个提交中的快照。什么都一样,没变。不同的文件 确实 改变了,并且 Git 通过玩某种 Spot the Difference 游戏弄清楚 在 [=372= 中改变了什么] 他们并生成一个配方:对该文件的 feedcab 版本执行此操作,您将获得 ee1f00d 版本。

现在,实际上没有人使用原始提交编号来查找提交。您最近一次提交的提交编号是多少?你知道吗?你 关心吗? 可能不关心:你只需使用 mainmasterdevelop 或一些 name找到它。

这是它的工作原理。假设我们有一个很小的存储库,其中只有三个提交。我们称它们为 ABC(而不是使用它们的真实哈希 ID,它们又大又丑,而且我们也不知道它们)。这三个提交看起来像这样:

A <-B <-C   <--main

提交 C 是我们最新的。它有一个快照(所有文件的完整副本)和元数据。它的元数据列出了早期提交 B 的原始哈希 ID:我们说 C 指向 B。与此同时,提交 B 有一个快照和一些元数据,并且 B 的元数据指向 AA 有一个快照和元数据,并且由于 A 第一次 提交,它的元数据根本没有列出 parent。这是一个孤儿,有点(所有的提交都是处女出生,有点——好吧,让我们不要再沿着这条路走下去了)。所以这就是操作停止的地方,这就是我们知道只有三个提交的方式。

但是我们找到 commit C by name: the name main points to C (保存 C 的原始哈希 ID),就像 C 指向 B.

要进行新的提交,我们检查 main,这样 C 就是我们的 当前 提交。我们更改内容、添加新文件、删除旧文件等等,然后使用 git add 然后 git commit 制作新快照。新快照获得一个新的 random-looking 哈希 ID,但我们将其称为 DD 指向 C:

A <-B <-C   <--main
         \
          D

现在 git commit 开始使用它的巧妙技巧:它将 D 的哈希 ID 写入 name main:

A--B--C--D   <-- main

现在 main 指向 D 而不是 C,现在有四个提交。

因为人们使用名称,而不是数字来查找提交,我们可以通过丢弃我们对较新提交的访问权来返回到一些旧提交。我们强制使用一个名称,如 main,指向一些较旧的提交,如 CB,并忘记 D 存在。这就是 git reset 的意义所在。不过,这可能不是您想要的,尤其是因为 Git 和 GitHub 喜欢 添加新提交 ,而不是将它们带走。特别是拉取请求不会让您取消提交。

不,您想要的是创建一个 new 提交,其 snapshot 匹配一些旧提交。


1如果您没有使用 GitHub,也许您正在使用其他一些也添加了 Pull Requests 的站点。这有点棘手,因为每个添加它们的站点都以自己的方式进行。例如,GitLab 有类似的东西,但称它们为 Merge Requests(我认为这是一个更好的名字)。

2这取决于一些密码技巧,最终会失败。哈希 ID 的大小(big-and-ugly-ness)会根据我们的需要推迟失败,尽管现在它有点太小了,它们很快就会变得更大更丑。


正常合并

在正常的日常 Git 用法中,我们创建分支名称,并使用这些分支名称来添加提交。我已经展示了一个非常简单的例子。让我们变得更复杂一点。和以前一样,我们将从一个小的存储库开始:

...--G--H   <-- br1 (HEAD)

我在这里添加了 HEAD 符号来表示这是我们签出 的分支的名称。现在让我们添加另一个分支名称 br2 现在也选择提交 H

...--G--H   <-- br1 (HEAD), br2

由于我们通过名称 br1 使用提交 H,因此我们现在所做的任何 new 提交仅更新名称 br1 .让我们做两个新的提交:

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

现在让我们再次检查提交 Hgit switch br2:

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

并再提交两次:

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

我们现在可以 运行 git checkout br1 然后 git merge br2,或者现在 运行 git merge br1。先做前者吧:最后得到的snapshot两种方式都是一样的,只是其他的东西有点变化,所以我们只好挑一个。

无论哪种方式,Git 现在必须执行 真正的合并 (不是 fast-forward 假合并,而是真正的合并)。要执行合并,Git 需要弄清楚 webr1 上发生了什么变化,以及 they 发生了什么(好吧,我们,但暂时没有)在 br2 上发生了变化。这意味着 Git 必须弄清楚我们俩 从哪里开始 — 如果我们只看图,就很清楚:我们都从提交 H 开始。我们进行了“我们的”更改并提交(多次)并获得了 J.

中的快照

HJ区别

git diff --find-renames <hash-of-H> <hash-of-J>

告诉 Git 我们br1.

上改变了什么

相似的区别:

git diff --find-renames <hash-of-H> <hash-of-L>

告诉 Git 他们 br2 上发生了什么变化。 (请注意 Git 在这里使用 commits:分支名称 br1br2,只是用于 find 提交。Git 然后使用历史记录(记录在每个提交的 parent 中)来找到 最佳共享 starting-point 提交 H.)

为了执行合并本身,Git 现在 合并 两个差异列表。我们更改了一些文件而他们没有更改的地方,Git 使用我们的更改。他们更改了文件而我们没有更改的地方,Git 使用他们的更改。我们都更改了 相同的 文件,Git 必须合并这些更改。

如果我们都进行了完全相同的更改,那很好。如果我们触及 不同的行 ,那也很好——尽管这里有一个边缘情况:如果我们的更改邻接,Git 声明一个 合并冲突; 但如果它们完全重叠,并进行相同的更改,那没关系)。如果一切顺利,合并更改时不会发生合并冲突,Git 可以将合并的更改应用到来自 H 的快照。这会保留我们的更改并添加他们的 - 或者,等效地保留他们的更改并添加我们的更改。在我们的更改完全重叠的地方,Git 只保留一份更改。

生成的快照——H加上两组更改——进入我们新的合并提交。不过,这个新的合并提交有一点特别之处。而不是只有 一个正常的 parent,在这种情况下——在分支 br1 上——将是 J,它得到两个 parents:

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

一如既往,Git 更新 当前分支名称 以指向新的 merge commit M .合并现已完成。

git merge -s ours

让我们你想要的。你是从这个开始的:

          o--o--...--R   <-- br-A
         /
...--o--*
         \
          o--o--...--L   <-- br-B (HEAD)

您想 git merge br-A,但 保留 br-B 末尾提交 L 的快照

要在原始 Git 中完成您想要的 ,您需要 运行:

git switch br-B
git merge -s ours br-A

Git 现在会找到合并基础 *(或者真的不麻烦),然后......完全忽略 他们的 更改,并进行当前分支上的新合并提交 M

          o--o--...--R   <-- br-A
         /            \
...--o--*              \
         \              \
          o--o--...--L---M   <-- br-B (HEAD)

其中合并提交 MLR 作为它的两个 parent,但是使用提交 L 作为 快照.

这很简单,原始 Git。但是 GitHub 不会这样做!我们如何让 GitHub 提供这种结果?

我们得对 GitHub 耍点小花招

为了论证,假设我们要 git switch br-A——即检查提交 R——然后创建一个新提交,其 快照 是来自提交 L 吗?也就是说,我们制作:

          o--...--R--L'  <-- br-A (HEAD)
         /
...--o--*
         \
          o--o--...--L   <-- br-B

提交 L' 与提交 L 具有不同的 哈希 ID ,并且具有不同的 元数据 — 我们刚刚完成,带有我们的姓名和电子邮件以及日期和时间等等,它的 parent 是 R——但具有与提交相同的 snapshot L.

如果我们 Git 在此处执行 正常合并 ,Git 将:

git diff --find-renames <hash-of-*> <hash-of-L>
git diff --find-renames <hash-of-*> <hash-of-L'>

获取 Git 需要合并的两个差异。 这些差异将显示完全相同的变化。

正常合并将合并这些更改,方法是获取所有更改的一个副本。这就是我们想要的!最终的合并结果将是:

          o--...--R--L'  <-- br-A
         /            \
...--o--*              M   <-- br-B (HEAD)
         \            /
          o--o--...--L

我没有特别的原因用另一种风格绘制它(中间有 M)。 M 中的快照将匹配提交 LL',分支 br-B 将在新提交处结束,没有 更改 到任何 文件,但最后有一个新的提交。

我们可以轻松地在 Git 中提交 L',然后通过 [=127] 上的 L' 向上发送提交,从而在 GitHub 上提出合并请求=] 分支。 PR 将顺利合并,通过在 br-B 中“改变”任何内容,只需添加新的合并提交 M。所以——除了额外的 L' 提交——我们在分支 br-B.[= 上得到与 git merge -s ours 运行 相同的 effect 154=]

困难重重

将快照 L' 添加到分支 br-A 的困难方法是:

git switch br-A
git rm -r .                         # from the top level
git restore -SW --source br-B -- .
git commit -C br-B

例如。第一步将我们置于 br-A 并检出提交 R。第二个——git rm -r .——从 Git 的索引 / staging-area 中删除所有文件,并从我们的工作树中删除相应的文件。 git restore 将所有文件 放回 但从 --source br-B 或提交 L 中取出它们,最后一步 git commit -C br-B 创建一个新文件使用来自提交 L 的消息提交。 (使用 -C 你可以编辑它。)

这很好用,就是有点慢。为了走得更快,我们可以使用两种技巧中的任何一种。这是第一个,可能是我实际使用的那个:

git switch br-A
git read-tree -u --reset br-B
git commit -C br-B

这消除了remove-and-restore,取而代之的是git read-tree,可以一举完成。 (您可以使用 -m 而不是 --reset 但两个标志之一是必需的,并且 git read-tree 是一个棘手的命令,我不喜欢使用太多,所以我从来不记得随手使用哪一个:幸运的是,在这里并不重要。)

或者,我们可以这样做:

git switch br-B      # so that we are not on br-A
git branch -f br-A $(git log --no-walk --format=%B br-B | git commit-tree -F - -p br-A br-B^{tree})

如果我没有打错字的话。但是,这使您没有机会编辑提交消息。你不需要直接签出 br-B,你只需要确保你不是 on br-A,或者你使用 git merge --ff-only提交后继续前进。

如果 GitHub 可以做一个 git merge -s ours

就好了

但不能,就这样。