首页 > 解决方案 > 如何将提交导入分支

问题描述

我有一个 git commit 发生在某个分支中,现在主人没有它。如何将提交导入到 master?

宁愿使用合并,因为需要通过拉取请求。

标签: gittags

解决方案


Git 没有拉取请求。1 Git Hub具有拉取请求,其他允许您将 Git 用作服务的 Web 托管提供商也是如此。有几种底层 Git 机制可供这些服务构建一个花哨的、对用户更友好的界面。

您具体询问的是git merge. 可以使用这个命令——和/或 GitHub 和其他人提供的花哨的 Web 界面——而不了解发生了什么。我不认为这是一个好主意,我自己。但是,要了解真正发生了什么,首先需要“了解”一些基本概念。

其中之一与术语分支本身的含义有关:请参阅“分支”到底是什么意思? 当有人说“一个分支”时,有时他们的意思是一个分支名称——或者 Git 的各种其他名称中的一个,而不是分支名称——有时他们的意思是一个模糊定义的一系列提交

同样,使用 GitHub 上的拉取请求界面不需要这些。要使用它,您只需单击一些网络按钮。但这是一个好主意——我认为,至少——知道这些按钮会发生什么,为此,你需要了解 Git 是如何工作的。因此,让我们深入了解细节。


1我的意思是没有git pull-request命令。有一个命令,它的git request-pull作用是生成一封电子邮件。这就是 Git 支持拉取请求的程度:它有一个命令来生成一封电子邮件,要求其他人做某事。


提交

Git 提交是您创建快照时所有文件状态的快照,以及一些描述快照的元数据。(从技术上讲,提交间接代表快照,因为快照作为对象单独存储,但在大多数情况下,您不需要知道这一点:从提交到快照有一个单向链接,因此给定提交Git 总能找到快照。这种链接意味着多个不同的提交可以代表同一个快照,而不会占用两次空间,但这对于多种用途很有用。)

每个提交都由一个哈希 ID 标识,例如b5101f929789889c2e536d915698f58d5c5c6b7a. 这些东西又大又丑,人类无法处理,但它们是 Git查找提交的方式,因此它们对于 Git 的操作至关重要。任何一个特定提交的哈希 ID 始终是唯一的:该哈希 ID 是提交,而不是其他任何提交;每个其他提交都有不同的哈希 ID。此外,宇宙中所有的 Git 都同意这些哈希 ID 计算。 给定两个不同的 Git 存储库,如果它们都有某个提交 ID H——也就是说,它们都有一个哈希为H的提交对象——则该对象的内容必须相同。2

提交中的元数据包括您的姓名(或提交人的姓名)和电子邮件地址,以及提交时间的时间戳。它包括您的日志消息,告诉所有人您做出此提交的原因。但它还包括紧接此之前的提交的哈希 ID。我们称之为提交的父级。结果是一个向后看的链条。如果我们有一个只有三个提交的小型存储库,我们可以这样绘制它:

A <-B <-C

C最后一次提交。它有一些独特的、又大又丑的哈希 ID。它还存储了提交的唯一哈希ID B,因此一旦找到C,我们就可以使用它来查找B。同时B具有 的哈希 ID A,所以我们可以使用B来查找A。由于A是第一次提交,它根本没有父提交——从技术上讲,它是一个提交——这让我们停止了倒退。

关于提交——以及所有具有哈希 ID 的 Git 对象——要了解的另一件事是,您永远无法更改它们中的任何一个。原因是哈希 ID 是对象内容的加密校验和。如果您要获取一个提交对象并更改一点点——例如在日志消息中修复一个单词的拼写——你最终会得到一个新的不同的 commit,并具有不同的哈希 ID。因此,一旦提交,它就是永远的:现在使用该哈希 ID3

这对我们来说意味着我们不需要将来自提交的箭头绘制箭头。一旦提交存在,它就是永久的,它与父级的联系也是永久的。我们只需要记住,它们只会一条路:倒退。新链接可以出现这个提交中,但它们不能这个提交出现到任何新的地方。


2请注意,相同的哈希 ID 表示相同的底层对象内容的要求仅在满足和交换对象的两个 Git 中维护。两个从不连接的存储库可以有这样的doppelänger 提交,只要它们从不尝试相互交谈。

3你可以完全删除一个提交,如果有点痛苦,只是不再引用它。最终,底层 Git 对象消失,有效地释放了哈希 ID。由于当前哈希 ID 系统中有 160 位,因此任何 Git 存储库中只有 2 160个可能的对象。幸运的是,这已经足够了。尽管如此,鸽子洞原理生日悖论相结合还是产生了一些有趣的理论问题,以及新发现的 SHA-1 碰撞对 Git 有何影响?对此进行了讨论。


分行名称

鉴于上述存储库,如果我们知道 commit 的哈希 ID,我们可以找到所有提交C。我们将把它存储在哪里?怎么样:在一个分支名称中? 让我们选择一个类似的名称master并用它来写下 的哈希 ID C

A--B--C   <-- master

现在让我们以通常的方式通过检查提交并做一些工作来进行新的提交:C

git checkout master
... do some work ...
git add ... various files ...
git commit

新提交将打包一个新快照,添加我们提供的任何日志消息,添加我们的姓名和电子邮件地址以及时间戳,并且至关重要的是,将新提交的级设置为 commit C

A--B--C--D

作为提交的最后一步,git commit将获取新提交的哈希 ID — 无论实际校验和是什么,现在所有部分都已永久固定到位 — 并将该校验和写在 name 中master

A--B--C--D   <-- master

这就是分支名称的含义:它是存储最后一次提交的哈希 ID 的地方。普通人不必记住哈希 ID,因为我们让 Git 为我们记住它们。我们只记得master保存了最新提交的哈希 ID,剩下的交给 Git。

这就是你的 HEAD 的用武之地

当然,您可以创建多个分支名称。每一个都只指向一个特定的提交。现在让我们创建一个新分支dev

A--B--C--D   <-- master, dev

请注意,masteranddev 指向提交D,并且所有四个提交都在两个分支上。但是 Git 需要一种方法来知道在我们进行新提交时要更改哪个名称。这就是特殊名称的HEAD来源。我们让 Git 将此名称附加到一个(也是唯一一个)分支名称:

A--B--C--D   <-- master (HEAD), dev

或者:

A--B--C--D   <-- master, dev (HEAD)

我们使用 来执行此操作git checkout,它不仅检查提交,还附加HEAD. 如果HEAD附加到master并且我们进行新的提交E,它看起来像这样:

           E   <-- master (HEAD)
          /
A--B--C--D   <-- dev

如果我们现在切换HEADdev(通过做git checkout dev)并进行新的提交F,我们得到:

           E   <-- master
          /
A--B--C--D
          \
           F   <-- dev (HEAD)

这就是合并的用武之地

假设我们有一个包含一堆提交的存储库,其中最后几个看起来像这样:

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

在这里,我们有一些以哈希为 的提交结尾的一系列提交H,并H持有一些快照。然后有人——也许是我们——又做了两次提交I,并J在 branch 上br1,我们现在就在上面。有人——可能是我们,也可能是其他人——开始H并做出了另外两个提交KL. 即使其他人制作KL在不同的存储库中也是如此。 我们都有H,而且由于所有地方的 Git 都同意哈希 ID 计算,我们都从同一个 commit 开始

git merge命令将做的是找出我们在分支中更改的br1内容以及他们在分支中更改的内容br2。然后它将结合这些更改。但是我们已经注意到分支这个词往往是模糊的和不明确的。Git在这里真正要做的是找到共同的提交,这是我们俩开始的地方。我们已经看到这是 commit H

因此,提交H是合并操作的合并基础。另外两个有趣的提交只是由我们当前分支命名的一个 -J在顶端提交br1- 另一个由另一个分支命名,L在顶端提交br2。所有三个提交都是快照,所以 Git 需要比较它们:

  • git diff --find-renames hash-of-H hash-of-J找到我们在 br1 中所做的
  • git diff --find-renames hash-of-H hash-of-L找到他们在 br2 中所做的事情

Git 现在可以组合这两组更改。如果我们更改了某个文件而他们没有更改,Git 应该获取我们的新文件。如果他们更改了某些文件而我们没有更改,Git 应该获取他们的新文件。如果我们都更改了同一个文件,Git 应该从合并基础提交H本身的文件副本开始,合并两个不同的更改,并将合并的更改应用于该文件。

这就是git merge在这种情况下所做的:它结合了我们的更改及其更改,从而生成了一个新的合并快照。这个合并变化的过程就是我喜欢将其称为动词的合并,或合并。重要的是要记住,这可以通过其他命令来完成,因为其他 Git 命令可以做到!合并合并为动词使用 Git 的合并引擎来组合工作。

但是,在这种情况下,git merge现在继续进行合并提交。这几乎只是一个普通的提交:它有一个快照、一个日志消息等等,就像任何其他提交一样。它的特别之处——合并提交——是它有两个父提交,而不是通常的一个。第一个父级和往常一样:它是我们在运行时签出的提交git merge。第二父级就是另一个提交——我们使用 name 选择的那个br2,或者在本例中是 commit L

所以现在git merge进行合并(作为名词合并)或合并提交(作为形容词合并),如下所示:

       I--J
      /    \
...--H      M
      \    /
       K--L   <-- br2

我们的分支名称会发生​​什么变化?当然,和往常一样。Git 将此新合并提交的新哈希 ID 写入M当前分支名称:

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

这就是我们将合并某人的提交的方式——在这种情况下,某人是任何人的提交KLbr2

(请注意,一般情况下,如果我们,我们会得到相同的快照git checkout br2; git merge br1。合并基础是静止H的,两个提示是LJ,并且组合工作会产生相同的结果。改变的是这个另一个合并的第一个父级将是L,不是J,所以父母被交换,最终的名称更新将更新名称br2而不是br1。如果我们开始投入额外的合并选项,但是,像-X oursor -X theirs,更多的事情可能会有所不同。)

并非所有git merge命令都会导致合并

值得注意的是这里有一两个额外的皱纹。假设我们有这个图:

...--A--B--C--D--E--H--I   <-- branch1 (HEAD)
            \      /
             F----G   <-- branch2

我们跑git merge branch2。我们之前已经在 commit 合并了 branch2 H,它有父级EG. 合并基础被定义为(松散地——从技术上讲,它是 DAG 中的最低共同祖先)作为两个分支上最近的提交,这就是 commit G,因为从I我们可以向后走到H然后G,当然G我们只是呆在那里在G.

在这种情况下,git merge branch2会说已经是最新的并且什么也不做。这是正确的:他们的提交是G,而我们的提交I已经G是祖先(在这种情况下是祖父母),因此没有新的工作可以组合。

我们也可以有这样的相关情况:

...--A--B--C--D--E--H--I   <-- branch1
            \      /
             F----G   <-- branch2 (HEAD)

我们跑的地方git merge branch1。这次我们的承诺是G他们的承诺是I。合并基础仍然G像以前一样提交。Git 在这种情况下默认做的是对自己说:根据定义,比较的结果是空G的。G根据定义,将无与物结合的结果就是某物。所以我真正要做的就是git checkout hash-of-I。所以我会这样做,但同时,也要让名字branch2指向提交I 结果是:

...--A--B--C--D--E--H--I   <-- branch1, branch2 (HEAD)
            \      /
             F----G

Git 将此称为快进操作。Git 有时将其称为快进合并,这不是一个好的术语,因为不涉及实际的合并。

你可以强制 Git 进行真正的合并——G与自身进行差异化,不将任何东西与某物结合,然后进行真正的合并提交——给出:

...--A--B--C--D--E--H--I   <-- branch1
            \      /    \
             F----G------J   <-- branch2 (HEAD)

要在此处强制进行真正的合并,请使用git merge --no-ff branch1.

(有时你想要或需要一个真正的合并,有时快进-而不是好的。不管怎样,GitHub Web 托管界面上的点击按钮不允许或执行快进合并,即使你希望它们. 实际上,他们总是使用git merge --no-ff.)

这一切与拉取请求有何关系

拉取请求,甚至是 Git 更原始的选项,只有在流程中涉及多个 Git 存储库git request-pull时才有用。

在这种情况下,我们可能在 Repository #1 中有一系列提交:

       I--J   <-- master
      /
...--H 

同时,在 Repository #2 中,我们有:

...--H
      \
       K--L   <-- master

由于这是两个不同的存储库,它们有自己的私有分支名称。一个有它的主人持有散列 ID I。另一个有它的主人持有散列 ID L。提交H两个存储库中,而提交I-J仅在 #1 中,并且K-L仅在 #2 中。

如果我们以某种方式合并这两个存储库,同时更改名称以使它们不会发生冲突,我们将回到我们的常规合并情况:

       I--J   <-- master of Repository #1
      /
...--H
      \
       K--L   <-- master of Repository #2

正是 GitHub通过其可点击的 Web 界面所做的。无论你是谁——#1 或#2;让我们选择 #2 来具体一点——你告诉 GitHub:我希望他们合并我的主节点,即提交 L。GitHub 然后将你的提交逐位复制,以便它们的哈希 ID 保持不变——到他们的存储库,将提交的哈希 IDL放在一个特殊的名称下,该名称不是 master也不是任何其他分支名称。4 然后他们,GitHub,运行一个git merge,使用同样的特殊名称,它根本不是分支名称。如果一切正常,那么他们会告诉控制存储库 #1 的任何人,有来自您的拉取请求。

控制存储库 #1 的人现在可以单击“合并拉取请求”按钮。这需要 GitHub 已经完成的合并5并在他们的 GitHub 存储库中适当地移动他们的 master或任何分支名称:

       I--J
      /    \
...--H      M   <-- master
      \    /
       K--L

提交KL现在出现在他们的存储库中,可以通过从master.

这对你来说意味着,作为一个想要提出拉取请求的人,你必须安排你在 GitHub 上的存储库有一个提交或提交链,GitHub 将能够为你测试合并。然后,GitHub 会将请求呈现给该存储库的所有者,该所有者只需单击一下即可通过更新其分支名称以使用 GitHub 进行的测试合并来完成合并。

提交和测试合并结果由您放入 GitHub 存储库的提交决定。如果您在自己的本地机器上拥有自己的单独存储库,则可以将提交放入其中并用于git push将这些提交发送您的 GitHub 存储库。

显然,这有点令人费解——但如果你让本地机器的存储库和你自己的 GitHub 存储库保持同步,这样它们总是“看起来一样”,你可以忽略这里的额外层。忽略这一层的问题是它仍然存在!如果你让你的仓库和你的 GitHub 仓库不同步,这会再次出现。


4当您发出拉取请求时,GitHub 会为其分配一个唯一编号(对于目标存储库而言是唯一的)。假设这是 Pull Request #123。GitHub 用于您的提交的名称,一旦它们被复制到他们的 GitHub 存储库中,就是refs/pull/123/head. GitHub 用于其进行的测试合并的名称是refs/pull/123/merge. 如果测试合并因冲突而失败,GitHub 根本不会创建一个,也不会创建第二个名称。

5如果控制 PR 目标存储库的人将提交推送到他们的分支,则 GitHub 所做的测试合并将变得无效(它是“过时的”)。GitHub 将在适当的时候进行新的测试合并。我不确定他们是否会删除中间的refs/pull/123/merge名称,因为我从未测试过。


推荐阅读