【问题标题】:Revert only a single file of a pushed commit仅恢复推送提交的单个文件
【发布时间】:2019-05-01 03:59:42
【问题描述】:

以下是推送的提交历史记录。

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)

我想恢复对 Commit 3File b 所做的更改。 但我希望在提交 4 和 5 中对该文件进行更改。

【问题讨论】:

标签: git version-control git-reset git-revert


【解决方案1】:

在 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指向它的父GG 的哈希 ID 为 F;等等。这就是 Git 设法首先向您显示提交 H 的方式:您问 Git 哪个提交是 master? 它说 H。你让 Git 向你展示H,Git 提取GH 并比较它们,然后告诉你H 发生了什么变化。

你的要求

我想恢复对提交 3 [F] 的文件 b 所做的更改。但我希望在提交 4 和 5 [GH] 中该文件发生的更改。

请注意,此版本的文件可能不会出现在任何提交中。如果我们采用提交E(您的提交2)中出现的文件,我们会得到一个没有来自F的更改,但它没有添加来自GH的更改。如果我们继续G的更改添加到它,那可能与in G的更改不一样;如果我们在那之后添加从 H 到它的更改,那可能与 in H 的内容不同。

显然,这会有点困难。

Git 提供了git revert 来做这件事,但是做的太多了

git revert 命令旨在执行此类操作,但它在提交范围内执行此操作。 git revert 所做的实际上是找出某个提交中发生了什么变化,然后(尝试)撤消只是那些更改

这是考虑git revert 的一种相当好的方法:它通过将提交与其父级进行比较,将提交(快照)转换为一组更改,就像查看提交的所有其他 Git 命令一样。因此,对于提交F,它会将其与提交E 进行比较,找到对文件abc 的更改。然后——这是第一个棘手的部分——它将这些更改反向应用到你的当前提交。也就是说,由于您实际上是在提交 Hgit revert 可以获取所有三个文件中的任何内容——abc——并且(尝试)撤消 em> 正是在提交E他们做了什么。

(实际上有点复杂,因为这个“撤消更改”的想法还考虑了自提交 F 以来所做的其他更改,使用 Git 的三向合并机制。)

从某个特定提交中反向应用所有更改后,git revert 现在进行 new 提交。所以如果你做了一个git revert &lt;hash of F&gt;,你会得到一个new提交,我们可以称之为I

...--D--E--F--G--H--I   <-- master

其中F 对所有三个文件的更改都被撤销,产生了三个可能不在早期提交中的版本任何。但这太过分了:您只希望 Fb 的更改退出。

因此,解决方案是少做一点,或者做太多然后修复它

我们已经将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 diffgit show。然后我们会得到说明,实质上是:如果您从F 的文件开始,并应用这些说明,您将获得来自E 的文件。

当然,这些说明会告诉我们更改所有三个文件,abc。但现在我们可以剥离三个文件中的两个文件的说明,只留下我们想要的说明。

同样,有多种方法可以做到这一点。显而易见的是将所有指令保存在一个文件中,然后编辑该文件:

git show -R <em>hash-of-F</em> &gt; /tmp/instructions

(然后编辑/tmp/instructions)。不过,还有一个更简单的方法,那就是告诉 Git:只麻烦显示特定文件的说明。我们关心的文件是b,所以:

git show -R <em>hash-of-F</em> -- b &gt; /tmp/instructions

如果您检查说明文件,它现在应该描述如何获取 F 中的内容并取消更改 b 以使其看起来像 E 中的内容。

现在我们只需要让 Git 应用这些指令,除了来自提交 F 的文件,我们将使用来自当前提交 H 的文件,它已经坐在我们的工作树上准备修补。应用补丁的 Git 命令(一组关于如何更改某些文件集的说明)是 git apply,所以:

git apply < /tmp/instructions

应该可以解决问题。 (但请注意,如果指令说要更改 b 中随后被提交 GH 更改的行,这将失败。这是 git revert 更聪明的地方,因为它可以完成整个“合并”的事情。)

成功应用指令后,我们可以查看文件,确保它看起来正确,然后照常使用git addgit 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 ac 这两个文件是微不足道的来自 提交H:

git checkout <em>hash-of-H</em> -- a c

然后进行新的提交J:

...--D--E--F--G--H--I--J   <-- master

IJ 中的文件 b 是我们想要的方式,J 中的文件 ac 是我们想要的方式 - 它们与文件 @ 987654451@ 和 cH 中——所以我们现在差不多完成了,除了烦人的额外提交 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 中获取文件 ac

    git revert -n <em>hash-of-F</em>
    git checkout HEAD -- a c
    git commit

    由于我们在提交H 时执行此操作,因此我们可以使用名称HEAD 来引用提交H 中的ac 的副本。

(Git 是 Git,还有六种额外的方法可以做到这一点;我只是使用我们在这里一般说明的那些。)

【讨论】:

    【解决方案2】:

    假设你想要的提交的哈希是 c77s87:

    git checkout c77s87-- file1/to/restore file2/to/restore
    

    git checkout 主页提供了更多信息。

    【讨论】:

    • “你想要的提交”是什么意思?
    • 你上次提交的地址。
    猜你喜欢
    • 1970-01-01
    • 2013-02-22
    • 2015-10-08
    • 2022-06-14
    • 2021-04-15
    • 2010-12-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多