繁体   English   中英

GIT 回到特定的commit id而不删除历史

[英]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 来绘制提交。 在这里,我们有您最初的四个提交,我们简称为ABCD

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显示您提交了BA并停止了。 为什么?

Git 使用你的 HEAD 可以看到东西

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)

也就是说,名称developmain都指向提交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将显示提交DCBA然后停止。 运行git log main将显示E ,然后是D ,然后C ,依此类推。

请注意,通过D向上提交在两个分支上。 但是现在我们在develop而不是main上,让我们进行另一个新的提交:

           E   <-- main
          /
A--B--C--D
          \
           F   <-- develop (HEAD)

提交AD仍然在两个分支上,但现在maindevelop每个分支都有一个提交,而另一个分支没有。 这两个名称选择最新的提交,即EF E是最新的main分支提交, F是最新的develop分支提交。 他们都是“最新的提交”! 如果我们在develop上进行另一个新的提交,就像这样:

           E   <-- main
          /
A--B--C--D
          \
           F--G   <-- develop (HEAD)

那么最近的两个提交现在是EG 每个分支名称“表示”特定的提交,根据定义,这是该分支上的最新提交。 此外,您(或 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。 但是,只要我们能找到JK ,我们就不会丢失H ,即使我们完全删除名称main 删除该名称仅意味着我们不再可以直接访问提交H :我们必须通过从K回溯一步或从J回溯两步来找到它。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM