撤消从上一次提交中删除文件

Undo removal of file from a previous commit

我正在使用 gitflow 分支模型进行开发。 我已经从 develop 分支到 feature/X 并且在 feature/X 的第一次提交中我还删除了一些文件(使用 git rm <file>)。 现在,在该分支中进行了几次其他提交后,我意识到,我需要之前删除的文件。

这里有一个简短的例子来说明我的意思:

git flow init -d
echo "file contents" > file.txt
git commit -m "Added file.txt"
git checkout -b feature/X
git rm file.txt
echo "foo" > foo.txt
git add --all
git commit -m "Deleted file.txt and added another file"
<some more commits in feature/X>



git log -u

...

commit 04948fc4fc36d83901a0862b057657f3ccb9cf0d
Author: <...>
Date:   Wed Aug 10 12:26:58 2016 +0200

    Deleted file.txt and added another file

diff --git a/file.txt b/file.txt
deleted file mode 100644
index d03e242..0000000
--- a/file.txt
+++ /dev/null
@@ -1 +0,0 @@
-file contents
diff --git a/foo.txt b/foo.txt
new file mode 100644
index 0000000..257cc56
--- /dev/null
+++ b/foo.txt
@@ -0,0 +1 @@
+foo

...

我不只是想在新提交中重新添加文件以避免将 feature/X 合并到 develop 时出现问题,而 file.txt 中有一些更改develop支线。

有什么方法可以从之前的提交中删除 file.txt 吗?

我已尝试 git reset <sha-of-previous-commit> file.txt,但未能将文件恢复到我的工作副本。

编辑 1:
我知道重写已经被推送到远程的历史的缺点。但是我知道除了我之外没有人在 feature/X 上做过任何提交,因此重写历史记录对我来说应该是安全的,尽管它已经被推送了。

Git不是SVN,我觉得只要在新提交中重新添加文件应该没有问题。
与 SVN 不同,Git 不跟踪文件,它跟踪恰好存储在文件中的内容。
如果您在 SVN 中删除并重新添加文件,则它们的历史记录没有关联,但它们对于 SVN 来说是不同的文件。

如果您在 Git 中删除并重新添加文件,则只是内容被删除并重新添加,因此合并应该可以正常工作。变基可能会有问题,因为提交会逐一应用于新的基础提交,因此您会遇到编辑/删除冲突。但是在合并时,只有所做的更改应该作为一个整体应用,因此不会出现问题。

您现在拥有的是某个分支上的一系列提交:

...--o--*--B1--B2--B3   <-- branch

其中 * 是一个很好的提交,但是 B1 是一个 "bad" 提交(已经删除了您决定的文件,现在,您毕竟不想删除) .

您需要将错误的提交重做为良好的提交——并且还要复制任何 良好的 提交 一个错误的提交之后。换句话说,你基本上会根据需要重复以下三个步骤:

  1. 将您不喜欢的提交复制到新的提交中(但实际上暂时不要提交)。从一个将从标记为 * 的提交中增长的新分支开始,使用 git cherry-pick -n 复制错误的提交,如 B1,但不实际提交它。

  2. 然后,恢复丢失的文件:git checkout -- <path>。 (你也可以用 git reset 来做到这一点,但是当我们在下面使用 git rebase -i 时,它变得太难了,所以我们将坚持使用 git checkout。)

  3. 最后,创建作为旧提交副本的新提交:git commit。由于这是一个樱桃选择,Git 将准备好旧提交的消息以供编辑。

现在 第一个 错误提交 B1 已被复制到一个新的、更正的良好提交 G1:

...--o--*--B1--B2--B3   <-- branch
         \
          G1            <-- new branch [new copies being made]

现在您可以复制 second 提交,B2。如果它有您不想要的删除,请使用相同的三部分序列 (cherry-pick -ncheckoutcommit)。如果没问题,请忽略 -n 以复制它并提交:

...--o--*--B1--B2--B3   <-- branch
         \
          G1--G2        <-- new branch [new copies being made]

现在您可以复制第三次提交,B3。和以前一样,如果它有您需要更改的内容,您可以一路修复它。 (假设 B1--...--Bn 链中有三个提交;如果有更多或更少,请根据需要进行调整。)

当一切都完成后,您将需要说服 Git 将 branch 标签从 分支上剥离并粘贴到 new 一个代替。 一种方法,但是...

git rebase -i

有一种更简单的方法。 git rebase 命令通过执行一系列的 cherry-picks 来工作。1 当 cherry-picks 全部完成后,git rebase 命令剥离旧的分支标签, 并将标签粘贴到刚刚构建的新分支上。这正是你需要做的,所以你所要做的就是说服 git rebase 让你告诉它哪些提交是 提交,哪些是 ]不好的,遇到不好的就停下来。

这就是 git rebase -i 的用武之地。它会根据一组说明调出您的编辑器。最初,所有指令都是 pick:对每个提交进行一次挑选。您可以将任何一条指令更改为 edit,它告诉 Git 进行特定的挑选,但随后停止并让您修复/更改内容。

有点恼人的是,rebase 在没有 -n 的情况下进行了 cherry-pick :它制作了副本并提交了它。所以你必须稍微调整第 2 步:而不是 git checkout -- <path>HEAD 取回文件,你需要 git checkout HEAD^ -- <path>git checkout HEAD~1 -- <path>。这意味着 "reach back into the previous commit, and get that version of the file." 完成后,您可以 运行 git commit --amend 更新提交。

然后,使用 git rebase --continue 继续变基过程。 rebase 代码将通过更多 picks 继续到下一个 edit,或者如果只剩下 picks,则将它们全部完成并完成 rebase 并移动分支标签。

主要是识别提交*:最后的"good"提交。以某种方式拼写提交 * 可以让您执行 git rebase -i。在我们的示例中,它从当前分支提示后退三步,因此:

git rebase -i HEAD~3

会成功的。如果后退的步数更多或更少,您需要调整波浪号 ~ 字符后的数字。

请注意,无论您做什么,您都会获得原始提交的 副本 。此外,git rebase 通常会在制作副本时删除合并提交。这两者都意味着,如果您已将这些提交提供给其他任何人(通过 git push 或类似方式),或者如果您在第一个错误提交之后进行了合并,您打算 "replace" (真的, 复制然后忽略原件)。


1实际上,git rebase字面上的运行s git cherry-pick有时。其他时候,它使用其他几乎相同的东西。

有几种方法可以恢复该文件,具体取决于重写 feature/X 分支的历史记录是否安全。

选项 1:在新提交中恢复文件

如果您已经推送了该提交,最简单的方法就是从删除它的提交的 父级检索 foo.txt 文件并再次提交。

例如,假设删除文件的提交的 SHA-1 是 123abc:

git checkout feature/X
git checkout 123abc^ path/to/file.txt
git add path/to/file.txt
git commit -m "Restore the file.txt file"

选项 2:恢复原始提交中的文件

另一方面,如果您没有推送这些提交,您可以安全地重写本地 feature/X 分支的历史记录以撤消该文件的删除。为此,您必须执行 interactive rebase 并编辑删除文件的提交:

git checkout feature/X
git rebase -i 123abc^

待办事项列表中,将该提交左侧的单词从pick更改为edit;然后保存文件并退出。

一旦 Git 到达您要编辑的提交,您可以通过说以下命令恢复已删除的文件:

git checkout HEAD^ path/to/file.txt
git add path/to/file.txt
git commit --amend -C HEAD  # where -C HEAD reuses the commit message of HEAD

然后完成变基:

git rebase --continue

回答

I don't just want to re-add the file in a new commit to avoid issues when merging feature/X to develop while there has been some changes in file.txt in develop branch.

是的,您只想在新提交中重新添加文件。 git 对此很明智(好吧,实际上这里没有涉及特殊的智能,它只是来自 git 处理文件的方式)。

例子

git init test                       # init a repos, create test.txt
cd test
echo start > test.txt
git add -A ; git commit -m 'x'

git checkout -b bra                 # create branch, add a line

echo line1 >> test.txt
git add -A ; git commit -m 'x'

git rm test.txt                     # delete file + commit
git commit -m "x"

echo start > test.txt               # restore file and add a line
echo line2 >> test.txt
git add -A ; git commit -m 'x'

git checkout master                 # back to master, add the same line and another one
echo line2 >> test.txt
echo line3 >> test.txt
git add -A ; git commit -m 'x'

git merge bra                       # merge

cat test.txt
   start
   line2
   <<<<<<< HEAD
   line3
   =======
   >>>>>>> bra

冲突符合预期(原文如此!);关于它的重要部分是 line2 而不是 冲突的一部分。 git merge不关心分支上文件发生的所有恶作剧,只关心最终结果。