首页 > 解决方案 > 在 master 之上合并一个特定的提交

问题描述

我有一个 git repo,在 git log -3 --online 上看起来像这样,

f5d394d (HEAD -> master, origin/master, origin/HEAD) Bug Fix
7d465b6 Show version and other important details
67a69a6 Bug fix in Timeout

顶部提交被错误地推送,我想去提交 7d465b6,进行必要的更改并推回 master 之上。

我努力了,

git checkout 7d465b6
//MAde Chnages

但在 git 分支上显示,

* (HEAD detached from 7d465b6)
master

而且我不能合并..

我怎样才能做到这一点?我想在提交 2 中进行更改并推回原点/主控

标签: gitgithub

解决方案


正如RomainValeri 在评论中指出的那样,您的最终目标可能不是最好的最终目标:如果您实现了它,您可能会给同一存储库的其他用户带来问题。但是,您遇到的直接问题很简单。这个:

* (HEAD detached from 7d465b6)

表示您处于 Git 所谓的分离 HEAD模式。你可能想知道这句话是什么意思。它有一个非常具体的定义,但出于您的目的,这意味着您没有使用master,这解释了为什么您没有得到您想要的。

在我们开始了解如何实现您想要实现的目标以及如何实现您可能应该做的事情之前,让我们看一下这个特定的git log输出:

f5d394d (HEAD -> master, origin/master, origin/HEAD) Bug Fix
7d465b6 Show version and other important details
67a69a6 Bug fix in Timeout

Git 在这里编码了大量的信息。但是,它没有编码的一个重要部分是提交图的结构。(要看到这一点,您必须添加--graph到您的git log命令中。)要正确讨论这个问题,我们必须讨论在 Git 中提交是什么以及对您有什么作用,并了解 Git 的真正工作原理。

Git 是关于提交的

Git 主要由两个数据库组成。其中一个——通常是最大的一个——是一个包含所有 Git提交对象和其他内部对象的数据库。该数据库是一个简单的键值存储,其中 Git 用来查找提交对象的键是哈希 ID。对 Git 来说重要的是,数据库中的每个对象都是只读的。这意味着任何对象的任何部分,包括提交对象,都不能被更改。

每行左侧的那些数字,例如f5d394d,是提交哈希 ID。(从技术上讲,它们也是缩写的。完整的哈希 ID 更长。这只是前七个十六进制数字。)所以这些数字是 Git 找到提交的方式:它只是在所有 Git 对象的大数据库中查找它们, 以获取提交对象。

每个提交由两部分组成:

  • 一部分保存每个文件的完整快照。提交“内部”的文件实际上作为单独的对象存储在 Git 对象数据库中,经过压缩和重复数据删除。这样,此提交是否重用了前一次提交中除一个文件之外的所有文件都没有关系。由于文件被存储对象,那些重用的文件只是重用现有的对象。当然,由于任何对象都无法更改,因此这些已提交的文件也无法更改(这使得共享它们正常)。

  • 提交的另一部分包含有关提交本身的信息:例如,谁提交、何时提交以及为什么提交(他们的日志消息)。Git 将此称为提交的元数据(数据是源快照)。在元数据中,Git 存储一个列表供自己使用:每个提交都有一组先前提交的原始提交哈希 ID 的列表。

大多数提交只存储一个先前提交的哈希 ID。这些是您日常的日常提交。他们存储的一个先前提交的哈希 ID 形成了一个简单的向后链接,在一个简单的向后查看的提交链中。我们可以画这个!我们可以使用真正的哈希 ID,但它们又大又丑而且看起来很随机。如果我们为每个提交使用一个大写字母,我们将在几次提交后用完(英文字母为 26,其他字母或多或少)但我们可以更好地跟踪我们的绘图,所以让我们这样做:

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

在这里,H代表f5d394d:链中的最后一个提交。Git 在它的列表顶部画了这个,而我把它画在右边,表明它是最后一个。

CommitH在其中包含每个文件的完整快照(作为其数据),以及您的姓名和电子邮件地址以及您的消息“错误修复”等作为其元数据。在该元数据中,Git 存储7d465b6了:我们将调用的提交的实际哈希 ID G

CommitG在其中包含每个文件的完整快照(与之前的形式一样),以及您的姓名和电子邮件地址以及您的消息“显示版本和其他重要细节”,以及67a69a6早期提交的哈希 ID ( ) F

CommitF里面有……嗯,你现在应该明白了。看看如何,如果我们知道 commit 的哈希 ID H,我们可以让 Git 提取 commit H,然后用它来查找 commit G?然后 Git 可以返回到 commit F,从F,Git 可以再往前返回。换句话说,Git 可以通过遍历,一次一个提交,从每个提交到其较早的提交或父提交,来找到这个提交链的整个历史

分支名称和其他名称,让 Git查找提交

第一git log行是:

f5d394d (HEAD -> master, origin/master, origin/HEAD) ...

括号中的内容(包括名称master)是Git 可以找到此提交的方式分支名称 master包含哈希 ID (完整的f5d394d,不是我们显示的缩写)。远程跟踪名称 具有相同的origin/master哈希 ID。其中的两个名字HEAD有点特殊,我们在这里暂时忽略它们。

在我缩短的绘制提交的方式中,我会这样做:

...--F--G--H   <-- master (HEAD)

在这里,我用一条直接连接线将每个提交的箭头替换回其父级。这部分是因为懒惰,部分是因为当我想画这样的线条时,我没有好的箭头字符:

          H
         /
...--F--G

例如。

特别的名字HEAD

在所有情况下,H仍然指向G,但是当我执行上述操作时,我可以绘制指向 的东西G,如下所示:

          H   <-- master
         /
...--F--G   <-- HEAD

在上一张图中,特殊名称HEAD不再附加在名称上master。在Git 的绘图中:

f5d394d (HEAD -> master, origin/master, origin/HEAD) ...

特殊名称HEAD指向namemaster这在某些方面更好。但是特殊名称附加master 上,如下所示:

...--F--G--H   <-- master (HEAD)

有助于解释为什么另一种模式称为分离 HEAD。特殊名称附加到分支名称HEAD分离

HEAD附加到一个分支名称时,Git 说你在那个分支上:

$ git status
On branch master

例如。

When HEADis detachedgit status不会说你“在”一个分支,你的git branch输出会说像HEAD detached from ....

但是,特殊名称始终HEAD存在,并且总是告诉 Git 你当前的提交是什么。如果它附加到分支名称,则当前提交的哈希 ID 存储在分支名称中,并且您“在”该分支。因此,如果指向并附加到,则找到 commit并且您在 branch 上。masterHHEADmasterHEADHmaster

当你跑的时候:

git checkout 7d465b6

你告诉你的 Git与 name分离 。现在只保存原始哈希 ID (尽管是完整的)。这是我正在调用的提交,这意味着您处于这种情况:HEADmasterHEAD7d465b6G

          H   <-- master
         /
...--F--G   <-- HEAD

我们在 Git 中所做的大部分工作都涉及进行新的提交

每当我们进行的提交时,我们都会告诉 Git:

  1. 制作一个新快照:这将是新提交的数据。
  2. 为新的提交收集新的元数据:您的姓名和电子邮件地址(来自git config设置user.nameuser.email)、一条日志消息,以及通过阅读找到的当前提交的哈希 IDHEAD
  3. 将所有这些写在一起作为一个新的提交,它会获得一个新的唯一哈希 ID。(为了保证唯一性,Git 添加了当前的日期和时间,这样即使我们以某种方式重用了所有文件,重用了用户名等等,新的提交仍然与之前不同一。)
  4. 有一个步骤 4,但让我们绘制前三个步骤的结果。

假设我们处于您的 detached-HEAD 状态:

          H   <-- master
         /
...--F--G   <-- HEAD

我们让 Git 写出一个新的提交,带有一个不可预测的哈希 ID。哈希 ID 包括您进行提交的确切秒数,我们无法预测,因此我们不知道哈希 ID 将是什么。但是我们将其称为 commit I,因为那是之后的下一个字母H

          H
         /
...--F--G
         \
          I

注意如何I点回 commit G。那是因为 commitGHEAD我们提交时的提交。

各种各样的名字呢?好吧,让我们现在完成第 4 步:

  1. 将新提交的哈希 ID 写入HEAD指示的分支名称。

而是HEAD超脱!我们不能将哈希 ID 写入任何分支名称。在这种情况下,Git 所做的是回退到将新的哈希 ID 写入HEAD

          H   <-- master
         /
...--F--G
         \
          I   <-- HEAD

所以我们的提交只能使用特殊名称findHEAD

如果我们运行:

git checkout master

重新附加我们的HEADto master,我们得到:

          H   <-- master (HEAD)
         /
...--F--G
         \
          I   ???

谁拥有 commit 的哈希 ID I?答案是:没有人。您再也找不到哈希 ID。 提交仍然存在,但您找不到它。

错误的方式:强制推送

现在,有一些方法可以找到丢失提交的哈希 ID,以便从这种错误中恢复。这些主要涉及使用 Git 所谓的reflogs。假设我们使用其中一个来查找 commit I。然后,我们告诉 Git 强制使用名称 master来识别 commit I。(有多种方法可以做到这一点,尽管我们不会在这里查看其中任何一种。)我们将得到的是:

          H   ???
         /
...--F--G
         \
          I   <-- master (HEAD)

发生了什么事H?和以前一样,什么都没有:它还在那里,只是更难找到了。在我们的例子中,它实际上现在仍然很容易找到,因为远程跟踪名称 origin/master找到了它:

          H   <-- origin/master
         /
...--F--G
         \
          I   <-- master (HEAD)

我们找到 commit的原因是,上次我们在 GitHub 上让 Git 与 Git 对话时,那里的存储库指向它的 commit 副本。origin/masterH masterH

(提交,当被推送到其他 Git 存储库时,保留其唯一的哈希 ID。所有 Git 始终为同一个内部 Git 对象计算相同的哈希 ID。这就是 Git 的“分布式”部分的工作方式。这些字母代表对于 Git 哈希 ID,在所有克隆之间共享。分支名称不是!)

如果我们想将此提交发送到另一个Git,例如 GitHub 上的那个,并让 GitHub Git 存储库使用 master来识别 commit I,我们将使另一个Git 存储库丢失的 commit 副本H

我们的 Git 中的结果将是这样的:

          H   ???
         /
...--F--G
         \
          I   <-- master (HEAD), origin/master

没有人能找到H更多的地方——除了克隆 GitHub 存储库的任何其他人,他们有自己的 master指向他们的 commit 副本H

所以,这个方法是可行的。只是如果我们这样做,我们会给所有拥有 GitHub 存储库克隆的其他人带来麻烦。他们现在必须采取措施更新他们的存储库,放弃提交Hf5d394d真的)。因此,虽然我们可以强制名称master指向 commit I,从而“丢失” commit H,然后使用git push --forcegit push --force-with-lease将我们的更新发送master到 GitHub 上的 Git 存储库,但我们不应该这样做。

我们应该做什么而不是强制推动

Git 通常是为添加提交而构建的。请注意,在我们尝试删除commitH时,我们违背了这一普遍理念。如果我们改为使用它会怎样?假设我们不删除H其中有错误的 commit ,而是添加一个提交来J撤消提交所做的操作H

我们可以这样做:

git checkout master
git revert master

第一个命令让我们回到 "on" master,因此该提交H是我们当前的提交:

          H   <-- master (HEAD)
         /
...--F--G
         \
          I   ???

我们再也找不到I了,所以我们将完全停止绘制它:

...--F--G--H   <-- master (HEAD)

现在我们运行git revert命令。Revert 需要获取我们想要退出的提交的哈希 ID。你之前这么说:

顶部提交被错误推送

所以这就是我们想要退出的提交。我们可以使用:

git revert f5d394d

在这里,给出我们想要撤消的提交的原始哈希 ID(嗯,缩写)。但是名称 master当前指向 f5d394d因此git revert master也可以使用。

Git 现在将查看提交G和(H两个快照)以找出commit 中发生了什么变化H。然后,Git 将撤消或退出这些更改。这保证可以工作,并生成一个与提交中的旧快照匹配的新快照G——这很好;这就是我们想要的快照——然后 Git 进行的提交,得到一个新的唯一哈希 ID:

...--F--G--H--J   <-- master (HEAD)

我们现在可以添加一个正确的修复,作为另一个新的提交K

...--F--G--H--J--K   <-- master (HEAD)

现在我们可以运行git push origin master以发送我们的提交并发送到J使用K我们的 GitHub 存储库的 Git。他们将添加提交JK,然后将 master更新为指向 commit K。这只是添加到其存储库中的提交。

(请注意,他们不会得到我们的 commit I。我们找不到它,所以我们的 Git 不会费心将其发送给他们。即使我们确实将其发送给他们——我们不会——我们也从不询问他们设置任何名称来找到它,所以他们不会找到它。)


推荐阅读