为什么冲突文件在 Git 中同时出现已暂存和未暂存?
Why does a conflicted file appear both staged and unstaged in Git?
为了编写 Git 教程,我一直在试验 Git。我创建了一个分支,在两个分支上都修改了文件,然后把分支合并回master就产生了冲突。我很好奇的是为什么冲突的文件看起来既是 "staged" 又是 "unstaged"。如果我在任一位置单击该文件,diff window 会显示完全相同的信息。
在 Git 中,文件既已暂存又未暂存很常见。 Git 将更改文件的不同部分识别为 'hunks',因此屏幕截图中的按钮 'Stage hunk' 和 'Discard hunk'。这种 UI 情况意味着某些文件已暂存,而另一些则未暂存。您可以在此处提交,并且只会提交顶部菜单中的更改。
我不确定为什么每个版本的文件都显示相同的信息;真令人惊讶。 Sourcetree 可能无法以有意义的方式显示冲突。
要继续,您需要取消暂存所有内容,解决合并冲突,然后提交更正后的文件。这意味着删除这些行:
<<<<<<< HEAD
=======
>>>>>>> new-feature
并在中心线上方或下方(或两者)保留您想要的代码。
其中一些取决于您的 GUI,但命令行 git status
命令也执行此操作——方式略有不同——因此并非所有都是 GUI-specific。为什么文件可以显示为暂存和未暂存的真正答案是:调用文件 "staged" 或 "unstaged" 是一个谎言。 它不是卑鄙恶毒的谎言它更像是人们用来软化残酷事实的好谎言。它大多是无害的,主要是帮助人们度过一天。
不幸的是,在 合并冲突 的情况下,谎言不再是无害的。细节在这里真的很重要。我们必须看看 Git 是如何运作的,并发现这个 "staged" 和 "unstaged" 谎言背后的真相。
索引/staging-area/缓存
所有这些混乱的核心在于——呃,立场?坐?—index。 Git 的索引是一个非常重要的核心数据结构(通常包含在一个名为 .git/index
的文件中,尽管现在有很多 semi-experimental 棘手的增强变体来提高速度)。索引包含的是一系列插槽,每个文件名一个 group-of-slots,用于 跟踪的每个文件 。事实上,跟踪文件的定义就是索引中的任何文件。 未跟踪文件 是 work-tree 中但不在索引中的文件。
要充分理解这个概念,您还需要知道 Git 以特殊的、冻结的、压缩的 read-only、Git-only 格式存储每个文件的数据,称为 blob 对象。每个唯一的 blob 对象都有一个唯一的哈希 ID,这意味着 非 -唯一的 blob 对象——多次使用的文件数据——实际上可以 re-use 相同的哈希身份证一遍又一遍。因此,当您进行提交并且它包含所有文件的完整快照时,Git 真正在做的是使用 blob 对象来保存文件。如果 此 提交中的文件与早期提交中的文件大部分相同,Git 只是 re-use 现有的 blob 对象。
使用 git ls-files --stage
可以更直接地看到索引真正包含的内容——尽管仍然是 prettied-up 形式。在大型存储库中,这会产生大量输出。这是 Git 存储库中 Git 的片段:
$ git ls-files --stage
[snip]
100644 82cd0569d51d0a2d69b013a3322b6d5985a1927c 0 .mailmap
100644 ffb1bc46f2d9605f7c3fba478f918fcc288bbdd6 0 .travis.yml
100644 8c85014a0a936892f6832c68e3db646b6f9d2ea2 0 .tsan-suppressions
100644 536e55524db72bd2acf175208aef4f3dfc148d42 0 COPYING
100644 ddb030137d54ef3fb0ee01d973ec5cee4bb2b2b3 0 Documentation/.gitattributes
100644 9022d4835545cbf40c9537efa8ca9a7678e42673 0 Documentation/.gitignore
[snip]
100755 122f6479ef9f772f575ecb673e0f960900526fc1 0 GIT-VERSION-GEN
[snip]
第一个数字是模式:对于常规文件总是 100644
或 100755
,对于符号 link 总是 120000
,对于符号 160000
a gitlink(子模块的东西)。第二个数字(嗯,十六进制数)是哈希 ID:对于文件,这是包含文件数据的 blob 对象的哈希 ID。第三个数字 - 上面总是零,但不是合并冲突 - 是 暂存插槽编号 。最后一个字段是文件的名称:文件的 内容 被存储为一个 blob 对象,但该 blob 对象的名称只是一个哈希 ID。 名称存储在别处(技术上,在树对象中,但大多数人不需要关心)。
所有这些的效果是,除了合并冲突期间,索引保存的是提议的新提交。它具有 already-compressed、冻结、Git-ified、read-only 文件数据的副本,或者实际上是通过 blob 哈希 ID 的引用,这些文件数据将与新提交一起使用。
我们还可以查看任何现有的提交。例如,这里是来自同一存储库 master
的片段(现在 public Git 稍微过时了):
$ git ls-tree HEAD
[snip]
100644 blob 82cd0569d51d0a2d69b013a3322b6d5985a1927c .mailmap
100644 blob ffb1bc46f2d9605f7c3fba478f918fcc288bbdd6 .travis.yml
100644 blob 8c85014a0a936892f6832c68e3db646b6f9d2ea2 .tsan-suppressions
100644 blob 536e55524db72bd2acf175208aef4f3dfc148d42 COPYING
040000 tree 0785e26289f9af7de3894161a78d00b2e1d720ef Documentation
100755 blob 122f6479ef9f772f575ecb673e0f960900526fc1 GIT-VERSION-GEN
[snip]
请注意,这次我们有一个新的 mode 040000 tree
对象,它不在索引中。那是因为一旦提交,Git 提交指的是像 一样工作的树对象 目录(尽管它们与 OS 的目录不完全相同)。索引省略了它们,因为索引只包含 files(好吧,对于子模块,gitlinks 也是)。 This is most of what keeps Git from storing an empty directory.
所有这一切的结果——当前(永远冻结)提交在索引中持有一个 tree-ized 扁平化版本的变体,它持有提议的新提交,是 Git 可以轻松地将当前提交与索引建议的新提交进行比较。无论这里 不同,Git 调用 staged.
索引、暂存区和(现在很少见)缓存都是同一个事物的术语。这个具有三个名称的事物是做出新的承诺。您的 work-tree,其中您的文件具有其正常的日常形式,您可以在其中查看和使用它们,Git 在很大程度上是一种 side-shadow。你用 work-tree 做什么取决于你。每隔一段时间,你告诉 Git:从我的 work-tree 复制一个文件,压缩并 Git-izing 并将其变成冻结的 Git-only 格式,然后将该对象放入索引中。 您可以使用 git add
执行此操作。新的 Git-ized 数据还没有提交——它没有一直冻结;你可以通过更换来改变它它在索引中 — 但现在 准备好 可以提交了。 运行 git commit
创建提交,它会一直冻结它,使 blob 对象永久化。1
请注意 git status
不只是将 HEAD
提交与索引进行比较。它还分别将索引与 work-tree 进行比较。此处不同的任何文件都打印为 unstaged。如果某个文件的三个活动副本——HEAD:file
、:file
和 file
——都不同,那么该文件将同时 staged和 未暂存.
1好吧,只要提交本身存在,blob 就是永久性的。如果你摆脱了一个提交,并且一些 blob 是特定于 that 提交的,那么这些 blob 最终也会消失。 git gc
命令负责确定仍然需要哪些提交,以及哪些提交使用了哪些树和 blob。任何未使用的对象——提交、树、blob 或最后一种,带注释的标记,此时都可以删除。
在有冲突的合并期间,索引扮演了一个扩展的角色
当你执行一个真正的合并时,它有三个输入——一个合并基础和两个提示提交——Git 暂时必须将每个文件的三个副本推送到索引中。这就是非零临时插槽编号的用途。
假设合并基础提交有一个 file
版本,内容为:
I am a file.
进一步假设 file
的 left-side(当前分支)版本为:
I am a file
with two lines.
同时 right-side 版本显示:
I am the ghost of a file, killed by Macbeth's two hired assassins.
由于无法自动合并对此文件的这两个更改,Git 将:
- 在
:1:file
中保留文件的合并基础版本
- 在
:2:file
中保留文件的 left/local/HEAD/--ours
版本
- 在
:3:file
中保留文件的 right/remote/--theirs
版本
除了这三个版本之外,当然还有一个 HEAD:file
——此时与 slot-2 版本相同——和一个 work-tree 版本的文件 file
. work-tree 版本包含 Git 的冲突标记。所以现在,文件的 三个 个活动副本现在有 五个 个!
此时你的工作是提出 正确的 组合 file
,然后将其放入索引的槽零,删除其他三个副本.您可以通过编辑 work-tree 副本和 运行 git add file
来完成此操作。 git add
命令知道如果有三个非零暂存副本,而您要添加一个,它应该进入该文件的暂存槽零,删除其他三个。现在你回到只有三个副本,git status
可以告诉你一个有用的故事——一个有用的谎言,谈论 staged-ness——关于索引副本是否匹配 HEAD
and/or work-tree份。
您还可以使用合并工具或某些 GUI 工具来生成正确的合并文件。与往常一样,最终目标是将 file
的 正确 副本填充到暂存槽零中,清空暂存槽 1、2 和 3。这解决了合并冲突和给你留下一些你可以承诺的东西。
虽然文件有 5 个副本,但是,只是说 staged 或 unstaged 或两者都没有涵盖真实情况。如果你想写一个合并工具,你需要知道如何提取每个冲突文件的三个版本——或者,在 modify/delete 或 rename/delete 或 rename/rename 冲突的情况下,什么其他事情要做。 (这有点问题,因为索引中留下的内容不足以理清一些重命名案例。)
为了编写 Git 教程,我一直在试验 Git。我创建了一个分支,在两个分支上都修改了文件,然后把分支合并回master就产生了冲突。我很好奇的是为什么冲突的文件看起来既是 "staged" 又是 "unstaged"。如果我在任一位置单击该文件,diff window 会显示完全相同的信息。
在 Git 中,文件既已暂存又未暂存很常见。 Git 将更改文件的不同部分识别为 'hunks',因此屏幕截图中的按钮 'Stage hunk' 和 'Discard hunk'。这种 UI 情况意味着某些文件已暂存,而另一些则未暂存。您可以在此处提交,并且只会提交顶部菜单中的更改。
我不确定为什么每个版本的文件都显示相同的信息;真令人惊讶。 Sourcetree 可能无法以有意义的方式显示冲突。
要继续,您需要取消暂存所有内容,解决合并冲突,然后提交更正后的文件。这意味着删除这些行:
<<<<<<< HEAD
=======
>>>>>>> new-feature
并在中心线上方或下方(或两者)保留您想要的代码。
其中一些取决于您的 GUI,但命令行 git status
命令也执行此操作——方式略有不同——因此并非所有都是 GUI-specific。为什么文件可以显示为暂存和未暂存的真正答案是:调用文件 "staged" 或 "unstaged" 是一个谎言。 它不是卑鄙恶毒的谎言它更像是人们用来软化残酷事实的好谎言。它大多是无害的,主要是帮助人们度过一天。
不幸的是,在 合并冲突 的情况下,谎言不再是无害的。细节在这里真的很重要。我们必须看看 Git 是如何运作的,并发现这个 "staged" 和 "unstaged" 谎言背后的真相。
索引/staging-area/缓存
所有这些混乱的核心在于——呃,立场?坐?—index。 Git 的索引是一个非常重要的核心数据结构(通常包含在一个名为 .git/index
的文件中,尽管现在有很多 semi-experimental 棘手的增强变体来提高速度)。索引包含的是一系列插槽,每个文件名一个 group-of-slots,用于 跟踪的每个文件 。事实上,跟踪文件的定义就是索引中的任何文件。 未跟踪文件 是 work-tree 中但不在索引中的文件。
要充分理解这个概念,您还需要知道 Git 以特殊的、冻结的、压缩的 read-only、Git-only 格式存储每个文件的数据,称为 blob 对象。每个唯一的 blob 对象都有一个唯一的哈希 ID,这意味着 非 -唯一的 blob 对象——多次使用的文件数据——实际上可以 re-use 相同的哈希身份证一遍又一遍。因此,当您进行提交并且它包含所有文件的完整快照时,Git 真正在做的是使用 blob 对象来保存文件。如果 此 提交中的文件与早期提交中的文件大部分相同,Git 只是 re-use 现有的 blob 对象。
使用 git ls-files --stage
可以更直接地看到索引真正包含的内容——尽管仍然是 prettied-up 形式。在大型存储库中,这会产生大量输出。这是 Git 存储库中 Git 的片段:
$ git ls-files --stage
[snip]
100644 82cd0569d51d0a2d69b013a3322b6d5985a1927c 0 .mailmap
100644 ffb1bc46f2d9605f7c3fba478f918fcc288bbdd6 0 .travis.yml
100644 8c85014a0a936892f6832c68e3db646b6f9d2ea2 0 .tsan-suppressions
100644 536e55524db72bd2acf175208aef4f3dfc148d42 0 COPYING
100644 ddb030137d54ef3fb0ee01d973ec5cee4bb2b2b3 0 Documentation/.gitattributes
100644 9022d4835545cbf40c9537efa8ca9a7678e42673 0 Documentation/.gitignore
[snip]
100755 122f6479ef9f772f575ecb673e0f960900526fc1 0 GIT-VERSION-GEN
[snip]
第一个数字是模式:对于常规文件总是 100644
或 100755
,对于符号 link 总是 120000
,对于符号 160000
a gitlink(子模块的东西)。第二个数字(嗯,十六进制数)是哈希 ID:对于文件,这是包含文件数据的 blob 对象的哈希 ID。第三个数字 - 上面总是零,但不是合并冲突 - 是 暂存插槽编号 。最后一个字段是文件的名称:文件的 内容 被存储为一个 blob 对象,但该 blob 对象的名称只是一个哈希 ID。 名称存储在别处(技术上,在树对象中,但大多数人不需要关心)。
所有这些的效果是,除了合并冲突期间,索引保存的是提议的新提交。它具有 already-compressed、冻结、Git-ified、read-only 文件数据的副本,或者实际上是通过 blob 哈希 ID 的引用,这些文件数据将与新提交一起使用。
我们还可以查看任何现有的提交。例如,这里是来自同一存储库 master
的片段(现在 public Git 稍微过时了):
$ git ls-tree HEAD
[snip]
100644 blob 82cd0569d51d0a2d69b013a3322b6d5985a1927c .mailmap
100644 blob ffb1bc46f2d9605f7c3fba478f918fcc288bbdd6 .travis.yml
100644 blob 8c85014a0a936892f6832c68e3db646b6f9d2ea2 .tsan-suppressions
100644 blob 536e55524db72bd2acf175208aef4f3dfc148d42 COPYING
040000 tree 0785e26289f9af7de3894161a78d00b2e1d720ef Documentation
100755 blob 122f6479ef9f772f575ecb673e0f960900526fc1 GIT-VERSION-GEN
[snip]
请注意,这次我们有一个新的 mode 040000 tree
对象,它不在索引中。那是因为一旦提交,Git 提交指的是像 一样工作的树对象 目录(尽管它们与 OS 的目录不完全相同)。索引省略了它们,因为索引只包含 files(好吧,对于子模块,gitlinks 也是)。 This is most of what keeps Git from storing an empty directory.
所有这一切的结果——当前(永远冻结)提交在索引中持有一个 tree-ized 扁平化版本的变体,它持有提议的新提交,是 Git 可以轻松地将当前提交与索引建议的新提交进行比较。无论这里 不同,Git 调用 staged.
索引、暂存区和(现在很少见)缓存都是同一个事物的术语。这个具有三个名称的事物是做出新的承诺。您的 work-tree,其中您的文件具有其正常的日常形式,您可以在其中查看和使用它们,Git 在很大程度上是一种 side-shadow。你用 work-tree 做什么取决于你。每隔一段时间,你告诉 Git:从我的 work-tree 复制一个文件,压缩并 Git-izing 并将其变成冻结的 Git-only 格式,然后将该对象放入索引中。 您可以使用 git add
执行此操作。新的 Git-ized 数据还没有提交——它没有一直冻结;你可以通过更换来改变它它在索引中 — 但现在 准备好 可以提交了。 运行 git commit
创建提交,它会一直冻结它,使 blob 对象永久化。1
请注意 git status
不只是将 HEAD
提交与索引进行比较。它还分别将索引与 work-tree 进行比较。此处不同的任何文件都打印为 unstaged。如果某个文件的三个活动副本——HEAD:file
、:file
和 file
——都不同,那么该文件将同时 staged和 未暂存.
1好吧,只要提交本身存在,blob 就是永久性的。如果你摆脱了一个提交,并且一些 blob 是特定于 that 提交的,那么这些 blob 最终也会消失。 git gc
命令负责确定仍然需要哪些提交,以及哪些提交使用了哪些树和 blob。任何未使用的对象——提交、树、blob 或最后一种,带注释的标记,此时都可以删除。
在有冲突的合并期间,索引扮演了一个扩展的角色
当你执行一个真正的合并时,它有三个输入——一个合并基础和两个提示提交——Git 暂时必须将每个文件的三个副本推送到索引中。这就是非零临时插槽编号的用途。
假设合并基础提交有一个 file
版本,内容为:
I am a file.
进一步假设 file
的 left-side(当前分支)版本为:
I am a file
with two lines.
同时 right-side 版本显示:
I am the ghost of a file, killed by Macbeth's two hired assassins.
由于无法自动合并对此文件的这两个更改,Git 将:
- 在
:1:file
中保留文件的合并基础版本
- 在
:2:file
中保留文件的 left/local/HEAD/ - 在
:3:file
中保留文件的 right/remote/
--ours
版本
--theirs
版本
除了这三个版本之外,当然还有一个 HEAD:file
——此时与 slot-2 版本相同——和一个 work-tree 版本的文件 file
. work-tree 版本包含 Git 的冲突标记。所以现在,文件的 三个 个活动副本现在有 五个 个!
此时你的工作是提出 正确的 组合 file
,然后将其放入索引的槽零,删除其他三个副本.您可以通过编辑 work-tree 副本和 运行 git add file
来完成此操作。 git add
命令知道如果有三个非零暂存副本,而您要添加一个,它应该进入该文件的暂存槽零,删除其他三个。现在你回到只有三个副本,git status
可以告诉你一个有用的故事——一个有用的谎言,谈论 staged-ness——关于索引副本是否匹配 HEAD
and/or work-tree份。
您还可以使用合并工具或某些 GUI 工具来生成正确的合并文件。与往常一样,最终目标是将 file
的 正确 副本填充到暂存槽零中,清空暂存槽 1、2 和 3。这解决了合并冲突和给你留下一些你可以承诺的东西。
虽然文件有 5 个副本,但是,只是说 staged 或 unstaged 或两者都没有涵盖真实情况。如果你想写一个合并工具,你需要知道如何提取每个冲突文件的三个版本——或者,在 modify/delete 或 rename/delete 或 rename/rename 冲突的情况下,什么其他事情要做。 (这有点问题,因为索引中留下的内容不足以理清一些重命名案例。)