git 痛苦第 2 部分:当 git 不会提交、拉取或推送文件时,这意味着什么?

git agony part 2: what does it mean when git won't commit, pull or push a file?

基本的 git ontology 仍然是虚幻的,令人深感沮丧。

我们在 github.com 有一个主存储库。假设我们在该存储库中有 1 个文件,一个 R 文件。

  1. 我克隆。

  2. 我的合作者克隆。

  3. 她编辑。她承诺。她推。

  4. 我编辑。我承诺。我推。我不能,因为她已经推了。

  5. 所以我拉。我合并代码。我让它工作正常。我再推一下。我不能。

! [rejected] HEAD -> master (non-fast-forward) error: failed to push some refs to 'https://github.com/PeterLeopold/myRepo' hint: Updates were rejected because the tip of your current branch is behind hint: its remote counterpart. Integrate the remote changes (e.g. hint: 'git pull ...') before pushing again. hint: See the 'Note about fast-forwards' in 'git push --help' for details.

  1. 我又拉了。我不能。

error: You have not concluded your merge (MERGE_HEAD exists). hint: Please, commit your changes before merging. fatal: Exiting because of unfinished merge.

  1. 我再次承诺。我不能。

error: Committing is not possible because you have unmerged files. hint: Fix them up in the work tree, and then use 'git add/rm ' hint: as appropriate to mark resolution and make a commit. fatal: Exiting because of an unresolved conflict.

U ksUpgrade.R

[冲突完美解决!]

所以。现在我已经完美地工作了 merged 代码并且没有办法将它发送回存储库。所以我将它通过电子邮件发送给我的合作者。 . .

我错过了什么?让我猜猜。 . . git 仍处于测试阶段?

P.S。在上一个问题中,我了解到 github 不能 versioned/shared 100% 的附带工作产品(文件、图形、表格),因为这些文件是每次执行代码时生成的,git 必须始终尝试合并字节,不能简单地将它们分层(除非它们被标记为二进制,但它们不是,所以为了最大限度地提高生产力,必须欺骗 git。是的,欺骗 git 是一种专业-提示它被记录了!)

我和我的合作者还没有使用分支或分叉。那些失败模式仍然存在。

(这种体验隐约让人想起 java 中根本无法解决的 class 路径不兼容问题。)

告诉我它变得更好了。 . . :<


这是 "git status" 输出。您会注意到我一直忙于尝试删除 每个 不是 R 或 Rmd 文件的文件。我取得了很大的成功,但并非完全成功。

Changes not staged for commit:

(use "git add/rm ..." to update what will be committed)

(use "git restore ..." to discard changes in working directory)

   deleted:    README.html

   deleted:    README.md

   deleted:    SourceRawData.csv

   modified:   age_dcu.png

   modified:   bar.af.png

   modified:   bar.all.png

   modified:   bar.ca.png

   deleted:    ksPrepForSSSSS.csv

   deleted:    prepForTTTTT.csv

   deleted:    senxxx.csv

   deleted:    timaaaa.csv

Untracked files:

(use "git add ..." to include in what will be committed)

   tables for manuscript_2.docx

   tables for manuscript_3.docx

   ~$bles for manuscript_2.docx

   ~$bles for manuscript_3.docx

您可以尝试以下步骤:

  • 请注意您在第 4 点中使用 git log 所做的提交的 <commit-id>
  • 将存储库的状态重置为该提交 git reset --hard <commit-id>您将丢失所有未提交的更改
  • 执行获取操作以防有任何新的变化
  • 执行合并操作(可能使用--no-commit --no-ff
  • 提交更改
  • 推送到远程

分布式协作工作很辛苦。

Git 试图让它变得简单。 Git 失败。不要责怪 Git ... 好吧,责怪它 some,因为它试图让它变得简单有很多缺陷。 :-) 但问题出在这里:

So I pull. I merge the code. I get it working OK. I push again. I can't.

这是因为您还没有提交您的合并。

你说你已经解决了它们(我会相信你)但现在你必须告诉 Git 你已经解决了它们。这是一个单独的步骤。您必须使用 git addgit rm 告诉 Git 这件事。

下面我有一个很长的答案;随心所欲地阅读。

关于提交的知识

你需要一个好的心智模型,这样你才不会迷路。从这个开始:Git 与文件无关,也与 b运行ches 无关。 Git 实际上是关于 提交 . 要使用 Git 完成工作,您需要提交。存储库主要是大量 collection 的提交。所以你需要知道: 到底是什么 提交?

提交分为两部分:

  • 它保存着所有文件的快照。这不是对文件的更改,而是实际文件本身。快照是一次提交的主要数据。

  • 并且,它包含一些 元数据,或有关提交的信息:例如,谁在何时提交的。

这些部分完全完全read-only,永远冻结。同时,为了找到一个提交,你——或者至少Git——需要知道它的名称

提交的 name 是一些丑陋的大哈希 ID,例如 9fadedd637b312089337d73c3ed8447e9f0aa775。这些东西对人类消费毫无用处;最多可以 cut-and-paste 一个,或者在必要时使用缩写。但重要的是要记住,这些是每个提交的 Git 的真实姓名。每个提交都有一个唯一的提交:一旦你做了一个新的提交,从那时起,任何地方、任何时间的其他提交都不能有 that 名称。从某种意义上说,该哈希名称甚至在 您提交之前就已保留给该提交 - 但哈希取决于您提交的确切秒数,因此基本上不可能预测未来身份证.

每个提交中的一个关键元数据是其上一个提交的原始哈希ID。这使得每个提交有点像链条上的珍珠,除了每个珍珠只连接向后。 Git 无法记录 future commit 的哈希 ID,因为它无法预测,而且一旦做出,任何 commit 都不会改变,所以这些链接只能倒退。

当我们在其中包含提交哈希 ID 时,我们说这个东西 指向 给定的提交。所以每次提交都指向它的前身:

... <-F <-G <-H

我在这里使用大写字母代表真正的哈希 ID。链中的最后一个H,所以H指向G,后者指向F,依此类推。如果我们沿着链条追踪足够长的时间,我们最终会找到 first 提交,它是第一个,毕竟不会向后指向。

关于 b运行ch 名称的知识

实际上有很多东西需要了解,但让我们从简单的部分开始吧。每个 Git 存储库——每个克隆——都有自己的 b运行ch 名称。每个 b运行ch 名称仅包含 one 提交的哈希 ID。

让我们画一个只有三个提交的简单链:

A--B--C   <-- master

名称master指向CC 指向 BB 指向 A,这是第一次提交,所以我们到此为止。同时,我懒得(好吧……)在提交之间画箭头。我们知道它们只走一条路——向后——这就足够了;一旦制作完成,就无法更改,因此我们可以在方便的时候忽略此处的方向。 b运行ch name中的箭头,不过,可以改变。

我们现在添加一个secondb运行ch名称,dev用于开发。我们也会让它指向 C,像这样:

A--B--C   <-- master, dev

现在我们需要知道:我们正在使用哪个 b运行ch 名称? Git 通过附加一个特殊名称来记住这个,HEAD,像这样全部大写,1 到 b运行ch 名称之一。 HEAD 附加的名称 当前 b运行ch 提交该名称指向的 当前提交 。因此,无论我们在此处附加 HEAD 哪个名称,当前 commit 都将是 C。让我们将 HEAD 附加到 dev:

A--B--C   <-- master, dev (HEAD)

现在,让我们以通常的 not-making-any-merges 方式(您已经熟悉)进行 new 提交。这个新提交我会得到一些丑陋的哈希 ID,但我们将其称为 D:

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

名称 HEAD 仍然附加到名称 dev,但现在名称 dev 指向新提交 D。新提交指向旧提交 C 作为其 parent,因为我们在创建 D.

时使用了提交 C

当我们这样做时,我们使 b运行ch 名称指向 lastlatest 提交。都是自动的。 current b运行ch name 在新提交上前进。通过从最后开始并向后工作,旧的提交仍然可以找到。这就是 Git 一直在做的事情:它向后工作。


1在 Windows 和 MacOS 上,您通常可以输入小写的 head。这是一个坏习惯,因为 (a) 它通常在 Linux 上不起作用,而 Git 只是不理解它,并且 (b) 当您开始使用 [=69 时它停止工作=],比它在 Linux 上不起作用更糟糕的方式:它开始选择 错误的提交


索引和你的work-tree

是时候谈谈索引和您的 work-tree 了。我们之前略过这一点,因为您已经知道如何进行新提交,例如 D,但现在了解所有细节很重要,因为我们需要它们进行合并。

提交中的文件被永久冻结。它们实际上无法更改。它们还以特殊的压缩 Git-only 格式存储。这意味着它们可以用于存档,但对于完成任何实际的 工作 却毫无用处。要对提交中的文件做一些工作,我们必须Git到复制这些文件,并将它们变回可用的普通文件。

可用文件放在您的 work-tree 中,您可以在其中查看并处理它们。这让我们了解了关于文件与提交的第一点:提交中的文件不是您可以看到和处理的文件。 您必须将这些文件 取出 首先提交。

git checkout 命令执行此操作。有一种简单但错误的方式来描述 git checkout 是如何做到这一点的:我们选择一个提交,例如 CD,使用 b运行ch 名称如 masterdev。 Git 然后从该提交中提取文件。

理论上,这就足够了:我们在提交中拥有所有已提交文件的一个副本,在您的 work-tree 中拥有每个文件的另一个可用副本。您可以处理它们,然后再次提交并从 work-tree 中的文件进行新的提交。那很容易,这就是其他一些版本控制系统所做的。唉,那不是 Git 所做的。

相反,Git 保留每个文件的 第三个 副本,介于已提交副本和可用副本之间。2 in-between 副本进入 Git 的 索引 。 Git 会在您第一次执行 git checkout 时从您已经 select 的提交中填充它。因此,每个文件在任何时候都有 三个 副本:提交中的冻结副本、索引副本以及您可以查看和使用的 work-tree 副本。

这个名字——"index"——太可怕了。所以最近Git经常称它为暂存区,描述了如何使用它。文件的索引副本处于冻结 格式 ,准备进入下一次提交。如果更改 work-tree 副本,您可能希望将 work-tree 副本复制回索引,替换 ready-to-freeze 副本。这就是 git add 所做的:它从您的 work-tree 复制到 Git 的索引,使文件 暂存 并因此更新以提交。

文件之前在那里!嗯,它是,也就是说,除非它不是——如果它是一个全新的文件,它现在不在 HEAD 中:它只在索引中,现在 work-tree。

因此,如果索引副本 副本 HEAD 不匹配,文件将 暂存以提交 。如果文件是全新的,当然不匹配;否则,它以明显的方式匹配或不匹配。

因为您的 work-tree 是一组常规文件并且 directories/folders 在您的计算机上,您还可以创建 work-tree 您 复制到索引中。 work-tree 中但不在索引中的文件是 未跟踪文件 。 Git 会经常抱怨未跟踪的文件:git status 会不断通知您它们存在。 (你可以让它闭嘴,但我们以后再说。)

这让我们想到了关于文件和提交的第二点:将进入下一次提交的文件是 Git 的 索引中的文件,而不是你在 work-tree. 中看到的那些 当你 运行 git commit,Git 只是打包那个时候的指数。 那就是下一个快照t.

关于索引和 work-tree 有很多微妙之处,我们在这里根本不会涉及。 Git 的索引也许是它最棘手的部分,尤其是因为您无法真正 看到 它。您也不能真正 see 提交那么好,但至少 git log 告诉您有关它们的信息。没有 user-facing 命令可以告诉您很多关于索引的信息,除了 git status ... 并且 git status 仅通过 比较 告诉您事情(请参阅下面有关 status 的更多信息。


2从技术上讲,索引中的内容是 模式文件名、以及对内部 Git blob object 的 引用。但是除非你开始使用一些 Git 的内部命令,否则你可以将索引视为持有一个单独的副本。


B运行ches最终分歧...或不

当我们有示例 three-commit 存储库时,我们在 dev 上进行了第四次提交并得到:

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

我们现在可以git checkout master:

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

这会从索引和work-tree 中删除提交D,并将提交C 放入其中。好吧,只要我们没有未提交的工作,它就可以。如果我们这样做,事情就会变得复杂。假设它确实如此。

现在我们可以进行一些更改,git add 它们——您现在知道将更新的 work-tree 文件复制回 索引中,使它们准备好commit—和 git commit 进行新的提交 E:

        E   <-- master (HEAD)
       /
A--B--C
       \
        D   <-- dev

当这种情况持续一段时间后,您会得到如下所示的内容:

          o--o--o   <-- branch1
         /
...--o--o
         \
          o--o   <-- branch2

等等。但有时你只是:

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

在这种情况下,你有一个 b运行ch 名称指向一个提交,该提交也是早期 b运行ch 的一部分—commit Cboth b运行ches 上,这是另一个 Git 特性——而且没有分歧,git merge dev 会发现在这种情况下,实际上不会进行任何合并。它只是将 name master 向前移动,并检查另一个提交:

git merge dev

结果:

A--B--C
       \
        D   <-- master (HEAD), dev

(之后我们可以再次将它们全部绘制在一条线上)。

Git把这种non-merge称为fast-forward合并。有时,这就是你想要的。有时不是。有时无论如何都不可能。

关于何时需要 fast-forward 合并以及何时不需要的问题没有正确答案。但是如果你不想要一个,并且 Git 认为它应该,你可以告诉 Git 不要做 fast-forward,做一个真正的合并 .在任何情况下,当 b运行ches have 发散时——当你有,例如:

          I--J   <-- branch1
         /
...--G--H
         \
          K--L   <-- branch2

你需要做一个真正的合并。

真正的合并

当Git 进行真正的合并时,它需要覆盖其索引和您的work-tree。这意味着它们在您开始时必须是 "clean"(匹配当前提交),至少在一般情况下是这样。如果它们不干净,你会得到一个错误,合并甚至不会开始,除了一些特殊情况。

您现在已经知道索引保存了下一个提议的提交。但是当您开始合并时,Git 扩展 索引。 Git 不是只保存一组文件,而是使索引保存 组文件。这三组文件放在索引中编号的暂存槽中。

合并被称为three-way合并,可能是因为它有三个输入。三个输入是:

  • a merge base 提交,Git 自己找到;
  • 你的当前提交,通过HEAD和当前b运行ch名称找到;和
  • 另一项提交,您通过 git merge 命令 select。

在这种情况下,假设您执行 git checkout branch1; git merge branch2 以便您拥有:

          I--J   <-- branch1 (HEAD)
         /
...--G--H
         \
          K--L   <-- branch2

合并基础最佳共享提交: 两者上的提交b运行ches,那离每个b运行ch tip都不太远。 b运行ch 提示是提交 JL,这里很明显,最好的共享提交因此是提交 H.3

那么 Git 现在所做的是,对于 H 快照中的每个文件,将该文件复制到索引中的插槽 #1。如果文件名为 README.mdbar.all.png,索引现在有一个 README.md #1 和一个 bar.all.png #1。对于 J 快照中的每个文件,将该文件复制到索引中的插槽 #2。如果 J 有相同的两个文件,它们进入 README.md 插槽 2 和 bar.all.png 插槽 2。然后 Git 将文件从 L 读入索引,到插槽 3。

现在每个索引槽中的每个提交都有每个文件的副本,Git开始正在解析 文件。已解析的副本进入 "slot zero",这是正常的 non-merge 索引状态。

  • 如果所有的ee 副本匹配,任何副本都可以。从三个编号的插槽中取出所有三个并将一个放入插槽 0。 (work-tree副本没问题,不用管它。)

  • 如果你的副本不同,但基础和他们的相同,拿你的副本。删除他们的并将您的放入插槽零。 (work-tree 副本仍然可以,因为我们使用了您的副本。)

  • 如果他们的副本不同,但基础和你的相同,拿他们的副本。 (用他们的替换 work-tree 副本。)

  • 如果三个都不一样,Git其实要努力

当三个暂存槽中的三个副本都不同时,Git实际上会尝试合并文件。 Git 是否以及何时可以做到这一点有点复杂。如果 Git 认为 它自己正确合并了这些文件,Git 会将结果文件写入您的 work-tree 和它自己的 slot-zero,并删除其他三个副本。如果 Git 认为它 没有 正确地合并这些,Git 将在你的 work-tree 中留下它混乱的合并尝试——除非它认为文件是二进制文件,在这种情况下它根本不接触文件—将所有三个副本留在三个编号的插槽中。

这些编号的插槽,如果有任何仍在使用中,将合并标记为冲突。你的工作是以某种方式——任何你喜欢的方式——提出 right 合并文件并将其复制到索引槽零中,清除槽 1、2 和 3。一种简单的方法来这样做——至少通常是——编辑你的 work-tree 中的混乱合并,然后 运行 git add,它复制到索引并完成所有 slot-resetting.

还有一些其他方法可以解决合并冲突问题,我们只会略微提及。假设你删除了一个文件,他们修改了它,所以基础副本和他们的副本是不同的,但你只是没有完全复制。这也会导致合并冲突,但这一次,插槽 #2——--ours 副本——只是空的。你仍然有未合并的文件,但现在你必须以其他方式合并它。您可能仍然使用 git add,它仍然以相同的方式工作,或者可能 git rm 删除 文件从其所在的任何插槽中。

无论如何,如果合并失败,这就是你的命运。你处于这种未合并的状态。 您必须完成合并,否则中止合并。这是您唯一的选择。 必须解析所有未解析的索引条目。这是您需要了解索引的原因之一。4 您只能在 解决所有冲突后提交。

一旦你有一个合并提交,你有这个:

          I--J
         /    \
...--G--H      M   <-- branch1 (HEAD)
         \    /
          K--L   <-- branch2

新的 merge commit 像往常一样有一个快照——基于你放入索引的任何内容,在正常的槽零位置——并且有 两个parent秒。它像普通提交一样指向现有提交 J,但它也指向提交 L,即您 select 编辑的合并。

您可以继续添加正常 (non-merge) 提交:

          I--J
         /    \
...--G--H      M--N--O--P   <-- branch1 (HEAD)
         \    /
          K--L   <-- branch2

并且图表正常增长,b运行ch 名称继续指向 b运行ch 中的 last 提交。请注意,以前仅在 branch2 上的提交 K-L 现在在两个 b运行 上,但提交 I-J-M-N-O-P 都仅在 branch1 上:您可以从 P 开始,然后按照 M 的两个链接向后处理所有提交,但您不能从 L 向后处理并到达 MJI.


3在比较复杂的图中,合并基点在哪里并不总是很清楚。在某些情况下,有 多个 "best" 提交,事情变得有点复杂。我们不会担心这里的那些。

4了解索引的原因有很多,但在合并时您必须对其进行操作这一事实可能是最重要的一个。但是未跟踪的文件在不知道索引的情况下也无法正确解释。


git fetchgit push

Git 已分发。这意味着 拥有一个充满提交的存储库,但其他人也是如此。在常见的设置中,不止一个 "someone else":可能有多个贡献者,在任何情况下,你和他们可能都同意有一些中央存储库 the 存储库:所有其他克隆都只是这个集中存储的影子。

这不是 Git 内部工作的方式。在 Git 中,每个存储库都是对等的:没有 more-masterful 或 less-masterful Git 存储库。但是很容易假装 Git 存储库在 GitHub 上,就是 "real one"。什么都没有以这种方式工作。请记住,GitHub 存储库只是另一个克隆,并且在获取和推送以及 b运行ch 名称等方面与任何 Git 克隆的工作方式相同。

现在,同样,每个 Git 存储库都有 自己的 b运行ch 名称。 Git 的份额是 提交 。这些提交具有唯一的哈希 ID,因此每个 Git 都可以判断它是否拥有另一个提交所拥有的所有提交。

当您和其他人都克隆中央存储库时,你们都从同一组提交开始。您的 Git 也采用中央存储库的 b运行ch 名称,并将它们复制到您自己的 remote-tracking 名称 中,例如 origin/masterorigin/develop。这些名称对您自己的存储库也是私有的:只是每次您将 Git 连接到中央存储库时,您的 Git 可以读出它们的 b运行ch 个名字,并更新你的 remote-tracking 个名字。

创建 remote-tracking 名称后,您的 Git 存储库现在会为您创建一个 b运行ch 名称,通常 master.此名称指向与您的 origin/master 相同的提交,您的 Git 通过哈希 ID 将其设置为指向相同的提交,如 their Git:

...--G--H   <-- master (HEAD), origin/master

让我们看看接下来会发生什么。

  1. I clone [from the central Git].
  2. My collaborator clones [from the central Git].
  3. She edits. She commits. She pushes.

当她 运行 git commit 时,她得到了一个新的、唯一的哈希 ID,与其他哈希 ID 不同。我们就称它为 I。她的 Git 更新了她的 master:

...--G--H   <-- origin/master
         \
          I   <-- master (HEAD)

然后她运行git push.

她的 Git 现在调用中央 Git。她的 Git 对中央 Git 说: 嘿,我被告知要向您提供带哈希的提交 I。它的 parent 是 H。你要I吗?他们说:没见过I,给我!不过,我确实已经 H 了。只要给我提交 I.

他们暂时将提交 I 粘贴到他们的存储库中,没有名称:

...--G--H   <-- master
         \
          I   [no name]

记住,这是中央存储库中的视图。我遗漏了 HEAD 因为我们不关心他们签出了什么 b运行ch,如果有的话,如果是 GitHub,他们没有签出任何东西all.5git push的最后一部分是她Git问他们Git:拜托,如果可以的话,现在将 master 设置为指向 I

他们没有理由 object,并且她有权提出请求,所以他们这样做了。他们的 Git 现在有:

...--G--H--I   <-- master

她的 Git 更新了她的 origin/master 以便她有:

...--G--H--I   <-- master (HEAD), origin/master

你呢,还有这个:

...--G--H   <-- master (HEAD), origin/master

您的 Git 与中央 Git 没有联系,所以您的 Git 不知道有新的提交 I.

I edit. I commit. I push.

您编辑并提交,并使用新的唯一哈希 ID 进行新提交。让我们画一下:

...--G--H   <-- origin/master
         \
          J   <-- master (HEAD)

你 运行 git push,所以你的 Git 调用中央 Git 并提供提交 J (与 parent H).他们拿走 J 并把它放在临时隔离区 运行tine:

...--G--H--I   <-- master
         \
          J   [no name]

现在你的Git询问他们的Git:请,如果没问题,请将你的master设置为指向提交J 但这次他们说 不! 他们说: 如果我这样做,我将失去找到提交的能力 I. Gits 向后工作。从 I 中找到 H,然后找到 G,依此类推。从J可以找到H,但是H只指向向后。他们的 Git 可以知道,如果他们将 他们的 master 移动到指向 J,他们将丢失提交 I。所以他们只是说不:不是fast-forward

这个 fast-forward 项与我们之前看到的 git merge 操作实际上不合并的项相匹配。 Git 需要更改为 b运行ch 名称的结果,如 git push 所建议的那样,作为这些 fast-forward 操作之一。

这就是你的推送被拒绝的原因。现在你需要采取纠正措施。


5服务器通常运行 存储库,没有 work-tree。这是必要的,因为当有人在 b运行ch 上工作时更改 b运行ch 名称会使一切变得混乱。尽管如此,server bare repositories还有一个HEAD,它控制着一个很少有人关心的特性,我们这里就不展开了。


关于git pull

要解决问题,您首先需要获得合作者的新提交。那是一个 git fetch 操作。此时您可以 运行 git fetch — 它总是安全的 — 您的 Git 存储库将像这样调整:

...--G--H--I   <-- origin/master
         \
          J   <-- master (HEAD)

您的 Git 已将共享提交 I 添加到您的 collection,并更新了您的 origin/* 名称以匹配他们的名称。 你的b运行ch名称全部不变

不过,现在您需要以某种方式将您所做的结合起来,提交 J,机智她做了什么,提交 I。这个组合步骤是——或者可以是,无论如何——一个普通的普通 git merge.

你可以运行:

git merge origin/master

它告诉你的 Git:在我的提交 J 和我的 HEAD 找到的提交 I 之间找到共同的合并基础,由 origin/master 发现。然后,将H的文件放入slot 1,将J的文件放入slot 2,将I的文件放入slot 3。然后尝试合并。

git pull命令是一个方便的命令,它简单地将运行git fetch与第二个Git命令组合在一起,通常git merge。如果 Git 能够单独合并你的工作和她的工作,Git 将提交结果。

(注意:合并不是你的唯一选项。你也可以使用git rebase。但是,rebase实际上比合并更复杂,这个答案是已经太久了...)

具体什么时候可以Git合并工作?

要回答这个问题,请回到提交是 快照 的事实。我们有:

       J   <-- master (HEAD)
      /
...--H
      \
       I   <-- origin/master

Git 必须将 H 中的快照(合并基础)与 J 中的快照(您的提交)进行比较。无论 你改变了什么,这就是 Git 必须对提交 H 中的文件做的事情。但是 Git 还必须将 H 中的快照与 I 中的快照进行比较: her 提交。无论 she 改变了什么,这就是 Git 必须对提交 H.

中的文件做的事情

当你在你的提交中进行了全面的更改——比如 "remove a file"——而她做了零碎的更改,比如 "modify the same file",Git 将无法合并这些根本。 Git 可以 合并的唯一更改是在 上(当然,或者在完全独立的文件中)所做的更改。 Git很line-oriented。 Git 将 运行 内部等效于:

git diff --find-renames <hash-of-H> <hash-of-J>    # what you changed
git diff --find-renames <hash-of-H> <hash-of-I>    # what she changed

如果这些差异显示她更改了 bar.all.png 的第 47 行,你最好不要更改第 47 行,否则 Git 不知道如何组合这些。

当然,*.png 个文件没有行。所以 Git 将 永远不会 能够结合这些! Git 是这里的错误工具。如果您有 image-merging 工具,请考虑从三个索引槽中提取所有三个 PNG 文件,然后 运行 在这三个文件上使用 image-merging 工具。

观察提交图

在 Git 中完成任何事情的一个关键是观察提交的 ,这样您就可以看到您的提交与其他人的提交有何不同,并且他们会合的地方。要查看图表,请考虑花哨的图形查看器(可能会画出漂亮的),或使用 git log --decorate --oneline --graph。 D、O 和 G 选项为您提供如下所示的输出:

*   9fadedd637 (HEAD -> master, origin/master, origin/HEAD) Merge branch 'ds/default-pack-use-sparse-to-true'
|\  
| * 2d657ab95f pack-objects: flip the use of GIT_TEST_PACK_SPARSE
| * de3a864114 config: set pack.useSparse=true by default
* | 3bab5d5625 The second batch post 2.26 cycle
[snip]

来自 HEAD 的当前提交位于顶部。这是一个 merge commit 所以它有两个指向以前提交的链接。 Git 将每个提交单独放在一行上——一行,由于 --oneline 选项——图形绘制非常粗糙,但可用。

更多关于git status

git status 命令有两种不同的模式,它将根据您是否有未合并的文件自动选择。

如果您 没有 未合并的文件——如果索引中的所有内容都处于阶段零——git status 的输出将仅列出 阶段文件 未暂存的文件。但是,如果您确实有未合并的文件,Git 将显示未合并的文件,而不是任何未暂存的文件。

当一切都处于零阶段时,git status 运行s,在内部,两个快速 git diffs:

  • 第一个比较 HEAD 与索引。无论这里 不同 是什么,Git 都表示 staged for commit。对于匹配的文件,Git 什么都不说。
  • 第二个将索引与您的 work-tree 进行比较。无论这里 不同 是什么,Git 都表示 not staged for commit。对于匹配的文件,Git 什么都不说。

所以这让您可以查看索引中的内容,有点:您可以看到它比较的方式,而不是其中实际包含的内容。

关于git stash

另一个答案推荐git stash。我一般避免 git stash。它所做的是提交,然后使用 git reset --hard 擦除您在索引和 work-tree 中所做的任何事情(尽管您所做的会保存在它所做的提交中)。它所做的提交不在 anyb运行ch 上,并且很难看到和使用,即使按照 Git 标准也是如此。所以我认为最好只进行普通提交。

请注意,因为 Git 从索引进行提交,所以 git stash 面对合并冲突变得无能为力。 Git 字面上不能提交冲突的合并。它可能应该能够——应该有一种方法来暂停和恢复合并——但它不能