撤销删除的文件并保留文件历史记录

9
假设我有一个文件 a.txt。一天,我把它删除了,提交并推送到远程仓库。
第二天,我想要撤销上次提交,恢复 a.txt 文件。我尝试使用 git revert 命令,但当我使用 git blame 命令时,所有行都显示了撤销提交的哈希值。原始代码贡献历史已经丢失。
那么,我可以如何恢复文件并保留文件的历史记录,就像这个文件之前从未被删除过一样呢?请注意,我不能改变提交的历史记录,因为提交已经被推送到远程仓库。
谢谢!

你的意思是说你不能对上游仓库进行--force推送吗? - shengy
2
Git不跟踪文件历史记录,它只跟踪整个根目录的历史记录。因此,在请求查看历史记录时重建文件历史记录是一个问题,而在还原文件时则不是。 - Nayuki
@shengy 不好意思,我不能这样做。 - fushar
3个回答

8

可以做到这一点!以下是具体步骤:

  1. 从要撤销的删除操作之前的提交开始创建一个新的分支。
  2. 使用git merge <sha> -s ours合并有问题的更改。
  3. 如果提交除了想保留的删除操作之外还有其他更改:
    1. 使用git diff <sha>^..<sha> | git apply将更改应用到您的工作副本中。
    2. 丢弃删除的部分(有许多技巧可供选择;git checkout -p可能适合您)。
  4. 将此分支合并回主分支(如master)。

这将生成具有两个分支的历史记录;其中一个分支是文件被删除的,另一个分支是它从未被删除过。因此,Git能够跟踪文件的历史记录,而不需要采取像-C -C -C这样的超级英雄措施。(实际上,即使使用-C -C -C,文件也没有被“恢复”,因为Git看到的是创建了一个新文件,作为一个先前存在文件的副本。使用这种技术,您重新引入了相同的文件到代码库中。)


运行得很好,而且我现在学到了东西,谢谢@Matthew!我的情况相当复杂,我需要读一些关于git checkout -p的内容,但即使在一个部分变更混合的有问题的提交上,这种方法最终也完全按照我所需的方式工作。 - bossi
1
这是一个非常好的答案,正好符合我的需求。如果碰巧有人在未来需要帮助,如果您在git配置中设置了 diff.noprefixtrue,则需要暂时取消该设置才能使 git diff [...] | git apply 命令正常工作。这将确保 git diff 输出源和目标前缀,以便在应用补丁时找到正确的文件。 - joel boonstra

2
使用指定了三次-C选项的git blame命令:
git blame -C -C -C

这会导致 git blame 查找之前提交中从其他文件复制的内容。
来自git blame文档的说明:
-C||选项除了-M以外,还可以检测从其他文件中复制或移动的行,而这些文件在同一次提交中被修改。当您重新组织程序并在文件之间移动代码时,这将非常有用。当此选项给出两次时,该命令还会查找在创建文件的提交中来自其他文件的副本。当此选项给出三次时,该命令还会查找任何提交中来自其他文件的副本。 是可选的,但它是Git必须检测到在文件之间移动/复制的最小字母数字字符数的下限,以便将这些行与父提交关联起来。默认值为40。如果给出多个-C选项,则最后一个-C的参数将生效。

你确定这个工作正常吗?我尝试了一些类似于 git init echo "test" > a.txt" git add a.txt git commit -m "Commit 1" echo "foobar" >> a.txt git add a.txt git commit -m "Commit 2" git rm a.txt git commit -m "Commit 3" git revert HEAD git blame -C -C -C a.txt 的命令,两行都显示了还原提交... - fushar
@fushar,我相信你需要不止一个单词才能让git注册你移动了什么。文档说最少需要40个字符。我已经编辑了我的答案中的引用以使其更完整。 - Ajedi32
我只是想展示你的解决方案在最简单的例子上并不可行。实际上,在我的真实项目中也不能正常工作(我的删除文件内容当然比40要大得多)。就你的编辑而言 -- git blame -C1 -C1 -C1 a.txt,很遗憾它在 a.txt 的例子中也不起作用。 - fushar
@fushar 嗯,你说得对。我似乎无法让它工作。文档似乎在说它“应该”能够工作,所以可能是个bug。要不然就是我完全误解了它的工作方式。 - Ajedi32

0
你可以使用git reset而不是git revert来完成它。 git reset会删除最新的提交并检出以前的提交。如果您已经将其推送到上游,则不建议这样做。
NAME
       git-reset - Reset current HEAD to the specified state

SYNOPSIS
       git reset [-q] [<tree-ish>] [--] <paths>...
       git reset (--patch | -p) [<tree-ish>] [--] [<paths>...]
       git reset [--soft | --mixed | --hard | --merge | --keep] [-q] [<commit>]

DESCRIPTION
       In the first and second form, copy entries from <tree-ish> to the index. In the third form, set the
       current branch head (HEAD) to <commit>, optionally modifying index and working tree to match. The
       <tree-ish>/<commit> defaults to HEAD in all forms.

既然你已经推送了:

  • 如果当天没有活跃的协作者拉取,使用 git reset 并强制推送 git push -f

1
他已经删除了文件并将其推送到上游,此时使用git reset对他无效。 - shengy

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接