如何在交互式变基期间取消暂存文件(从旧提交中删除文件)?

Mar*_*020 4 git

我在Pro Git书中读到过有关交互式变基以更改多个提交的内容。所以我正在做git rebase HEAD~3,改变了我想修改的一项edit。然后我就可以通过更改消息了git commit --amend。并添加了一个文件,git add file3之后git commit --amend开始输出“3 个文件已更改”而不是之前的“2​​ 个文件”。但如何删除文件呢?为什么既不工作git restore --staged file1也不git reset HEAD file1工作?(输出 git commit --amend仍然是“3 个文件已更改”,并且输出git log --patch仍然显示file1带有最近修改的提交消息的提交)。

我使用网络搜索并阅读了什么是 `git Restore` 命令以及 `git Restore` 和 `git Reset` 之间的区别是什么?(如上所示,尝试了restorereset),从 Git 提交中删除文件(谈论上次提交),删除旧提交中提交的文件(据我所知,git filter-branch --index-filter "git rm -rf --cached --ignore-unmatch <path_to_file>" HEAD从分支中的所有提交中删除文件)。

还有没有被赞成的答案删除对旧的、已推送的 git commit 中的文件的更改?

git rebase -i HEAD~3 //Change "pick" to "edit" for commit to change
git reset HEAD^ -- F2 //reset F2 to previous version in staging area  
Run Code Online (Sandbox Code Playgroud)

是否要删除文件需要重置整个提交?在变基期间无法删除单个文件?

tor*_*rek 11

当您谈论在交互式变基期间删除文件时,您可能指的是以下两件事之一:

\n
    \n
  • 使文件与先前的提交匹配。有些人会将此称为“删除对文件的更改”,并且由于有些人将提交视为更改,因此有些人会将其缩短为“删除文件”

    \n
  • \n
  • 从字面上删除该文件,以便您新的和改进的提交忽略该文件。

    \n
  • \n
\n

两者都相对容易做到。在我说如何做之前,我先介绍一些背景知识。

\n

背景

\n

要了解您正在做什么及其原因,拥有正确的 Git 提交思维模型会有所帮助:

\n
    \n
  • 每个提交都有一个唯一的编号(哈希 IDOID,其中 OID 代表对象 ID)。提交的编号是 Git 真正找到提交的方式:分支名称实际上并不重要。

    \n
  • \n
  • 所有 Git 提交\xe2\x80\x94 事实上,所有 Git 内部对象\xe2\x80\x94 都是完全只读的。您无法更改提交,因此这并不是git rebase真正的作用。

    \n
  • \n
  • 每次提交都会存储两件事:(a)所有文件的快照,以及 (b) 一些元数据。

    \n
  • \n
\n

提交快照中的文件会被压缩并进行 Git 化,更重要的是,当某个文件的内容与其他文件的内容匹配时,会进行重复数据删除(在提交内和跨提交)。因此,大多数提交主要包含大多数先前提交的所有文件这一事实不会导致存储库膨胀:这些重复的文件仅存储一次。只要您不在 Git 中存储大型、不可压缩的二进制文件,Git 就会以无形的方式很好地处理这一切(如果您这样做,那么 Git 处理此问题的能力会很差,并且存储库会膨胀并变得无法使用)。但这也意味着提交存储更改

\n

提交中的元数据记录了诸如谁进行了提交、何时以及为什么(他们的日志消息)之类的内容,但它也记录了对 Git 内部操作至关重要的数据:每个提交都存储父提交哈希的列表身份证。该列表通常只有一个元素长,为每个提交提供一个父项。父提交也有一个快照,为了将提交转换为更改(用于查看目的),Git 提取两个快照并查看哪些文件发生了更改。由于重复数据删除,Git 可以缩短这一过程,根本不需要提取相同的文件;然后它只需要为两次提交中匹配的文件提供一个更改方案。这就是您使用git showor看到的内容git log -p:提交的(单个)父级快照与提交的快照之间的差异。

\n

因为提交是只读的,并且只有 Git 本身可以读取它们,所以我们实际上并不处理或使用提交。相反,当我们选择要使用的某个提交时,我们让 Git将提交(例如解压缩或解压缩某些存档)提取到工作区域中,Git 将其称为工作树工作树。虽然这些工作树文件是Git 中提取的,但当您完成工作时,它们实际上根本不在Git中。

\n

因为提交是只读的,git rebase所以不能修复任何错误的提交,也不能git commit --amend更改提交。相反,Git 利用了这样一个事实:人类\xe2\x80\x94 与 Git 本身\xe2\x80\x94 不同,永远不会通过哈希 ID 找到提交。相反,我们使用分支名称。分支名称仅保存我们想要声明为“分支的一部分”的最后一次提交的哈希 ID。然后,该提交在其元数据中保存前一个提交的哈希 ID ,而前一个提交在元数据中保存另一个更早提交的哈希 ID,依此类推。这会产生一个简单的向后看链:

\n
... <-F <-G <-H   <--branch\n
Run Code Online (Sandbox Code Playgroud)\n

其中分支名称保存链中最后一次提交的哈希 ID,一切都从那里开始向后进行。H

\n

当我们以正常的日常方式添加提交时,Git 会创建一个新的(只读)提交,I其父级为H,将其添加到链中,并将新提交的哈希 ID 写入I分支名称:

\n
...--G--H--I   <-- branch (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n

为了“修改” commit H,Git 只需写入新的 commit I,并将 commitG作为其父级,而不是 commit H,结果是:

\n
       H\n      /\n...--G--I   <-- branch (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n

提交H仍然存在,但除非我们记住了它的哈希 ID,否则我们将永远不会再看到它。(Git可以,只要 Git 能找到它的哈希 ID。)

\n

变基只是包括进行多个新的和改进的提交。如果我们有:

\n
...--F--G--H   <-- main\n         \\\n          I--J   <-- feature (HEAD)\n
Run Code Online (Sandbox Code Playgroud)\n

并且我们希望修订I出现在之后 H而不是之前/与之并行,我们进行一个新的快照和元数据提交I\'(否则看起来像I,除了我们选择H\ 的快照作为我们的“基础”并且“重新添加”我们的更改I,因此“re-base”-ing I):

\n
             I\'  <-- HEAD [detached]\n            /\n...--F--G--H   <-- main\n         \\\n          I--J   <-- feature\n
Run Code Online (Sandbox Code Playgroud)\n

然后我们重复此操作以J获取J\'

\n
             I\'-J\'  <-- HEAD [detached]\n            /\n...--F--G--H   <-- main\n         \\\n          I--J   <-- feature\n
Run Code Online (Sandbox Code Playgroud)\n

一旦我们将所有提交复制到新的和改进的提交中,我们就让 Git 移动名称 feature指向最后复制的提交:

\n
             I\'-J\'  <-- feature (HEAD)\n            /\n...--F--G--H   <-- main\n         \\\n          I--J   [abandoned]\n
Run Code Online (Sandbox Code Playgroud)\n

原来的提交仍然存在;我们就是找不到他们。

\n
\n

1(在此插入 Gilbert & Sullivan HMS Pinafore 例程)

\n
\n

交互式变基

\n

交互式变基使用与非交互式变基相同的过程,2但让我们停下来进行调整。为此,Git 为我们提供了一份说明表。pick它最初包含我们将复制的每个提交的一系列命令。这些指示 Git 运行git cherry-pick,这是复制提交的步骤,如上面I所示I\'

\n

更改pickedit使 Git 进行樱桃选择,但随后停止在分离头模式。请注意,这里我们将其复制II\'放置在与之前相同的物理位置,而不是将其移动到 commit 之后H

\n
          I\'  <-- HEAD [detached]\n         /\n...--G--H   <-- main\n         \\\n          I--J   <-- feature\n
Run Code Online (Sandbox Code Playgroud)\n

现在我们处于这种状态,我们可以用来git commit --amend进行另一次提交,I". 在此提交中,我们可以存储任何我们喜欢的快照,并使用任何我们喜欢的提交消息。will的父级I"是,与andH的父级相同。II\'

\n

进入新提交的快照与任何新的 Git 提交具有相同的来源:它来自 Git的索引AKA 暂存 。目前,它包含来自 commit 的所有文件I\',这些文件与来自 commit 的所有文件匹配I(因此不会使用任何空间,因为它们都是已经预先去重复的重复项)。这些文件的 Git 化副本也位于您的工作树中。因此,您可以修改或删除工作树中的文件并运行git add

\n
vim foo.py\ngit add foo.py\n
Run Code Online (Sandbox Code Playgroud)\n

或者:

\n
rm foo.py\ngit add foo.py\n
Run Code Online (Sandbox Code Playgroud)\n

git add步骤告诉 Git 通过读取、压缩和去重复文件,或者在删除 \xe2\x80\x94 后foo.py完全删除索引副本,使索引副本与工作树副本匹配。或者:

\n
git rm foo.py\n
Run Code Online (Sandbox Code Playgroud)\n

rm和合并git add为一个步骤。无论哪种方式,我们都已将正确的(更新或删除的)文件安排在 Git 的索引中,因此我们现在运行git commit --amend,就像您所做的那样:

\n
git commit --amend\n
Run Code Online (Sandbox Code Playgroud)\n

这会将提交I\'推到一边,让提交I"指向H

\n
         I\'  [abandoned]\n        /\n        | I"  <-- HEAD [detached]\n        |/\n...--G--H   <-- main\n         \\\n          I--J   <-- feature\n
Run Code Online (Sandbox Code Playgroud)\n

运行git rebase --continue告诉变基代码继续执行指令表中的下一条指令:另一个pick、或edit、或reword或其他任何指令。一旦遵循了最后一条指令,rebase 将像以前一样拉动分支名称:

\n
         I\'  [abandoned]\n        /\n        | I"-J\'  <-- feature (HEAD)\n        |/\n...--G--H   <-- main\n         \\\n          I--J   [abandoned]\n
Run Code Online (Sandbox Code Playgroud)\n

(在通常的设置中,废弃的提交会保留一段时间\xe2\x80\x94,默认情况下至少 30 天\xe2\x80\x94,然后 Git 最终注意到它们已经闲置了足够长的时间,删除了引用日志条目保持它们活着,并真正清除它们。不过,在那之前,您可以轻松取回原始文件。请注意,特殊名称还会ORIG_HEAD记住提交J一段时间,直到您执行其他操作,让 Git 用ORIG_HEAD另一个哈希 ID覆盖. 成功变基后,如果您不喜欢结果,则ORIG_HEAD与 中的引用日志条目一样有效branch@{1}。)

\n
\n

2在旧版本的 Git 中,存在许多技术差异。在现代 Git 中,这些现在基本上已经消失了,尽管如果您确实愿意,您仍然可以有意调用它们。我还将省略一些 Git 通常用于您将要执行的交互式变基的优化,这确实使 Git 的情况变得更好,但不会改变这里的最终结果。

\n
\n

我们现在可以看到什么git resetgit restore将要做什么

\n
\n

为什么既不git restore --staged file1也不git reset HEAD file1工作?

\n
\n

git reset都会git restore从某处读取文件的内容并将该文件的内容写入某处。该git reset命令本身非常复杂,因此最好坚持使用更新的、更有针对性的(更有限的)git restore,但任何一个都可以工作:我们只需要在这里了解几件事。

\n
\n
git reset HEAD^ -- F2 //reset F2 to previous version in staging area\n
Run Code Online (Sandbox Code Playgroud)\n
\n

在这里,我们在其恢复一个文件操作模式中使用git reset,而不是。git restore如果我们使用:

\n
git reset HEAD -- file1\n
Run Code Online (Sandbox Code Playgroud)\n

我们告诉 Git:从 指定的提交中读取 的 Git 化副本file1HEAD。如果我们使用:

\n
git reset HEAD^ -- F2\n
Run Code Online (Sandbox Code Playgroud)\n

我们告诉 Git:从 指定的提交中读取 的 Git 化副本。F2HEAD^

\n

在这两种情况下,从指定的提交读取指定的文件后,git reset将(Git 化的、预先去重复的)内容写入索引/暂存区域,准备进入新的提交。file1暂存区域中的文件名与所选提交(或)中的文件名相同F2。文件的工作树副本此处未更改!这是不可取的,因为它使你很难看到你在做什么,但由于 Git 此时实际上并没有使用工作树副本,所以现在它也不是完全有害的。

\n

使用git checkout效果更好:

\n
git checkout HEAD^ -- F2\n
Run Code Online (Sandbox Code Playgroud)\n

git checkout\xe2\x80\x94的这种形式,就像 一样git reset,非常复杂,这反过来就是为什么git checkout被分割成git switchgit restore在 Git 2.23\xe2\x80\x94 中从指定的提交中读取文件并将其写入Git \ 的索引你的工作树。这使得查看您所做的事情变得更加容易,因为工作树副本现在很明显。

\n

F2如果您的目标是使新提交中的副本与I"提交中的副本相匹配H,那么这些HEAD^形式的命令将达到目的。原因是HEAD当前名称为 commit I\',即 commit 的副本II\'\ 的父级是H,因此从提交中检索文件F2(或file1)的副本将恢复索引和工作树版本以与H的版本匹配,现在您使用\xe2\x80\x94进行的提交the commit\xe2\x80\x94has其中包含该文件的相同副本(已删除重复)。 Hgit commit --amendI"

\n

如果您的目标是真正完全删除 F2,那么提交根本I"没有文件(或者例如避免名为 的文件出现问题)就会这样做。F2git rm F2git rm -- F2--cached

\n

如果我们想要F2匹配 中的副本H,但git restore为了避免过于复杂的签出命令相关错误,我们可以运行:

\n
git restore -SW --source=HEAD^ -- F2\n
Run Code Online (Sandbox Code Playgroud)\n

例如。这与git checkout:我们指定HEAD^为文件源,-S( --staged) 告诉git restore将文件写入暂存区域,-W( --worktree) 告诉git restore将文件写入工作树。

\n

请注意,在所有情况下,我们的目标是使索引包含正确的文件,就像git commit --amend从 Git 索引创建新快照一样。作为人类,我们通常应该同时更新这些文件的工作树副本,因为我们看不到索引(暂存区域)副本,但我们可以在我们喜欢的任何编辑器或文件查看器中看到工作树副本树副本。

\n

我们还必须记住,如果我们运行,Git 将为我们git status运行两个操作:git diff --name-status

\n
    \n
  • 我们将比较HEAD提交与 Git 的索引。但HEAD提交是提交I\',不是提交H!所以我们不必对此过于关注。
  • \n
  • 另一个将比较 Git 的索引和我们的工作树。理想情况下,此差异应该为空,以便我们在工作树中查看与 Git 将在下一次提交中使用的文件相同的文件。
  • \n
\n

选项reset --soft

\n

我们还可以做另一件事,但我自己实际上从未做过:git commit --amend我们可以使用 来开始整个修改过程,而不是git reset --soft。也就是我们开始git rebase -i,改一个pickedit,写出指令表,然后让rebase开始。我们现在处于这样的状态:

\n
          I\'  <-- HEAD [detached]\n         /\n...--G--H   <-- main\n         \\\n          I--J   <-- feature\n
Run Code Online (Sandbox Code Playgroud)\n

该命令让我们可以在更改Git 索引工作树的情况下git reset --soft移动分离的内容。运行会产生这样的结果:HEADgit reset --soft HEAD^

\n
          I\'  [abandoned]\n         /\n...--G--H   <-- main, HEAD [detached]\n         \\\n          I--J   <-- feature\n
Run Code Online (Sandbox Code Playgroud)\n

也就是说,我们I\' 立即放弃 commit 。我们在 Git 的索引/暂存区和工作树中拥有我们想要的大部分内容。通过完全放弃I\',我们现在安排的事情就好像I我们根本没有进行过提交一样:当前提交是 now H,而不是I

\n

git restore -SW --source HEAD -- file1如果这就是我们想要的,我们现在就可以了。事实上,-S,--source HEAD是默认值,所以我们可以将其缩短为:

\n
git restore -SW -- file1\n
Run Code Online (Sandbox Code Playgroud)\n

这会将提交的内容file1从提交H\xe2\x80\x94 我们现在调整的HEAD\xe2\x80\x94 复制到 Git\ 的索引和我们的工作树,并丢弃我们在 commit 中所做的任何更改I。现在git statusgit diff --cached给我们与第一次进行此提交时得到的相同结果。

\n

edit(如果的模式总是自动执行此操作,那可能会很好rebase -i,但事实并非如此,而且现在更改它已经太晚了。)

\n

  • 在本次讲座之后,对于“删除对文件的更改”没有明确的答案。 (2认同)