在 Git 中,每次提交都会保存一个快照——即每个文件的状态——而不是一组更改。
然而,每个提交——嗯,几乎每个提交——也有一个父(上一个)提交。如果你问 Git,例如 commit a123456 发生了什么?,Git 所做的就是找到 a123456 的 父,提取 那个 快照,然后提取a123456 本身,然后将两者进行比较。无论a123456 中有什么不同,Git 都会告诉你。
由于每次提交都是一个完整的快照,因此很容易在特定提交中将特定文件的特定版本还原为到。例如,您只需告诉 Git:Get me file b.ext from commit a123456,现在您就获得了来自 commit a123456 的文件 b.ext 的版本.这就是您似乎在问的问题,因此链接的问题和当前答案(截至我输入此内容时)提供了什么。不过,您编辑了您的问题,要求提供一些完全不同的东西。
多一点背景
我现在必须猜测五次提交中每一次的实际哈希 ID。 (每个提交都有一个唯一的哈希 ID。哈希 ID(由字母和数字组成的大而难看的字符串)是提交的“真实名称”。这个哈希 ID 永远不会改变;使用它总是会让你那个 提交,只要该提交存在。)但它们是由字母和数字组成的又大又丑的字符串,所以不要猜测,比如说,8858448bb49332d353febc078ce4a3abcc962efe,我只会称你的“提交 1”D,你的“提交 2" E,以此类推。
由于大多数提交都有一个父提交,这让 Git 从较新的提交向后跳到较旧的提交,让我们用这些向后箭头将它们排列成一行:
... <-D <-E <-F <-G <-H <--master
像master 这样的分支名称 实际上只是保存了该分支上最新 提交的哈希ID。我们说名称指向提交,因为它具有让 Git 检索提交的哈希 ID。所以master 指向H。但是H的哈希ID是G,所以H指向它的父G; G 的哈希 ID 为 F;等等。这就是 Git 设法首先向您显示提交 H 的方式:您问 Git 哪个提交是 master? 它说 H。你让 Git 向你展示H,Git 提取G 和H 并比较它们,然后告诉你H 发生了什么变化。
你的要求
我想恢复对提交 3 [F] 的文件 b 所做的更改。但我希望在提交 4 和 5 [G 和 H] 中该文件发生的更改。
请注意,此版本的文件可能不会出现在任何提交中。如果我们采用提交E(您的提交2)中出现的文件,我们会得到一个没有来自F的更改,但它没有添加来自G和H的更改。如果我们继续做将G的更改添加到它,那可能与in G的更改不一样;如果我们在那之后添加从 H 到它的更改,那可能与 in H 的内容不同。
显然,这会有点困难。
Git 提供了git revert 来做这件事,但是做的太多了
git revert 命令旨在执行此类操作,但它在提交范围内执行此操作。 git revert 所做的实际上是找出某个提交中发生了什么变化,然后(尝试)撤消只是那些更改。
这是考虑git revert 的一种相当好的方法:它通过将提交与其父级进行比较,将提交(快照)转换为一组更改,就像查看提交的所有其他 Git 命令一样。因此,对于提交F,它会将其与提交E 进行比较,找到对文件a、b 和c 的更改。然后——这是第一个棘手的部分——它将这些更改反向应用到你的当前提交。也就是说,由于您实际上是在提交 H,git revert 可以获取所有三个文件中的任何内容——a、b 和 c——并且(尝试)撤消 em> 正是在提交E 中对他们做了什么。
(实际上有点复杂,因为这个“撤消更改”的想法还考虑了自提交 F 以来所做的其他更改,使用 Git 的三向合并机制。)
从某个特定提交中反向应用所有更改后,git revert 现在进行 new 提交。所以如果你做了一个git revert <hash of F>,你会得到一个new提交,我们可以称之为I:
...--D--E--F--G--H--I <-- master
其中F 对所有三个文件的更改都被撤销,产生了三个可能不在早期提交中的版本任何。但这太过分了:您只希望 F 对 b 的更改退出。
因此,解决方案是少做一点,或者做太多然后修复它
我们已经将git revert 操作描述为:找到更改,然后反向应用相同的更改。我们可以使用一些 Git 命令自己手动执行此操作。让我们从git diff 或简写版本git show 开始:这两者都将快照变成了变化。
使用git diff,我们将Git指向父E和子F并问Git:这两者有什么区别?Git提取文件,比较他们,并向我们展示发生了什么变化。
使用git show,我们将Git 指向提交F; Git 自己找到父 E,并提取文件并比较它们并向我们展示发生了什么变化(以日志消息为前缀)。也就是说,git show <em>commit</em> 等于 git log(仅用于那一次提交),然后是 git diff(从该提交的父级到该提交)。
Git 将显示的更改本质上是说明:它们告诉我们,如果我们从 E 中的文件开始,删除一些行,并插入一些其他行,我们将获得F 中的文件。所以我们只需要 reverse 差异,这很容易。事实上,我们有两种方法可以做到这一点:我们可以将哈希 ID 与 git diff 交换,或者我们可以使用 -R 标志到 git diff 或 git show。然后我们会得到说明,实质上是:如果您从F 的文件开始,并应用这些说明,您将获得来自E 的文件。
当然,这些说明会告诉我们更改所有三个文件,a、b 和 c。但现在我们可以剥离三个文件中的两个文件的说明,只留下我们想要的说明。
同样,有多种方法可以做到这一点。显而易见的是将所有指令保存在一个文件中,然后编辑该文件:
git show -R <em>hash-of-F</em> > /tmp/instructions
(然后编辑/tmp/instructions)。不过,还有一个更简单的方法,那就是告诉 Git:只麻烦显示特定文件的说明。我们关心的文件是b,所以:
git show -R <em>hash-of-F</em> -- b > /tmp/instructions
如果您检查说明文件,它现在应该描述如何获取 F 中的内容并取消更改 b 以使其看起来像 E 中的内容。
现在我们只需要让 Git 应用这些指令,除了来自提交 F 的文件,我们将使用来自当前提交 H 的文件,它已经坐在我们的工作树上准备修补。应用补丁的 Git 命令(一组关于如何更改某些文件集的说明)是 git apply,所以:
git apply < /tmp/instructions
应该可以解决问题。 (但请注意,如果指令说要更改 b 中随后被提交 G 或 H 更改的行,这将失败。这是 git revert 更聪明的地方,因为它可以完成整个“合并”的事情。)
成功应用指令后,我们可以查看文件,确保它看起来正确,然后照常使用git add 和git commit。
(旁注:您可以使用以下方法一次性完成所有操作:
git show -R <em>hash</em> -- b | git apply
而且,git apply 也有自己的 -R 或 --reverse 标志,因此您可以拼写为:
git show <em>hash</em> -- b | git apply -R
它做同样的事情。还有额外的git apply 标志,包括-3 / --3way,可以让它做更有趣的事情,就像git revert 一样。)
“做得太多,然后放弃一些”的方法
另一种相对简单的处理方法是让git revert 完成所有工作。当然,这将取消对 other 文件的更改,这是您不想取消的。但是我们在顶部展示了从 any 提交中获取 any 文件是多么容易。那么,假设我们让 Git 撤消 所有 F 中的更改:
git revert <em>hash-of-F</em>
这使得新的提交I 退出F 中的一切:
...--D--E--F--G--H--I <-- master
现在git checkout a 和 c 这两个文件是微不足道的来自 提交H:
git checkout <em>hash-of-H</em> -- a c
然后进行新的提交J:
...--D--E--F--G--H--I--J <-- master
I 和 J 中的文件 b 是我们想要的方式,J 中的文件 a 和 c 是我们想要的方式 - 它们与文件 @ 987654451@ 和 c 在 H 中——所以我们现在差不多完成了,除了烦人的额外提交 I。
我们可以通过多种方式摆脱I:
-
在创建J 时使用git commit --amend:这会将I 推开,通过提交J 使用提交H 作为J 的父级。提交I 仍然存在,只是被放弃了。最终(大约一个月后)它会过期并真正消失。
如果我们这样做,提交图将如下所示:
I [abandoned]
/
...--D--E--F--G--H--J <-- master
-
或者,git revert 有一个 -n 标志告诉 Git:执行还原,但不要提交结果。(这也可以使用脏索引和/或工作树,但如果您确保从提交H 的干净签出开始,则无需担心这意味着什么。)这里我们将从H 开始,还原F,然后告诉 Git 从提交 H 中获取文件 a 和 c:
git revert -n <em>hash-of-F</em>
git checkout HEAD -- a c
git commit
由于我们在提交H 时执行此操作,因此我们可以使用名称HEAD 来引用提交H 中的a 和c 的副本。
(Git 是 Git,还有六种额外的方法可以做到这一点;我只是使用我们在这里一般说明的那些。)