将一个分支与另一个首先提交的分支合并

Posted

技术标签:

【中文标题】将一个分支与另一个首先提交的分支合并【英文标题】:Merge a branch with another branch which first committed 【发布时间】:2021-12-29 08:20:56 【问题描述】:

我有以下担忧。我在一个分支(我们称之为 A)上工作,在那里我实现了一个新功能。我只提交了更改,但我没有推送它们。现在我后来意识到我在错误的分支上。所以我换到了右边的分支(B)。如何将更改从分支 A 转移到分支 B?

所以到目前为止所有的东西都留在 B 中,而 B 中来自 A 的所有新东西都存放在 B 中。

【问题讨论】:

推的时候可以用git push <remote> <branch>,这里可以指定分支 所以要做到这一点,你首先必须去分支 A 然后: git push 大部分情况(如果你已经克隆或者配置了remote),'origin' 【参考方案1】:

如果:

喜欢某些提交,但是 你喜欢这些相同的提交

那么通常解决此问题的正确方法是使用git rebase。关于git rebase 总是有一个警告,我稍后会描述,但是由于您还没有发送这些提交到一些其他 Git 存储库——您想以某种方式更改的提交完全属于您,仅存在于您自己的 Git 存储库中——此警告不适用于您的情况。

不过,在您的特定情况下,您根本不需要使用 rebase。您将改为使用git cherry-pick,然后使用git resetgit branch -f。或者,您甚至可能不需要挑选樱桃。

关于提交(以及一般的 Git)的知识

Git 真的是关于提交。它与文件无关,尽管提交确实 hold 文件。这也与分支无关,尽管分支名称可以帮助我们(和 Git)find 提交。不过,最后,重要的只是 commits。这意味着您需要了解有关提交的所有信息。

在 Git 中:

每个提交都有编号,具有唯一但又大又丑又随机的hash IDobject ID。这些实际上根本不是随机的:这些数字是加密哈希函数的输出。每个 Git 都使用相同的计算,因此宇宙中的每个 Git 都会同意某个特定的提交获得那个数字。没有其他提交可以拥有那个数字,不管它是什么:这个数字现在被那个特定的提交用完了。由于数字必须是普遍唯一的,因此它们必须很大(因此丑陋且人类无法使用)。

Git 将这些提交以及支持这些提交的其他内部对象存储在一个大数据库(key-value store)中,其中哈希 ID 是键,提交(或其他对象)是值。你给 Git 密钥,例如,通过从git log 输出剪切和粘贴,Git 可以找到提交并因此使用它。这通常不是我们实际使用 Git 的方式,但重要的是要知道:Git 需要密钥,即哈希 ID。

每个提交存储两件事:

每个提交都存储一个每个文件的完整快照,截至您创建它的时间。这些文件以特殊的、只读的、仅限 Git 的、压缩的和去重复的格式存储,而不是作为计算机上的普通文件。根据您的操作系统,Git 可能能够存储您的计算机实际上无法使用或提取的文件(例如,在 Windows 上名为 aux.h 的文件),这有时是个问题。 (您必须在可以命名它们的操作系统上制作这些文件,当然,例如 Linux。不过,所有这些的目的只是为了表明这些文件不是常规文件。)

每个提交还存储一些元数据,或者关于提交本身的信息:例如,谁做了它,什么时候做的。元数据包括git log 显示的日志消息。对于 Git 来说至关重要的是,每个提交的元数据都包含一个列表(通常只有一个条目长)以前的提交哈希 ID

由于 Git 使用散列技巧,任何提交(任何类型的内部对象)一旦存储就无法更改。 (这也是文件存储的工作原理,Git 就是这样对文件进行重复数据删除并可以存储您的计算机无法存储的文件。它们都只是那个大数据库中的数据。)

再次,提交的元数据存储一些先前提交的哈希 ID。大多数提交在此列表中只有一个条目,并且该条目是此提交的 parent。这意味着子提交记住了他们父母的名字,但父母不记得他们的孩子:父母在他们被创造的那一刻就被冻结了,他们的孩子最终存在不能被添加到他们的记录中。但是当孩子出生时,父母存在,所以孩子可以保存其父母提交的编号。

这一切的意思是提交形成向后看的链,其中最新的提交指向一个跳到下一个最新的提交,并且该提交指向回另一跳,依此类推。也就是说,如果我们绘制一个小提交链,其 last 提交具有哈希 H,我们得到:

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

hash 为H 的提交保存了所有文件的快照,以及元数据; H 的元数据让 Git 找到提交 G,因为 H 指向它的父 G。提交G 反过来保存所有文件和元数据的快照,G 的元数据指向F。这一直重复到第一次提交,即第一次提交不能向后指向。它有一个空的父列表。

git log 程序因此只需要知道一个 提交哈希ID,即H 的。从那里,git log 可以显示H,然后向后移动一跳到G 并显示G。从那里,它可以再回到F,以此类推。当您厌倦了阅读 git log 输出并退出程序时,或者当它一直回到第一次提交时,该操作将停止。

分支名称帮助我们找到提交

这里的问题是我们仍然需要以某种方式记住提交H 的哈希ID,链中的最后一个。我们可以将它记在白板上、纸上或其他东西上——但我们有一台计算机。为什么不让计算机为我们保存哈希 ID?这就是分支名称的意义所在。

Git 中的每个分支名称仅保存 一个 哈希 ID。无论分支名称中的哈希 ID 是什么,我们都说该名称​​指向该提交,并且该提交是该分支的提示提交。所以:

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

这里我们有分支名称main 指向提交H。我们不再需要记住哈希 ID H:我们可以直接输入 main。 Git 会使用名称main 查找H,然后使用H 查找G,并使用G 查找F,以此类推。

一旦我们这样做了,我们就有一个简单的方法来添加新的提交:我们只需进行一个新的提交,例如I,让它指向H,然后I 的哈希 ID 写入名称 main,如下所示:

...--F--G--H--I   <-- main

或者,如果我们不想更改我们的名称main,我们可以创建一个新名称,例如developbr1

...--F--G--H   <-- br1, main

现在我们有多个name,我们需要知道我们使用哪一个来查找提交H,所以我们将绘制特殊名称HEAD,附件到其中一个分支名称,以表明:

...--F--G--H   <-- br1, main (HEAD)

这里我们通过名称main 使用提交H。如果我们运行:

git switch br1

我们得到:

...--F--G--H   <-- br1 (HEAD), main

没有其他变化——Git 注意到我们正在“从 H 移动到 H”,实际上是这样——所以 Git 采取了一些捷径,并且不会为这个案例做任何其他工作。但现在我们是on branch br1,正如git status 所说。现在,当我们进行新的提交 I 时,我们会得到:

             I   <-- br1 (HEAD)
            /
...--F--G--H   <-- main

名称main 保持不变,而名称br1 移动到指向新提交I

您所描述的情况

我在一个分支(我们称之为 A)上工作,在那里我实现了一个新功能。我只提交了更改,但我没有推送它们。现在我后来意识到我在错误的分支上。所以我换到了右边的分支(B)。如何将更改从分支 A 转移到分支 B?

让我们画这个:

...--G--H   <-- br-A (HEAD), main
      \
       I--J   <-- br-B

你是 on branch br-A 并做了一个新的提交,我们称之为 K

          K   <-- br-A (HEAD)
         /
...--G--H   <-- main
      \
       I--J   <-- br-B

对于提交K,有一些你喜欢的事情:例如,它的快照与提交H 中的快照不同,无论你做了什么更改。它的日志消息也说明了您希望日志消息说的内容。

但是你喜欢提交K的一件事:它出现在提交H之后,而你希望它出现在提交J之后。

您不能更改提交

我们在顶部附近指出,一旦提交,就无法更改。你现有的提交 K 是一成不变的:没有人,没有任何东西,甚至 Git 本身,都可以更改关于提交 K任何事情。它出现在H 之后,并且具有它所拥有的快照和日志消息,并且永远都是如此。

但是......如果我们可以复制 K 到一个新的和改进的提交中呢?让我们将此新的改进提交称为K',以表明它是K副本,但有些不同。

应该有什么不同?好吧,一方面,我们希望它出现在J 之后。然后我们希望它对J 进行与KH 所做的相同更改。也就是说,如果我们问H-vs-K 快照有什么不同,然后问我们即将制作的J-vs-K' 快照有什么不同,我们想获得相同的更改

有一个相当低级的 Git 命令可以像这样复制一个提交,称为git cherry-pick。这实际上就是我们最终要使用的。

不过,我们应该在这里谈谈git rebase。如果我们有十几个或一百个提交复制,那么挑选每一个可能会很乏味; git rebase 也会自动执行重复的樱桃采摘。所以rebase是常用的命令。

rebase 的工作原理如下:

首先,我们让 Git 列出它需要复制的所有提交。在这种情况下,只需提交 K。 然后,我们让 Git 签出(切换到)我们希望将副本发送到 go 的提交。在这种情况下,提交 J。 接下来,我们让 Git 从它创建的列表中复制每个提交,一次一个。 然后我们让 Git 使用找到要复制的提交的 last分支名称,并将该名称移动到最后复制的提交。李>

所有这一切的最终结果,在这种情况下,是:

          K   ???
         /
...--G--H   <-- main
      \
       I--J   <-- br-B
           \
            K'  <-- br-A (HEAD)

注意提交K 仍然存在。只是没有人能找到了。名称br-A 现在找到副本,提交K'

樱桃采摘

这不是我们想要的,所以我们不使用git rebase,而是使用git cherry-pick。我们将首先运行:

git switch br-B

得到:

          K   <-- br-A
         /
...--G--H   <-- main
      \
       I--J   <-- br-B (HEAD)

现在我们将运行:

git cherry-pick br-A

这使用名称br-A 来查找提交K,然后将其复制到我们现在所在的位置。也就是说,我们得到了一个新的提交,它做出了与提交K 所做的相同的更改,并且具有相同的日志消息。这个提交在我们现在所在的分支上进行,所以br-B 被更新为指向副本:

          K   <-- br-A
         /
...--G--H   <-- main
      \
       I--J--K'  <-- br-B (HEAD)

我们现在应该检查和测试新的提交,以确保我们真的喜欢结果(因为如果我们不这样做,你可以在这里做更多的事情)。但假设一切顺利,现在我们想丢弃br-A 末尾提交K

我们实际上不能删除提交K。但是分支名称只是保存了我们想要说是“在分支上”的最后一次提交的哈希 ID,我们可以更改存储在分支名称中的哈希 ID

这里的事情有点复杂,因为 Git 有两种不同的方法来做到这一点。使用哪一个取决于我们是否检查了该特定分支。

git reset

如果我们现在运行:

git switch br-A

得到:

          K   <-- br-A (HEAD)
         /
...--G--H   <-- main
      \
       I--J--K'  <-- br-B

我们可以使用git reset --hard 将提交K 从当前分支的末尾删除。我们只需找到 previous 提交的哈希 ID,即哈希 ID H。我们可以使用git log 来做到这一点,然后剪切并粘贴哈希 ID,或者我们可以使用 Git 内置的一些特殊语法:

git reset --hard HEAD~

语法HEAD~ 的意思是:找到以HEAD 命名的提交,然后返回到它的(第一个也是唯一的)父提交。这将在此特定绘图中定位提交 H

reset 命令然后将分支名称移动到该提交,并且——因为--hard——更新了我们的工作树和Git 的索引aka暂存区 匹配:

          K   ???
         /
...--G--H   <-- br-A (HEAD), main
      \
       I--J--K'  <-- br-B

提交K 不再有办法找到它,所以除非你告诉他们,否则没人会知道它在那里。

请注意,鉴于此特定绘图,我们也可以完成git reset --hard main。不过,HEAD~1 样式语法甚至在其他情况下也有效。

git branch -f

如果我们不先签出br-A,我们可以使用git branch -f 强制它退一步。这和git reset有同样的效果,但是因为我们没有按名称检查分支,所以我们不用担心我们的工作树和Git的index/staging-area:

git branch -f br-A br-A~

在这里,我们使用名称 br-A 的波浪线后缀让 Git 后退一个第一父跃点。效果是完全一样的,但是我们只有在还没有签出分支br-A的情况下才能这样做。

一种特殊情况

假设我们上面的图不太正确。也就是说,假设在我们提交K 之前,分支br-Abr-B 指向不同的提交,它们都指向相同的提交。例如,我们可能有:

...--G--H   <-- main
         \
          I--J   <-- br-A (HEAD), br-B

如果我们在这种情况下然后提交K,我们会得到这个:

...--G--H   <-- main
         \
          I--J   <-- br-B
              \
               K   <-- br-A (HEAD)

请注意,在这种情况下,没有我们不喜欢提交K:它有正确的快照并且它有正确的元数据。 唯一的问题是名称br-A 指向K,而br-B 指向J。我们希望br-B 指向Kbr-A 指向J

我们可以通过以下方式得到我们想要的:

移动两个分支名称,或 交换分支名称

我们可以使用git resetgit branch -f 的组合来完成第一个。我们只需要小心不要丢失提交 K 的哈希 ID。

我们可以运行git log并剪切粘贴K的hash ID,这样就不会丢失,然后运行:

git reset --hard HEAD~

得到:

...--G--H   <-- main
         \
          I--J   <-- br-A (HEAD), br-B
              \
               K   ???

然后我们就可以运行了:

git branch -f br-B <hash-of-K>

粘贴正确的哈希,得到:

...--G--H   <-- main
         \
          I--J   <-- br-A (HEAD)
              \
               K   <-- br-B

例如。或者,我们可以更新br-Bfirst,而不是采取那种有点冒险的方法(如果我们不小心剪切了一些其他文本并丢失了哈希 ID,会发生什么情况?):

git branch -f br-B br-A

或:

git checkout br-B; git merge --ff-only br-A

(其中引入了--ff-only合并的概念,这里我就不解释了)得到:

...--G--H   <-- main
         \
          I--J
              \
               K   <-- br-A, br-B

其中一个是当前分支。然后我们可以修复br-A 将其移回一跳。

最后,我们可以使用“重命名两个分支”的技巧。这需要选择第三个名称来临时使用:

git branch -m temp        # rename br-A to temp
git branch -m br-B br-A   # rename br-B to br-A
git branch -m br-B        # rename temp to br-B

在所有这些情况下,不必复制任何提交,因为K 的格式已经正确。我们只需要稍微调整一下names

关键通常是画图

如果您不确定这些事情,画图

您可以让 Git 或其他程序为您绘制图表:请参阅 Pretty Git branch graphs。请注意,绘制和阅读图表需要一些练习,但这是 Git 中的一项重要技能。

绘制图表后,您可以判断是否需要新的和改进的提交——您可以通过git cherry-pick 和也许git rebase 获得——和/或哪个分支名称你需要重新指出。

这也让您深入了解我提到的那个警告。 当您将提交复制到新的和改进的提交时,任何已经包含旧的和糟糕的提交的 Git 存储库1 也需要更新。因此,如果您使用过git push 发送旧的和糟糕的提交到其他 Git 存储库,确保他们——无论“他们”是谁——也愿意更新。如果您无法让他们切换,那么进行新的和改进的提交只会造成大量重复提交,因为他们会不断将旧的和糟糕的提交重新放入如果你继续把它们拿出来。因此,如果您发布了一些提交,请确保他们——无论他们是谁——再次同意切换到改进的提交,然后再进行 rebase 或其他操作。


1如果某些东西是新的和改进的,那么旧版本会告诉您什么?也许这里的“糟糕”太强烈了,但至少是令人难忘的。

【讨论】:

以上是关于将一个分支与另一个首先提交的分支合并的主要内容,如果未能解决你的问题,请参考以下文章

从错误分支恢复合并后无法从另一个远程拉取

git 怎样将分支上的一个单文件合并到主分支上(master)

git---怎样将分支上的一个单文件合并到主分支上(master)

撤消 Git 分支提交和合并

我们可以提交已经与 master 合并的分支吗?

如何恢复从一个分支合并的所有提交