Git 应该显示没有合并冲突

Git shows no merge conflicts when it should

根据我对合并冲突的理解,当两个人更改了同一个文件,and/or修改了该文件中的同一行时,就会发生合并冲突。所以当我做了

git pull origin master

我预计会发生合并冲突,因为同一行在两个版本中都不同,但看起来 git 决定覆盖我的本地文件。

要提供更多信息, 几天前,我在 Github 上推送了我的版本。然后有人将其拉出,对其进行处理,然后将其推回 github。我对另一个人修改的两个文件感兴趣。

第一个文件是一个配置文件,另一个人在里面修改了密码。因此,当我从 github 拉取时,本地版本的密码与 github 上的密码不同。但是,在我的终端中,它说

Auto-merging <filename>

而且,它覆盖了我的文件,密码是另一个人设置的。

第二个感兴趣的文件是用模板引擎 (PUG) 编写的 HTML 文件。另一个人在该文件中更改了很多东西,比如添加了很多 css 类,删除了一些我用过的 类,添加了指向 css 文件的链接以及所有.但是当我拉它时,终端甚至没有提到它正在自动合并它,只是覆盖了我本地存储库中的整个文件并使用了 Github.

中的那个

对于这两个文件,我的问题是,这是否是使用 git pull 的预期行为,还是我做错了什么?

以下是我使用的命令。

git checkout -b "misc"
git pull origin master

此外,我尝试只使用 fetch 然后手动 merge/commit 它,但是当我使用 fetch 时,没有任何反应。文件根本没有改变。

我以前使用过 git/github,但从未真正在使用分支机构的团队中广泛工作过,pushing/pulling 来自 github。

检查存储库中的 .git/config,如果配置包含以下内容,git 也会选择其他人的更改:

[branch "master"]
    mergeoptions = --strategy-option theirs

如果这是真的,删除 mergeoptions 行。

来自documentation

recursive

...This is the default merge strategy when pulling or merging one branch. The recursive strategy can take the following options:

ours This option forces conflicting hunks to be auto-resolved cleanly by favoring our version. Changes from the other tree that do not conflict with our side are reflected to the merge result. For a binary file, the entire contents are taken from our side.

...

theirs This is the opposite of ours.

Git 运行正常。这是预期的(虽然在您的情况下不是真正“期望的”)结果。

底部有一些关于如何使用 Git 使其对您真正有用的信息。

除了,还有一种可能性更大。你这样做了:

git checkout -b "misc"
git pull origin master

第一行很直白。第二个是 extra-complicated,因为 git pullgit fetch 后面是 git merge,这两个本身都有点复杂。

绘制图表(参见Pretty git branch graphs

每当您在 Git 中使用分支时,您 总是 使用分支,所以这实际上只是“每当您使用 Git"—牢记 提交图 很重要。图,或 DAG(有向无环图),总是在那里,通常潜伏在看不见的地方。要使用 git log 查看它,请使用 --graph,通常使用 --oneline。要使用可视化工具查看它,请使用 gitk 之类的东西或许多烦人的 GUI 之一,它会为您提供类似于 所示的视图(这只是 Whosebug 上的一个 randomly-chosen 问题,关于什么是参见 gitkgit-gui).

图形决定了合并的工作方式,因此在当时非常重要。在其他时候,它大多只是潜伏,不碍事但 ever-present。 Git 中的几乎所有内容都围绕 添加 提交,这会向该图添加条目。1

所以,让我们画一点图,然后观察 git fetchgit merge 的作用。

这是一个只有 master 分支的存储库图,上面有四个提交:

o--o--o--o   <-- master

master 分支“指向”tip-most 提交。在此图中,右侧是较新的提交,即 right-most 提交。

每个提交也向后指向它的提交。也就是说,o--o--o 中的行实际上应该是箭头:o <- o <- o。但是这些箭头都指向后面,这对人类来说很烦人而且几乎没有用,所以最好把它们画成线。问题是这些向后箭头是 如何 Git 找到更早的提交,因为分支名称 指向 tip-most提交!

Git 也有名称 HEAD,这是“当前提交”的符号。 HEAD 通常的工作方式是它实际上包含分支名称,然后分支名称指向 tip 提交。我们可以用一个单独的箭头画出来:

                  HEAD
                   |
                   v
o--o--o--o   <-- master

但这太占地方了,所以我通常用这个:

o--o--o--o   <-- master (HEAD)

Git 会发现 HEAD“附加到”(包含名称)master,然后按照从 master 的向后箭头到提示提交。

Hint: use git log --decorate to show branch names and HEAD. It's particularly good with --oneline --graph: think of this as a friendly dog: Decorate, Oneline, Graph. In Git 2.1 and later, --decorate happens automatically, so you don't have to turn it on yourself most of the time. See also this answer to Pretty git branch graphs.

Note that git log --decorate prints the decoration as HEAD -> master when HEAD points to master. When HEAD points directly to a commit, Git calls this a detached HEAD, and you might see HEAD, master instead. This formatting trick was new in Git 2.4: before that, it just showed HEAD, master for both detached HEAD mode, and non-detached-HEAD mode, for this case. In any case, I call "non-detached" an attached HEAD, and I think master (HEAD) shows this attachment pretty well.)

现在,git checkout -b misc 步骤创建了一个新的分支名称。默认情况下,这个新分支名称指向当前(HEAD)提交,所以现在我们有:

o--o--o--o   <-- master, misc (HEAD)

1事实上,您永远无法更改 提交。看似改变提交的东西,实际上是通过添加一个类似于旧提交的 new 提交来工作的,然后它们会掩盖旧提交并向您展示新提交。这使得它 看起来像 提交已更改,但实际上并没有。您也不能 删除 提交,或者至少不能直接删除:您所能做的就是让它们 unreachable,从分支和标签名称和类似。一旦无法访问提交,Git 的维护“垃圾收集器”最终 将其删除。 git gc现在删除它们可能很困难。 Git 非常努力地让您取回您的提交,即使您希望它们消失。

但是,所有这些仅适用于 提交 ,因此经验法则:“尽早并经常提交”。您实际提交的任何内容,Git 都会尝试让您稍后再次检索,通常最多 30 或 90 天。


git fetch

git fetch的作用可以概括为:

  • 召唤另一个Git;
  • 问它有哪些提交;和
  • 收集这些提交,以及使这些提交合理所需的任何其他内容,并将它们添加到您的存储库中。

这样一来,Git就相当于The Borg。但是,Git 不会说:“我们是博格人。我们会将你的生物和技术独特性添加到我们自己的产品中,”而是说“我是 Git。你的 technologically-distinctive 提交将被添加到我自己的!"

那么,让我们看看当您 git fetch origin 时会发生什么。你有这个:

o--o--o--o   <-- master, misc (HEAD)

他们有这个,他们的master有几个额外的提交(我们现在不关心他们的 HEAD):

o--o--o--o--o--o   <-- master

你的 Git 重命名 他们的主人,在你自己的那端称它为 origin/master ,这样你就可以让他们保持正直。他们的两个新提交是 adde到您的存储库,所有 Borg-like。这些新的提交指向现有的四个提交,带有通常的向后箭头,但现在绘制图形需要更多空间:

o--o--o--o     <-- master, misc (HEAD)
          \
           o--o   <-- origin/master

请注意,none 个 您的 个分支已更改。只有 origin 改变了。您的 Git 添加了它们的技术独特性,2 和 re-points 您的 origin/master 以跟踪“哪里master 我上次查看时在 origin。"


2这就是那些又大又丑的 SHA-1 ID 的用武之地。散列是 Git 如何判断的哪些提交对于哪个存储库是唯一的。关键是相同的commit总是会产生相同的hash ID,所以如果theirGit 有提交 12ab9fc7...,并且 你的 Git 有提交 12ab9fc7...,你的 Git 已经有他们的提交,反之亦然。这一切背后的数学是相当深刻和美丽的。


git merge

git pull的后半部分是运行git merge。它 运行 相当于 git merge origin/master3git merge 命令首先找到 合并基础 ,这就是图形突然变得重要的地方。

两次提交之间的 merge base 笼统地说是“图中所有线都回到一起的点”。通常这两个提交是由两个分支 names 进行的两次 branch-tips, pointed-to。一个典型的、非常明显的案例发生在:

           o--o      <-- branch1 (HEAD)
          /
o--o--o--*
          \
           o--o--o   <-- branch2

git merge 所做的是找到最近的 common-ancestor 提交,我在这里将其绘制为 * 而不是 o。那就是合并基地。这只是两个分支“分叉”的点。

git merge 目标 是找出“你”改变了什么——自提交 [=81] 以来你在 branch1 中做了什么=]——以及“他们”发生了什么变化,即自提交 * 以来 branch2 发生了什么变化。要获得这些更改,Git 运行 两个 git diff 命令。

即使我们像这样绘制提交也是如此:

o--o--o--*--o--o     <-- branch1 (HEAD)
          \
           o--o--o   <-- branch2

这是同一个graph,所以也是同一个merge。 Git 将提交 *branch1 的提示进行比较(“我们的两次提交发生了什么变化?”),并将 *branch2 的提示进行比较( “他们的三个提交有什么变化?”)。然后 Git 尽最大努力 合并 这些更改,并根据结果进行新的 merge 提交。所有这一切的确切细节 combining-and-committing 还不重要,因为我们没有那样的图表。

我们有的是这个:

o--o--o--*        <-- master, misc (HEAD)
          \
           o--o   <-- origin/master

请注意,我在这里保留了 * 概念。那是因为 git merge 仍然找到合并基础。这里的问题是合并基础分支提示:名称misc直接指向提交*.

如果Git 执行git diff <commit-*> <commit-*>,diff 显然是空的。提交 * 与提交 * 相同。那么我们如何合并这些呢?

Git的回答是:我们根本不合并。我们做 Git 所谓的 快进 。请注意,虽然内部提交箭头都指向后方,但如果我们只是想象它们指向前方,那么现在很容易将 misc branch-label 和 向前滑动 , 沿着 dog-leg 向下,然后向右。结果如下所示:

o--o--o--o        <-- master
          \
           o--o   <-- origin/master, misc (HEAD)

所以现在我们的配置文件是 HEAD 提交中的那个,它是 misc 的 tip-most 提交,与 origin/master 相同的提交。

换句话说,我们丢失了对配置文件的我们的更改,因为它们被他们对配置文件的更改覆盖了。


3为什么它实际上不使用git merge origin/master的细节在这里大多无关紧要,但与历史有很大关系。在过去的 Git 版本 1.8.4 之前,一些 git fetch origin 从来没有真正费心去更新 origin/master。这是一个糟糕的设计决定,在所有现代 Git 版本中,git fetch 更新它。


进行“真正的合并”而不是 fast-forward 会有帮助吗?

如果我们回到原来的设置(并删除名称 master,因为它挡住了路):

o--o--o--*        <-- misc (HEAD)
          \
           o--o   <-- origin/master

我们可以,而不是让 git pull 运行 git merge,运行 我们自己的 git merge --no-ff origin/master,合并 origin/master 但 允许Git做一个fast-forward。这会有帮助吗?

唉,没有。请记住,合并的目标合并 自merge-base 以来的所有更改。所以 Git 将 运行 两个差异:

git diff <commit-*> <commit-*>     # this diff is empty
git diff <commit-*> origin/master  # this is "what they changed"

Git 然后将我们的更改 (none) 与他们的更改合并,并进行新的合并提交:

o--o--o--o------o   <-- misc (HEAD)
          \    /
           o--o   <-- origin/master

我们有一个不同的 graph(它有点像汤勺或 Big Dipper),但我们采用了他们的更改,包括密码 cange,同时不保留我们的任何东西(自合并基础以来我们没有任何变化)。

使合并有用

我们需要的是确保“我们的”改变——它们必须我们的改变,在Git的眼里——“看起来不同”他们的”变化。这意味着我们需要 Git 选择一个 不同的 合并基础。

正如我上面所说,合并基础是我们的提交和他们的提交开始分歧的点。这意味着我们需要创建自己的分支,并确保我们不会“快进”太多,甚至根本不会“快进”。

所以,我们可能 do 想避免 git pull.4 我们也可能想选择一个更早的点我们创建自己的分支机构。我们希望我们的图的分支能够保持自己的独特性,就像它们的分支一样。我已经给出了其中的一些提交 letter-names 以便我可以谈论它们:

     A-----B      <-- misc (HEAD)
    /     /
o--o--o--o        <-- master
          \
           o--C   <-- origin/master

在提交 A 中,我们更改配置文件以使用不同的密码。然后我们git merge(不是fast-forward)master的提示来获取新的东西,而不让密码改变。这一步可能是非常手动的,也可能是完全自动的,但是一旦提交,我们就完成了:提交是永久的;它们无法更改。5

现在我们可以让 master 像往常一样“快进”:

     A-----B      <-- misc (HEAD)
    /     /
o--o--o--*--o--C   <-- master, origin/master

现在,当我们 git merge origin/mastergit merge master6 时,合并基础将是我标记为 * 的提交。如果我们没有将密码从 * 更改为 B,而他们将密码从 * 更改为 C,我们将接受他们的更改——但他们不应该更长 需要 来更改它,因为我们从不向他们发送提交 AB;我们将这些留给自己。所以密码从 *C 应该没有变化,我们将在进行新合并时保留更改后的密码:

     A-----B-----D   <-- misc (HEAD)
    /     /     /
o--o--o--o--o--C     <-- master, origin/master

稍后,我们将获取更多提交,将它们合并(快进)到 master,并准备好再次合并:

     A-----B-----D   <-- misc (HEAD)
    /     /     /
o--o--o--o--o--C--o--o   <-- master, origin/master

这一次,合并基础将提交 C——它是在 misc 和它们的分支上最近的一个——并且 Git 将 diff Corigin/master。估计他们还是不会改密码,因为我们还是没有给他们commit D.


4我尽可能避免使用git pull,但是根据你的处理方式,你可能无论如何都可以使用它,特别是对于master.

5我们通过将分支标签移动到新提交来进行任何普通的新提交:记住分支名称只是指向 tip-most 提交。我们只是做了一个新的 tip 提交,它的父级是前一个 tip-most 提交,re-point 标签,向前移动了一步。但是看看当我们为它的父项进行指向 更远 的新提交时会发生什么,而不仅仅是旧的提示提交。现在我们通过隐藏一些以前的提交来“重写历史”。 (尝试绘制此图。)这就是 git commit --amendgit rebase 的工作方式。

6注意这些做同样的事情,作为master的提示和[=68的提示=] 是 相同的提交 。一个区别是默认的提交消息会改变:一个会说“merge master”,另一个会说“merge origin/master”。 (在 Git 的提交消息格式中有一些繁琐的东西将 master 与其他所有内容区别对待,但我们可以忽略它。这只是一个历史产物。)


最后一点:提交中的配置和密码 = 错误

因为提交 如此永久,所以通常 非常 将密码放入其中是个坏主意。任何有权访问您的存储库的人都可以查看历史提交并找到密码。

配置文件通常也不应该提交,尽管这里没有真正的安全问题。相反,这是您遇到的问题 运行:每个人都需要不同的配置。将你的提交到共享存储库是没有意义的。如果它是一个 private 存储库,这更有意义,如果它是一个私有 branch 就没问题(如果在大多数情况下仍然是 sub-optimal例)。

需要某种 示例 配置或默认初始配置是很常见的。 这些确实应该提交。诀窍是确保示例或默认初始配置与“实时”配置分开。例如,对于某些系统,您将包括:

config.default

还有一点代码,比如:

[ -f .config ] || cp config.default .config

将默认配置设置为第一个 运行 上的 .config 文件。然后在 .gitignore 中使用 .config,它永远不会被放入 存储库中,因此它永远不会出现在任何提交和 y你一开始就不会遇到这个问题。