当我在git合并过程中进行git添加时,文件会从更改列表中消失



使用git将分支develop合并到分支my-feature-branch。有一个二进制文件的两个分支都发生了更改,因此它显示为合并冲突。

On branch my-feature-branch
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Changes to be committed:
modified: my-file.txt
Unmerged paths:
(use "git add <file>..." to mark resolution)
both modified:   mybinary.gde

然后我进入文件,修复了合并冲突。这就是我的问题所在。当我执行git add .git status时,我的二进制文件并没有像我预期的那样出现在要提交的更改列表下。现在,当我做git status时,它看起来像这样:

On branch my-feature-branch
You have unmerged paths.
(fix conflicts and run "git commit")
(use "git merge --abort" to abort the merge)
Changes to be committed:
modified: my-file.txt

这意味着什么,当我git added一个冲突文件时,它没有像我预期的那样出现在"要提交的更改"列表中?

您运行了git add .,这意味着将所有文件添加到我的工作树的该级别及以下。它大致相当于非Windows系统上的git add *,几乎完全等同于Windows上CMD.EXE中的git add *1

这样做的效果是:

git add my-file.txt mybinary.gde

第一个没有效果(我们将在下面更详细的部分进行讨论);第二个效果是告诉Git使用工作树中剩下的mybinary.gde版本。

然而,为了真正理解git status的输出,我们需要了解一些相当棘手的细节。在使用Git时,你真的应该知道这些,尽管许多人似乎在没有完全学习过这一点的情况下就可以使用Git。


1这里的差异原因是在Unix风格的shell中,包括bash(无论是在Linux、Windows还是macOS上)或zsh等,是shell扩展了星号*。如果命令行解释器没有(或者由于某种原因未能)扩展*,就像CMD.EXE的情况一样,那么——并且只有——Git本身会看到*,Git可以进行自己的扩展。

我们稍后将在另一个脚注中重新讨论这一点。


细节的第一部分,或Git的索引/暂存区

您可能已经知道,Git在每次提交时都会存储每个文件的完整快照。这个快照完全是只读的:一旦制作完成,它的任何部分都无法更改。为了防止存储库很快变得非常胖,这些快照在多个提交中被共享:每个文件都以压缩和消除重复的形式存储。如果以前和以后的提交将相同的数据存储在某些文件中,则数据实际上是共享的:只有一个实际副本进入存储库。由于任何提交的任何部分都不能更改,因此共享这些数据是安全的。

然而,这确实给使用存储在提交中的文件带来了问题。只有Git可以读取这些文件,实际上没有任何东西可以写入这些文件。Git对此问题的解决方案是,当您签出某个提交时,Git会将存档的文件提取到一个工作区中。Git将此工作区域称为工作树。因此,您看到和使用的文件实际上根本不是Git中的

不过,一旦您更改了其中一些文件,您将需要进行新的提交。新的提交需要包含一个新的快照。这里有趣的是这个快照的。新快照的明显来源是您的工作树,它包含您一直在处理/使用的所有文件。但这些文件不是Git格式的:这些只是普通的日常文件,没有压缩和消除重复。

因此,Git所做的就是保留一个额外的";复制";的所有文件。这些文件存在于Git所称的索引暂存区中,或者——现在很少——缓存。单词";复制";在这里加引号是因为,在消除重复后,这些文件实际上不占用空间:它们已经在提交中了,所以这些"副本";只是对现有文件的引用。

当您更改某个工作树副本时,索引副本(或"副本")不会受到影响。Git的索引因此逐渐与您的工作树不同步。。。直到运行git add,这告诉Git:获取某个文件的工作树副本,并使用它来替换索引副本Git将在那时压缩工作树副本,检查它是否已经在某个现有的提交中,然后重用该压缩副本,或者准备存储文件的新的唯一压缩副本。Git的索引/暂存区记录该文件的新版本或重复使用的版本(视情况而定)。

因此,暂存区几乎在任何时候都包含建议的下一次提交。在运行git add之前,您尚未在下一次提交中提出任何更新。如果运行git commit,并且索引与当前提交完全匹配,那么Git通常会告诉您没有什么要提交的2一旦索引——建议的下一次提交——与当前提交不同,git status将告诉您为提交准备的更改。具体来说,对于索引中与当前提交的副本不匹配的每个文件,该文件都将被暂存以供提交


2这里的主要异常是如果您指定--allow-empty,这告诉Git无论如何都要进行新的提交,即使它与当前提交相同。CCD_ 20可能也有一些有趣的角落案例;如何计算这些还不清楚。


合并期间Git索引的血腥细节

当我说Git的索引在任何时候——或者更确切地说,几乎在所有时候——都包含拟议的下一次提交时,我几乎在上面使用了这个词。有一次,索引包含更多,这发生在合并期间。

Git的合并操作涉及三个提交。这是你的当前提交,Git早些时候提取到Git的索引和你的工作树中。这是三个输入提交之一。您选择了一些其他提交进行合并。您运行了git merge otherbranch,它告诉Git将名称otherbranch转换为哈希ID——尝试在每个分支名称上运行git rev-parse,或者查看git branch -vgit for-each-ref refs/heads的输出,看看每个分支名称对应于某个丑陋的提交哈希ID——而另一个哈希ID是三个输入提交之一。最后——或者说,从某种意义上说,首先——Git自己发现了一些合并基提交。

当我们进行普通合并时,这三个提交有时是显而易见的。例如,假设我们有以下分支和提交:

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

K--L   <-- their-branch

我们已经运行了git checkout our-branch,所以我们使用了commitJ(这里的字母代表真正的哈希ID,它们又大又丑,看起来很随机)。然后我们运行了git merge their-branch,所以Git也找到了提交L。然后Git做了一点魔术,那就是同时从提交JL向后走,并找到它们的共同起点,即提交H3

在这一点上,Git进行合并工作的方式——有些过于简单化了——是Git现在将所有三个提交读取到其索引中。首先,Git有效地扩展索引,使每个文件有四个槽。Git获取现在在它的索引中的文件,以及在你的工作树中,并将它们标记为";槽#2";。它将合并基提交读取到插槽1中,将另一个提交读取到槽3中。

现在,每个索引条目中最多有三个文件:文件的合并基础版本在插槽1中,我们的版本在插槽2中,而它们的版本则在插槽3中。如果他们完全删除了文件,插槽3将为空:Git的合并基础版本将在插槽1中,我们的版本将在槽2中。有很多类似的复杂情况,我们将忽略:我们只假设所有三个提交都有完全相同的文件,因此每个索引条目都使用所有三个槽。

现在Git通过索引,一次一组插槽(一个文件):

  • 如果没有人更改文件的任何副本,则所有三个索引槽中的副本都将匹配。因为Git已经提前消除了文件的重复,所以Git可以很快检测到这一点。在这种情况下,Git对自己说:天哪,根本没有人更改文件。让我们去掉这三个版本,只将一个版本放入插槽0此文件已被解析,现在可以提交了。

  • 但是假设我们在提交H和提交J之间的某个位置更改了文件的副本,但他们没有触摸文件的副本。Git会看到插槽1和插槽3(基本插槽和它们的插槽)相互匹配,但与我们的条目不匹配。所以Git只会获取slot-2条目,并将其移动到slot-0。Git接受了我们的更改,文件被解析

  • 或者,假设他们更改了他们的副本,而我们没有触摸我们的。然后Git会看到插槽1和插槽2(基本插槽和我们的插槽)匹配,但插槽3不同。因此,Git将获取slot-3条目,并将其移动到slot-0,同时检查该版本的文件。Git现在已经对进行了更改,文件已被解析

  • 这留下了最困难的情况:三个副本都不同现在Git确实必须合并更改。Git将尽其所能,尽最大努力将文件合并到我们的工作树副本中:

    • 这可能会完全解决所有问题。如果是,那太好了!Git会将已解析的文件从工作树复制到插槽0,擦除插槽1到插槽3,文件现在已解析。

    • 或者,它可能会留下某种合并冲突,或者——例如,如果文件是二进制——Git可能根本不知道如何合并文件。在这种情况下,Git在我们的工作树中留下的东西,并在其索引中的插槽1到3中留下文件的所有三个版本。

当Git完成对所有索引槽的遍历时,合并要么完成了——所有问题都解决了,所有索引项都在默认槽零位置——要么出现了一些问题,一些索引项的槽号为非零。同时,在你的工作树中有一些文件:对于每个问题文件,这要么是Git最好的合并尝试,要么就是Git在没有尝试合并的情况下留下的任何文件。

如果Git没有自行完成合并(或者如果您告诉git merge在提交之前停止),git merge现在将停止。您的工作是通过修复任何需要修复的文件的工作树副本并运行git add来清理遗留的任何混乱任何位于非零槽中的文件都表示未合并文件git status将表示未合并

运行git add命令Git:引导文件的当前索引副本,并放入通过压缩和去复制工作树副本而生成的索引副本如果文件处于未合并状态,即具有非零索引项,则这些项也会被引导出去。文件在零号插槽中放入。插槽零表示文件已被解析


3为了便于说明,我在这里特别简单地完成了这项任务。事实上,Git使用图算法来找到有向无循环图的最低公共祖先,这可能会出现一些复杂情况。我们将在这里完全忽略它们。


这就是git add .解析未合并文件的原因

你的git add .有做git add mybinary.gde的效果。这告诉Git:读取mybinary.gde的工作树副本,然后启动插槽1-3副本,并将您刚刚构建的消除重复的副本放入插槽0换句话说,您告诉Git合并的正确结果是保留Git留在工作树中的mybinary.gde文件。该文件是先前工作树中的文件,是先前git checkoutgit switch的结果,它提取了作为您签出的分支顶端的提交。

git status运行时,它所做的是将当前提交中的内容与Git索引中的内容进行比较,作为建议的下一次提交。此时,索引中的mybinary.gde与当前提交中的mybinary.gde匹配。所以git status根本没有说任何关于那个文件的事情。

请注意,git add .也运行了git add my-file.txt,但这没有效果,因为Git索引中my-file.txt的副本已经与工作树中的副本匹配。这两个副本都与当前提交中的副本不同,因此git status在此git add .之前和之后都表示staged for commit

运行git add *会对这两个文件产生相同的效果。不过,如果您使用的是Unix风格的shell,git add *可能会添加一些未跟踪的文件4

现在您已经了解了Git是如何使用其索引的,我们可以很容易地描述一个未跟踪的文件:未跟踪文件是指目前不在Git的索引中,但在您的工作树中的文件这里的部分很重要,因为Git的索引中的内容会随着时间的推移而变化。当您使用git checkoutgit switch来选择某个提交时,Git会从该提交中填充其索引。如果提交中没有特定的文件,例如,如果提交是旧的,在文件存在之前,文件就不会在Git的索引中。如果该提交确实有文件——例如,如果您从没有文件的历史提交移回有文件的现代提交——那么该文件将返回Git的索引


4正如我之前在脚注1中提到的,我们需要考虑哪个实体正在扩展*。当shell(一个命令行解释器)展开*时,它是通过查看工作树中的文件来完成的。当Git扩展*时,它通过查看提交或Git索引中的文件来实现。由于未跟踪的文件根据定义不在Git的索引中,因此可能也不在提交中,因此Git可能看起来的地方不会有未跟踪的文件。所以git add *不会添加未跟踪的文件。但是,如果shell扩展*,则shell在不查看Git的提交和/或索引的情况下进行扩展,因此shell会将未跟踪的文件名放入git add的参数列表中,就像您自己键入的一样。


结论

Git的索引是在Git中完成任务的关键组成部分,如果你愿意,可以称之为暂存区,因为这主要是你使用它的方式。你应该知道它,以及Git在大多数情况下是如何将它用作建议的下一次提交的,以及在冲突合并期间以某种扩展的方式。你不需要记住这里的所有细节,但你确实需要了解Git的索引。试图在不知道Git索引的情况下通过会导致无法解释的行为,例如,为什么预提交挂钩会这样做(因为Git是从索引中的内容提交的,而不是从工作树中的内容)。

最新更新