我在 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
,用于最近修改的提交消息)。
我已经使用网络搜索并阅读了What is the git restore
command and what is the difference between git restore
and git reset
?(如上所述,尝试了restore
和reset
),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期间不能删除单个文件?
2条答案
按热度按时间0md85ypi1#
当你谈论在交互式变基过程中删除一个文件时,你可能意味着两件事之一:
这两个都相对容易做到。在我说如何做到这一点之前,我将介绍一些背景知识。
背景
要理解你在做什么以及为什么要做,有一个正确的Git提交的心理模型是很有帮助的:
git rebase
真正做的。提交快照中的文件经过压缩和Git化,重要的是,* 重复数据消除 * 当某个文件的内容与另一个文件的内容相匹配时,仓库会出现膨胀(包括在提交内和提交间)。因此,大多数提交都包含了之前提交的所有文件,这一事实并不会导致仓库膨胀:这些重复的文件只存储一次,Git可以很好地处理这一切,只要你不在Git中存储大型的、不可压缩的二进制文件。(如果你这样做了,那么Git会处理得很糟糕,仓库会膨胀并变得不可用)。但这也意味着提交 * 不 * 存储 * 更改 *。
提交中的 * 元数据 * 记录了诸如谁提交了该提交、何时提交以及为什么提交等内容(他们的日志消息),但它也记录了对Git内部操作至关重要的数据:每个提交都存储一个 parent commit hash ID的列表。这个列表通常正好是一个元素长,给每个提交一个单一的父提交。父提交也有一个快照,为了将提交转换为更改,(为了查看),Git提取两个快照并查看哪些文件被更改。由于重复数据删除,Git可以短路这一点,根本不需要提取相同的文件;然后它只需要为两次提交中 * 不 * 匹配的文件提供更改配方。这就是你在
git show
或git log -p
中看到的:从提交的(单个)父快照到提交的快照的差异。因为提交是只读的,只有Git自己可以读取,所以我们实际上并不处理提交。相反,当我们选择一些提交使用时,我们让Git将提交提取到一个工作区,Git称之为“工作树”或“工作树”。当这些工作树文件从Git提取时,它们实际上根本不在Git中,而你可以完成你的工作。
因为提交是只读的,所以
git rebase
不能 * 修复 * 任何错误的提交,git commit --amend
也不能更改提交。相反,Git利用了这样一个事实,即人类-与Git本身不同-从不1通过哈希ID来查找提交。相反,我们使用 * 分支名称 *。分支名称只是保存我们想要声称为“分支的一部分”的 * 最后一次 * 提交的哈希ID。该提交然后保持,在它的元数据中保存 previous 提交的hash ID,在 its 元数据中保存另一个更早提交的hash ID,以此类推。这产生了一个简单的向后看的链:字符串
其中,分支名称保存链中 last commit
H
的哈希ID,一切都从那里向后工作。当我们以正常的日常方式添加提交时,Git创建一个新的(只读)提交
I
,其父提交是H
,将其添加到链中,并将新提交I
的哈希ID写入分支名称:型
要“修改”提交
H
,Git只需写入新提交I
,并将提交G
作为其父提交,而不是提交H
,结果是:型
Commit
H
仍然存在,但是除非我们记住它的哈希ID,否则我们永远不会再看到它。(Git可以,只要Git能找到它的哈希ID。)换基就是进行 * 多于一次 * 新的和改进的提交。如果我们有:
型
我们希望修改后的
I
出现在 *H
之后,而不是之前/并行-与此同时,我们创建新的快照和元数据提交I'
(它看起来像I
,除了我们选择H
的快照作为我们的“基础”并从I
“重新添加”我们的更改,因此“re-base”-ingI
):型
然后,我们对commit
J
重复此操作以获得J'
:型
一旦我们将 * 所有 * 提交复制到新的和改进的提交中,我们让Git将 name
feature
移动到最后一次复制的提交:型
原始提交仍然存在;我们只是找不到它们。
1(此处插入吉尔伯特和沙利文HMS Pinafore例行程序)
交互式变基
交互式rebase使用与非交互式rebase相同的过程,2但允许我们停止并进行调整。为此,Git提供了一个指令表。它最初包含一系列
pick
命令,用于我们将要复制的每个提交。这些命令指示Git运行git cherry-pick
,这是复制提交的步骤,就像上面的I
到I'
一样。将
pick
更改为edit
会让Git执行cherry-pick操作,但随后会在detached-HEAD模式下停止。请注意,这里我们将I
复制到I'
,并将其放置在与之前相同的物理位置,而不是将其移动到提交H
之后:型
现在我们处于这种状态,我们可以使用
git commit --amend
来进行 * 另一个 * 提交,I"
。在这个提交中,我们可以存储任何我们喜欢的快照,并使用任何我们喜欢的提交消息。I"
的父节点将是H
,与I
和I'
的父节点相同。进入新提交的 snapshot 与任何新的Git提交具有相同的源:它来自Git的 indexAKAstaging area。这当前包含来自提交
I'
的所有文件,与提交I
中的所有文件匹配(因此不会占用任何空间,因为它们都是已经预先消除重复的副本)。这些是文件的Git化副本,也在您的工作中-树。因此,您可以在工作树中修改或删除该文件,然后运行git add
:型
或:
git add
步骤告诉Git通过阅读、压缩和删除重复文件,或者-在删除foo.py
之后-* 删除 * 整个索引副本,使索引副本与工作树副本匹配。或者:将
rm
和git add
合并到一个步骤中。无论哪种方式,我们都已经将正确的(更新或删除的)文件放置在Git的索引中,所以我们现在运行git commit --amend
,就像你做的那样:这会将提交
I'
推到一边,留下提交I"
指向H
:运行
git rebase --continue
命令rebase代码继续执行指令表中的下一条指令:另一条pick
,或edit
,或reword
,或其他任何指令。一旦执行了最后一条指令,rebase将像以前一样调用分支名称:(The被放弃的提交会被搁置一段时间--在通常的设置中,默认情况下至少30天--然后Git最终会注意到它们已经被闲置了足够长的时间,删除使它们保持活跃的reflog条目,并将它们清除为真实的。在此之前,你可以很容易地恢复原始提交。注意特殊名称
ORIG_HEAD
也会记住提交J
一段时间,直到你做了其他的事情,让Git用另一个hash ID覆盖ORIG_HEAD
。在成功的rebase之后,如果你不喜欢这个结果,ORIG_HEAD
和*branch*@{1}
中的reflog条目一样工作。2在旧版本的Git中,有许多技术上的差异。在现代的Git中,这些差异现在基本上已经消失了,尽管如果你真的想的话,你仍然可以有目的地调用它们。我也会省略一些Git通常用于交互式变基的优化,这些优化确实会使Git的工作变得更好,但不会改变最终的结果。
我们现在可以看到
git reset
或git restore
将执行什么操作为什么
git restore --staged file1
和git reset HEAD file1
都不工作?git reset
和git restore
都将从某处读取文件的内容并将该文件的内容写入某处。git reset
命令本身非常复杂,所以在我看来,最好还是使用更新的,更专注(更有限)的git restore
,但任何一个都可以工作:我们只需要知道这里的几件事。在这里,我们使用的是
git reset
,而不是git restore
,其操作模式是restore-one-file。如果我们使用:用途:我们告诉Git:* 从
HEAD
* 指定的提交中读取file1
的Git化副本。如果我们用途:我们告诉Git:* 从
HEAD^
指定的提交中读取F2
的Git化副本。*在这两种情况下,从指定的提交中读取指定的文件后,
git reset
将(Git-ified,pre-de-duplicate)内容到索引/暂存区,准备进入新的提交。暂存区中的文件名与所选提交中的文件名相同(file1
orF2
).文件的 * 工作树副本 * 在这里没有改变!这是不可取的,因为它很难看到你在做什么,但是由于Git现在实际上并没有 * 使用 * 工作树副本,所以它现在也不完全是 * 有害的 *。使用
git checkout
更好:这种形式的
git checkout
--和git reset
一样,非常复杂,这也是为什么在Git 2.23中git checkout
被分为git switch
和git restore
的原因--从指定的提交中读取一个文件,并将其写入到 * Git的索引 * 和 * 你的工作树中。因为工作树副本现在是明显的。如果你的目标是使新的
I"
提交中的F2
副本与提交H
中的副本相匹配,那么下面这些HEAD^
形式的命令就可以做到这一点。原因是HEAD
当前命名为提交I'
,提交I
的副本。I'
的父级是H
,因此从提交H
中检索文件F2
(或file1
)的副本将恢复索引和工作树版本以匹配 inH
,现在您使用git commit --amend
进行的提交-I"
提交-具有该文件的相同副本(已消除重复)。如果你的目标是真正地 * 删除 *
F2
,这样提交I"
就没有文件F2
,git rm F2
(或者git rm -- F2
,以避免文件名为--cached
的问题,例如)将做到这一点。如果我们想让
F2
匹配H
中的副本,但使用git restore
来避免过于复杂的checkout-command相关错误,我们会运行:这与
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
!所以我们不需要太注意这一点。reset --soft
选项还有一件我们可以做的事情,我自己从来没有真正做过:我们可以用
git reset --soft
代替git commit --amend
来开始整个修改过程。也就是说,我们启动git rebase -i
,改变一个pick
来编辑,写出指令表,让rebase开始。我们现在处于这样的状态:git reset --soft
命令允许我们移动分离的HEAD
,而不需要改变 * Git的索引 * 或 * 我们的工作树。运行git reset --soft HEAD^
会产生以下结果:也就是说,我们给予提交
I'
* 马上 。我们在Git的索引/暂存区和工作树中拥有了我们想要的大部分内容。通过完全放弃I'
,我们现在安排的事情 * 就好像 * 我们根本没有提交I
: 当前提交 * 现在是H
,而不是I
。我们现在可以使用
git restore -SW --source HEAD -- file1
,如果这是我们想要的。实际上,对于-S
,--source HEAD
是默认值,因此我们可以将其缩短为:这将从提交
H
(我们现在调整的HEAD
)中复制提交的file1
到Git的索引和工作树中,丢弃我们在提交I
中所做的任何 * 更改 *。现在git status
和git diff --cached
给予的结果与我们第一次执行此提交时得到的结果相同。(It如果
rebase -i
的edit
模式总是自动完成这一点,那可能会很好,但它没有,现在要改变它已经太晚了。)gwbalxhn2#
字符串