我正在尝试做一个git rebase
掌握。我有28
变基。所以,在某些阶段,我会遇到冲突。我做了调整,然后我做git status
了,修改后的文件出现了。但是,当我这样做时,有时文件会从列表中git add {filename}
消失。modified
changes to be committed
是因为一些git
错误还是因为我无意中使代码与master
分支相同?
我正在尝试做一个git rebase
掌握。我有28
变基。所以,在某些阶段,我会遇到冲突。我做了调整,然后我做git status
了,修改后的文件出现了。但是,当我这样做时,有时文件会从列表中git add {filename}
消失。modified
changes to be committed
是因为一些git
错误还是因为我无意中使代码与master
分支相同?
[消失状态] 是不是因为我无意中让代码与
master
分支相同?
可能——尽管“无意”可能是错误的;也许你是故意这样做的,却没有意识到这是你的目的。master
但是,说“与分支相同”并不完全正确。正如j6t 在评论中所说,这意味着该文件现在与HEAD
提交相同。
在我们讨论细节之前,让我回到这个:
但是,当我这样做时,有时文件会从列表中
git add {filename}
消失。modified
changes to be committed
让我们来看看git status
实际做了什么。首先,让我们定义工作树、索引和一般的提交,特别是HEAD提交。然后,让我们看看 Git diff 是什么。然后我们可以进入git status
并查看 的过程git rebase
。
为此,请记住文件树(或只是树)是文件的集合,从顶级目录(或“文件夹”,如果您更喜欢该术语)开始,其中可能包含其他子目录(“子文件夹") 以及包含文件。树是具有所有内容的顶级目录:所有它自己的文件,加上任何子树及其文件,以及任何子子树等等。
HEAD
你的工作树就是这样:你工作的树(目录)。它具有您的编辑器和计算机其余部分可以使用的正常格式的所有文件。(它也可以有不参与 Git 的文件:这些被称为未跟踪文件。如果您将源代码构建为目标代码,或者将 Python 转换为字节编译*.pyc
文件,例如,这些文件将保留为仅工作树,即,未跟踪,故意的。)
索引——也称为暂存区,有时也称为缓存——只是你构建下一次提交的地方。Using将工作树中的给定内容git add <path>
复制<path>
到索引中,替换之前存在的文件版本。当您最终运行 时git commit
,Git 会将索引中的任何内容(包括任何子目录及其文件以及所有顶级文件)转换为新的提交。1
提交是 Git 存在的主要原因。每个提交存储一棵树。该树是您提交时索引中的内容的快照。每个提交还存储一些元数据。我不会在这里完全定义这个术语,而只是使用每个提交的实际元数据示例。这些是:
git commit
。例如,通过电子邮件发送的补丁会发生这种情况。因为每个提交都存储了之前提交的 ID,所以一系列或一系列提交让我们可以查看开发历史:
A <- B <- C <-- master
这里 commitC
是最新的master
. (它的实际 ID 是一些大而丑陋的 SHA-1 哈希,badf00daddc0ffee...
或其他什么。) CommitC
的哈希 ID 为 commit B
,它让 Git找到commit B
,并且B
ID 为A
. 这个名字master
是 Git 如何找到 commitC
的。
总是有一个HEAD
提交。2 这是您当前的提交。通常,这也是某个分支的尖端:例如,通常你可能是on branch master
,正如git status
所说的那样,然后HEAD
会决定提交C
。但是您可以HEAD
指向其他一些提交,在这种情况下,HEAD
它只是“当前提交”。
进行新提交会将索引转换为快照(树)并使用该树进行新提交。新提交的父级是 old HEAD
,然后 Git 更新HEAD
以使其指向新提交。如果你在一个分支上,Git 通过使分支名称指向新的提交来进行更新:
A <- B <- C <- D <-- master (HEAD)
如果您不在分支上,则HEAD
实际上包含原始提交 ID。在这种情况下,git commit
将新的提交 ID 直接写入HEAD
. (这就是在你的冲突期间发生的事情git rebase
,这就是我提到它的原因。)但无论如何,看看D
这里的 commit 是如何指向 commitC
的:新的快照总是引用前一个快照。
同样,HEAD
提交始终是当前提交。当我们进入 rebase 操作时,我们将需要这个。
1这不是很精确。如果递归地展平一棵树,就会得到索引。这使得将索引转换为树变得容易(ish)——所以这就是 Git 在这里所做的:它将索引转换为树,使用git write-tree
. 这使 Git 成为那些丑陋的 SHA-1 哈希 ID 之一。然后 Git 使用这个哈希 ID 进行新的提交。通过将索引复制到树,然后将树 ID 放入提交中,Git 最终将索引的内容保存为新提交的快照。
2这条规则有一个例外。由于初始的空存储库没有提交,因此需要此异常。显然,如果没有提交,就不可能解析HEAD
为提交哈希 ID。但是,出于我们的目的,我们不需要关心“孤儿”或“未出生”分支的这种特殊情况。
git diff
, 和两对三棵树虽然git diff
有很多选项和使用模式,但最简单和最直接的方法是比较两棵树。一棵树被标记a
,另一棵被标记b
。diff 本身由一组指令组成,这些指令主要是这样的:“要更改a/README.txt
为b/README.txt
,请删除现在存在的第 12 行,并在第 12 行插入另一行。这也是第 12 行周围的一些上下文。” 这意味着有问题的文件被命名README.txt
并且位于树的顶层——如果它在某个子树中,例如,输出将显示a/subdir/README.txt
and b/subdir/README.txt
。
两棵树中的一棵通常是您的工作树。您也可以像使用树一样使用索引。或者,您可以将任何提交(例如 HEAD(当前)提交)用作树;Git 只是找到与该提交相关的快照树。
我们通常只想要一个文件名列表,而不是获得一组指令,“这是如何更改 README.txt”、“这是如何更改 main.py”等等。git diff
我们可以通过使用--name-only
or得到这个--name-status
。该--name-only
标志告诉它只打印 name:README.txt
或main.py
. 使用也会--name-status
添加一个状态:M
对于已修改、A
对于新添加等等。
请注意,给定任何普通的快照提交,通过一个父提交,我们可以git diff
针对其(单个)父提交。这将向我们展示该提交中发生了什么变化。就是这样git show
做的git log -p
:他们打印一些关于提交的信息,然后git diff
针对提交的父级运行。
但是,无论如何,git diff
一次只比较两棵树。3 但是现在你已经准备好运行git commit
了,实际上你已经拥有了三棵树:
能够比较所有三个将是很好的。输入git status
。
3实际上,git diff
可以比较两棵以上的树,产生所谓的组合 diff。该git show
命令为合并提交执行此操作(git log -p
通常只是跳过它们,差异)。但这很棘手,更重要的是,它不会做我们想要的git status
。
git status
什么git status
是运行两个 git diff
s。每个都有一个轻微的--name-status
应用变体。
第一个差异是HEAD
vs 索引。当前提交和您的索引之间的这个差异是“要提交的更改”。请记住,这git commit
会将索引写入新提交。如果我们现在这样做——如果我们将当前索引变成一个新的提交——然后将那个提交与当前提交进行比较,我们会看到git log -p
或git show
将显示什么。这些将是我们提交的更改。这就是这部分所git status
展示的。
它不打印实际的差异,只打印文件名和详细状态(例如,modified
而不是仅仅M
)。如果我们想要实际的差异,我们必须运行git diff --cached
. 这 - 它使用旧的“缓存”名称作为索引 -HEAD
与索引进行比较。
向我们展示了这一点,git status
现在运行第二个 git diff
。这将索引与工作树进行比较。如果有我们尚未git add
编辑的文件,这将向我们显示这些文件是哪些文件。同样,我们看不到实际的差异,只有文件名和状态。如果我们想要实际的差异,我们必须运行git diff
,它比较索引与工作树。由于这些是我们尚未修改的更改git add
,因此第二种--name-status
风格的差异git status
显示了我们可以做到 git add
的。一旦我们这样做git add
了,它们就会在索引中,所以这个 diff fromgit status
将停止提及文件。
请注意,在所有这些过程中,我们仍然得到两个单独的差异:HEAD
-vs-index 和 index-vs-work-tree。如果我们直接使用HEAD
-vs-work-tree 会怎样?
好吧,git status
不会那样做,但我们可以:我们可以运行git diff HEAD
(没有--cached
这个时间)。与往常一样,我们可以使用它--name-status
来获取文件名和状态,或者将其保留以获取完整的差异。
现在,假设git status
说README.txt
有要提交的更改,并且有README.txt
没有为提交暂存的更改。这意味着HEAD
-vs-index 不同,index-vs-work-tree 不同。但是如果第一个变化——<code>HEAD vs index——是,比如说:
-the color purple
+the colour purple
(即,我们去了英式拼写)。如果从索引到工作树的第二个更改是:
-the colour purple
+the color purple
(即,我们改回美式拼写)。如果我们比较HEAD
与工作树,使用git diff HEAD
,我们根本看不到任何变化!
如果在这一点上,我们git add README.txt
将从“要提交的更改”和“未暂存以提交的更改”变为没有更改。这就是你所看到的。
该git rebase
命令非常类似于重复许多单独的git cherry-pick
命令。记住我们在上面绘制的那些图,在master
. 让我们画一个更大的图,带有一个侧枝:
...--D--E--F <-- master
\
G--H--I--J--K <-- sidebr
注意master
点提交F
,而sidebr
点提交K
。有五个sidebr
未提交的提交master
。(提交E
和之前的提交都在和 sidebr
上 master
。这对 Git 来说有点特殊。)要重新定位 sidebr
到master
,我们需要让 Git复制这五个提交中的每一个。
复制一个提交的 Git 命令是git cherry-pick
. 它复制一个提交的方式是将其转换为差异,通过将其与其父提交进行比较,然后将该差异应用到您希望将其复制到的位置。我们想要复制G
并让副本紧跟在 之后F
,如下所示:
G' <-- HEAD
/
...--D--E--F <-- master
\
G--H--I--J--K <-- sidebr
新副本——新提交——“相似G
但略有不同”,所以我们称之为G'
. 一旦我们有了G'
,我们接下来要复制,然后H
有新的副本: G'
G'-H' <-- HEAD
/
...--D--E--F <-- master
\
G--H--I--J--K <-- sidebr
我们想重复这个序列,直到我们复制K
到K'
:
G'-H'-I'-J'-K' <-- HEAD
/
...--D--E--F <-- master
\
G--H--I--J--K <-- sidebr
一旦它们都被复制,我们想要的最后一件事——最后一步git rebase
——是移动分支标签sidebr
以指向我们复制的最后一个提交,放弃旧链:
G'-H'-I'-J'-K' <-- sidebr (HEAD)
/
...--D--E--F <-- master
\
G--H--I--J--K [abandoned]
现在,在所有这些挑选过程中,有可能其中一个提交(甚至其中许多提交)中的某些内容已经在 commit 中完成F
。在这种情况下,由于我们将通过扫描旧链获得的更改应用于从 start 派生的快照F
,因此我们将遇到精心挑选的提交无法正确应用的情况。
解决冲突可能会导致删除更改:它不需要作为更改,因为它已经在新的base中。HEAD
在这种情况下,我们将停止从我们成功复制的最后一次提交到我们的索引的任何更改。
如果我们最终从其中一个提交中删除所有更改,我们将拥有 Git 喜欢称之为“空”提交的东西。(这些实际上不是空的,它们与之前的提交相同。不是空的提交,而是空的git log -p
补丁。)Git 默认情况下不会进行空提交,所以对于这些情况,我们必须使用git rebase --skip
而不是git rebase --continue
. Git 试图提前弄清楚是否会有这样的“空副本”,如果有,就提前跳过它们。但有时它无法弄清楚——当我们到达那里并解决冲突时,我们才发现跳过是正确的。
我总是觉得有点可疑:我真的正确解决了这个问题吗?变化真的是在新基地吗?值得查看git log
新基地的结果,以确保您确实正确解决了冲突。但它可能是正确的;毕竟这可能是故意的。