将新代码签入空 Bitbucket 存储库时如何解决 Git 合并冲突

How to resolve Git merge conflict when checking in new code into an empty Bitbucket repository

在我 post 我的问题之前,我想提一下我在另一个 Stack Exchange 网站上问过这个问题,并被告知这个问题需要在 Stack Overflow 中提出。

我最近在 Bitbucket 中创建了一个新的存储库,我打算在其中签入一个我已经工作了一段时间的新项目。当我在 Bitbucket 中创建新项目时,我选择了包含 .gitignore 文件的选项。当我尝试推送我的新项目时,它导致与我无法解决的这个文件发生冲突。目前,我的代码卡在我的本地存储库中。


我试过的

  1. 我试图在 Eclipse 中将 .gitignore 标记为已合并。我收到此错误:无法拉入状态为:MERGING 的存储库。然后我按照 .
  2. 中的建议执行了 git merge --abort
error: Entry '.gitignore' not uptodate. Cannot merge.
fatal: Could not reset index file to revision 'HEAD'.
  1. 出现上述错误后,我尝试了硬重置(从 Eclipse)。然后我再次尝试中止(从上面的第 1 项开始。这导致了以下错误:
fatal: There is no merge to abort (MERGE_HEAD missing).
  1. 然后,我尝试从 Git Bash 拉取结果:
$ git pull
error: Pulling is not possible because you have unmerged files.
hint: Fix them up in the work tree, and then use 'git add/rm <file>'
hint: as appropriate to mark resolution and make a commit.
fatal: Exiting because of an unresolved conflict.
  1. 试图删除 .gitignore by 运行 git rm .gitignore 然后调用 git拉。我收到以下错误:
fatal: refusing to merge unrelated histories

如您所见,我尝试从 Git Bash 和 Eclipse 进行修复,但没有取得任何进展。我不在乎丢失 .gitignore 文件中的信息。我总是可以重新创建文件。我只需要通过任何必要的方式解决这个冲突,这样我就可以将本地仓库中的东西推送到远程仓库。我很茫然。

TL;DR

如果您只想覆盖 Bitbucket 上的 .gitignore,请考虑使用 git push --force 完全丢弃初始 Bitbucket 提交。

如果你想保留那个文件,从那个提交中抓取它:

git show origin/master:.gitignore > ignore.bitbucket

例如,然后根据需要合并文件,然后然后使用force-push丢弃(单个)Bitbucket 提交。

这是问题的根源:

When I created the new project in Bitbucket, I selected the option to include a .gitignore file.

Git 与 files 无关,因此不存储 files——至少不是在你的意义上重新可能在想。 Git 和 Git 因此存储的是 commits.1 Commits——但不是 b运行ch 名称——可以通过祖先关联起来:就像大多数人有两个 parent 一样,大多数提交都有一个 parent。所以一些提交可以是其他一些提交的 great-great-great-grand-child,例如,或者两个提交可能是兄弟姐妹(都具有相同的 parent),或者其他类似的关系。

(这些关系形成了一个有向无环图DAG。特别是提交图允许Git找到两个[=530的共同祖先=] 提交。)

我们 Git 通过它们的哈希 ID 找到的提交本身,每个都包含两件事:

  • 每个提交都包含每个文件的完整快照,采用压缩和 read-only 格式,内容为 de-duplicated(跨所有提交)。这些快照类似于 tar 或 rar 或 winzip 档案;在您实际使用这些文件之前需要将它们提取出来(然后提取的文件 不在 Git 中,尽管显然有一个 copy that is in Git: 它只是存储在这种压缩和 de-duplicated Git-ified 格式。

  • 每个提交还存储一些元数据,或有关提交的信息。元数据包括提交人的姓名和电子邮件地址,例如,以及显示他们何时提交的 date-and-time-stamp。在这个元数据中,Git 添加,为了它自己的 commit-graph 目的,previous 提交列表的原始哈希 ID——通常只有一个,所以 Git 必须一次向后移动一个提交,从最近的提交到 second-latest,再到第三个最新的提交,依此类推。

用户当然想要他们的文件。这些文件在提交中,在它们的 stored-for-all-time 档案中。因此,我们必须选择一些提交并 Git 提取该提交,以便我们可以查看和使用一些文件。

如果您有 Bitbucket(或任何其他网站托管站点)创建一个新的 totally-empty 存储库,它还没有提交。没关系,但是如果当您 克隆 这个空的存储库时,您将收到警告: Git 会说没有任何东西可以检出(这是真的! ).这意味着如果您创建自己的新 totally-empty 存储库,您的新空存储库与您的 Bitbucket 存储库具有相同的 lack-of-commits:两者完全相同,因为两者都没有。

然后您可以在您自己的存储库中、在您的笔记本电脑上或存储它的任何地方进行您自己的第一次提交。作为有史以来的第一次提交,此提交将有 no parent 提交。它不能有一个parent,因为没有更早的提交,所以它没有有一个parent,一切都很好。不过这个提交有点特殊,Git 会告诉你你做了一个——或“那个”——root commit:

$ git commit -m initial
[master (root-commit) c5f8984] initial
 1 file changed, 1 insertion(+)
 create mode 100644 README.txt

但您没有这样做:您让 Bitbucket 创建了一个存储库,然后 创建了自己的根提交 该存储库中,这样 他们的 存储库就不会为空。他们的第一次提交包含一个 .gitignore 文件(也许没有别的,或者可能是一个 README and/or LICENSE 文件;这些是典型的 GitHub 选项)。

如果您继续在自己的 initially-empty 新存储库中进行根提交,这两个根提交不相关。他们不是 parent-and-child。他们不是兄妹,有一个共同点parent。他们完全没有关系。

我在这里使用术语你的Git他们的Git作为shorthand请参考您的 Git 软件与您的存储库一起工作,以及他们的 Git 软件与他们的存储库一起工作。当你 cross-connect 两个 Git 这样的时候,其中一个是发送者,一个是接收者,如果接收者不发送,发送者将他的部分或全部提交发送给接收者还没有。这就是我们 同步 两个存储库的方式,尤其是当一个存储库在某个时候是另一个存储库的克隆时。

克隆复制所有提交(和none b运行ches,有点),所以克隆在一些存储库的生命周期将 start out with the sameommits 是原始的,任何 添加的 提交通常会有某种 ancestry-relationship。但是同样,这不适用于 存储库,因为没有初始提交。

所以,在这种情况下发生的事情是,在你设置了远程之后,你让你的 Git 调用了他们的 Git 并从他们那里得到了他们的根提交,其中包含一个 .gitignore 文件。同时,您有自己的根提交,其中包含一个 .gitignore 文件。

我不清楚 Eclipse 中究竟发生了什么(Eclipse 有它自己的 Java-based Git 实现,它与你使用 command-line Git 来自 bash 或其他 shell)。但是,它使您处于一种奇怪的状态,您可以使用 git rm 命令修复这种状态。此时git pull from bash 运行:

  1. git fetch: 任何 git pull 总是 运行 首先。不过,这 git fetch 没有任何作用,因为您已经获得了 Bitbucket root 提交。
  2. git mergegit pull命令意味着——shorthand对于——运行git fetch,然后运行第二个 Git 命令 并且默认的第二个 Git 命令是 git merge.

对于 git mergework,它需要找到两个 tip 提交之间的最佳共同祖先。为了正确描述这一点,我们需要一些注释。


1更准确地说,一个 Git 存储库由两个数据库组成,一个保存提交,另一个保存内部 Git object,并且一个拿着名字。这两个数据库很简单 key-value stores,objects 数据库以哈希 ID 为键,允许 Git 通过哈希 ID 检索原始 object 内容,名称数据库配对个人 refs—b运行ch 名称、标签名称和其他类似名称—具有单个哈希 ID。


在 Git

中绘制 b运行 棋子

正如我上面提到的,每个提交都是一个 two-part 实体,包含元数据(关于提交的信息)和数据(快照存档,格式为 Git-ified)。每个提交都有一个唯一的 2 哈希 ID。为了绘制这个,以适合人类理解的格式,我们需要删除很多不相关的细节,只保留两件事:

  • 每个提交的名称,以及
  • 来自提交的箭头在有向图中形成弧。

结果通常是这样的:

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

右侧的 H 代表我们的 latest 提交的哈希 ID。提交 H 的元数据包含早期提交的原始哈希 ID,我们称之为 G。提交 G 具有散列 ID 为 still-earlier 提交 F 的元数据,依此类推。通过保存每个 previous-commit 哈希 ID,提交可以说是“指向”较早的提交。

为了快速找到任何特定的提交——在我们的例子中,我们最新的提交 H——Git 使用 b运行ch 名称,如 mastermain,或者像 v1.2 这样的标签名称,或者像 origin/master 这样的其他名称。此名称包含实际提交的原始哈希 ID,因此也可以说它“指向”一个提交:

...--G--H   <-- main

b运行ch 名称在 Git 中的特殊之处在于您可以 查看 切换到 some b运行ch 以便从其最新提交中提取文件。 运行 git switch main 此处告诉 Git 您希望拥有、查看和工作 on/with,文件的内容在提交时永久保存 H. Git 因此 将这些文件提取 到一个工作区——你的工作树——并且,一旦完成, 记住哪个 b运行ch 您要求的名称。为了记住b运行ch名称,Git附上特殊名称HEAD,我画成这样:

...--G--H   <-- main (HEAD)

您可以随时创建新的 b运行ch 名称。每个名称必须恰好指向一个提交。例如,如果您要选择提交 G,以创建新名称 br1,我们将这样绘制:

...--G   <-- br1
      \
       H   <-- main (HEAD)

我们仍在“使用”b运行ch main,并且仍在使用提交 H 中的文件,但现在有一种直接的方法可以找到的哈希 ID较早提交 G,而不是例如 运行ning git log 并手动查找 G 的哈希 ID。但大多数情况下,当我们创建一个新的 b运行ch 名称时,我们会使其指向 current commit:

...--G--H   <-- br1, main (HEAD)

我们现在可以切换到 b运行ch with git switch br1。这会将我们的 HEAD 附加到名称 br1,并从 H 中提取文件——除非我们已经拥有 H 中的所有文件,因此 Git 不会除了在此处改组 HEAD 的附件外,什么都不用做:

...--G--H   <-- br1 (HEAD), main

如果我们现在创建一个新提交,新提交将获得一个新的、唯一的哈希 ID。为简单起见,我们将其称为“commit I”。新提交 I 将指向 t我们在 做出 新提交 I 时使用的提交,即提交 H,一旦 Git 保存了所有文件和元数据对于新提交 I,Git 会将 I 的哈希 ID 写入 name 中,HEAD 附加到该名称中:

          I   <-- br1 (HEAD)
         /
...--G--H   <-- main

如果我们再次提交 JJ 将向后指向 I,并且 Git 会将 J 的哈希 ID 写入br1:

          I--J   <-- br1 (HEAD)
         /
...--G--H   <-- main

如果我们现在切换回 main,Git 将从我们的工作区中删除带有 J 的文件,并放入带有 H 相反,留给我们这个:

          I--J   <-- br1
         /
...--G--H   <-- main (HEAD)

我们现在可以创建并切换到新的 b运行ch br2:

          I--J   <-- br1
         /
...--G--H   <-- br2 (HEAD), main

随着我们进行更多提交,现在 br2 将得到更新:

          I--J   <-- br1
         /
...--G--H   <-- main
         \
          K--L   <-- br2 (HEAD)

简而言之,这就是 Git b运行ches 真正工作的方式:我们 向它们添加提交 并且它们成长,并且 b运行ch name 始终表示 b运行ch.

上的最后一次提交

Git 允许我们随时移动甚至删除任何 b运行ch 名称(尽管您不能删除一个 HEAD 是 attached-to;你必须先离开它)。如果我们将名称 br2 后退一步以提交 K,我们将得到:

          I--J   <-- br1
         /
...--G--H   <-- main
         \
          K   <-- br2 (HEAD)
           \
            L   ???

我们不会查看如何我们这样做,在这里,我们只注意到提交L仍然存在——但现在你不能找到它,除非你记住了它的random-looking哈希ID。3


2唯一的哈希 ID 是使 Git 工作的真正关键。哈希 ID 在 every 存储库中的 every 提交中是唯一的,因此如果两个 Git 相遇并比较哈希 ID,它们具有相同的哈希 ID 当且仅当它们具有 相同的提交 ,它们必须通过获取或推送(或在初始克隆期间,这主要是一次大获取)获得。

3Git 有办法让提交 L 再次回来,但如果你离开它 unreachable像这样持续一个多月左右,Git 可能会决定您不关心它,并真正将其删除。所以提交不会永远 always,但是删除它们很棘手:你 ar运行ge for Git not to see他们——没有能让你和Git找到他们的名字——然后他们最终会消失,除非他们被送到一些其他 Git 决定坚持下去。

因为 Git 对新提交非常贪婪,一旦你 发送 一个提交到其他地方,就很难真正摆脱。如果您将 commit 想象成没有疫苗的病毒,那么您就离得不远了。


正在合并

Git 有很多 git merge 会做的事情 不会 合并,但要理解它们和 Git,最好用 git merge 和 full-blown 合并的那种 git merge 来 start。假设我们有:

          I--J   <-- br1 (HEAD)
         /
...--G--H
         \
          K--L   <-- br2

从前。我们在 br1,使用提交 J——我们的工作树有来自 J 的文件——我们决定我们想要 combine我们在 br1 上的工作与某人所做的工作——也许是我们,也许是其他人; Git 不会真正关心,因为 Git 只关心 提交 。所以我们 运行:

git merge br2

Git 不会找到一个或两个,而是 三个 提交。 Git starts 与我们当前的提交 J,很容易找到:阅读 HEAD,查找 b运行ch name,这是哈希 ID,我们提到的另一个提交也很容易找到:查看名称 br2,查找 b运行 ch 名称,它给出哈希 ID L。所以 JL 是要合并的两个 tip 提交。

但是:为了找到work done,Git有点问题。提交持有 快照 。要查看 更改 ,Git 必须 比较 两个快照。 Git 可以比较哪些快照?有些人认为 Git 会直接比较 JL,但这是行不通的。假设 J 中的某些文件与 L 中的对应文件不同。是我们改变了,还是他们改变了?或者也许我们 both 改变了它。我们回到 当所有提交都持有 快照 .

时更改文件 到底意味着什么

但是假设我们从 J 向后 工作。向后移动一跳,我们发现提交 I 不是 on b运行ch br2,但向后移动 two 步我们找到提交 H br2。同样,从 L 向后移动两跳,我们降落在 Hbr1。所以提交 Hboth b运行ches 上。这使它成为候选人。 Git 使用特定算法在这里找到 best 合并基础提交,4 在这种情况下,总是找到 H.

Git然后运行s 两个差异操作,一个从HJ找出我们改变了,还有一个从 HL 来找出 他们 改变了什么。这两个差异告诉 Git 哪些文件的哪些行 we 发生了变化,哪些文件的哪些行 they 发生了变化。通过将这两组更改加在一起,Git 可能而且经常能够将这些更改结合起来。 Git 然后将合并的更改应用于合并库中的文件。根据您喜欢的查看方式,这可以在添加他们的更改的同时保留我们的更改,或者在添加我们的更改的同时保留他们的更改。

如果 Git 能够单独完成此组合,git merge 将像往常一样进行新提交,并具有一个特殊功能。新的提交将像往常一样有一个快照。它也将像往常一样具有元数据,将我们列为作者和提交者,并将“现在”列为 date-and-time-stamps。合并提交可以有一条日志消息,我们在其中解释 为什么 我们进行了合并,并且合并提交有一个以前提交哈希 ID 的列表。一旦写出新的合并提交,Git 也会像往常一样更新当前的 b运行ch 名称。

唯一这个新的合并提交的特别之处在于它的先前提交列表不只是列出当前提交J。它还列出了我们说要合并的提交,即提交 L。所以更新后的图表如下所示:

          I--J
         /    \
...--G--H      M   <-- br1 (HEAD)
         \    /
          K--L   <-- br2

这是 git merge 的基础知识。 (所有的并发症都伴随着诸如合并冲突之类的事情,以及告诉 Git 不要真正合并的选项,或者强制 Git 进行真正的合并,即使 Git 默认不费心合并。我们不会在这里介绍任何内容。)


4有问题的算法是Lowest Common Ancestor one, modified for directed graphs。一些复杂的图中可能存在多个LCA;这个答案不包括这种情况。


由于子图不相交

,您的案例 Git 胃灼热

在你的情况下,在你自己的存储库中,你有一个或多个提交——我只画一个——和一个指向最后一个的 b运行ch 名称:

A   <-- main (HEAD)

再加上从 Bitbucket Git 获得的一个提交,带有 remote-tracking 名称(origin/mainorigin/master;我在这里使用 main ):

B   <-- origin/main

您现在要求 Git 合并这两个根提交:

A   <-- main (HEAD)

B   <-- origin/main

Git 从 A 向后工作,然后......哎呀,那里什么都没有。 Git 从 B 向后工作,再次,哎呀! 没有最佳共同祖先。

此时git merge的输出是:

fatal: refusing to merge unrelated histories

因为没有共同的祖先。

Git 曾经 只是伪造一个(在 Git 2.9 之前的版本中)。5 如果Git 只是 假装 有一个共同的 pre-root 提交 ε 并使事情看起来像这样:

  A   <-- main (HEAD)
 /
ε
 \
  B   <-- origin/main

然后通用合并算法突然起作用了。但是这个假的epsilon-commitε里面有什么文件呢?答案是:完全 none。这意味着 A 中的每个文件都是全新的,B 中的每个文件也是全新的。您通常会从这种合并中遇到“add/add 冲突”,并且必须手动解决冲突。

要告诉 Git 您想要这样做,请使用 git merge--allow-unrelated-histories 选项。这模拟了 pre-2.9 的行为。一旦您解决了任何冲突(如果需要)并完成合并,您最终会得到如下图表:

A--C   <-- main (HEAD)
  /
 B   <-- origin/main

有两个根提交。提交 AB 保持无关,但是提交 C 有两个 parent,AB,因此您可以 git pushC 提交到一个存储库,该存储库有自己的 main 指向提交 B.

在存储库中进行两次根提交没有什么好处。这不是 错误 ,但它可能会使那些不了解图论的人感到困惑。只要简单,最好避免这种情况,并提供初始设置的描述, 很简单。


5我这里的猜测是 Eclipse 也在这样做,所以你在 Eclipse 中没有得到一个“不相关的历史”错误,而是得到了一些合并冲突(秒)。那时您可能还没有进行自己的根提交。