仅还原推送提交的单个文件
Revert only a single file of a pushed commit
下面是推送的提交历史。
Commit Changed Files
Commit 1| (File a, File b, File c)
Commit 2| (File a, File b, File c)
Commit 3| (File a, File b, File c)
Commit 4| (File a, File b, File c)
Commit 5| (File a, File b, File c)
我想还原 文件 b 的 提交 3 发生的更改。
但我希望在提交 4 和 5 中对该文件进行更改。
假设您想要的提交的哈希是 c77s87:
git checkout c77s87-- file1/to/restore file2/to/restore
git结帐主页提供更多信息。
在Git中,每次提交都会保存一个快照——也就是说,每个文件的状态——而不是一个一组更改。
然而,每个提交——好吧,几乎每个提交——也有一个parent(前一个)提交。如果你问 Git,例如,提交 a123456
时发生了什么?,Git 所做的就是找到 parent 的 a123456
,提取 那个 快照,然后提取 a123456
本身,然后比较两者。 与a123456
有什么不同,Git就会告诉你。
由于每次提交都是一个完整的快照,因此很容易将恢复为特定提交中特定文件的特定版本。你只要告诉 Git: Get me file b.ext
from commit a123456
,例如,现在你有文件 [=262= 的版本]b.ext
来自提交 a123456
。这就是您似乎要问的问题,因此链接的问题和当前答案(截至我输入此内容时)提供的内容。不过,您编辑了您的问题,要求的内容完全不同。
更多背景知识
我现在必须猜测您的五次提交中每一次的实际哈希 ID。 (每个提交都有一个唯一的哈希 ID。哈希 ID——由字母和数字组成的又大又丑的字符串——是提交的 "true name"。这个哈希 ID 永远不会改变;使用它总是会让你 提交,只要该提交存在即可。)但它们是由字母和数字组成的又大又丑的字符串,所以不用猜测,比如 8858448bb49332d353febc078ce4a3abcc962efe
,我会直接调用你的 "commit 1" D
,你的"commit 2" E
,等等。
由于大多数提交都有一个 parent,它允许 Git 从较新的提交向后跳到较旧的提交,让我们用这些向后箭头将它们排列成一行:
... <-D <-E <-F <-G <-H <--master
一个 分支名称 就像 master
实际上只是保存了该分支上 最新 提交的哈希 ID。我们说名称 指向 提交,因为它具有让 Git 检索提交的哈希 ID。所以 master
指向 H
。但是H
的哈希ID是G
,所以H
指向它的parentG
; G
的哈希 ID 为 F
;等等。这就是 Git 首先设法向您展示提交 H
的方式:您问 Git 哪个提交是 master
? 并且它说 H
。你让 Git 给你看 H
和 Git 提取 G
和 H
并比较它们,然后告诉你在 H
.
发生了什么变化
您要求的内容
I want to revert the changes happened to File b of Commit 3 [F
]. But I want the changes [that] happened to this file in commit 4 and 5 [G
and H
].
请注意,此版本的文件可能不会出现在任何提交中。如果我们采用提交 E
(您的提交 2)中出现的文件,我们会得到一个没有来自 F
的更改的文件,但它没有来自 G
和 [= 的更改29=] 添加到它。如果我们继续 do 将 G
的更改添加到它,那可能与 in G
;如果我们在那之后将 H
的更改添加到它,那可能与 in H
.
的内容不同
显然,这会有点困难。
Git 提供了 git revert
来做这个,但是做的太多了
git revert
命令旨在执行此类操作,但它是在 commit-wide 基础上执行的。 git revert
所做的实际上是弄清楚某些提交中发生了什么变化,然后(尝试)撤消 只是 那些变化.
这里有一个很好的思考 git revert
的方法:它将提交——快照——变成一组更改,就像所有其他查看提交的 Git 命令一样,通过将提交与其 parent 进行比较。因此,对于提交 F
,它会将其与提交 E
进行比较,找到对文件 a
、b
和 c
的更改。然后——这是第一个棘手的部分——它 reverse-applies 那些更改 到你的 current 提交。也就是说,由于您实际上是在提交 H
,git revert
可以获取所有三个文件中的任何内容——a
、b
和 c
——并且(尝试)撤消 确切地到 他们提交E
.
(实际上有点复杂,因为这个"undo the changes"想法还考虑了自提交F
以来所做的其他更改,使用Git的three-way合并机械。)
拥有来自某个特定提交的所有更改 reverse-applied,git revert
现在进行 new 提交。所以如果你做了 git revert <hash of F>
你会得到一个 new 提交,我们可以称之为 I
:
...--D--E--F--G--H--I <-- master
其中 F
对所有三个文件的更改都被撤消,生成的三个版本可能不在早期提交的 any 中。但这太多了:您只想要 F
对 b
backed-out.
的更改
所以解决的办法就是少做一点,或者做多了,然后再去解决
我们我已经将 git revert
操作描述为: 找到更改,然后 reverse-apply 相同的更改 。我们可以使用一些 Git 命令自己手动完成此操作。让我们从 git diff
或 short-hand 版本 git show
开始:这两个版本都将 快照 转换为更改。
使用git diff
,我们将Git指向parentE
和childF
并询问Git: 这两个有什么区别? Git 提取文件,比较它们,并向我们展示发生了什么变化。
使用git show
,我们指向Git提交F
; Git 自己找到 parent E
,然后提取文件并比较它们并向我们展示发生了什么变化(以日志消息为前缀)。也就是说,git show <em>commit</em>
相当于 git log
(仅针对那一次提交),然后是 git diff
(从该提交的 parent,到该提交)。
Git 将显示的更改本质上是 说明: 它们告诉我们,如果我们从 E
中的文件开始,删除一些行,并插入一些其他行,我们将得到 F
中的文件。所以我们只需要反转差异,这很容易。事实上,我们有两种方法可以做到这一点:我们可以用 git diff
交换哈希 ID,或者我们可以使用 -R
标志来 git diff
或 git show
。然后我们会得到说明,本质上说:如果您从 F
中的文件开始,并应用这些说明,您将获得 E
中的文件。
当然,这些说明会告诉我们更改所有三个 文件,a
、b
和c
。但是现在我们可以剥离三个文件中两个文件的指令,只留下我们想要.
的指令
同样,有多种方法可以做到这一点。显而易见的一种是将所有指令保存在一个文件中,然后编辑该文件:
git 显示-R <em>hash-of-F</em> > /tmp/instructions
(然后编辑 /tmp/instructions
)。不过,还有一种更简单的方法,那就是告诉 Git: 只需为特定文件显示说明即可 。我们关心的文件是b
,所以:
git show -R <em>hash-of-F</em> -- b > /tmp/instructions
如果您查看说明文件,它现在应该描述如何使用 F
和 un-change b
中的内容,使其看起来像 E
中的内容.
现在我们只需要 Git 应用这些指令,除了我们将使用来自 current[= 的文件而不是来自提交 F
的文件338=] 提交 H
,它已经在我们的 work-tree 中准备好进行修补。应用补丁的 Git 命令——一组关于如何更改某些文件集的指令——是 git apply
,因此:
git apply < /tmp/instructions
应该可以解决问题。 (不过请注意,如果指令说要更改 b
中随后被提交 G
或 H
更改的行,这将 失败 。这就是 git revert
更聪明的地方,因为它可以完成整个 "merge" 的事情。)
成功应用指令后,我们可以查看文件,确保它看起来正确,然后像往常一样使用 git add
和 git commit
。
(旁注:您可以使用以下方法一次完成所有操作:
git show -R <em>hash</em> -- b | git申请
而且,git apply
也有自己的 -R
或 --reverse
标志,因此您可以这样拼写:
git 显示 <em>hash</em> -- b | git 应用-R
做同样的事情。还有额外的 git apply
标志,包括 -3
/ --3way
可以让它做更奇特的事情,很像 git revert
做的。)
"do too much, then back some of it out" 方法
另一种相对简单的处理方法是让 git revert
完成所有工作。当然,这将撤销对 other 文件的更改,您不希望 backed-out。但是我们在顶部展示了从 any 提交中获取 any 文件是多么容易得离谱。那么,假设我们让 Git 撤消 all F
:
中的更改
git 还原 <em>hash-of-F</em>
这使得新提交 I
退出 一切 in F
:
...--D--E--F--G--H--I <-- master
现在 git checkout
这两个文件 a
和 c
from commit H
:
git结帐<em>hash-of-H</em> -- a c
然后进行新的提交 J
:
...--D--E--F--G--H--I--J <-- master
I
和J
中的文件b
是我们想要的方式,[=127中的文件a
和c
=] 是我们想要的方式——它们匹配 H
中的文件 a
和 c
——所以我们现在差不多完成了,除了烦人的额外提交 I
.
我们可以去掉I
有几种方式:
在制作 J
时使用 git commit --amend
:这会将 I
推开,使提交 J
使用提交 H
作为 J
的 parent。 Commit I
仍然存在,只是被废弃了。最终(大约一个月后)它会过期并真正消失。
提交图,如果我们这样做,看起来像这样:
I [abandoned]
/
...--D--E--F--G--H--J <-- master
或者,git revert
有一个 -n
标志告诉 Git: 执行还原,但不要提交结果。 (这也允许使用脏索引 and/or work-tree 进行还原,但如果您确保从提交 H
的干净检出开始,则不需要担心这意味着什么。)这里我们将从 H
开始,还原 F
,然后告诉 Git 获取文件 a
和 c
从提交返回 H
:
git revert -n <em>hash-of-F</em>
git checkout HEAD -- a c
git commit
由于我们在执行此操作时正在提交 H
,因此我们可以使用名称 HEAD
来引用 a
和 c
的副本在提交 H
.
(Git 是 Git,还有六种其他方法可以做到这一点;我只是使用我们在这里一般说明的方法。)
下面是推送的提交历史。
Commit Changed Files
Commit 1| (File a, File b, File c)
Commit 2| (File a, File b, File c)
Commit 3| (File a, File b, File c)
Commit 4| (File a, File b, File c)
Commit 5| (File a, File b, File c)
我想还原 文件 b 的 提交 3 发生的更改。 但我希望在提交 4 和 5 中对该文件进行更改。
假设您想要的提交的哈希是 c77s87:
git checkout c77s87-- file1/to/restore file2/to/restore
git结帐主页提供更多信息。
在Git中,每次提交都会保存一个快照——也就是说,每个文件的状态——而不是一个一组更改。
然而,每个提交——好吧,几乎每个提交——也有一个parent(前一个)提交。如果你问 Git,例如,提交 a123456
时发生了什么?,Git 所做的就是找到 parent 的 a123456
,提取 那个 快照,然后提取 a123456
本身,然后比较两者。 与a123456
有什么不同,Git就会告诉你。
由于每次提交都是一个完整的快照,因此很容易将恢复为特定提交中特定文件的特定版本。你只要告诉 Git: Get me file b.ext
from commit a123456
,例如,现在你有文件 [=262= 的版本]b.ext
来自提交 a123456
。这就是您似乎要问的问题,因此链接的问题和当前答案(截至我输入此内容时)提供的内容。不过,您编辑了您的问题,要求的内容完全不同。
更多背景知识
我现在必须猜测您的五次提交中每一次的实际哈希 ID。 (每个提交都有一个唯一的哈希 ID。哈希 ID——由字母和数字组成的又大又丑的字符串——是提交的 "true name"。这个哈希 ID 永远不会改变;使用它总是会让你 提交,只要该提交存在即可。)但它们是由字母和数字组成的又大又丑的字符串,所以不用猜测,比如 8858448bb49332d353febc078ce4a3abcc962efe
,我会直接调用你的 "commit 1" D
,你的"commit 2" E
,等等。
由于大多数提交都有一个 parent,它允许 Git 从较新的提交向后跳到较旧的提交,让我们用这些向后箭头将它们排列成一行:
... <-D <-E <-F <-G <-H <--master
一个 分支名称 就像 master
实际上只是保存了该分支上 最新 提交的哈希 ID。我们说名称 指向 提交,因为它具有让 Git 检索提交的哈希 ID。所以 master
指向 H
。但是H
的哈希ID是G
,所以H
指向它的parentG
; G
的哈希 ID 为 F
;等等。这就是 Git 首先设法向您展示提交 H
的方式:您问 Git 哪个提交是 master
? 并且它说 H
。你让 Git 给你看 H
和 Git 提取 G
和 H
并比较它们,然后告诉你在 H
.
您要求的内容
I want to revert the changes happened to File b of Commit 3 [
F
]. But I want the changes [that] happened to this file in commit 4 and 5 [G
andH
].
请注意,此版本的文件可能不会出现在任何提交中。如果我们采用提交 E
(您的提交 2)中出现的文件,我们会得到一个没有来自 F
的更改的文件,但它没有来自 G
和 [= 的更改29=] 添加到它。如果我们继续 do 将 G
的更改添加到它,那可能与 in G
;如果我们在那之后将 H
的更改添加到它,那可能与 in H
.
显然,这会有点困难。
Git 提供了 git revert
来做这个,但是做的太多了
git revert
命令旨在执行此类操作,但它是在 commit-wide 基础上执行的。 git revert
所做的实际上是弄清楚某些提交中发生了什么变化,然后(尝试)撤消 只是 那些变化.
这里有一个很好的思考 git revert
的方法:它将提交——快照——变成一组更改,就像所有其他查看提交的 Git 命令一样,通过将提交与其 parent 进行比较。因此,对于提交 F
,它会将其与提交 E
进行比较,找到对文件 a
、b
和 c
的更改。然后——这是第一个棘手的部分——它 reverse-applies 那些更改 到你的 current 提交。也就是说,由于您实际上是在提交 H
,git revert
可以获取所有三个文件中的任何内容——a
、b
和 c
——并且(尝试)撤消 确切地到 他们提交E
.
(实际上有点复杂,因为这个"undo the changes"想法还考虑了自提交F
以来所做的其他更改,使用Git的three-way合并机械。)
拥有来自某个特定提交的所有更改 reverse-applied,git revert
现在进行 new 提交。所以如果你做了 git revert <hash of F>
你会得到一个 new 提交,我们可以称之为 I
:
...--D--E--F--G--H--I <-- master
其中 F
对所有三个文件的更改都被撤消,生成的三个版本可能不在早期提交的 any 中。但这太多了:您只想要 F
对 b
backed-out.
所以解决的办法就是少做一点,或者做多了,然后再去解决
我们我已经将 git revert
操作描述为: 找到更改,然后 reverse-apply 相同的更改 。我们可以使用一些 Git 命令自己手动完成此操作。让我们从 git diff
或 short-hand 版本 git show
开始:这两个版本都将 快照 转换为更改。
使用
git diff
,我们将Git指向parentE
和childF
并询问Git: 这两个有什么区别? Git 提取文件,比较它们,并向我们展示发生了什么变化。使用
git show
,我们指向Git提交F
; Git 自己找到 parentE
,然后提取文件并比较它们并向我们展示发生了什么变化(以日志消息为前缀)。也就是说,git show <em>commit</em>
相当于git log
(仅针对那一次提交),然后是git diff
(从该提交的 parent,到该提交)。
Git 将显示的更改本质上是 说明: 它们告诉我们,如果我们从 E
中的文件开始,删除一些行,并插入一些其他行,我们将得到 F
中的文件。所以我们只需要反转差异,这很容易。事实上,我们有两种方法可以做到这一点:我们可以用 git diff
交换哈希 ID,或者我们可以使用 -R
标志来 git diff
或 git show
。然后我们会得到说明,本质上说:如果您从 F
中的文件开始,并应用这些说明,您将获得 E
中的文件。
当然,这些说明会告诉我们更改所有三个 文件,a
、b
和c
。但是现在我们可以剥离三个文件中两个文件的指令,只留下我们想要.
同样,有多种方法可以做到这一点。显而易见的一种是将所有指令保存在一个文件中,然后编辑该文件:
git 显示-R <em>hash-of-F</em> > /tmp/instructions
(然后编辑 /tmp/instructions
)。不过,还有一种更简单的方法,那就是告诉 Git: 只需为特定文件显示说明即可 。我们关心的文件是b
,所以:
git show -R <em>hash-of-F</em> -- b > /tmp/instructions
如果您查看说明文件,它现在应该描述如何使用 F
和 un-change b
中的内容,使其看起来像 E
中的内容.
现在我们只需要 Git 应用这些指令,除了我们将使用来自 current[= 的文件而不是来自提交 F
的文件338=] 提交 H
,它已经在我们的 work-tree 中准备好进行修补。应用补丁的 Git 命令——一组关于如何更改某些文件集的指令——是 git apply
,因此:
git apply < /tmp/instructions
应该可以解决问题。 (不过请注意,如果指令说要更改 b
中随后被提交 G
或 H
更改的行,这将 失败 。这就是 git revert
更聪明的地方,因为它可以完成整个 "merge" 的事情。)
成功应用指令后,我们可以查看文件,确保它看起来正确,然后像往常一样使用 git add
和 git commit
。
(旁注:您可以使用以下方法一次完成所有操作:
git show -R <em>hash</em> -- b | git申请
而且,git apply
也有自己的 -R
或 --reverse
标志,因此您可以这样拼写:
git 显示 <em>hash</em> -- b | git 应用-R
做同样的事情。还有额外的 git apply
标志,包括 -3
/ --3way
可以让它做更奇特的事情,很像 git revert
做的。)
"do too much, then back some of it out" 方法
另一种相对简单的处理方法是让 git revert
完成所有工作。当然,这将撤销对 other 文件的更改,您不希望 backed-out。但是我们在顶部展示了从 any 提交中获取 any 文件是多么容易得离谱。那么,假设我们让 Git 撤消 all F
:
git 还原 <em>hash-of-F</em>
这使得新提交 I
退出 一切 in F
:
...--D--E--F--G--H--I <-- master
现在 git checkout
这两个文件 a
和 c
from commit H
:
git结帐<em>hash-of-H</em> -- a c
然后进行新的提交 J
:
...--D--E--F--G--H--I--J <-- master
I
和J
中的文件b
是我们想要的方式,[=127中的文件a
和c
=] 是我们想要的方式——它们匹配 H
中的文件 a
和 c
——所以我们现在差不多完成了,除了烦人的额外提交 I
.
我们可以去掉I
有几种方式:
在制作
J
时使用git commit --amend
:这会将I
推开,使提交J
使用提交H
作为J
的 parent。 CommitI
仍然存在,只是被废弃了。最终(大约一个月后)它会过期并真正消失。提交图,如果我们这样做,看起来像这样:
I [abandoned] / ...--D--E--F--G--H--J <-- master
或者,
git revert
有一个-n
标志告诉 Git: 执行还原,但不要提交结果。 (这也允许使用脏索引 and/or work-tree 进行还原,但如果您确保从提交H
的干净检出开始,则不需要担心这意味着什么。)这里我们将从H
开始,还原F
,然后告诉 Git 获取文件a
和c
从提交返回H
:git revert -n <em>hash-of-F</em>
git checkout HEAD -- a c
git commit
由于我们在执行此操作时正在提交
H
,因此我们可以使用名称HEAD
来引用a
和c
的副本在提交H
.
(Git 是 Git,还有六种其他方法可以做到这一点;我只是使用我们在这里一般说明的方法。)