在 Git 变基并修复冲突时,文件似乎不再被修改

While Git rebasing, and fixing the conflict, the file doesn't appear to be modified anymore

我正在努力git rebase掌握。我有 28 个变基。所以,在某些阶段,我会遇到冲突。我进行了调整,然后我做了 git status,修改后的文件出现了。但是,当我执行 git add {filename} 时,有时文件会从 modifiedchanges to be committed 列表中消失。

是因为某些 git 错误还是因为我无意中将代码与 master 分支相同?

Is [the disappearing status] ... because I have unintentionally made the code to be same as the master branch?

可能——尽管"unintentionally"可能是错误的;也许您是故意这样做的,而没有意识到这 您的目的。不过,说 "the same as the master branch" 并不完全正确。作为 ,这意味着该文件现在与 HEAD 提交相同。

在我们讨论细节之前,让我回到这个:

However, when I do git add {filename}, sometimes the files disappear from the modified and the changes to be committed list.

让我们看看 git status 实际做了什么。首先,让我们定义 work treeindexcommit,特别是HEAD 提交。然后,让我们看看什么是 Git diff。然后我们可以到git status,看看git rebase.

的过程

为此,请记住文件树(或只是)是文件的集合,从顶层开始目录(或 "folder",如果您喜欢该术语),它可能包含其他 sub-directories ("sub-folders") 以及包含文件。 tree 是包含所有内容的顶级目录:所有自己的文件,加上任何 sub-trees 及其文件,以及任何子 sub-trees 等等.

工作树、索引、提交和 HEAD

您的 工作树 就是:您进行工作的树(目录)。它以您的编辑器和您的计算机的其余部分可以使用的正常格式包含您的所有文件。 (它也可以有不参与 Git 的文件:这些被称为 untracked 文件。如果将源代码构建为目标代码,或将 Python 转换为 byte-compiled *.pyc 文件,例如,这些文件被保留为 work-tree-only,即,故意未跟踪。)

索引——也称为暂存区,有时也称为缓存 — 就是构建 next 提交的地方。使用 git add <path> 将给定的 <path> 从 work-tree 复制 到索引中,替换之前存在的文件版本。当您最终 运行 git commit、Git 将索引中的任何内容(包括任何子目录及其文件,以及所有 top-level 文件)变成一个新的提交.1

提交 是 Git 存在的主要原因。每个提交存储一棵树。该树是您提交时索引中的内容的快照。每个提交还存储一些 元数据 。我不会在这里完全定义这个术语,而只是使用每个提交的实际元数据示例。它们是:

  • 树本身。 (快照是独立于提交的实体。我们在这里并不真正需要关心它,但稍后可能会很重要,我们不妨适当地描述一下。)
  • parent 提交 ID 的列表,通常只有一个 ID。这是 在您进行新提交之前的提交。
  • 作者:姓名、电子邮件地址和 time-stamp。与 之前 的提交相比,这是编写新代码、新文本或任何关于此提交的 "new" 的人现在。
  • A committer:与作者相同的想法,只是第二个人,以防编写新提交的人不是 运行 git commit 的人。例如,通过电子邮件发送的补丁会发生这种情况。
  • 一条日志消息。这是 free-form 文本,旨在让提交的人能够很好地描述 为什么 他们做出此提交。

因为每个提交都存储了之前提交的 ID,一系列或一系列的提交让我们可以查看开发历史:

A <- B <- C   <-- master

此处提交 Cmaster 上的最新提交。 (它的实际 ID 是一些丑陋的 SHA-1 散列,badf00daddc0ffee... 或其他任何东西。)提交 C 具有提交 B 的散列 ID,这使得 Git find 提交 BB 的 ID 为 A。名称 master 是 Git 查找提交 C.

的方式

总有一个 HEAD 提交。2 这是您的 当前 提交。通常,这也是一些 b运行ch 的提示:例如,通常您可能 on branch master,正如 git status 所说,然后 HEAD 将决定提交C。但是你可以让 HEAD 指向其他提交,在这种情况下,HEAD 只是 "the current commit".

进行 new 提交将索引变成快照(树)并使用该树进行新提交。新提交的父级是旧的 HEAD,然后 Git 更新 HEAD 使其指向 new 提交。如果你在 b运行ch 上,Git 通过使 b运行ch name 指向新提交来进行更新:

A <- B <- C <- D   <-- master (HEAD)

如果您 不是 b运行ch,那么 HEAD 实际上包含原始提交 ID。在这种情况下,git commit 将新提交 ID 直接写入 HEAD。 (这是什么appens during your conflicted git rebase, which is why I mention it.) But anything case, see how commit D here points back to commit C:新快照总是指回上一个

同样,HEAD 提交始终是当前 提交。当我们进入变基操作时,我们很快就会需要它。


1这不是很准确。如果您 递归地展平 一棵树,那么索引就是您得到的。这使得将索引变成一棵树变得容易(ish)——所以这就是 Git 在这里所做的:它将索引 变成 一棵树,使用 git write-tree.这会得到 Git 那些又大又丑的 SHA-1 哈希 ID 之一。 Git 然后将此哈希 ID 用于新提交。通过将索引复制到树中,然后将树 ID 放入提交中,Git 最终将索引的内容保存为新提交的快照。

2这条规则有一个例外。由于初始的空存储库有 no 提交,因此需要此异常。显然,如果有 no 提交,则不可能将 HEAD 解析为提交哈希 ID。不过,出于我们的目的,我们不需要关心 "orphan" 或 "unborn" b运行ch.

的这种特殊情况

git diff,二树对三树

虽然 git diff 很多 选项和使用模式,但最简单直接的方法是比较两棵树。一棵树标记为 a,另一棵树标记为 b。 diff 本身由一组指令组成,主要相当于: "To change a/README.txt to b/README.txt, remove the 12th line that's there now, and insert this other line for line 12. Here is some context around line 12 as well." 这意味着所讨论的文件名为 README.txt 并且位于树的顶层——如果它在例如,一些 sub-tree,输出会显示 a/subdir/README.txtb/subdir/README.txt

两棵树中的一棵通常是您的work-tree。您还可以像使用树一样使用索引。或者,您可以将任何提交(例如 HEAD(当前)提交)用作树; Git 只是找到与该提交一起使用的快照树。

而不是得到一组指令,"here's how to change README.txt","here's how to change main.py",等等,我们通常只想要一个文件名列表。我们可以使用 --name-only--name-statusgit diff 得到它。 --name-only 标志告诉它只打印名称:README.txtmain.py。使用 --name-status 还会添加一个 statusM 表示已修改,A 表示新添加,依此类推。

请注意,给定任何普通快照提交和一个父提交,我们可以git diff针对其(单个)父提交。这将向我们展示该提交中更改的内容。这就是 git showgit log -p 所做的:它们打印一些关于提交的信息,然后 运行 git diff 针对提交的父级。

无论如何,git diff一次只比较两棵棵树。3但是你在这里,正准备 运行 git commit,实际上,你有 棵树:

  1. 你的 HEAD(当前)提交;
  2. 你的索引;和
  3. 你的work-tree.

如果能够对这三个进行比较就好了。输入 git status.


3实际上,git diff可以比较两棵以上的树,产生所谓的组合差异git show 命令为合并提交执行此操作(git log -p 通常只是跳过它们,diff-wise)。但这很棘手,更重要的是,没有做我们想要的 git status.


git status

git status所做的是运行两个git diffs。每个都应用了 --name-status 的细微变体。

第一个差异是 HEAD 与索引。当前提交和您的索引之间的差异是 "changes to be committed"。请记住 git commit 会将索引写入新提交。如果我们现在这样做——如果我们将当前索引变成一个新的提交——然后将该提交与 当前 提交进行比较,我们将看到 git log -pgit show 会显示。这些将是我们承诺的改变。这就是 git status 的这一部分显示的内容。

它不打印实际的差异,只打印文件名和详细状态(例如,modified 而不是 M)。如果我们想要实际的差异,我们必须 运行 git diff --cached。这使用旧的 "cache" 名称作为索引 - 将 HEAD 与索引进行比较。

向我们表明,git status 现在 运行 是 git diff。这会将索引与 work-tree 进行比较。如果有我们尚未 git add-ed 的文件,这将向我们显示这些文件是哪些文件。同样,我们看不到实际的差异,只看到文件名和状态。如果我们想要实际的差异,我们必须 运行 git diff,它比较索引与 work-tree。由于这些是我们尚未 git add 编辑的更改,因此与 git status 的第二个 --name-status 样式差异显示了我们 可以 git add .一旦我们执行 git add 它们,它们将在索引中,因此来自 git 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 不同。但是如果第一个变化——HEAD vs index——是,比如说:

-the color purple
+the colour purple

(即,我们使用了英式拼写)。如果第二个变化,从索引到 work-tree,是:

-the colour purple
+the color purple

(即,我们改回美式拼写)。如果我们比较 HEAD 和 work-tree,使用 git diff HEAD,我们根本看不到任何变化!

如果在这一点上,我们 git add README.txt,我们将从 "changes to be committed" "changes not staged for commit" 变为没有变化。这就是您所看到的。

Rebase 重复cherry-pick

git rebase 命令非常像重复许多单独的 git cherry-pick 命令。请记住我们在上面绘制的那些图表,在 master 上有三到四次提交。让我们画一个更大的图,边 b运行ch:

...--D--E--F       <-- master
         \
          G--H--I--J--K   <-- sidebr

请注意 master 指向提交 F,而 sidebr 指向提交 Ksidebr 上有五个不在 master 上的提交。 (提交 E 及更早版本在 both sidebr and master 上。这有点奇怪Git.) 要 rebase sidebrmaster,我们需要 Git copy 这五个提交中的每一个。

复制一个提交的Git命令是git cherry-pick方式它复制一个提交是将它变成一个差异,通过将它与它的父提交进行比较,然后将该差异应用到你想要的地方copied-to。我们想复制 G 并让副本紧跟在 F 之后,像这样:

             G'  <-- HEAD
            /
...--D--E--F       <-- master
         \
          G--H--I--J--K   <-- sidebr

新副本——新提交——是"like G but slightly different",所以我们称之为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的最后一步——就是移动b运行ch标签sidebr 指向我们复制的最后一次提交,放弃旧链:

             G'-H'-I'-J'-K'  <-- sidebr (HEAD)
            /
...--D--E--F       <-- master
         \
          G--H--I--J--K   [abandoned]

现在,在所有这些 cherry-picking 过程中,有可能其中一个提交中的某些内容——甚至许多提交中的内容——已经在提交 F 中完成了。在那种情况下,由于我们正在将扫描 old 链的变化应用到从 F 开始派生的快照,我们将遇到 [=460] 的情况=]ed commit 不适用。

解决冲突可能会导致删除更改:不需要作为 更改 因为它已经在新的 base 中。在这种情况下,我们将停止从 HEAD(我们成功复制的最后一次提交)到我们的索引的任何更改。

如果我们最终从其中一个提交中删除 所有 更改,我们将拥有 Git 喜欢称之为 "empty" 提交的内容。 (这些实际上不是空的,它们只是 same 和之前的提交。空的不是 commit,而是 git log -p patch 那是空的。) Git 默认情况下不会进行空提交,所以对于这些情况,我们必须使用 git rebase --skip 而不是 git rebase --continue. Git 试图提前弄清楚是否会有这样的 "empty copies",如果有,则提前跳过它们。但有时它无法弄清楚这一点——我们只是在到达那里并解决冲突时才发现跳过是正确的。

我总觉得有点可疑:我真的正确地解决了这个问题吗?新基地真的的变化?值得查看新基础的 git log 结果,以确保您确实正确解决了冲突。但它可以是正确的;毕竟可能是故意的。