合并主分支将所有更改添加到我的分支。

4
我正在处理一个分支,在这个过程中我修改了5个文件。但是其他人已将100多个文件的更改推送到了主干上。在处理我的分支时,我想经常把主干合并到我的本地分支。我会像这样操作:
git checkout master git pull git checkout my-branch git merge master git push
但现在,由于某种原因,其他人在主干上更改的所有文件都被加入到了我的更改中。所以如果我在合并主干后实际上执行push,则会显示我更改了100多个文件而不仅仅是5个。我做错了什么?谢谢。

1
如果你不想获取其他人对主分支的更改,为什么要运行 git merge 呢? - D Malan
1
这些不是你的更改,它们是别人的更改。当被询问时,Git会弄清楚这一点。 - torek
但是似乎并不是这样,因为当我推送它时,它说我改变了100多个文件,而不仅仅是我实际改变的5个文件。 - suuuriam
1
这是因为你只询问了“在那时和现在之间,有多少个文件发生了变化”,而没有询问:“对于文件X,谁更改了第Y行”。 - torek
1
我正在撰写一些你需要知道的东西... - torek
显示剩余2条评论
2个回答

7

这里实际上并没有问题:你只是误解了Git的说法。(当然,Git可能会被误解,但在实践中,无论是Git还是其他版本控制系统,这些东西都很,需要学习和经验。)

关于Git、文件和提交(commit)有一些关键的知识点:

  • 在与Git交互的层面上,Git存储的是提交。像master这样的分支名称很有用,但它们实际上只是帮助Git(和你)找到提交。稍后我们将看到它的工作原理。

  • 提交确实存储文件,但通常您会一次处理整个提交。您告诉Git:获取给定标识符X的提交,然后您就会得到该提交的所有文件。您要么拥有该提交——因此拥有了所有文件——要么根本没有该提交,因此也没有任何文件。

  • 每个提交都有一个唯一的ID。这个ID是它的哈希ID,它是一个由随机字母和数字组成的一长串丑陋的字符串,例如9fadedd637b312089337d73c3ed8447e9f0aa775。一旦存在了这个哈希ID,它就代表了那个提交,而永远不会是任何其他提交。

  • 任何一个提交的内容都是完全、完整、100%只读的。提交中存储的文件以及任何提交的元数据都不能被更改。(原因是哈希ID是提交内容的加密校验和。如果您将提交取出来,修改其中的任何位,然后再放回去,您就得到了一个新的、不同的提交,有着新的、不同的哈希ID。旧提交仍然存在:您只是添加了一个更多的提交。)

  • 每个提交的所有文件的快照仅仅是一个快照。也就是说,提交不会存储更改

  • 但是当您查看提交时,Git经常会向您显示更改。这是一个技巧!但这也是一件好事,因为通常更有趣。

  • Git之所以可以将提交显示为更改,是因为大多数提交都存储了单个先前或提交的原始哈希ID。因此,对于给定的任何一个提交X,Git可以反向一步找到在X之前的提交。那个提交也有一个快照。

Git可以提取父快照和子孙快照,然后进行比较。对于每个文件,“相同”的文件,Git不会提示任何内容,而对于“不同”的文件,Git会向您展示一个文本:“从父快照中开始复制这个文件,添加此行,删除那一行,根据需要重复此过程,完成添加和删除后,您将得到该文件的子孙版本。”

如果您有一系列简单的提交记录,依次排列,您可以将它们绘制出来,或者像这样思考:

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

这里,H 代表一个用于找到提交的哈希 ID。提交 H 本身包含其父提交的哈希 ID,我们将其称为 G。这使得 Git 能够找到 G。G 包含其父提交的哈希 ID,即 F,这使得 Git 能够找到 F,以此类推。

master 这样的 分支名称 只是持有链中最后一个提交的哈希 ID。最后一个提交向后指向其父提交,其父提交再向后指,以此类推。因此我们可以将其表示为:

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

我们实际上不需要将一个提交到下一个提交的连接箭头绘制为“箭头”,因为它们不会改变。任何提交的任何部分永远都不会改变。因此,它们始终指向后面。然而,从分支名称出来的箭头则发生变化。我们可能会开始使用:

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

然后添加一个新的分支名称,以便我们可以在不触及master的情况下进行新的提交:

...--G--H   <-- master, dev

但是最终我们将在分支中添加一个新的提交。让我们将特殊名称HEAD添加到dev上,以便记住这是我们正在使用的名称 - 当我们运行git checkout dev时使用的名称 - 并像这样绘制它:

...--G--H   <-- master, dev (HEAD)

现在我们将创建一个新的提交。它会得到一个大而且难看的随机哈希ID,但我们只称之为 "I",并像这样绘制出来:
          I
         /
...--G--H   <-- master, dev (HEAD)

I指回H,因为我们在创建I时,H是当前的提交。

现在来看一个巧妙的技巧:Git将I的哈希ID写入分支名称中。被更改的分支名称是HEAD所附加的分支名称:dev。因此,现在dev指向I而不是H

          I   <-- dev (HEAD)
         /
...--G--H   <-- master

没有现有的提交发生变化(毕竟没有)。但是我们的新提交I现在存在,并指向现有的提交H,现在我们的名称dev指向提交I,这是当前提交。

当我们创建新的提交J时,Git执行相同的操作,给我们:

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

现在我们可能会运行 git checkout mastergit pull(或 git fetch && git merge)来获取其他人创建的一些新提交。为了对称起见,我将画出其他人创建的两个提交。这也使得我们的 master 分支上移了超过他们的两个新提交:

          I--J   <-- dev
         /
...--G--H
         \
          K--L   <-- master (HEAD)
<代码>当前分支现在是<代码>master,而<代码>当前提交是<代码>L。你可能会想知道为什么我把它们分别画在一行上:这主要是为了强调提交直到<代码>H都在<代码>两个分支上。 这个奇怪的事实——提交可以同时存在于多个分支上——对Git来说有些特别。
我们现在可以运行<代码>git checkout dev来准备将<代码>master合并到<代码>dev中。这第一步只是将<代码>HEAD移到<代码>dev上:
          I--J   <-- dev (HEAD)
         /
...--G--H
         \
          K--L   <-- master

我们现在可以合并这两个分支。我们实际上是在合并“提交”,因为Git的关键就是该提交,但让我们看看这是如何运作的。
在我们的提交 I-J 中,我们对某些文件进行了更改,在他们的提交 K-L 中,他们(谁都无所谓)对某些文件进行了更改。我们将要创建一个新的“合并提交”,此合并提交将保持一个快照,就像每个提交一样。那么这个快照应该包含什么信息呢?
答案是:我们希望这个快照将我们的工作与他们的工作结合起来。也就是说,我们需要从一个“共享的常规提交”开始。最佳共享起点从图中很清楚,它是提交 H。该提交在两个分支上。G也是,但H更好,因为它是靠近J和L的东西。
因此,Git会从H开始。它会比较H与J,以查看我们做了什么更改。我们每个更改的文件都有一份配方:添加某些行,删除某些行。然后,Git再次从H开始,比较H与L,以查看他们做了什么更改。他们更改的每个文件都有一份配方:添加某些行,删除某些行。
Git现在将合并这些更改配方。我们改变了文件而他们没有,结果就是我们的文件。他们改变了我们没有改变的文件,结果就是他们的文件。如果我们都改变了一个特定文件,Git将合并我们的更改。这是合并的难点:合并更改。
如果我们更改了一行,而他们更改了不同的一行(而且配方中也没有相邻或接壤的行),Git将可以自行合并这些更改。如果我们和他们对某些行进行了完全相同的更改,例如,我们都在某处修复了相同的拼写错误,Git将只需要使用其中一个更改副本。否则,如果我们以不同方式更改了一行,Git将为该文件产生“合并冲突”的错误,并留下一堆混乱让我们清理。
一旦将所有文件合并到最佳状态,不再有冲突,那么Git要么停止合并并报告合并冲突,要么继续制作“合并提交”。我们假设没有冲突,这样会使事情变得简单。
这个合并提交唯一特殊的地方就是它不同于普通的提交只有一个父节点,而是有两个。我们可以画成这样:
          I--J
         /    \
...--G--H      M   <-- dev (HEAD)
         \    /
          K--L   <-- master

新提交 M 的第一个父提交是提交 J,像往常一样将分支 dev 推进了一步。新提交 M 的第二个父提交是提交 L,仍然是分支 master 的尖端提交。名称 master 不会发生任何变化,也没有任何现有提交发生改变(因为没有提交可以修改),但新的合并提交 M 使得提交 KL 以及通过提交 J 到达的提交都在分支 dev 上。

合并是如何工作的

如果我们现在问 Git:某个特定文件 F 的某一行(比如第42行)来自哪里,Git可以查看提交 M 中的快照,然后查看提交 JL 中的两个快照。如果文件 F 中的第42行在提交 MJ 中匹配,但在提交 ML 中不同,那么第42行“来自”提交 J:合并保留了来自提交 J 的行。Git现在将向后回溯一个提交,到提交 I,查看文件 F 中的第42行是否在提交 IJ 中匹配。如果它们在此处不同,则Git将说第42行来自制作提交 I 的人,在他们制作提交 I 的日期。
但是,如果文件 F 中的第42行在提交 ML 中匹配,并且在提交 J 中不同,那么这意味着合并保留了来自提交 L 的第42行。因此,Git应该向后回溯到提交 L,然后是 K,以此类推。
如果第42行在提交 MLJ 中都匹配,那么它可能没有改变过,来自于提交 H,Git将继续向后移动,一次一个提交地进行追溯,以查看它是否在从 GH 过渡中发生了变化,或者是否来自更早期的更改。
查看特定文件的特定行的命令是 git blame(或 git annotate)。请注意,就像许多 Git 命令一样,它必须逐步通过提交,在时间上向后移动一步。这些提交,一次一个,是存储库中的历史记录。历史记录是提交;提交就是历史记录。

不要取出别人的更改(除非它们是错误的)

任何合并的结果都是自动正确的文件。未来的合并会假设你所提交的代码是正确的。如果你去掉了他们的更改,这意味着你认为他们的代码有问题,应该被遗忘。

如果事实确实如此,请移除这些代码 - 但最好在另一个单独的提交中执行此操作,而不是直接在合并中进行。

关于快速合并的附注

尽管我们没有在这里完全涵盖它,但Chuck Lu的回答提到了快速合并。 假设我们绘制了一系列提交,如下所示:

...--C--D--E   <-- branch1 (HEAD)
            \
             F--G--H   <-- branch2

说明我们当前已经检出了分支branch1,因此也检出了提交E。如果我们运行git merge branch2,Git会发现两个分支上最佳的共同提交是 当前 提交E。在这种情况下,Git不必进行真正的合并。如果有选择的话,Git将执行一个快进操作,这实际上是通过执行git checkout提交H,同时将分支名称branch1向前拖曳:

...--C--D--E
            \
             F--G--H   <-- branch1 (HEAD), branch2

在绘制图形时,现在没有理由保留对角线;在您自己绘制时请随意删除它。

当Git执行此操作时,还会比较旧提交E中的快照与新当前提交H。 对于每个更改的文件,它告诉您有关该更改的一些信息。

您可以通过运行以下命令来查看相同的比较:

git diff --stat <hash-of-E> HEAD

现在HEAD指向提交H,这个git diff比较的是E快照和H快照之间的区别,与git pull做的事情完全相同,因此再次打印相同的信息。

当您进行实际的合并(如我们使用M合并时),您看到的信息是基于您先前提交的比较(J)和M中的提交。由于M 合并了来自分支“两侧”的更改,但J具有您的更改,所以您看到的是他们的更改。不过,您可以运行git diff --stat master dev以比较提交L和提交M:这时,您将看到合并从分支“您的一侧”带来的内容。

通常很难看出真正合并M中有什么,因为它有两个父提交。实际上需要两个单独的git diff命令才能正确查看。如果给git show命令加上-m标志,它可以自动执行此操作,但我们在这里不涉及它。


非常感谢您花时间如此详细地解释 - 我真的很感激!我现在确实明白为什么会发生这种情况。但是,如果我想避免提交这些文件更改,那么(技术上)由其他人进行的更改可能只能将我的分支合并到主分支而不是将主分支合并到我的分支中? - suuuriam
通常情况下,将你的分支合并到master更为正确。不同的团队在这里会有不同的工作流程。请注意,因为Git将一个共同的合并基础提交与两个提示提交进行比较,所以无论哪种方式合并的结果几乎相同:区别在于哪个父级是第一个父级,以及哪个分支名称通过合并操作得到推进。 - torek
1
Git的git log有一个非常有用的标志:--first-parent。执行git log --first-parent master告诉Git仅查看每个合并提交的第一个父提交。如果您维护“将开发合并到主分支,不要将主分支合并到开发分支”的工作流程,则此类git log会向您显示摘要概述,而不是来自功能的所有单个提交。 - torek
1
如果您允许将主分支合并到开发分支,然后再将开发分支合并到主分支,那么 --first-parent 技巧就无法正常工作。(您可以选择 变基 而不是合并,但这会变得更加复杂。一些 Git 用户非常反对变基。我与一个赞成在我们认为适当的情况下进行变基的团队一起工作,但是...好吧,在这里我们可以说出什么是“正确”的 :-) )。 - torek

1

有两种git合并方式,fast-forwardno-fast-forward

看起来你遇到了no-fast-forward类型,这将生成一个新的合并提交。

如果您不想生成合并提交,可以尝试使用git rebase。

git checkout master  
git pull  
git rebase master my-branch (might encounter conflicts here)  
git push  

您可以在这里找到有关 rebase 的演示动画demo about rebase here


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接