![](/img/trans.png)
[英]git revert back to a specific commit that is already in the branch history but was undone
[英]GIT back to a specific commit id without deleting the history
这是我的提交日志,我想切换回特定的提交 ID(例如第二个),当我使用 git 结帐时,没问题,但是我无法再切换回最后一个提交(第四个)。
HEAD 指向第二次提交,之后当我记录我的提交时什么也没有。
如何在不删除历史记录的情况下在我的提交之间切换?
commit 61c71a9e5a6d9e29a4172e687172dd4b8523eb4a (HEAD -> main)
Author: mohhhe <mohhhe@gmail.com>
Date: Fri Feb 25 19:08:36 2022 +0330
Fourth
commit 9c3e8919cfa2c970f14056eef34ca12b49025f65
Author: mohhhe <mohhhe@gmail.com>
Date: Fri Feb 25 19:08:13 2022 +0330
Third
commit d33795596001197f382038a72d20faf0cfbe7ab7
Author: mohhhe <mohhhe@gmail.com>
Date: Fri Feb 25 19:07:55 2022 +0330
Second
commit 2fe7b1d8270fcfb41d73e69293da10734e37b069
Author: mohhhe <mohhhe@gmail.com>
Date: Fri Feb 25 19:07:39 2022 +0330
First
想象一下,您正站在大城市的一条狭窄街道或小巷的入口处,周围是摩天大楼。 沿着小巷往下看,可以看到一排排的垃圾箱。 现在,沿着小巷走一半,看看前方。 一半的垃圾箱不见了? 他们在哪里 go:无处。 他们就在你身后。
同样的想法也适用于这里:Git 没有删除你看不到的提交。 你只是看不到它们。 回到一个你能看到它们的有利位置,你会再次看到它们。
在 Git 中,提交是一个由两部分组成的实体:它包含所有文件的快照——嗯,Git 知道的所有文件,在你(或任何人)制作该快照时——以及一些元数据。 每个提交都有编号,带有一个大的、丑陋的、随机的 hash ID,例如61c71a9e5a6d9e29a4172e687172dd4b8523eb4a
,如您的 output 中所示。
hash ID 是 Git找到提交所需的 ID。 提交本身存储为一堆部分,使用提交 object和其他内部支持对象。 您在此处看到的 hash ID 是提交 object 本身的 ID,它仅包含元数据:快照位于树object 中,该树还有更多子对象。 但是您通常不需要知道这一点; 你需要知道的是 Git 有一个大数据库保存它的所有对象,每个对象都有编号,而 Git 本身需要这个编号来检索object。1
然而,人类非常不擅长数字。 那四个 hash ID 又是什么? 无论如何,不值得记住它们。 Git 为您提供了一种非常快速的方法来查找这四个 hash ID 中的一个:名称main
,您很容易记住,可以找到其中一个hash ID。
随着时间的推移, main
找到的 ID hash 可能会发生变化,但现在,它会为您找到61c71a9e5a6d9e29a4172e687172dd4b8523eb4a
,并为您找到 Git。该提交是分支main
上的最新提交。 根据定义,这是因为名称main
包含该 ID。 因此,如果您希望 Git 找到main
上的最新提交,您只需向 Git 询问main
,Git 将查找名称main
并找到该 ID,从而找到该提交。
如果以及当您进行新提交时,这是 Git 将执行的操作(按某种顺序;您真的看不到是否有任何特定的顺序):
为 Git 知道的每个文件制作快照。 要让 Git 看到您对 Git已经知道的文件所做的任何更新,您必须运行git add
到它。 要使 Git 看到您创建的直到现在才存在的任何新文件,您必须在其上运行git add
。 除了这个之外还有很多,但这是您必须继续运行的第一个近似值git add
:告诉 Git新快照应该使用新的或更新的文件。
收集一堆元数据。 Git 将收集的元数据包括您的姓名(在您的user.name
设置中设置)和 email 地址(来自您的user.email
设置)。 它包括精确到秒的当前日期和时间。 而且,它包括当前最新的提交,不管它是什么,在你所在的分支上——在本例中是main
。
Git 写出所有这些以进行新的提交,这将获得一个新的、唯一的、以前从未使用过、永远不会再次使用的 ID hash。 此 hash ID 绝不能出现在任何Git 存储库中,除非用于标识您刚刚进行的此提交。 (这就是为什么 hash ID 又大又丑的原因:所以它们可以是唯一的。)
Git 然后将新提交的 hash ID 存储在当前分支名称中。 所以现在名称main
选择了你的新提交——你刚做的那个。
1那是因为这个大数据库是一个键值存储,以 hash ID 为键。 有一种遍历整个数据库并获取每个 <key, value> 对的方法很慢,但是在大型存储库中这会花费很多秒甚至几分钟:太慢而无用。 密钥查找需要几毫秒,因此这就是您希望 Git 执行的操作。
这一切意味着名称main
自动并始终选择名为main
的分支中的最后一次提交。 根据定义, main
是街道/小巷/高速公路/高速公路/无论是什么的尽头。 当你在这条“道路”上时,你通过进行新的提交来添加新的提交,这进一步扩展了“道路”。
显示这一点的另一种方法是使用大写字母代表真实的 hash ID 来绘制提交。 在这里,我们有您最初的四个提交,我们简称为A
、 B
、 C
和D
:
A <-B <-C <-D <--main
名称main
将“指向”(包含 hash ID)这些提交中的最后一个,提交D
。 提交D
有一个快照——所有文件的副本,一直冻结——和一些元数据,而D
的元数据表明上一个提交是提交C
。 我们说D
指向C
。
提交C
,当然有快照和元数据。 快照保存 Git 在您制作C
时知道的文件,一直冻结,元数据保存日期和时间等,包括早期提交B
的 hash ID。 我们说C
指向B
。
提交B
也包含快照和元数据,并向后指向包含快照和元数据的提交A
。 但是提交A
是您所做的第一个提交,在您创建A
之前,它一直是一个完全空的存储库。 因此,提交A
不会向后指向更远的地方:它不能。
这就是您的存储库中的四次提交的方式。 他们永远不会改变,他们是完全只读的。 这四个 hash ID 现在永远用完了。 2名称main
指向最后一个 - 直到您进行新的提交。 然后新的提交E
出现,向后指向D
,并且 Git 更新名称main
以指向E
:
A <-B <-C <-D <-E <--main
2这在技术上是不可能的,并且 Git 并没有真正试图阻止任何其他人获得相同的 hash ID,除非使用加密技巧使它不太可能发生,以至于我们不必担心它。 没有人会不小心重复使用您的 hash ID。 加密货币也很难故意这样做。
但是当你想访问一个旧的提交时会发生什么? 你跑了:
git checkout d33795596001197f382038a72d20faf0cfbe7ab7
告诉 Git 从您的工作区中删除所有永久安全存储在提交D
中的文件,并告诉 go 返回提交B
:将永久存储的文件从提交B
提取到您的工作区中。 Git 这样做了,然后git log
显示您提交了B
和A
并停止了。 为什么?
Git 有一个非常特殊的名称HEAD
,它根本不是分支名称。 3相反,此名称HEAD
通常附加到分支名称。 这就是您的第一个git log
显示的内容:
commit 61c71a9e5a6d9e29a4172e687172dd4b8523eb4a (HEAD -> main)
Git 的名称HEAD
在这里“指向”名称main
。 我喜欢这样画:
A--B--C--D <-- main (HEAD)
名称HEAD
“附加到”名称main
。 (我也懒得在提交之间绘制箭头。请记住,从 A 到 B 到 C 到 D 的连接线实际上是向后指向的箭头。)
运行git log
告诉 Git:首先,使用HEAD
查找提交。 由于HEAD
附加到main
, Git 使用main
找到提交D
。 git log
命令然后显示您提交D
— 好吧,默认显示它; 有一些选项你可以给git log
来改变这个 - 然后跟随D
的箭头回到C
并显示C
。 然后git log
C
的箭头指向B
,显示B
,顺着B
的箭头指向A
,显示A
提交A
没有向后箭头,所以git log
终于可以停止了。
但是,当您 git 通过其hash ID git checkout
出提交时,Git 会进入 Git 所称的分离 HEAD模式。 在这里,名称HEAD
不再附加到分支名称。 相反,它直接指向提交。 如果你选择提交B
,你会得到这个:
A--B <-- HEAD
\
C--D <-- main
git log
命令像以前一样工作:它使用HEAD
来查找提交。 但是这次HEAD
找到提交B
,而不是名称main
然后提交D
。 所以git log
显示B
,然后跟随B
的箭头回到A
并显示A
,然后用完要显示的提交并停止。
如果您想查看所有提交,您可以:
git checkout main
切换回分支main
,重新连接你的HEAD
:
A--B--C--D <-- main (HEAD)
现在您从路的尽头开始main
git log
上的最后一次提交——您将看到所有四个提交。 或者,您可以运行:
git log main
它告诉git log
它应该使用名称main
来查找开始的提交。 这将找到提交D
,即使HEAD
仍然直接指向提交B
。
3创建一个名为HEAD
的分支在技术上是可行的。 不要这样做。
了解以上内容后,您就可以处理多个分支名称了。 假设我们有这个:
A--B--C--D <-- main (HEAD)
然后我们通过运行以下命令创建一个新名称,例如develop
,指向提交D
:
git branch develop
我们现在有这个:
A--B--C--D <-- develop, main (HEAD)
也就是说,名称develop
和main
都指向提交D
特殊名称HEAD
目前附加到名称main
上。 让我们在main
上做一个新的提交,提交E
,然后把它画进去:
E <-- main (HEAD)
/
A--B--C--D <-- develop
提交E
现在是main
上的最新提交,而提交D
仍然是develop
上的最新提交。
如果你现在运行:
git checkout develop
或者:
git switch develop
切换到分支develop
,我们得到:
E <-- main
/
A--B--C--D <-- develop (HEAD)
提交E
仍然存在,但是 Git 会将E
的所有文件带出我们的工作区,并放入D
的所有文件。 名称HEAD
现在附加到名称develop
,而不是名称main
,因此git log
将显示提交D
、 C
、 B
和A
然后停止。 运行git log main
将显示E
,然后是D
,然后C
,依此类推。
请注意,通过D
向上提交在两个分支上。 但是现在我们在develop
而不是main
上,让我们进行另一个新的提交:
E <-- main
/
A--B--C--D
\
F <-- develop (HEAD)
提交A
到D
仍然在两个分支上,但现在main
和develop
每个分支都有一个提交,而另一个分支没有。 这两个名称选择最新的提交,即E
和F
。 E
是最新的main
分支提交, F
是最新的develop
分支提交。 他们都是“最新的提交”! 如果我们在develop
上进行另一个新的提交,就像这样:
E <-- main
/
A--B--C--D
\
F--G <-- develop (HEAD)
那么最近的两个提交现在是E
和G
。 每个分支名称“表示”特定的提交,根据定义,这是该分支上的最新提交。 此外,您(或 Git)从那个“最新”提交开始并向后工作可以找到的所有提交都在该分支“上”。 所以当我们有:
I--J <-- br1
/
...--G--H <-- main
\
K--L <-- br2
我们有三个最新的提交,并且通过H
的提交在所有三个分支上。 选择一个名称进行检查,这就是您将在git log
中看到的一组提交; 您工作区中的文件将来自最新的(或最新的)提交。
请注意,提交永远不会改变:一旦您进行了提交,它就永远有效。 但是,我们通过分支名称找到提交,并且这些确实会移动。 如果我们采用最后一个示例并将名称br2
向后移动一跳:
I--J <-- br1
/
...--G--H <-- main
\
K <-- br2
\
L ???
我们可能永远无法再次找到提交L
它已“丢失”,因为无法恢复其 hash ID。 但是,只要我们能找到J
和K
,我们就不会丢失H
,即使我们完全删除名称main
。 删除该名称仅意味着我们不再可以直接访问提交H
:我们必须通过从K
回溯一步或从J
回溯两步来找到它。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.