在主服务器上合并特定提交



我有一个 git 存储库,在 git log -3 上 - 在线看起来像这样,

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

顶部提交被错误推送,我想去提交 7d465b6,进行所需的更改并推回主节点顶部。

我试过了,

git checkout 7d465b6
//MAde Chnages

但是在 git 分支上它显示,

* (HEAD detached from 7d465b6)
master

而且我不能合并..

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

正如 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 在其列表的顶部绘制了这个,而我把它画到了右边,以表明它是最后一个。

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

提交G包含每个文件的完整快照(如之前的形式),您的姓名和电子邮件地址以及消息"显示版本和其他重要详细信息",以及早期提交F的哈希ID(67a69a6)。

提交F里面有...好吧,你现在应该明白了。 看看如何,如果我们知道提交H的哈希 ID,我们可以让 Git 提取提交H,然后用它来查找提交G? 然后 Git 可以回去提交F,从F开始,Git 可以再往后追溯。 换句话说,Git 可以通过遍历(一次一次一次)从每次提交到其早期或提交来查找此提交链的整个历史记录

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

第一行git log内容如下:

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

括号中的内容(包括名称master)是Git 可以找到此提交的方式分支名称master包含哈希 IDf5d394d(完整的,而不是我们显示的缩写)。远程跟踪名称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指向名称master这在某些方面更好。 但是特殊名称附加到 master,例如:

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

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

HEAD附加到分支名称时,Git 会说你"在"该分支上:

$ git status
On branch master

例如。

HEAD分离时,git status不会说你"在"一个分支上,你的git branch输出会说HEAD detached from ...

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

当您运行时:

git checkout 7d465b6

你告诉你的 Git 将HEAD从名称中分离出来master. 现在HEAD只保存原始哈希 ID7d465b6(不过是完整的)。 这是我G调用的提交,这意味着您处于这种情况:

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

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

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

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

假设我们处于分离的 HEAD 状态:

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

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

H
/
...--F--G

I

请注意I如何指向回点以提交G。 这是因为提交G是我们进行提交时HEAD提交。

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

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

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

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

I   <-- HEAD

因此,我们的提交只能使用特殊名称HEAD找到。

如果我们运行:

git checkout master

要将我们的HEAD重新附加到master,我们得到:

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

I   ???

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

错误的方式:力推

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

H   ???
/
...--F--G

I   <-- master (HEAD)

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

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

I   <-- master (HEAD)

我们的origin/master发现提交H的原因是,上次我们在 GitHub 上与 Git 进行 Git 对话时,那里的存储库master指向其提交H副本。

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

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

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

H   ???
/
...--F--G

I   <-- master (HEAD), origin/master

没有人能再找到H- 除了克隆 GitHub 存储库的任何其他人,他们有自己的master指向他们的提交H副本。

因此,此方法可以工作。 只是如果我们这样做,我们就会在给拥有 GitHub 存储库克隆的其他人制造问题。 他们现在必须采取措施更新他们的存储库,删除提交H(真的是f5d394d)。 因此,虽然我们可以强制名称master指向提交I,从而"丢失"提交H,然后使用git push --forcegit push --force-with-lease将我们的更新发送到 GitHub 上的 Git 存储库master,但我们不应该这样做。

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

Git 通常是为添加提交而构建的。 请注意,在我们尝试删除提交H时,我们违背了这种一般哲学。 如果我们改用它呢? 假设我们没有删除有错误的提交H,而是添加一个提交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命令。 还原需要获取我们要回退的提交的哈希 ID。 你说:

顶部提交被错误推送

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

git revert f5d394d

在这里,给出我们要撤消的提交的原始哈希 ID(嗯,缩写的那个)。 但是这个名字master目前指向f5d394d,所以git revert master也可以。

Git 现在将查看提交GH(两个快照),以找出提交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以指向提交K。 这只是添加到其存储库中的提交中。

(请注意,他们不会得到我们的提交I。 我们找不到它,所以我们的 Git 懒得把它发送给他们。 即使我们确实把它发送给他们——我们不会——我们也不会要求他们设置任何名称来找到它,所以他们不会找到它。

最新更新