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