Aki*_*i T 13 git version-control git-revert git-reset
以下是推送的提交历史记录。
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)
Run Code Online (Sandbox Code Playgroud)
我想恢复对Commit 3 的File b发生的更改 。 但我希望在提交 4 和 5 中对这个文件进行更改。
小智 12
假设您想要的提交的哈希是 c77s87:
git checkout c77s87-- file1/to/restore file2/to/restore
Run Code Online (Sandbox Code Playgroud)
git checkout 主页提供了更多信息。
tor*_*rek 12
在 Git 中,每次提交都会保存一个快照——即每个文件的状态——而不是一组更改。
然而,每个提交——嗯,几乎每个提交——也有一个父(上一个)提交。如果你问 Git,例如,在 commit 中发生了a123456什么?,Git 所做的是找到 的父项a123456,提取该快照,然后提取a123456自身,然后比较两者。无论是不同的a123456,这就是Git会告诉你。
由于每次提交都是一个完整的快照,因此很容易在特定提交中恢复到特定文件的特定版本。例如,您只需告诉 Git:从 commit获取我的文件b.exta123456,现在您就拥有了b.ext来自 commit的文件版本a123456。这就是您似乎在问什么,因此链接的问题和当前答案(截至我打字时)提供了什么。不过,您编辑了您的问题,要求提出一些完全不同的问题。
我现在必须猜测您五次提交中每一次的实际哈希 ID。(每个提交都有一个唯一的哈希 ID。哈希 ID——由字母和数字组成的丑陋的大字符串——是提交的“真实名称”。这个哈希 ID 永远不会改变;使用它总是让你得到那个提交,只要commit 存在。)但它们是由字母和数字组成的又大又丑的字符串,所以8858448bb49332d353febc078ce4a3abcc962efe我不会猜测,比如说,我只会调用你的 "commit 1" D,你的 "commit 2" E,等等。
由于大多数提交都有一个父提交,这让 Git 从新提交向后跳转到旧提交,让我们用这些向后箭头将它们排列成一行:
... <-D <-E <-F <-G <-H <--master
Run Code Online (Sandbox Code Playgroud)
一个分支名像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。
我想恢复对 Commit 3 [
F] 的文件 b 发生的更改。但我希望在提交 4 和 5 [G和H] 中[that] 发生在这个文件上的更改。
请注意,此版本的文件可能不会出现在任何提交中。如果我们采用提交中出现的文件E(您的提交 2),我们会得到一个没有来自 的更改F,但它没有来自G和H添加到其中的更改。如果我们继续前进,做添加从改变G它,这可能不一样,什么中 G; 如果我们从加入改变H后它,这可能不一样,有什么在 H。
显然,这将变得有点困难。
git revert了这样做,但它做的太多了该git revert命令旨在执行此类操作,但它是在提交范围内执行的。是什么git revert呢,实际上就是要弄清楚什么改变了一些承诺,然后(尝试)撤消只是这些变化。
这是一个很好的思考方式git revert:通过将提交与其父项进行比较,它将提交(快照)转换为一组更改,就像查看提交的每个其他 Git 命令一样。因此,对于犯F,那就把它比作承诺E,找到文件的更改a,b以及c。然后——这是第一个棘手的地方——它将这些更改反向应用到您当前的提交。也就是说,因为你实际上是在犯H,git revert可以采取无论是在所有三个文件- a,b和c-和(尝试)撤消正是得到了做给他们承诺E。
(实际上有点复杂,因为这个“撤消更改”的想法还考虑了自 commit 以来所做的其他更改F,使用 Git 的三向合并机制。)
反向应用来自某个特定提交的所有更改后,git revert现在进行新的提交。所以如果你做了一个git revert <hash of F>你会得到一个新的提交,我们可以称之为I:
...--D--E--F--G--H--I <-- master
Run Code Online (Sandbox Code Playgroud)
其中F对所有三个文件的更改都被取消,产生三个可能不在任何早期提交中的版本。但这太过分了:您只想取消F's 的更改b。
我们已经将git revert操作描述为:找到更改,然后反向应用相同的更改。我们可以使用一些 Git 命令自行手动执行此操作。让我们从git diff或 简写版本开始git show:这两者都将快照转换为更改。
使用git diff,我们将 Git 指向父项E和子项F并询问 Git:这两者之间有什么区别? Git 提取文件,比较它们,并向我们展示发生了什么变化。
使用git show,我们将 Git 指向 commit F;GitE自己找到父文件,提取文件并比较它们并向我们展示发生了什么变化(以日志消息为前缀)。也就是说,相当于(仅针对该提交)后跟(从该提交的父级到该提交)。git show commitgit loggit diff
这些变化git会显示,在本质上,指示:他们告诉我们,如果我们开始与那些在文件E,删除一些线路,并插入一些其他的线,我们会是在文件F。所以我们只需要反转差异,这很容易。事实上,我们有两种方法可以做到这一点:我们可以将哈希 ID 与 交换git diff,或者我们可以将-R标志用于git diff或git show。然后我们会得到说明,本质上说:如果您从 中的文件开始F,并应用这些说明,您将从E.
当然,这些说明会告诉我们对所有三个文件a、b、 和进行更改c。但是现在我们可以去掉三个文件中两个的指令,只留下我们想要的指令。
同样,有多种方法可以做到这一点。显而易见的一种是将所有指令保存在一个文件中,然后编辑该文件:
git show -R hash-of-F > /tmp/instructions
(然后编辑/tmp/instructions)。不过,还有一种更简单的方法,那就是告诉 Git:只麻烦显示特定文件的说明。我们关心的文件是b,所以:
git show -R hash-of-F -- b > /tmp/instructions
如果您检查说明文件,它现在应该描述如何获取内容F并取消更改b以使其看起来像内容一样E。
现在我们只需要让 Git 应用这些指令,除了不是来自 commit 的文件F,我们将使用来自当前commit的文件H,它已经位于我们的工作树中准备修补。应用补丁的 Git 命令(一组关于如何更改某些文件集的指令)是git apply,所以:
git apply < /tmp/instructions
Run Code Online (Sandbox Code Playgroud)
应该做的伎俩。(但请注意,如果指令说更改随后由 commits或更改的行,这将失败。这是更聪明的地方,因为它可以完成整个“合并”的事情。)bGHgit 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
Run Code Online (Sandbox Code Playgroud)
它现在微不足道的git checkout两个文件a,并c 从提交H:
git checkout hash-of-H -- a c
然后进行新的提交J:
...--D--E--F--G--H--I--J <-- master
Run Code Online (Sandbox Code Playgroud)
和bin 中的文件是我们想要的方式,而文件和in是我们想要的方式——它们与文件和in匹配——所以我们现在几乎完成了,除了烦人的额外 commit 。IJacJacHI
我们可以通过I以下几种方式摆脱:
git commit --amend制作时使用J:I通过使 commitJ使用 commitH作为J其父级来推开。CommitI仍然存在,只是被放弃了。最终(大约一个月后)它会过期并真正消失。
如果我们这样做,提交图如下所示:
I [abandoned]
/
...--D--E--F--G--H--J <-- master
Run Code Online (Sandbox Code Playgroud)或者,git revert有一个-n标志告诉 Git:执行还原,但不提交结果。 (这也可以使用脏索引和/或工作树进行还原,但如果您确保从提交的干净检出开始,H则无需担心这意味着什么。)在这里,我们将从H,还原F,然后告诉 Git获取文件a并c从提交返回H:
git revert -n hash-of-F
git checkout HEAD -- a c
git commit
由于我们在H执行此操作时处于提交状态,因此我们可以使用名称HEAD来引用commit 中的a和的副本。cH
(Git 就是 Git,有六种其他方法可以做到这一点;我只是使用我们在这里一般说明的方法。)