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

noj0wjuj  于 12个月前  发布在  Git
关注(0)|答案(2)|浏览(129)

我在 Pro Git 一本书中读到过关于交互式重基来改变几个提交的内容。所以我做了git rebase HEAD~3,改变了一个我想修改为edit .然后我能够通过git commit --amend更改消息.并通过git add file3添加了一个文件后,git commit --amend开始输出“3文件更改”,而不是以前的“2文件”.但是如何删除一个文件?为什么git restore --staged file1git reset HEAD file1都不工作?(git commit --amend的输出仍然是“3个文件已更改”,git log --patch的输出仍然显示file1,用于最近修改的提交消息)。
我已经使用网络搜索并阅读了What is the git restore command and what is the difference between git restore and git reset?(如上所述,尝试了restorereset),Remove files from Git commit(谈论最后一次提交),Remove file commited in old commit(因为我理解git filter-branch --index-filter "git rm -rf --cached --ignore-unmatch <path_to_file>" HEAD从分支的所有提交中删除一个文件)。
也有不赞成的答案Remove changes to file in old, already pushed, 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

字符串
是不是要删除一个文件需要重置整个提交?在rebase期间不能删除单个文件?

0md85ypi

0md85ypi1#

当你谈论在交互式变基过程中删除一个文件时,你可能意味着两件事之一:

  • 使文件 * 匹配前一次提交 *。有些人会称之为 * 删除对文件的更改 *,由于有些人认为提交 * 是 * 更改,有些人会将其缩短为 * 删除文件 *。
  • 从字面上删除文件,这样你的新的和改进的提交 * 省略 * 文件。

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

背景

要理解你在做什么以及为什么要做,有一个正确的Git提交的心理模型是很有帮助的:

  • 每个提交都有一个唯一的编号(hash ID 或 * bash ,其中bash代表对象ID)。提交的编号是Git真正找到提交的方式: 分支名称 * 实际上并不重要。
  • 所有的Git提交--事实上,所有的Git内部对象--都是完全只读的。你不能 * 更改 * 一个提交,所以这不是git rebase真正做的。
  • 每个提交存储两件事:(a)所有 * 文件的快照,和(B)一些元数据。

提交快照中的文件经过压缩和Git化,重要的是,* 重复数据消除 * 当某个文件的内容与另一个文件的内容相匹配时,仓库会出现膨胀(包括在提交内和提交间)。因此,大多数提交都包含了之前提交的所有文件,这一事实并不会导致仓库膨胀:这些重复的文件只存储一次,Git可以很好地处理这一切,只要你不在Git中存储大型的、不可压缩的二进制文件。(如果你这样做了,那么Git会处理得很糟糕,仓库会膨胀并变得不可用)。但这也意味着提交 * 不 * 存储 * 更改 *。
提交中的 * 元数据 * 记录了诸如谁提交了该提交、何时提交以及为什么提交等内容(他们的日志消息),但它也记录了对Git内部操作至关重要的数据:每个提交都存储一个 parent commit hash ID的列表。这个列表通常正好是一个元素长,给每个提交一个单一的父提交。父提交也有一个快照,为了将提交转换为更改,(为了查看),Git提取两个快照并查看哪些文件被更改。由于重复数据删除,Git可以短路这一点,根本不需要提取相同的文件;然后它只需要为两次提交中 * 不 * 匹配的文件提供更改配方。这就是你在git showgit log -p中看到的:从提交的(单个)父快照到提交的快照的差异。
因为提交是只读的,只有Git自己可以读取,所以我们实际上并不处理提交。相反,当我们选择一些提交使用时,我们让Git将提交提取到一个工作区,Git称之为“工作树”或“工作树”。当这些工作树文件从Git提取时,它们实际上根本不在Git中,而你可以完成你的工作。
因为提交是只读的,所以git rebase不能 * 修复 * 任何错误的提交,git commit --amend也不能更改提交。相反,Git利用了这样一个事实,即人类-与Git本身不同-从不1通过哈希ID来查找提交。相反,我们使用 * 分支名称 *。分支名称只是保存我们想要声称为“分支的一部分”的 * 最后一次 * 提交的哈希ID。该提交然后保持,在它的元数据中保存 previous 提交的hash ID,在 its 元数据中保存另一个更早提交的hash ID,以此类推。这产生了一个简单的向后看的链:

... <-F <-G <-H   <--branch

字符串
其中,分支名称保存链中 last commit H的哈希ID,一切都从那里向后工作。
当我们以正常的日常方式添加提交时,Git创建一个新的(只读)提交I,其父提交是H,将其添加到链中,并将新提交I的哈希ID写入分支名称:

...--G--H--I   <-- branch (HEAD)


要“修改”提交H,Git只需写入新提交I,并将提交G作为其父提交,而不是提交H,结果是:

H
      /
...--G--I   <-- branch (HEAD)


Commit H仍然存在,但是除非我们记住它的哈希ID,否则我们永远不会再看到它。(Git可以,只要Git能找到它的哈希ID。)
换基就是进行 * 多于一次 * 新的和改进的提交。如果我们有:

...--F--G--H   <-- main
         \
          I--J   <-- feature (HEAD)


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

I'  <-- HEAD [detached]
            /
...--F--G--H   <-- main
         \
          I--J   <-- feature


然后,我们对commit J重复此操作以获得J'

I'-J'  <-- HEAD [detached]
            /
...--F--G--H   <-- main
         \
          I--J   <-- feature


一旦我们将 * 所有 * 提交复制到新的和改进的提交中,我们让Git将 namefeature移动到最后一次复制的提交:

I'-J'  <-- feature (HEAD)
            /
...--F--G--H   <-- main
         \
          I--J   [abandoned]


原始提交仍然存在;我们只是找不到它们。
1(此处插入吉尔伯特和沙利文HMS Pinafore例行程序)

交互式变基

交互式rebase使用与非交互式rebase相同的过程,2但允许我们停止并进行调整。为此,Git提供了一个指令表。它最初包含一系列pick命令,用于我们将要复制的每个提交。这些命令指示Git运行git cherry-pick,这是复制提交的步骤,就像上面的II'一样。
pick更改为edit会让Git执行cherry-pick操作,但随后会在detached-HEAD模式下停止。请注意,这里我们将I复制到I',并将其放置在与之前相同的物理位置,而不是将其移动到提交H之后:

I'  <-- HEAD [detached]
         /
...--G--H   <-- main
         \
          I--J   <-- feature


现在我们处于这种状态,我们可以使用git commit --amend来进行 * 另一个 * 提交,I"。在这个提交中,我们可以存储任何我们喜欢的快照,并使用任何我们喜欢的提交消息。I"的父节点将是H,与II'的父节点相同。
进入新提交的 snapshot 与任何新的Git提交具有相同的源:它来自Git的 indexAKAstaging area。这当前包含来自提交I'的所有文件,与提交I中的所有文件匹配(因此不会占用任何空间,因为它们都是已经预先消除重复的副本)。这些是文件的Git化副本,也在您的工作中-树。因此,您可以在工作树中修改或删除该文件,然后运行git add

vim foo.py
git add foo.py


或:

rm foo.py
git add foo.py

git add步骤告诉Git通过阅读、压缩和删除重复文件,或者-在删除foo.py之后-* 删除 * 整个索引副本,使索引副本与工作树副本匹配。或者:

git rm foo.py

rmgit add合并到一个步骤中。无论哪种方式,我们都已经将正确的(更新或删除的)文件放置在Git的索引中,所以我们现在运行git commit --amend,就像你做的那样:

git commit --amend

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

I'  [abandoned]
        /
        | I"  <-- HEAD [detached]
        |/
...--G--H   <-- main
         \
          I--J   <-- feature

运行git rebase --continue命令rebase代码继续执行指令表中的下一条指令:另一条pick,或edit,或reword,或其他任何指令。一旦执行了最后一条指令,rebase将像以前一样调用分支名称:

I'  [abandoned]
        /
        | I"-J'  <-- feature (HEAD)
        |/
...--G--H   <-- main
         \
          I--J   [abandoned]

(The被放弃的提交会被搁置一段时间--在通常的设置中,默认情况下至少30天--然后Git最终会注意到它们已经被闲置了足够长的时间,删除使它们保持活跃的reflog条目,并将它们清除为真实的。在此之前,你可以很容易地恢复原始提交。注意特殊名称ORIG_HEAD也会记住提交J一段时间,直到你做了其他的事情,让Git用另一个hash ID覆盖ORIG_HEAD。在成功的rebase之后,如果你不喜欢这个结果,ORIG_HEAD*branch*@{1}中的reflog条目一样工作。
2在旧版本的Git中,有许多技术上的差异。在现代的Git中,这些差异现在基本上已经消失了,尽管如果你真的想的话,你仍然可以有目的地调用它们。我也会省略一些Git通常用于交互式变基的优化,这些优化确实会使Git的工作变得更好,但不会改变最终的结果。

我们现在可以看到git resetgit restore将执行什么操作

为什么git restore --staged file1git reset HEAD file1都不工作?
git resetgit restore都将从某处读取文件的内容并将该文件的内容写入某处。git reset命令本身非常复杂,所以在我看来,最好还是使用更新的,更专注(更有限)的git restore,但任何一个都可以工作:我们只需要知道这里的几件事。

git reset HEAD^ -- F2 //reset F2 to previous version in staging area

在这里,我们使用的是git reset,而不是git restore,其操作模式是restore-one-file。如果我们使用:用途:

git reset HEAD -- file1

我们告诉Git:* 从HEAD * 指定的提交中读取file1的Git化副本。如果我们用途:

git reset HEAD^ -- F2

我们告诉Git:* 从HEAD^指定的提交中读取F2的Git化副本。*
在这两种情况下,从指定的提交中读取指定的文件后,git reset将(Git-ified,pre-de-duplicate)内容到索引/暂存区,准备进入新的提交。暂存区中的文件名与所选提交中的文件名相同(file1 or F2).文件的 * 工作树副本 * 在这里没有改变!这是不可取的,因为它很难看到你在做什么,但是由于Git现在实际上并没有 * 使用 * 工作树副本,所以它现在也不完全是 * 有害的 *。
使用git checkout更好:

git checkout HEAD^ -- F2

这种形式的git checkout--和git reset一样,非常复杂,这也是为什么在Git 2.23中git checkout被分为git switchgit restore的原因--从指定的提交中读取一个文件,并将其写入到 * Git的索引 * 和 * 你的工作树中。因为工作树副本现在是明显的。

如果你的目标是使新的I"提交中的F2副本与提交H中的副本相匹配,那么下面这些HEAD^形式的命令就可以做到这一点。原因是HEAD当前命名为提交I',提交I的副本。I'的父级是H,因此从提交H中检索文件F2(或file1)的副本将恢复索引和工作树版本以匹配 inH,现在您使用git commit --amend进行的提交-I"提交-具有该文件的相同副本(已消除重复)。
如果你的目标是真正地 * 删除 * F2,这样提交I"就没有文件F2git rm F2(或者git rm -- F2,以避免文件名为--cached的问题,例如)将做到这一点。
如果我们想让F2匹配H中的副本,但使用git restore来避免过于复杂的checkout-command相关错误,我们会运行:

git restore -SW --source=HEAD^ -- F2

这与git checkout的作用相同:我们指定HEAD^作为文件的源,-S--staged)告诉git restore将文件写入暂存区,-W--worktree)告诉git restore将文件写入工作树。
请注意,在所有情况下,我们的目标是 * 使索引包含正确的文件 *,因为git commit --amend将从Git的索引中创建新快照。作为人类,我们通常应该同时更新这些文件的工作树副本,因为我们看不到 * 索引(暂存区)复制,但我们可以看到,在任何编辑器或文件查看器,我们喜欢,工作树复制。
我们还必须记住,如果我们运行git status,Git将为我们运行两个git diff --name-status操作:

  • 我们会比较HEAD提交和Git的索引。但是HEAD提交是提交I',而不是提交H!所以我们不需要太注意这一点。
  • 另一个比较Git的索引和我们的工作树。理想情况下,这个diff应该是空的,这样我们在工作树中看到的文件就和Git在下一次提交中使用的文件一样。

reset --soft选项

还有一件我们可以做的事情,我自己从来没有真正做过:我们可以用git reset --soft代替git commit --amend来开始整个修改过程。也就是说,我们启动git rebase -i,改变一个pick来编辑,写出指令表,让rebase开始。我们现在处于这样的状态:

I'  <-- HEAD [detached]
         /
...--G--H   <-- main
         \
          I--J   <-- feature

git reset --soft命令允许我们移动分离的HEAD,而不需要改变 * Git的索引 * 或 * 我们的工作树。运行git reset --soft HEAD^会产生以下结果:

I'  [abandoned]
         /
...--G--H   <-- main, HEAD [detached]
         \
          I--J   <-- feature

也就是说,我们给予提交I' * 马上 。我们在Git的索引/暂存区和工作树中拥有了我们想要的大部分内容。通过完全放弃I',我们现在安排的事情 * 就好像 * 我们根本没有提交I 当前提交 * 现在是H,而不是I
我们现在可以使用git restore -SW --source HEAD -- file1,如果这是我们想要的。实际上,对于-S--source HEAD是默认值,因此我们可以将其缩短为:

git restore -SW -- file1

这将从提交H(我们现在调整的HEAD)中复制提交的file1到Git的索引和工作树中,丢弃我们在提交I中所做的任何 * 更改 *。现在git statusgit diff --cached给予的结果与我们第一次执行此提交时得到的结果相同。
(It如果rebase -iedit模式总是自动完成这一点,那可能会很好,但它没有,现在要改变它已经太晚了。)

gwbalxhn

gwbalxhn2#

git rebase -i <hash>
# edit action on commit list to show 'edit'
# when rebase plays and on that edit commit:
git --amend
git reset HEAD~1

字符串

相关问题