【问题标题】:GitHub cascade down main branch changes without overriding client customisationGitHub 级联主分支更改而不覆盖客户端自定义
【发布时间】:2022-01-08 04:31:32
【问题描述】:

我有一个保存在 Github 中的 Larave 项目。它是名为core 的主分支。

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

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

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

我尝试了Pull,但它覆盖了我为客户所做的事情。

【问题讨论】:

    标签: git github version-control


    【解决方案1】:

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

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

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

    第一步——git fetch——从其他 Git 存储库获取新提交:在这种情况下,您控制或使用的某个存储库中的错误修复和更改。第二步——你可以选择git rebase而不是git merge,但是第二步的目标是一样的——作为它的目标,利用 在 first 步骤中获得的新提交。我们通过组合工作来做到这一点。

    git merge 命令是组合在多个不同的提交字符串中完成的工作的主要方式。 git rebase 命令改为重复挑选现有提交,目的是“改进”每个提交;每个cherry-pick都是一种特殊的merge形式,所以最后你还是要理解merging。因此,此时正确的介绍是合并。如果您还不太熟悉 Git 提交是什么以及为您做了什么,您应该go read up on that now

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

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

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

    虽然每个提交都有每个文件的完整快照,但两个分支显然在提交 H收敛。也就是说,当我们从当前最新的提交J 开始向后工作时,我们逐步提交I,然后是H,然后是G,依此类推。同时,如果我们从他们的最新提交L 开始向后工作,我们会逐步提交K,然后是H,然后是G,依此类推。这意味着提交H共享的——它在两个分支上,连同它的所有祖先——我们可以很容易地选择它作为最佳 em> 这样的共享提交,因为根据定义它是最新的共享提交(所有其他共享提交都必须早于H)。

    Git 的合并操作将使用由提交的存在形成的提交图自动找到提交H——Git 称之为 merge base。我们所要做的就是告诉 Git:

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

    我们通过运行git merge branch2git merge <em>hash-of-L</em> 来做到这一点。任何允许 Git 定位提交 L 的东西在这里就足够了:我们不必使用分支名称。我们通常确实使用分支名称,因为这对我们来说最简单,但 Git 需要的只是一种找到 L 的方法:它会自行解决其余的问题。

    Git 如何执行合并操作

    找到合并基础提交H,Git 现在准备好执行合并操作。这包括:

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

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

    对于许多合并中的其他文件,我们更改了某些内容,或者他们更改了某些内容,但是如果我们更改了某些内容,他们并没有触及该文件,反之亦然.同样,这使得合并该文件变得微不足道:Git 只需要获取更改中的任何一个。但是,我们可以将其视为第三种也是最复杂的情​​况的特例。

    最后,我们遇到了第三种也是最复杂的情​​况:我们和他们都对同一个文件进行了更改。对于这种情况,Git 所做的很简单,一点也不聪明:Git 只是组合更改。如果我们删除了第 3 行,而他们没有对第 3 行做任何事情,Git 将删除第 3 行。如果他们在第 10 行和第 11 行之间添加了一行,而我们没有,Git 将采用他们添加的行。 Git 会为每一个 修改重复这个过程——一行一行,因为 Git 的内部 git diff 是逐行工作的。1 只要我们所做的更改对某些行所做的更改不会触及或重叠他们对其他行所做的更改,Git 能够自己完成这项逐行工作,并且这样做了。

    如果 Git 能够自行解析所有文件,Git 通常也会自行继续进行新的合并提交。合并提交与任何其他提交相同:它有一个唯一的哈希 ID 并包含一个快照——每个文件的副本,以后提取时应具有的形式——以及一些元数据。关于合并提交的唯一特殊是父提交的元数据列表列出了两个父提交哈希ID,而不是一个父提交哈希ID。 sup>2 我们可以在这里画成:

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

    请注意,像往常一样,当前分支——branch1——现在指向新的提交,而新的提交M 又指向你之前的提交 em> 刚才,像往常一样提交JM 的不同之处在于它有一个 secondL,表明此提交加入了两个历史记录:一个是从 J 开始并向后工作的结果,另一个是从从L 开始并向后工作。


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

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


    合并冲突

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

    the red ball
    

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

    the blue ball
    

    但他们将其更改为:

    the red cube
    

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

    无论如何,Git 现在将采用 combined 更改(以及任何冲突)并将 combined 更改应用到 merge base 中的文件时间>。这样,Git 会保留我们的更改并添加他们的更改,或者根据您的观点,保留他们的更改并添加我们的更改。 (结果都是一样的。)如果 Git 没有遇到任何它声明为合并冲突的东西,它会继续安排组合更改文件进入下一次提交。否则,Git 会留下一团糟:

    • Git 的索引(有关索引 AKA 暂存区的更多信息,请参阅 this answer)将包含文件的所有三个版本,来自合并库,HEAD--ours 提交,而另一个或--theirs 提交;
    • 文件的工作树副本将包含 Git 在组合更改方面的最大努力,包括冲突标记。

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

    1. 运行您选择的一些工具来执行“修复工作树文件”步骤,然后
    2. 为您运行 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 定位提交J,使用git merge 的参数定位提交L,并使用提交图定位合并基础提交H。然后,Git 将根据需要将H 中的快照与JL 中的快照进行比较,以查找更改、合并更改并将合并后的更改应用于合并基础H 中的文件。如果合并顺利,Git 将自行生成合并提交M。如果没有,Git 将在合并过程中停止,强制我们完成合并——由于 Git 索引的混乱状态3,我们无法在没有完成或中止合并的情况下继续进行——当我们完成合并时,我们会得到相同的合并提交M,这次是人工干预。


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


    一种特殊情况

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

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

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

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

    结果如下所示:

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

    我再次使用字母M 代表“合并”。但是:提交M 的快照中有什么?我们让 Git diff commit H 与 commit H 看看我们改变了什么,根据定义,如果我们将 H 与它自己进行比较,什么都没有改变。因此,Git 将我们的“无”与他们所做的任何事情(大概是某事)结合起来,并且生成的 文件 必然与提交 L 中的所有文件完全匹配。

    人们可能会想:为什么要打扰?事实上,默认情况下,git merge打扰。它检测到提交H 是提交H,因此没有我们的 可以继续进行。 Git 没有进行合并,而是执行快进操作git merge 相当厚颜无耻地选择称为“快进合并”,即使没有合并任何内容)。那么,Git 不会合并,而是将 branch name branch1 "forward" 拖动,如下所示:

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

    绘图中不再需要扭结,所以我们现在可以像这样绘制这个图形:

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

    快进非合并“合并”现在已经完成,只需让 both 分支名称指向提交 J

    有时无事可做

    假设我们有一个如下图:

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

    也就是说,这类似于快进的情况,只是我们在之后提交J,而不是之前的提交H。如果我们现在运行git merge branch2,Git 会说“已经是最新的”并退出。几乎没有什么可做的:commit H 已经是我们历史的一部分,在 commit J

    这两个特殊情况通过像往常一样找到合并基础来工作:如果合并基础是两个端点提交之一,我们有一个特殊情况。特殊情况是“无事可做”(合并基础另一个提交)或“快进”(合并基础不是另一个提交,而是我们的 em> 提交)。所以 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 有多种处理方式,但在 Git-2.34 之前的默认策略是合并合并基础 AB 以产生临时提交,然后使用临时提交作为合并基础合并LR。在 Git 2.34 中,一种新算法尝试做与合并递归相同的事情,但没有那么疯狂和浪费精力。4 我自己还没有研究过新算法,因此不会尝试在这里解释一下。


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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-07-11
      • 2012-11-21
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-06-24
      • 2023-04-08
      • 1970-01-01
      相关资源
      最近更新 更多