GitHub 在不覆盖客户端自定义的情况下级联主分支更改

GitHub cascade down main branch changes without overriding client customisation

我有一个保存在 Github 中的 Larave 项目。这是名为 core.

的主要分支

我有一个客户需要对这个项目进行一些修改。所以我克隆了这个 repo 并为客户设置了它。然后我为他做了改变。更改包括 - 一些额外的数据库字段、不同的发票格式和一些逻辑更改。

我们经常对 core 存储库进行错误修复和更改。如何在不覆盖我们为客户所做的定制的情况下将这些更改引入客户的项目?

我研究了分支和合并。但这不会被合并。因为我为客户所做的改变不是暂时的。它是永久性的。我们将同时拥有项目的多个版本 运行。但是当我们在核心中进行更新时。我们希望它向下级联。我应该使用什么结构?

我试过 Pull 但它覆盖了我为客户所做的。

这不是——或者至少不应该是——关于 Git 的问题,而是关于如何构建您自己的系统以支持可配置客户端的问题。但是既然你问的是 Git,让我们看看 Git 对 git merge 做了什么。 (此外,关于如何构建软件的问题通常“太大”并且对于 Whosebug 来说没有重点;考虑姐妹站点之一,例如 SoftwareEngineering。)

记住 git pull 字面上的 意思是:

  1. 运行git fetch,然后
  2. 运行 第二个 Git 命令,默认为 git merge.

第一步——git fetch——从其他一些 Git 存储库获取新的提交:在这种情况下,错误修复和您控制或使用的某些存储库中的更改。第二步——你可以选择 git rebase 而不是 git merge,但是第二步的 goal 无论哪种方式都是一样的——作为其目标, 利用第一步步骤中获得的新提交。我们通过 合并工作 .

来做到这一点

git merge 命令是合并在多个不同 strings-of-commits 中完成的工作的主要方式。 git rebase 命令会重复 cherry-picking 现有提交,目的是“改进”每个提交; eachcherry-pick是merge的一种特殊形式,所以最后还是要理解merge。因此此时正确的介绍是合并。如果您还不太熟悉 Git 提交是什么以及对您有何作用,您应该 .

现在,鉴于我们有一些形成图表的提交集,我们可以很容易地进入这样的情况:

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

也就是说,我们在“分支”branch1 上,使用提交 J。其他人已经向我们提供了提交 K-L,我们现在使用名称 branch2(或者可能是 origin/branch2,但为了简单起见,我将其绘制为 branch2)。

虽然每个提交都有每个文件的完整快照,但显然这两个分支 会合 在提交 H 处。也就是说,随着我们从 currently-latest 提交 J 开始的时间倒退,我们逐步提交 I,然后是 H,然后是 G,依此类推.同时,如果我们从他们最近的提交 L 开始向后工作,我们将逐步提交 K,然后是 H,然后是 G,依此类推。这意味着提交 Hshared——它在 both branches,连同它的所有祖先——我们可以很容易地选择它作为最好的这样的共享提交,因为根据定义它是最新的共享提交(所有其他共享提交必然早于H)。

Git 的合并操作将找到提交 H——Git 调用 合并基础 ——自动使用提交图这是由提交的存在形成的。我们所要做的就是告诉 Git:

  • 看看我们当前的提交 H;
  • 查看提交 L;
  • 找到合并基地,开始合并过程。

我们通过 运行ning git merge branch2git 合并 <em>hash-of-L</em>.任何允许 Git 定位提交 L 的东西在这里就足够了:我们不必使用分支名称。我们通常 使用分支名称,因为这对 us 来说是最简单的,但是 Git 需要的只是找到 L:它自己解决了剩下的问题。

如何Git执行合并操作

找到合并基础提交 H,Git 现在可以进行合并操作了。这包括:

  • H 中的每个文件与 J 中的每个文件进行比较,以查看我们在每个文件中更改了什么(如果有的话);
  • H 中的每个文件与 L 中的每个文件进行比较,以查看它们在每个文件中的更改(如果有的话);
  • 并且在 Git 进行时,弄清楚我们是否重命名、添加或删除了任何整个文件,对它们也是如此。通常所有三个提交都具有相同的文件集,因此这种额外的皱纹不会引起任何胃灼热,对于这个特定的答案,我们将忽略这种可能性。

对于许多合并中的许多文件,没有人更改任何东西。这使得合并文件变得微不足道:三个提交中的三个版本中的任何一个都可以,因为所有三个版本都是相同的。 (Git 的自动文件 de-duplication 在这里非常有用:Git 立即知道文件是否在两次或三次提交中重复。)

对于许多合并中的其他文件,要么 我们 改变了一些东西,要么 他们 改变了一些东西,但是如果我们改变了一些东西他们没有'不要触摸那个文件,反之亦然。同样,这使得合并该文件变得微不足道:Git 只需要采用 changed 中的任何一个。但是,我们可以将其视为第三种情况的特例,也是最复杂的情​​况。

最后,我们有第三种也是最复杂的情​​况:我们和他们都对 同一个 文件进行了更改。 Git 在这种情况下所做的事情很简单,一点也不 聪明 :Git 只是 组合更改 。如果我们删除了第 3 行并且他们没有对第 3 行做任何事情,Git 将删除第 3 行。如果他们在第 10 行和第 11 行之间添加了一行而我们没有,Git 将接受他们的添加的行。 Git 对 每个 修改逐行重复此过程,因为 Git 的内部 git diff 在 line-by-line 基础上工作。 1 只要我们对某些行所做的更改不触及或重叠他们对其他行所做的更改,Git 就可以做到这一点line-by-line 自己工作,并且确实如此。

如果Git能够自行解决所有文件,Git通常会继续进行新的合并commit 自己也是如此。合并提交与任何其他提交相同:它有一个唯一的哈希 ID 并包含一个快照——每个文件的副本,以及一些元数据。关于合并提交的唯一 特殊 是父提交的元数据列表 而不是 one 父提交哈希 ID two parents.2 我们可以把它画在这里:

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

注意,和往常一样,当前分支branch1—现在指向新提交,新提交M指向回您刚才 的提交,像往常一样提交 JM 的不同之处在于它有一个 second parent L,表明这个提交连接了两个历史:一个从 J 开始的结果并向后工作,以及从 L 开始并向后工作的结果。


1请注意,这意味着 Git 完全无法合并对 binary 文件的更改。如果您在二进制文件中遇到冲突,Git 将拒绝在这里帮助您。

2从技术上讲,这是 两个或更多 ,“更多”产生 Git 所谓的 章鱼合并。 Octopus 合并不会做普通合并不能做的任何事情。 (事实上​​ ,除了连接多个分支之外,它们比普通合并做的,这是他们的最终价值主张:如果你看到章鱼合并 Git历史,你知道,尽管有很多输入,合并本身很简单——或者as simple as it could be based on the number of inputs。)


合并冲突

有时我们和他们对相同行进行不同更改。例如,一行可能是:

the red ball

在合并基础提交中的某个文件中 H。我们将其更改为:

the blue ball

但他们将其更改为:

the red cube

Git 不知道如何将这两个更改结合起来。如果结果应该是“蓝色立方体”, 将不得不自己做出改变。 Git 如果我们更改“接触”的两行,也会声明冲突,即使在某些情况下这可能不是必需的。这是基于多年的合并算法经验:这 似乎 产生了大多数人认为最令人愉悦的结果,或者至少,历史上曾经这样做过。

无论如何,Git 现在将采用 combined 更改——加上任何冲突——并应用 combined 更改从 合并库 到文件。这样,Git 保留我们的更改并添加他们的更改,或者,根据您的观点,保留他们的更改并添加我们的更改。 (两种方式的结果都是一样的。)如果 Git 没有遇到它声明为合并冲突的任何内容,它将继续安排 combined-changes 文件进入下一次提交。否则,Git 留下一团糟:

  • Git 的索引(参见 this answer 了解更多关于索引 AKA 临时区域的信息)将包含文件的 所有三个 版本,来自合并基础,HEAD--ours 提交,以及另一个或 --theirs 提交;
  • 文件的工作树副本将包含 Git 在合并更改方面所做的最大努力,包括冲突标记。

您的工作是提出正确的组合文件——您可以以任何您喜欢的方式来完成此操作——然后调整 Git 的索引以保存文件的正确副本。 Git根本不需要工作树副本,但是git add告诉Git:使文件的索引版本与工作树版本匹配,副作用是从索引中删除阻止提交的额外版本。因此,大多数人发现修复工作树副本然后 运行 git add 或使用 git mergetool 最简单,这是一个命令:

  1. 运行您可以选择一些工具来“修复工作 tre文件”步骤,然后
  2. 运行s git add 给你。

请注意,这实际上就是 git mergetool 所做的全部,因此 git mergetool 并没有增加很多价值。但是,提取所有三个输入文件(合并基础、--ours--theirs)的过程有点乏味,并且在 git mergetool 运行 之前选择合并工具(vimdiff、kdiff3、Beyond Compare 或您可能喜欢的任何其他内容),它会自动完成这部分工作。

一旦我们解决了所有冲突,我们就会告诉 Git 完成合并,方法是 运行 宁 git merge --continuegit commit。 Git 然后继续像往常一样进行合并提交 M

真正合并的结论

无论如何,我们现在对 git merge 对我们开始的情况做了什么有一个完整的概述:

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

Git 将使用 HEAD 定位提交 Jgit merge 的参数定位提交 L,并使用提交图定位提交合并基本提交 H。 Git 然后根据需要将 H 中的快照与 JL 中的快照进行比较,以查找更改、合并更改并将合并的更改应用到来自合并基地 H。如果合并顺利进行,Git 将自行生成合并提交 M。如果不是,Git 将在合并中间停止,迫使我们完成合并——由于 Git 的 messed-up 状态,我们无法在不完成或中止合并的情况下继续进行的索引3——当我们完成合并时,我们得到相同的合并提交M,这次是人工干预。


3这是一个真正的问题。没有合适的方法将部分合并交付给其他人,这使得协作合并变得困难。幸运的是,对于大多数较小的合并,一个人就可以完成这项工作。


特例

作为一个特例,考虑如果我们处于以下情况会发生什么:

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

假设我们现在 运行 git merge branch2。如果 Git 遵循通常的合并规则,它将:

  • 定位提交 HJ 作为我们的和他们的;
  • 找到公共合并基础,再次提交H
  • diff H vs H 看看我们改变了什么;
  • diff H vs J 看看他们改变了什么;
  • 结合这些变化;和
  • 进行新的合并提交。

结果如下所示:

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

我在这里再次使用字母 M 来表示“合并”。但是:提交 M 中的快照中有什么?我们有 Git diff commit H 与 commit H 来查看我们更改了什么,根据定义,如果我们将 H 与它自己进行比较,nothing改了。因此 Git 将我们的“无”与他们所做的任何事情结合起来——大概是一些东西——并且生成的 文件 必然 完全匹配提交 [=42 中的所有文件=].

人们可能会git立即想知道:为什么要这么麻烦? 事实上,默认情况下,git merge 不会麻烦。它检测到提交 H 是提交 H,因此没有任何 ours 可以继续执行。 Git 没有合并,而是执行了 fast-forward 操作 git merge 厚颜无耻地选择调用“fast-forward 合并”即使没有合并任何东西)。然后,Git 不合并,只是将 分支名称 branch1 “向前”拖动,如下所示:

...--G--H
         \
          I--J   <-- branch1 (HEAD), branch2

画中的扭结已经不需要了,所以我们现在可以画这个图了:

...--G--H--I--J   <-- branch1 (HEAD), branch2

fast-forward non-merge “合并”现已完成,只需让 both 分支名称指向提交 J

有时候无事可做

假设我们有一个如下所示的图表:

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

也就是说,这类似于 fast-forward 案例,只是我们在 之后的 提交 J,而不是更早的提交 H。如果我们现在 运行 git merge branch2,Git 会说“已经是最新的”并退出。几乎没有什么可做的:提交 H 已经是我们在提交 J.

时的历史的一部分

这两个特例通过像往常一样找到合并基础来工作:如果合并基础是两个 end-point 提交之一,我们有一个特例。特殊情况是“无事可做”(合并基础 另一个提交)或“fast-forward”(合并基础不是另一个提交,而是 我们的 提交)。所以Git总能找到合并基。

最后一个特例

还有最后一个特例,运气好的话你永远不会遇到。假设我们有这样一张图:

...--o--A---M1--o--L   <-- branch1 (HEAD)
         \ /
          X
         / \
...--o--B---M2--o--R   <-- branch2

其中 o 表示不感兴趣的提交(或任意数量的提交),并且两个 M 提交是两个合并,其输入提交是 AB(加上一些合并基础,此处未显示,Git 自动找到)。

如果我们 运行 git merge branch2 现在合并 LR 中的工作,提交 AB 都是“平等的”好”作为合并基础候选人提交。两个提交都在两个分支上,并且没有一个“离末尾更远”(如果我们使用通常的 lowest common ancestor 算法,两个提交哈希 ID 都将以未确定的顺序出现)。

Git有多种方法来处理这个问题,但是默认策略pre-Git-2.34是合并合并基AB产生一个临时的提交,然后使用临时提交作为合并基础来合并 LR。在 Git 2.34 中,一种新算法尝试做与 merge-recursive 相同的事情,但没有那么疯狂和浪费精力。4 我还没有研究我自己的新算法,因此不会尝试在这里解释它。


4“正常”,non-recursive 合并主要发生在 Git 的索引中,工作树文件偶尔用于临时存储。 2.34 之前的 merge-recursive 代码使用相同的 merge-recursive code 执行每个内部合并,字面上 从结果中创建一个提交 — 或者至少是一个树对象,添加如果需要,到包含冲突标记的索引文件。这为“外部”合并提供了一个适当的合并基础,但意味着合并冲突向前传播,这意味着像 GitHub 这样的站点不能使用此代码。新的 merge-ort 代码在内存和 Git 的索引中完成了整个合并,没有使用临时文件,而且——据我所知——也直接处理递归,启用了几个新功能并具有作为目标,能够将此代码用于 GitHub.

等托管网站