首页 > 解决方案 > 与我在分支中更改的文件无关的 Git 冲突

问题描述

我正在尝试重新设置分支,但它会引发与我的分支无关的冲突。

所以我创建了分支app/feature-1并进行了更改然后我分支并做了app/feature-2。在这里,我更改了 1 个文件。

我最终对它进行了更多更改app/feature-1并掌握了它。

然后我尝试重新设置基准app/feature-2,但现在我遇到的冲突与我更改的 1 个文件完全无关。

我如何变基但只处理与我的更改相关的冲突?Featute 2 分支应该只接受 master 中的内容并理想地查看 1 文件。

标签: git

解决方案


TL;博士

你几乎肯定想要git rebase --onto。有关详细信息,请参阅下面的最后一节(如何使用git rebase --onto)。(我不能直接链接,抱歉。)

我正在尝试重新设置分支,但它会引发与我的分支无关的冲突。

那是因为分支不存在。1 不要考虑分支。在提交方面思考和工作。记住提交是什么,为你做了什么,它git rebase通过复制一些现有的提交来工作,一次一个,好像 by git cherry-pick, 2到新的和改进的(嗯,至少是新的:人们可以希望改进的)提交. 在您回答matt 的问题之前,我只需要对提交进行一些猜测。


1好吧,这是夸大其词了——<em>这里肯定存在一些东西——但这确实为什么你会看到你没有做的事情的冲突:你认为你的分支是由你的提交组成的,但事实并非如此。这里的根本问题是分支这个词。它没有单一的固定含义。因此,当松散地使用时,它可以具有您当时想要的任何含义,但是如果您不确定该含义是什么,当您第二次使用它并且它具有不同的含义时,您可能会被绊倒这个。仿佛这个词根本没有任何意义。

说真的,再看看你自己的句子。您正在尝试重新设置分支。那究竟是什么?它有与我的分支无关的冲突。那究竟是什么?

2git rebase字面上运行的某些形式git cherry-pick,而其他形式则表现得好像部分(并且在Git中逐渐变得不那么常见)。


Git 是关于提交的

我将很快重新审视这个声明:

我最终对它进行了更多更改app/feature-1并掌握了它。

这里还有另一个问题,这就是掌握部分。但是让我们先谈谈提交。

提交是我们在 Git 中的关键存储单元。由于 Git 的构建方式,我们实际上可以引用不同的存储单元,但提交是有趣的。文件存储提交中,就像它一样,分支名称app/feature-1可以帮助我们 - 和 Git -<em>查找提交,但重要的是提交本身。因此,我们需要一种绝对、积极地识别任何个人提交的方法。然后我们可以指向那个提交,讨论它,看看它是什么,做了什么。

谈论提交的方式不止一种,但只有一种方式是绝对积极的,那就是使用提交的哈希 ID。哈希 ID 实际上是一个唯一编号,特定于那个提交。任何地方的其他提交都不会使用相同的数字。3 因此,以十六进制表示的数字是——至少在以完整的难看长度拼写时——是提交的真实名称,就像它一样。

这些数字,例如faefdd61ec7c7f6f3c8c9907891465ac9a2a1475,又大又丑,人类不可能正确(除了通过剪切和粘贴),所以我们大多不使用它们。但归根结底,它们是Git查找提交的方式。我们可以在需要时使用它们,通过剪切和粘贴,或者从提交中获取前几个字符并输入:只要没有其他以相同的四个或更多字符开头的内部对象,哈希 ID 的前缀与完整的东西一样好。

这些哈希 ID 是永久的——嗯,就像 Git 主数据库中的底层对象一样永久——并且不可变。地球上的任何电源都无法更改哈希 ID,因为尽管它们看起来是随机,但它们实际上完全由内部对象的内容决定。所以一个提交的内容不能被改变——一点也不能改变——因此提交的哈希 ID 始终是那个ID,在每个Git 存储库中无处不在。

所以这是找到提交的绝对可靠方法:将其原始编号提供给 Git。您的 Git 将查看您的主存储库数据库,并查看它是否有一个提交对象,其编号是给定的哈希 ID。如果是这样,那是正确的提交。这也是Git 相互交换提交和其他 Git 内部对象的方式:它们只是提供 ID。如果两个 Git 都有 ID,则它是同一个对象并且它们都有它;如果没有,缺少 ID 的 Git 需要从拥有它的 Git 中获取对象,现在它们都具有相同 ID 的相同对象。


3从技术上讲,某个地方的其他 Git 存储库可以使用相同的编号进行不同的提交,但前提是这两个 Git 存储库永远不会相遇。这些不同的commits-with-same-number就像某种doppelgänger,就像神话一样,遇到它的dopppelgänger的提交可能会遭遇不幸。但在实践中,这实际上并没有发生。


提交里面有什么

这解释了一种方法——确定的方法——找到一个提交,而不是提交是什么以及为你做什么。我们不会在这里深入讨论所有细节,但每个提交都有两个部分:

  • 一部分是你所有文件的快照,就像 Git 在你提交时知道它们的方式一样。这些不是更改,而是完整的快照。4 为防止 Git 数据库变得异常庞大,这些文件以一种特殊的、只读的、仅限 Git 的、压缩和去重的格式存储。因此,如果您提交一个包含 5000 个文件的提交,更改一个文件,然后进行另一个提交,则可以保证这两个提交中的第二个不必添加超过一个文件,因为第一个有其中的所有 5000 个原始文件(也可能与更早的提交共享)。

  • 提交的另一部分包含元数据:有关提交的人员、时间和原因等信息。如果您进行了提交,这些都是您的user.nameuser.email以及您作为日志消息输入的任何内容。对于 Git 自己的操作至关重要,Git 向此元数据添加了一组父提交哈希 ID5 这个集合中通常只有一个哈希 ID,这就是我们将在此处说明的内容。提交的(单一)父级是它之前的提交。


4在许多方面,是否将它们存储为更改并不重要但 Git 作者为他们仅使用快照的方式感到自豪,并让它到处查看,所以您不妨了解一下。奇怪的是,一旦文件存在一段时间,内部对象就会被存储为打包对象,其中一些是delta-compressed,这意味着它们被存储为更改。但这在对象范围之下消失了:您可以与 Git 交互以查看对象的点将它们视为完整的文件,而不是增量链。你甚至无法分辨一个物体是松散物体还是包装物体,更不用说是否正在进行增量压缩。并非所有打包对象都经过增量压缩;只有当它有用时才会发生这种情况。

5从技术上讲,这是一个有序列表,其中不应有重复项。顺序主要取决于列表中的第一个条目,但我们不会在这里担心这一点。


这意味着提交形成了向后看的链

假设我们有一个简单的线性提交链,如下所示:

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

这里代表链中最后一次提交H的实际大而丑陋的哈希 ID 。我们将不得不记住这个哈希 ID,但稍后我们会看到我们是如何作弊的,并且实际上不必记住它,对于这篇文章,我们将称之为“提交”。H

在 commit 内部H,有两部分:源快照和一些元数据。在 的元数据中H,Git 会找到较早提交的哈希 ID G。同样,G它代表一些实际的大而丑陋的哈希 ID,但请注意,我们不必记住它:我们可以让 Git 从 commit 中找出它H

我们之前说过我们可以指向一个提交,现在我们知道如何指向它了:使用哈希 ID。我们保存在H某个地方,这就是我们让 Git 找到H. 但H保存G的哈希 ID,所以H自动指向G。这就是我们刚刚在上面画的。

同时, CommitG具有快照和元数据,因此指向较早的 commit F。提交F反过来又指向一些我们没有费心绘制的更早的提交。

这贯穿整个历史——存储库中的提交就是历史——直到我们回到某人所做的第一个提交。它的特别之处在于它根本不指向更早的提交。这就是 Git 知道它可以停止倒退的方式。

所以这就是提交的意义和作用:它存储一个快照,以及git log例如需要向我们显示元数据并将快照与之前的提交快照进行比较以查看发生了什么变化所需的机制。一旦为 commit 完成此操作,就可以后退一步 commit并重复它。这种情况一直持续到我们厌倦了查看输出,或者达到了第一次提交。git logHgit logGgit loggit log

分支名称查找最后一次提交

为了完成所有这些工作,我们必须在某个地方保存分支中最后一次提交的哈希 ID: commit H。我们可以把它写下来,在一张纸或白板上或其他东西上。但是我们为什么要为此烦恼呢?我们有一台电脑。让计算机将它保存在某个地方:例如,可能在一个文件中,或者可能在一个大数据库中,我们将所有分支名称放入其中,并让每个分支存储一个哈希 ID。

这就是 Git 所做的。它将分支名称和哈希 ID 对存储在文件和/或某种数据库中。6 事实上,Git 将此概括为将所有名称存储在此数据库中:分支名称、标签名称以及 Git 使用的所有其他名称。每个存储一个哈希 ID。7 这就是 Git 所需要的一切,所以这就是它所做的一切。但这件事给了我们很多:

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

在这里,我们现在有了commit的名称H。我们可以使用分支名称main来查找 commit H。此外,我们可以向链中添加新的提交,当我们这样做时,Git 会自动更新分支名称。我们马上就会看到这一点。


6这些文件在使用时一个数据库。他们只是不是一个很好的人。Git 在内部有一个可插拔后端的想法,并且正在进行工作以放入适当的数据库。这将解决目前 Git 存储这些名称的方式的一系列问题,尽管它会引入一些新问题:数据库总是很困难的。

7名称可以存储另一个名称而不是原始哈希 ID。这是一个符号引用,目前它只适用于和其他一些特殊的读取情况,但它是一种通用机制。HEAD


使用多个名称

假设我们有这样的设置:

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

我们添加一个新的分支名称,例如br1. 结果将如下所示(好吧,除非我们告诉 Git 更多信息并让它看起来不同):

...--G--H   <-- br1, main

请注意两个名称H现在如何选择提交。这即将改变,因为我们要添加一个新的提交,我们称之为 commit I。(这部分是为了方便,部分是因为在提交之前我们无法知道真正的哈希 ID。8)但是现在我们需要告诉 Git我们使用哪个名称来选择提交H,以便它知道要使用哪个名称更新。因此,我们将特殊名称附加HEAD到一个分支名称:

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

git checkout我们用or得到这个状态git switch,通过它我们告诉 Git:使用分支名称 ______(填空),Git 会找出哪个提交并将其提取并准备好供我们使用,依此类推。如果我们已经在使用该提交,就像这里的情况一样,没有其他事情发生,所以没有其他事情发生:我们只是附加HEAD到新名称。

现在我们修改一些文件并像往常一样使用git addand git commit——这当然涵盖了很多细节!——但最终结果是一个新的提交I,其父级是现有的提交H。Git 写出新的提交,然后将哈希 ID 写入当前分支,这HEAD是附加的名称,给我们:

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

现在main,如果我们在and之间来回切换br1,Git 实际上必须做一些实际的工作,以换出提交HI.

请注意,通过提交H现在在两个分支上,而提交I仅在分支上br1。“在”某个分支上的提交是 Git 在以分支名称开头以查找最后一个提交时会找到的提交,然后向后工作。如果我们告诉 Git 以 name 开头,commit是最后一次提交br1这一事实并不重要:我们从 commit 开始,使用 that 返回 commit ,使用 that 返回 commit ,等等。HmainIHG


8真正的哈希 ID 将取决于我们提交的确切日期和时间。每个输入字节都会对哈希 ID 产生巨大影响,因此,如果我们进行两次相同的提交,但时间戳相差一秒,它们将得到两个完全不同的哈希 ID。


合并分支

假设此时我们只添加一个这样的提交:

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

然后返回main并告诉 Git:将这两个分支上的工作结合起来

现在,Git 有很多方法可以做到这一点。但是您根本没有使用原始 Git 来执行此操作。您使用了 GitHub(并在此过程中涉及多个存储库)。当你这样做时,GitHub 使用几种方法之一来组合工作。它使用哪一个取决于您使用的 GitHub clicky 按钮。

最直接的方法是让 Git 使用 Git 所谓的快进合并。这实际上根本不是合并。但是,您实际上无法让 GitHub 来做这个,所以根据定义,您一定没有做过那个。(另外,如果你有,你就不会遇到这个问题。)

下一个最直接的方法是让 Git 进行真正的合并。有时需要真正的合并,但这种特殊情况并不需要真正的合并。不过,如果您使用主要的绿色大MERGE按钮,GitHub 会执行此操作。结果如下所示:

          I--J   <-- br1
         /    \
...--G--H------M   <-- main

(我们可以在HEAD此处省略附件,因为 GitHub 上的存储库不是您可以在其中进行工作的存储库,因此毫无疑问要使用哪个分支名称。您实际上无法在该存储库中运行git checkout或运行git switch:您必须复制提交到某个其他 Git 存储库。)

有趣的是,现在,在合并之后,提交IJ两个分支上。(提交HG更早的提交仍然在两个分支上。)合并提交M只有一个特殊之处:它有两个父级,而不是通常的一个父级,因此H可以通过从 向后一步找到M提交,但提交J可以M可以通过从合并的另一“腿”向后一步找到。CommitM有一个快照,该快照结合了从Honmain开始的所有工作——尽管没有这样的工作——以及从Hon开始完成的所有工作br1. 在这种情况下,这意味着 inM中的快照与 中的快照匹配J

如果您使用此按钮,您将不会看到您所看到的问题。所以你一定没有使用过这个按钮。这留下了其他两种在 GitHub 上合并的方法,即将绿色的大MERGE按钮更改为REBASE AND MERGESQUASH AND MERGE。虽然这两个操作并不完全相同,但它们都有一个共同的关键行为:它们会留下旧的提交

REBASE AND MERGE按钮或多或少像这样工作:

          I--J   <-- br1
         /
...--G--H------I'-J'  <-- main

也就是说,它获取尚未在合并目标分支上的提交(在这种情况下,提交IJ分支名称main)并复制它们。这与所做的事情相同git rebase,这就是 GitHub 以他们的方式标记按钮的原因。但是,在制作了副本之后,GitHub 现在将目标分支名称向前移动,以便main命名最后一个复制的提交。

SQUASH AND MERGE按钮或多或少像这样工作:

          I--J   <-- br1
         /
...--G--H------IJ   <-- main

在这里,底层 Git 存储库获得了一个新的提交,我称之为IJ;一个提交的快照与您从 normal 获得的快照相同git merge,但不是表示提交I并且J现在“在”分支上的main合并提交,而是没有合并提交。上只有一个新的提交main

这些都是结合工作的正确方法

虽然所有三种方法都不同——MERGE按钮进行真正的合并,而其他两个按钮没有——它们都是合并工作的有效方式。但它们有不同的后遗症。如果我们使用该MERGE按钮,您的 rebase 操作将出于一个特定原因而工作。如果我们使用这个REBASE AND MERGE按钮,你的 rebase通常会因为不同的原因正常工作。9 但是如果我们使用SQUASH AND MERGE按钮,你的 rebase 通常不会起作用。

只要您知道自己在做什么就可以了,因为有一种方法可以使您的 rebase起作用。但是你需要了解你在做什么。因为SQUASH AND MERGE操作模式会以这种方式影响未来的变基,所以需要考虑避免——但你仍然应该知道所有不同的合并方法会发生什么,包括在你自己的 Git 存储库中进行真正的快进合并,然后使用git push更新 GitHub 存储库。

不过,将所有这些放在一起,我敢打赌你SQUASH AND MERGE在这里使用过。


9 GitHub 可能会REBASE AND MERGE在它不起作用的情况下停用该选项,但我还没有实际测试过。无论如何,我不会在这里讨论所有的变基细节,因为这会使这个答案变得更长。


如何使用git rebase --onto来处理这些

假设现在你有一个 GitHub 存储库——甚至可能是一个 fork——你有一些与我们已经绘制的内容类似的东西,但是除了or之外你还有两个分支,就像这样,在你自己的本地克隆中:mainmaster

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

也许你写了提交I-J,或者你从别人那里得到它们——甚至可能来自 GitHub 存储库。

您现在让 Git 将您的提交发送到您的 GitHub 存储库,并创建 name br2,以便在 GitHub 上,它们具有以下内容:

               K--L   <-- br2
              /
          I--J   <-- br1
         /
...--G--H   <-- main

请注意,此时这如何反映您自己的本地存储库中的内容。他们可能有也可能没有名字 br1——这个名字并不重要,因为分支名称大多是无关紧要的——但他们肯定会在I-J这一点上有提交:这些是连接 commitK和 commit所必需的H

但是现在,此时,控制此 GitHub 存储库的任何人都使用该SQUASH AND MERGE按钮在 GitHub 存储库中生成

               K--L   <-- br2
              /
          I--J   <-- br1
         /
...--G--H---IJ   <-- main

他们甚至可能删除名称 (如果他们有的br1话),这样他们就有了:

               K--L   <-- br2
              /
          I--J
         /
...--G--H---IJ   <-- main

请注意,所有提交仍然存在。Git 是关于提交,而不是分支;分支名称仅允许我们找到提交。所以名字 br2现在找到了L,找到了K,找到了J,找到了I,找到了H。是否有J直接查找的名称无关紧要:J从 开始查找就足够了L

假设您现在将更新的提交带入您自己的存储库。您运行git checkout main(或git switch main)然后运行git fetch,也许通过运行git pullwhich runs git fetch

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

main然后你以快进的方式移动你的名字,也许是通过运行git pullwhich (已经运行git fetch)运行git merge

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

如果你现在运行git checkout br2

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

然后运行git rebase main,您的 Git 将枚举要复制的提交集:L, and K, and J, and I, 但不是HorG或任何更早的内容,因为它们已经 main. 接下来,您的 Git 将尝试复制 commit I,就像 by 一样git cherry-pick,以便从Hto的更改I可以应用于 commit IJ

I保证复制尝试会失败,但由于 commit 有很多机会失败IJ 如果它确实失败了,你会遇到一堆冲突。如果你自己没有写提交I,这可能会令人费解!但这是因为你告诉你的 Git 复制 commit I,然后复制IJ. 你已经要求你的 Git 生成:

               K--L   <-- br2
              /
          I--J   <-- br1
         /
...--G--H---IJ   <-- main, origin/main
              \
               I'  <-- HEAD [detached]

(变基操作使用临时“分离的 HEAD”运行,直到变基完成)。如果副本I确实有效,或者您手动使其工作并使用git rebase --continue以使 Git 继续,Git 现在也会尝试复制提交J,以便产生:

               K--L   <-- br2
              /
          I--J   <-- br1
         /
...--G--H---IJ   <-- main, origin/main
              \
               I'-J'  <-- HEAD [detached]

这也至少在某种程度上可能会失败。

可以直接 rebase跳过这两个提交,前提是它们确实失败了。不过,尝试复制它们可能是个坏主意。

为了避免复制这些已经存在的通过壁球提交,您只需告诉git rebase 不要复制这两个提交。一般来说,告诉 Git 这样做的正确方法git rebase --onto是使用命令的形式:

git checkout br2
git rebase --onto main br1

这里br1使用了你的名字 br1——正如我们在图中看到的,它标识了提交J——告诉 Git 哪个是它不应该复制的最后一个提交。然后 Git 不会复制那个提交,也不会复制它之前的提交——提交——并且不会复制之前的提交,也不复制之前的提交,等等。IHG

当你尝试时,这与 Git 自己做的事情是一样的:

git rebase main

除了当您在没有单独--onto部分的情况下执行此操作时,Git 决定复制的提交集作为最后一个此类提交,即名称main标识的提交,即 commit IJ。现在,提交永远不会出现IJ要复制的提交列表中,因为它不在 br1但没关系:IJ向后指向H,因此 Git 也不会尝试复制提交H。重要的--onto确保 Git 不会尝试复制提交I和.J

所以,最后,我们只想告诉 Git复制以当前提交结束的提交 ( L),但不包括从我给出的名称或哈希 ID ( br1= commit J) 向后退的提交,目标是在main. 为了使目标和排除部分是两个不同的东西,我们需要--onto. 10git rebase

当然,这一切都是基于有人SQUASH AND MERGE在 GitHub 上使用的想法。您没有说有人这样做,但听起来确实如此。


10从技术上讲,您也可以使用git rebase -i并简单地删除正确的pick命令集。但是这种--onto方法更好,因为它更容易正确。请注意,在有意义的情况下,您也可以组合--ontoand 。-i


推荐阅读