在Git中,每个提交都保存一个快照,即每个文件的状态,而不是一组更改。但是,每个提交(几乎每个提交)也有一个父提交(上一个提交)。如果您问Git:“在提交a123456中发生了什么?”,Git会找到a123456的父提交,提取该快照,然后提取a123456本身,然后进行比较。a123456中不同的内容就是Git将告诉您的内容。由于每个提交都是完整的快照,因此很容易还原到特定提交中特定文件的特定版本。例如,您只需告诉Git:“从提交a123456获取文件b.ext”,现在您就有了来自提交a123456的文件b.ext的版本。这可能是您要求的内容,并且是链接问题和当前答案(从我打字时的情况)提供的内容。不过,您编辑了您的问题,要求不同的东西。
稍微多一点背景信息
现在我必须猜测您的五个提交的实际哈希ID。 (每个提交都有唯一的哈希ID。哈希ID - 大而丑陋的字母和数字字符串 - 是提交的“真名”。这个哈希ID永远不会改变;只要该提交存在,使用它将始终得到该提交。)但它们是大而丑陋的字母和数字字符串,因此,不是猜测例如8858448bb49332d353febc078ce4a3abcc962efe,我将只称您的“提交1”为D,您的“提交2”为E等。由于大多数提交只有一个父提交,这使Git能够从新的提交向旧的提交向后跳转,因此让我们用这些向后箭头将它们排成一行。
分支名称像“master”一样实际上只是保存了该分支上的最新提交的哈希ID。我们说该名称“指向”该提交,因为它具有使Git检索提交的哈希ID。因此,“master”指向“H”。但是“H”具有“G”的哈希ID,因此“H”指向其父项“G”;“G”具有“F”的哈希ID;依此类推。这就是Git如何能够在第一时间向您显示提交“H”的方式:您询问Git“
、和的更改。然后,它将这些更改反向应用于当前提交。与提交F
所做的完全相反(尝试)撤消它们。
(这实际上更加复杂,因为这个“撤消更改”的想法还考虑了自提交F
以来所做的其他更改,使用Git的三次合并机制。)
撤消某个特定提交中的所有更改后,git revert
现在会创建一个新提交。因此,如果您执行git revert <哈希值F>
,则会得到一个新提交,我们可以称之为I
:...--D--E--F--G--H--I <-- master
撤销F
对三个文件所做的更改,产生三个版本,这些版本可能都不是之前提交中的版本。但这太过了:你只想要撤销F
对b
所做的更改。
因此解决方案是少做一点,或者做得太多然后再修复它。我们可以手动使用几个Git命令来找到差异并反向应用相同的更改,就像我们已经描述的git revert
行为一样。让我们从git diff
或缩写版的git show
开始:这两个命令都将快照转换为更改。
使用git diff
,我们指向父级E
和子级F
,并询问Git:“这两者之间有什么区别?”。Git提取文件,比较它们,并显示更改内容。使用git show
,我们将Git指向提交F
;Git会自动找到父级E
,提取文件并比较它们,然后展示更改内容(前缀为日志消息)。也就是说,git showcommit
等同于git log
(只针对一个提交)后跟git diff
(从该提交的父级到该提交)。Git将显示的更改本质上是指令:它们告诉我们,如果我们从E
中开始,移除一些行并插入一些其他行,我们将得到F
中的文件。所以我们只需要反转差异即可,这很容易做到。事实上,我们有两种方法可以做到这一点:git diff
中交换哈希ID,或使用-R
标志到git diff
或git show
。然后,我们会收到这样的指令:如果您从F
中开始,并应用这些指令,则将获得E
的文件。当然,这些指令将告诉我们对于全部三个文件a
、b
和c
进行更改。但现在我们可以剥离两个文件的指令,只保留我们想要的那个。
再次说明,有多种方法可以做到这一点。其中一个显而易见的方法是将所有指令保存在文件中,然后编辑文件:git show -R F的哈希值> /tmp/instructions
(然后编辑/tmp/instructions
)。不过,还有一种更简单的方法,即告诉Git:只显示特定文件的指令。我们关心的文件是b
,因此:git show -RF的哈希值-- b> /tmp/instructions
。如果您检查指令文件,现在应该描述如何对b
进行反更改,以使其看起来像E
中的内容。现在我们只需要让Git应用这些指令,但不是从提交F
的文件中,而是使用当前提交H
中已经准备好打补丁的工作目录中的文件。应用补丁(一组关于如何更改某些文件的指令)的Git命令是git apply
,
git apply < /tmp/instructions
使用这个命令应该就行了。请注意,如果指令要求更改之后由提交 G 或 H 更改的 b
中的行,则此方法将失败。这就是 git revert
更为智能的地方,因为它可以完成整个“合并”过程。
一旦指令成功应用,我们可以检查文件,确保它看起来正确,并像往常一样使用 git add
和 git commit
。
(附注:您可以使用以下命令一次性完成所有操作:
git show -R hash -- b | git apply
而且,git apply
还有其自己的 -R
或 --reverse
标志,因此您可以拼写如下命令:
git show hash -- b | git apply -R
它的作用相同。还有其他 git apply
标志,包括 -3
/ --3way
,让它做更高级的事情,就像 git revert
一样。)
“做太多,然后撤销一些”的方法
另一种相对容易处理这种情况的方法是让 git revert
执行全部撤销操作。当然,这将撤消您不想撤消的其他文件的更改。但是我们在前面展示了如何轻松从任何提交中获取任何文件。因此,假设我们让 Git 撤消 F
中的所有更改:
git revert hash-of-F
这样会创建新的提交 I
,以撤消 F
中的所有更改。
...--D--E--F--G--H--I <-- master
现在可以轻松地使用 git checkout
命令从提交记录 H
中检出两个文件 a
和 c
:
git checkout H的哈希值 -- a c
然后创建一个新的提交记录 J
:
...--D--E--F--G--H--I--J <-- master
I
is no longer needed and can be abandoned by using git commit --amend
when creating J
, which sets H
as the parent of J
and effectively pushes I
out of the way. However, I
still exists and will expire after a month.
The original files a
, b
, and c
, in both I
and J
, are as intended. The files a
and c
in J
also match those in H
.
I [abandoned]
/
...--D--E--F--G--H--J <-- master
或者,git revert
有一个-n
标识,告诉Git:撤销更改,但不要提交结果。(这也允许使用脏索引和/或工作树进行撤销,尽管如果确保从提交H
开始清洁检出,则不必担心这意味着什么。)这里我们将从H
开始,撤销F
,然后告诉Git:从提交H
获取文件a
和c
:git revert -n F的哈希值
git checkout HEAD -- a c
git commit
由于在执行此操作时我们在提交H
上,因此可以使用名称HEAD
来引用提交H
中的a
和c
的副本。
cherry-pick
。 - icc97