git - 在 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 中进行更改并推回原点/主控
解决方案
正如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
指向name,master
这在某些方面更好。但是将特殊名称附加到master 上,如下所示:
...--F--G--H <-- master (HEAD)
有助于解释为什么另一种模式称为分离 HEAD。特殊名称附加到分支名称HEAD
或分离。
当HEAD
附加到一个分支名称时,Git 说你在那个分支上:
$ git status On branch master
例如。
When HEAD
is detached,git status
不会说你“在”一个分支,你的git branch
输出会说像HEAD detached from ...
.
但是,特殊名称始终HEAD
存在,并且总是告诉 Git 你当前的提交是什么。如果它附加到分支名称,则当前提交的哈希 ID 存储在分支名称中,并且您“在”该分支。因此,如果指向并附加到,则找到 commit并且您在 branch 上。master
H
HEAD
master
HEAD
H
master
当你跑的时候:
git checkout 7d465b6
你告诉你的 Git与 name分离 。现在只保存原始哈希 ID (尽管是完整的)。这是我正在调用的提交,这意味着您处于这种情况:HEAD
master
HEAD
7d465b6
G
H <-- master
/
...--F--G <-- HEAD
我们在 Git 中所做的大部分工作都涉及进行新的提交
每当我们进行新的提交时,我们都会告诉 Git:
- 制作一个新快照:这将是新提交的数据。
- 为新的提交收集新的元数据:您的姓名和电子邮件地址(来自
git config
设置user.name
和user.email
)、一条日志消息,以及通过阅读找到的当前提交的哈希 IDHEAD
。 - 将所有这些写在一起作为一个新的提交,它会获得一个新的唯一哈希 ID。(为了保证唯一性,Git 添加了当前的日期和时间,这样即使我们以某种方式重用了所有文件,重用了用户名等等,新的提交仍然与之前不同一。)
- 有一个步骤 4,但让我们绘制前三个步骤的结果。
假设我们处于您的 detached-HEAD 状态:
H <-- master
/
...--F--G <-- HEAD
我们让 Git 写出一个新的提交,带有一个不可预测的哈希 ID。哈希 ID 包括您进行提交的确切秒数,我们无法预测,因此我们不知道哈希 ID 将是什么。但是我们将其称为 commit I
,因为那是之后的下一个字母H
:
H
/
...--F--G
\
I
注意如何I
点回 commit G
。那是因为 commitG
是HEAD
我们提交时的提交。
各种各样的名字呢?好吧,让我们现在完成第 4 步:
- 将新提交的哈希 ID 写入
HEAD
指示的分支名称。
而是HEAD
超脱!我们不能将哈希 ID 写入任何分支名称。在这种情况下,Git 所做的是回退到将新的哈希 ID 写入HEAD
:
H <-- master
/
...--F--G
\
I <-- HEAD
所以我们的新提交只能使用特殊名称findHEAD
。
如果我们运行:
git checkout master
重新附加我们的HEAD
to 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/master
H
master
H
(提交,当被推送到其他 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 存储库克隆的其他人带来麻烦。他们现在必须采取措施更新他们的存储库,放弃提交H
(f5d394d
真的)。因此,虽然我们可以强制名称master
指向 commit I
,从而“丢失” commit H
,然后使用git push --force
或git 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。他们将添加提交J
和K
,然后将其 master
更新为指向 commit K
。这只是添加到其存储库中的提交。
(请注意,他们不会得到我们的 commit I
。我们找不到它,所以我们的 Git 不会费心将其发送给他们。即使我们确实将其发送给他们——我们不会——我们也从不询问他们设置任何名称来找到它,所以他们不会找到它。)
推荐阅读
- r - 在 R 函数内部,我如何访问调用环境?
- java - 将特定字段的 Gson 自定义反序列化器转换为 String
- python - 如何将此循环转换为python中的经典方式?
- node.js - 为什么 node.js 发送整个公用文件夹,当只指定根 index.html 时?
- javascript - 如何检查对象是否在 Javascript 的数组中?
- python - ModuleNotFoundError:没有名为“googlesearch”的模块
- authentication - 当 iframe 被 X-Frame-Options 阻止时如何在 iframe 内进行身份验证
- php - Wordpress 分页不更新内容
- python - Python 和 pygame 错误消息“找不到图像”
- c# - 不点击时关闭表单