当你谈论删除文件在交互式变基期间,您可能意味着以下两件事之一:
两者都相对容易做到。在我说如何做之前,我先介绍一些背景知识。
背景
要了解您在做什么及其原因,拥有正确的 Git 提交思维模型会有所帮助:
-
每个提交都有一个唯一的编号(hash ID or OID,其中 OID 代表对象 ID)。提交的编号是 Git 真正找到提交的方式:分支机构名称其实并不重要。
-
所有 Git 提交(实际上,所有 Git 内部对象)都是完全只读的。你不能change一个提交,所以这不是git rebase
确实如此。
-
每个提交都存储两件事:(a) 的快照all文件,以及 (b) 一些元数据。
提交快照中的文件经过压缩和 Git 化,重要的是,去重(在提交内和跨提交)当某个文件的内容与其他文件的内容匹配时。因此,大多数提交主要包含大多数先前提交的所有文件这一事实不会导致存储库膨胀:这些重复的文件仅存储一次。只要您不在 Git 中存储大型、不可压缩的二进制文件,Git 就会以无形的方式很好地处理这一切(如果您这样做,那么 Git 处理此问题的能力很差,并且存储库会膨胀并变得无法使用)。但这也意味着承诺don't store changes.
The metadata提交中记录了诸如提交人、提交时间和原因(他们的日志消息)之类的内容,但它也记录了对 Git 内部操作至关重要的数据:每个提交都存储一个列表父提交哈希 ID。该列表通常只有一个元素长,为每个提交提供一个父项。父提交也有一个快照,为了将提交转换为更改(用于查看目的),Git 提取两个快照并查看哪些文件发生了更改。由于重复数据删除,Git 可以缩短这一过程,根本不需要提取相同的文件;然后它只需要为以下文件提出一个更改方案:don't两次提交中的匹配。这就是你所看到的git show
or git log -p
:提交的(单个)父级快照与提交快照的差异。
因为提交是只读的,只有 Git 本身可以read对于他们,我们实际上并没有处理或处理这些提交。相反,当我们选择一些提交来使用时,我们就有了 Gitextract提交(例如解压缩或解压缩某些存档)到工作区域,Git 将其称为我们的工作区域工作树 or 工作树。当这些工作树文件被提取时fromGit,他们实际上不是in当你完成工作时,完全可以使用 Git。
Because commits are read-only, git rebase
can't fix any bad commit, nor does git commit --amend
change a commit. Instead, Git makes use of the fact that humans—unlike Git itself—never1 find a commit by hash ID. Instead, we use branch names. A branch name simply holds the hash ID of the last commit we want to claim to be "part of the branch". That commit then holds, in its metadata, the hash ID of the previous commit, which holds in its metadata the hash ID of another even-earlier commit, and so on. This produces a simple backwards-looking chain:
... <-F <-G <-H <--branch
其中分支名称保存了该分支的哈希 IDlast commit H
在链条中,一切都从那里开始向后进行。
当我们以正常的日常方式添加提交时,Git 会创建一个新的(只读)提交I
谁的父母是H
,将其添加到链中,并写入新提交的哈希 IDI
进入分支名称:
...--G--H--I <-- branch (HEAD)
“修改”提交H
, Git 只是写入新的提交I
与提交G
作为其父级,而不是提交H
, 导致:
H
/
...--G--I <-- branch (HEAD)
Commit H
仍然存在,但除非我们记住了它的哈希 ID,否则我们永远不会see再来一次。 (Git可以,只要 Git 能找到它的哈希 ID。)
变基简单地包括超过一个新的和改进的提交。如果我们有:
...--F--G--H <-- main
\
I--J <-- feature (HEAD)
我们想要一个修订版I
出现after H
我们进行新的快照和元数据提交,而不是之前/并行I'
(否则看起来像I
,除了我们拿起H
的快照作为我们的“基础”并“重新添加”我们的更改I
,因此“re-base”-ingI
):
I' <-- HEAD [detached]
/
...--F--G--H <-- main
\
I--J <-- feature
然后我们重复此操作以进行提交J
to get J'
:
I'-J' <-- HEAD [detached]
/
...--F--G--H <-- main
\
I--J <-- feature
一旦我们复制了all对新的和改进的提交,我们让 Git 移动了name feature
指向最后复制的提交:
I'-J' <-- feature (HEAD)
/
...--F--G--H <-- main
\
I--J [abandoned]
原来的提交仍然存在;我们就是找不到他们。
1(insert Gilbert & Sullivan HMS Pinafore routine here)
交互式变基
Interactive rebase uses the same process as non-interactive rebase,2 but lets us stop and make adjustments. To do that, Git provides us with an instruction sheet. It contains, initially, a series of pick
commands for each commit that we will copy. These instruct Git to run git cherry-pick
, which is the step that copies a commit, like I
to I'
above.
改变pick
to edit
让 Git 进行优先选择,但随后会停止在 detached-HEAD 模式下。请注意,这里我们正在复制I
to I'
并将其放置在与以前相同的物理位置,而不是将其移动到提交后H
:
I' <-- HEAD [detached]
/
...--G--H <-- main
\
I--J <-- feature
现在我们处于这种状态,我们可以使用git commit --amend
使完后还有犯罪,I"
。在此提交中,我们可以存储任何我们喜欢的快照,并使用任何我们喜欢的提交消息。的父母I"
将H
,与父级相同I
and I'
.
The snapshot进入新提交的内容与任何新 Git 提交具有相同的来源:它来自 Gitindex AKA https://www.merriam-webster.com/dictionary/aka 暂存区。当前包含提交的所有文件I'
,它匹配提交中的所有文件I
(因此,不会使用任何空间,因为它们都是已经预先去重复的重复项)。这些文件的 Git 化副本也位于您的工作树中。因此您可以修改或删除工作树中的文件并运行git add
:
vim foo.py
git add foo.py
or:
rm foo.py
git add foo.py
The git add
步骤告诉 Git 让索引副本与工作树副本匹配,通过读取、压缩和去重文件,或者在删除之后foo.py
—removing索引完全复制。或者:
git rm foo.py
结合了rm
and git add
变成一步。不管怎样,我们已经安排了正确的(更新或删除的)文件在 Git 的索引中,所以我们现在运行git commit --amend
,就像你所做的那样:
git commit --amend
这推动了提交I'
让开,留下提交I"
指向H
:
I' [abandoned]
/
| I" <-- HEAD [detached]
|/
...--G--H <-- main
\
I--J <-- feature
Running git rebase --continue
告诉变基代码继续执行指令表中的下一条指令:另一个pick
, or edit
, or reword
, 管他呢。一旦遵循了最后一条指令,rebase 将像以前一样拉动分支名称:
I' [abandoned]
/
| I"-J' <-- feature (HEAD)
|/
...--G--H <-- main
\
I--J [abandoned]
(废弃的提交会保留一段时间——在通常的设置中,默认情况下至少为 30 天——然后 Git 最终注意到它们已经闲置了足够长的时间,删除了使它们保持活动状态的引用日志条目,并清除了它们是真的。不过,在那之前,您可以轻松取回原件。请注意,特殊名称ORIG_HEAD
还记得提交J
一段时间,直到你做了其他让 Git 覆盖的事情ORIG_HEAD
与另一个哈希 ID。成功变基后,如果您不喜欢结果,ORIG_HEAD
与 reflog 条目一样有效branch@{1}
.)
2In older versions of Git, there were numerous technical differences. In modern Git, these are largely gone now, though you can still invoke them on purpose if you really want to. I will also elide a number of optimizations Git normally uses for the kind of interactive rebase you'll be doing, that do make things better for Git but don't change the final outcome here.
我们现在可以看到什么git reset
or git restore
will do
为什么两者都没有git restore --staged file1
nor git reset HEAD file1
works?
Both git reset
and git restore
将从某处读取文件的内容并将该文件的内容写入某处。这git reset
命令本身非常复杂,所以最好坚持使用更新的、更有针对性的(更有限的)git restore
在我看来,但任何一种都可以:我们只需要知道这里的几件事。
git reset HEAD^ -- F2 //reset F2 to previous version in staging area
在这里,我们使用的是git reset
, not git restore
,以其恢复一个文件的操作模式。如果我们使用:
git reset HEAD -- file1
我们告诉 Git:阅读 Git 化的副本file1
来自指定的提交HEAD
。如果我们使用:
git reset HEAD^ -- F2
我们告诉 Git:阅读 Git 化的副本F2
来自指定的提交HEAD^
.
在这两种情况下,从指定的提交读取指定的文件后,git reset
将(Git 化、预先去重的)内容写入索引/暂存区域,准备进入新的提交。暂存区中的文件名与所选提交中的文件名相同(file1
or F2
). The 工作树副本文件的内容在这里没有改变!这是不可取的,因为它使你很难看到你在做什么,但因为 Git 实际上并不是using此时的工作树副本,它并不完全是harmful现在也是。
Using git checkout
更好:
git checkout HEAD^ -- F2
这种形式的git checkout
——这,就像git reset
,极其复杂,这就是为什么git checkout
被分成git switch
and git restore
在 Git 2.23 中——从指定的提交读取文件并将其写入bothGit 的索引and你的工作树。这使得查看您所做的事情变得更加容易,因为工作树副本现在很明显。
如果您的目标是复制F2
在新的I"
提交与提交中的副本匹配H
, these HEAD^
命令的形式就可以解决问题。原因是HEAD
当前名称提交I'
, 提交的副本I
. I'
的父母是H
,因此检索文件的副本F2
(or file1
) 来自提交H
将恢复索引和工作树版本以匹配in H
,现在你所做的承诺git commit --amend
—the I"
提交——其中包含该文件的相同副本(已消除重复)。
如果你的目标是真正remove F2
完全,以便提交I"
没有文件F2
无论如何,git rm F2
(or git rm -- F2
以避免名为的文件出现问题--cached
,例如)会这样做。
如果我们想做F2
匹配中的副本H
,但是使用git restore
为了避免与过于复杂的签出命令相关的错误,我们运行:
git restore -SW --source=HEAD^ -- F2
例如。这与git checkout
:我们指定HEAD^
作为文件的来源,-S
(--staged
) 告诉git restore
将文件写入暂存区,并且-W
(--worktree
) 告诉git restore
将文件写入我们的工作树。
请注意,在所有情况下,我们的目标都是使索引包含正确的文件 as git commit --amend
将从 Git 的索引中创建新的快照。作为人类,我们通常应该同时更新这些文件的工作树副本,因为我们不能see索引(暂存区)副本,但我们can在我们喜欢的任何编辑器或文件查看器中查看工作树副本。
我们还必须记住,如果当我们跑步时git status
,Git 将运行两个git diff --name-status
为我们运营:
- 人们将比较
HEAD
commit 与 Git 的索引。但是HEAD
提交就是提交I'
,不提交H
!所以我们不必对此过于关注。
- 另一个将比较 Git 的索引和我们的工作树。理想情况下,该 diff 应该为空,以便我们在工作树中查看与 Git 将在下一次提交中使用的文件相同的文件。
The reset --soft
option
我们还有一件事can这样做,我自己实际上从未这样做过:而不是git commit --amend
,我们可以开始整个修改过程git reset --soft
。也就是说,我们开始git rebase -i
, 换一个pick
编辑时,写出说明表,然后开始变基。我们现在处于这样的状态:
I' <-- HEAD [detached]
/
...--G--H <-- main
\
I--J <-- feature
The git reset --soft
命令让我们移动分离的HEAD
不改变eitherGit 的索引or我们的工作树。跑步git reset --soft HEAD^
产生这个:
I' [abandoned]
/
...--G--H <-- main, HEAD [detached]
\
I--J <-- feature
也就是我们放弃commitI'
马上。我们在 Git 的索引/暂存区和工作树中拥有我们想要的大部分内容。通过放弃I'
完全,我们现在已经安排好了as if我们从未做出承诺I
根本:当前提交 is now H
, not I
.
我们现在可以git restore -SW --source HEAD -- file1
,如果这就是我们想要的。事实上,与-S
, --source HEAD
是默认值,所以我们可以将其缩短为:
git restore -SW -- file1
这将复制提交的file1
来自提交H
——我们现在调整后的HEAD
——对于 Git 的索引和我们的工作树,丢弃任何changes我们已经提交了I
. Now git status
and git diff --cached
给我们与第一次进行此提交时得到的结果相同的结果。
(如果edit
的模式rebase -i
一直都是自动完成的,但它没有,现在改变它已经太晚了。)