【问题标题】:How to make branch B have the same code as branch A?如何使分支 B 具有与分支 A 相同的代码?
【发布时间】:2021-12-21 21:12:21
【问题描述】:

分支 A 的代码比分支 B 少。 我想将分支 A 合并到 B 中,以便 B 最终得到更少的代码,并且基本上具有与 A 完全相同的代码。类似于撤消多个提交。问题是我必须通过拉取请求合并来做到这一点。我不能直接推送到 B,它必须通过 A(功能分支)。

拉取请求应该是什么样子?当我尝试将 A 合并到 B 时,它没有检测到任何差异 - 为什么会这样? 如果我翻转拉取请求(B 到 A),它会显示 B 具有但 A 没有的所有更改。

【问题讨论】:

  • A 到 B 应该做的事情。也许图形界面只是不显示已删除的内容
  • 没有拉取请求会从目标分支中删除提交。
  • 您是否调查过将 A 拉到 A 然后将 B 重新设置为 A 的头部

标签: git github git-commit


【解决方案1】:

TL;DR

你想要一个新的提交,它的快照来自一个旧的提交。然后,您可以从中进行 PR。使用普通的 Git 工具进行这个新的提交是很棘手的,但是绕过它很容易。不过,我会把它留到很长的部分。

我们需要在这里区分 拉取请求——GitHub 添加的东西,1 在 Git 所做的事情之外——和 Git 自己做的事情。一旦我们这样做了,事情就会变得更清楚一些,尽管因为这是 Git,它们可能仍然很不清楚。

Git 真的是关于提交。 Git 不是关于文件的,尽管提交 contain 文件。 Git 也不是关于分支,尽管我们(和 Git)使用分支名称来查找提交。所以 Git 就是关于 commits 的。这意味着我们需要确切地知道提交是什么以及为我们做了什么:

  • 每个提交都有编号。然而,这些数字又大又丑而且看起来很随机,用hexadecimal 表示,例如,e9e5ba39a78c8f5057262d49e261b42a8660d5b9。我们将这些 hash IDs 称为(或者更正式地说,object IDs 或 OIDs)。没有人知道未来的提交会有什么哈希 ID。但是,一旦进行了提交,that 哈希 ID 指的是 that 提交,而不是其他任何地方的提交。2 这允许两个只需比较提交编号,即可查看不同的 Git 存储库是否具有相同的提交。 (我们不会在这里使用该属性,但它很重要。)

  • 每个提交存储两件事:

    • 提交具有每个文件的完整快照(尽管这些文件是经过压缩的——有时是非常压缩的——并且,通过用于制作提交编号的相同类型的加密技巧,去重)。

    • 一个提交也有一些关于提交本身的元数据:信息,比如谁做了它,什么时候做的。在此提交数据中,每个提交都存储了 previous 个提交哈希 ID 的列表,通常只有一个元素长。单个先前提交的哈希 ID 是此提交的 父级

这个 my-parent-is-Frank,Frank's-is-Barb 的东西将提交粘合到它们的祖先链中。当我们使用普通的git merge 时,Git 使用祖先链来确定要合并的内容。不过,我们不想要 正常 在这里合并。同时,同样的父级内容是 Git 如何将提交(快照)转变为“更改”:如果我的父级提交 feedcab(不能是frank,其中有太多非十六进制字母)我提交ee1f00d,Git 比较这两个提交中的快照。什么都一样,没变。不同的文件确实发生了变化,Git 通过玩一种Spot the Difference 游戏来找出它们中发生了什么变化并产生了一个秘诀:对@此文件的 987654328@ 版本,您将获得 ee1f00d 版本。

现在,实际上没有人使用原始提交编号来查找提交。你最近一次提交的提交号是多少?你知道吗?你在乎吗?可能不会:你只需使用mainmasterdevelop 或一些名称 即可找到它。

这就是它的工作原理。假设我们有一个很小的存储库,其中只有三个提交。让我们称它们为ABC(而不是使用它们真正的哈希ID,它们又大又丑,而且我们也不认识它们)。这三个提交如下所示:

A <-B <-C   <--main

提交C 是我们最新的。它有一个快照(所有文件的完整副本)和元数据。它的元数据列出了早期提交 B 的原始哈希 ID:我们说 C 指向 B。同时,提交B 有一个快照和一些元数据,B 的元数据指向AA 有一个快照和元数据,由于A第一个 提交,它的元数据根本没有列出父级。它是一个孤儿,有点(所有的提交都是处女出生,有点——好吧,让我们不要再沿着这条路走下去了)。所以这就是动作停止的地方,这就是我们知道只有三个提交的方式。

但是我们find commit C by name: name main 指向 C(持有原始哈希ID C),就像 C 指向 B

要进行新的提交,我们检查main,因此C 是我们的当前 提交。我们更改内容、添加新文件、删除旧文件等等,然后使用git addgit commit 制作新快照。新快照会获得一个看起来随机的新哈希 ID,但我们将其命名为 DD 指向C

A <-B <-C   <--main
         \
          D

现在git commit 使用了它的巧妙技巧:它将D 的哈希ID 写入名称 main

A--B--C--D   <-- main

现在main 指向D 而不是C,现在有四个提交。

因为人们使用名称而不是数字来查找提交,我们可以通过放弃对较新提交的访问权来返回一些旧提交。我们强制一个名称,如main,指向一些较旧的提交,如CB,并忘记D 的存在。这就是git reset 的意义所在。不过,这可能不是您想要的,尤其是因为 Git 和 GitHub 喜欢添加新的提交,而不是将它们删除。尤其是拉取请求不会让您取消提交。

不,您想要的是创建一个 提交,其 快照 匹配某个旧提交。


1如果您不使用 GitHub,也许您正在使用其他一些也添加了 Pull Requests 的网站。这有点棘手,因为添加它们的每个站点都以自己的方式进行。例如,GitLab 有类似的东西,但称它们为 Merge 请求(我认为这是一个更好的名称)。

2这取决于一些 最终会失败的加密技巧。散列 ID 的大小(又大又丑)会在我们需要时将故障推开,尽管现在它有点太小了,它们很快就会变得更大更丑。


正常合并

在日常 Git 使用中,我们创建分支名称,并使用这些分支名称来添加提交。我已经展示了一个非常简单的例子。让我们稍微复杂一点。和以前一样,我们将从一个小型存储库开始:

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

我在此处添加了HEAD 符号以表明这是我们已签出的分支的名称。现在让我们添加另一个分支名称br2,它现在也选择了提交H

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

由于我们通过名称 br1 使用提交 H,因此我们现在所做的任何 提交仅更新名称 br1。让我们做两个新的提交:

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

现在让我们再次检查提交H,使用git switch br2

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

再做两次提交:

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

我们现在可以运行git checkout br1,然后运行git merge br2,或者现在就运行git merge br1。让我们做前者:我们最终得到的 snapshot 无论哪种方式都是一样的,但是其他的事情会发生一些变化,所以我们必须选择一个。

无论哪种方式,Git 现在都必须执行 真正的合并(不是快进的假合并,而是真正的合并)。为了执行合并,Git 需要弄清楚 webr1 上发生了什么变化,以及 他们(好吧,我们,但暂时没有)在 @987654389 上发生了什么变化@。这意味着 Git 必须弄清楚我们俩从哪里开始——如果我们只看图纸,就很清楚:我们都是从提交 H 开始的。我们进行了“我们的”更改并提交了(多次)并获得了J 中的快照。

HJ区别

git diff --find-renames <hash-of-H> <hash-of-J>

告诉 Git 我们br1 上发生了什么变化。

类似的区别:

git diff --find-renames <hash-of-H> <hash-of-L>

告诉 Git 他们br2 上发生了什么变化。 (请注意,Git 在此处使用 commits:分支名称 br1br2,只是用于 find 提交。Git 然后使用历史记录——如记录在每次提交的父节点中——以找到最佳共享起始点提交H。)

为了执行合并本身,Git 现在合并这两个差异列表。在我们更改了一些文件而他们没有更改的地方,Git 使用我们的更改。在他们更改了文件而我们没有更改的地方,Git 使用了他们的更改。我们都更改了 same 文件,Git 必须合并这些更改。

如果我们都进行了完全相同的更改,那很好。如果我们接触了不同的行,那也没关系——尽管这里有一个极端情况:如果我们的更改相邻,Git 会声明 合并冲突; 但如果它们完全重叠,则同样的变化,没关系)。如果一切顺利,那么在合并更改时不会发生合并冲突,Git 可以将合并后的更改应用到来自H 的快照。这会保留我们的更改并添加他们的更改,或者等效地保留他们的更改并添加我们的更改。在我们的更改完全重叠的地方,Git 只保留一份更改副本。

生成的快照——H 加上两组更改——进入我们新的合并提交。不过,这个新的合并提交有一个特别之处。而不是只有 一个普通父级,在这种情况下——在分支br1——将是J,它有两个父级:

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

与往常一样,Git 更新 当前分支名称 以指向新的 合并提交 M。合并现已完成。

git merge -s ours

让我们你想要的。你从这个开始:

          o--o--...--R   <-- br-A
         /
...--o--*
         \
          o--o--...--L   <-- br-B (HEAD)

您想要git merge br-A,但保留快照来自L 提交br-B 的提示。

要完成您想要在原始 Git 中,您将运行:

git switch br-B
git merge -s ours br-A

Git 现在会找到合并基础 *(或者真的不打扰),然后......完全忽略 他们的 更改,并在当前分支:

          o--o--...--R   <-- br-A
         /            \
...--o--*              \
         \              \
          o--o--...--L---M   <-- br-B (HEAD)

其中合并提交MLR 作为其两个父级,但使用提交L 作为快照

这很简单,在原始 Git 中。但 GitHub 不会这样做!我们如何让 GitHub 提供这种结果?

我们不得不欺骗一下 GitHub

为了论证起见,假设我们要去git switch br-A——即检查提交R——然后进行一个新的提交,其快照来自提交L?也就是说,我们制作:

          o--...--R--L'  <-- br-A (HEAD)
         /
...--o--*
         \
          o--o--...--L   <-- br-B

提交L' 与提交L 有不同的hash ID,并且有不同的元数据——我们刚刚创建了我们的姓名、电子邮件和日期和时间等等,它的父级是R——但与提交L具有相同的快照

如果我们让 Git 在此处进行正常合并,Git 会:

git diff --find-renames <hash-of-*> <hash-of-L>
git diff --find-renames <hash-of-*> <hash-of-L'>

获取 Git 需要组合的两个差异。 这些差异将显示完全相同的变化。

正常的合并将合并这些更改,方法是获取所有更改的一个副本。所以这正是我们想要的!最终的合并结果是:

          o--...--R--L'  <-- br-A
         /            \
...--o--*              M   <-- br-B (HEAD)
         \            /
          o--o--...--L

我用另一种风格(中间有M)画了这个,没有什么特别的原因。 M 中的快照将匹配提交 LL',分支 br-B 将在新提交处结束,对任何 文件 没有更改 ,但最后有一个新的提交。

我们可以轻松地在 Git 中提交 L',然后通过在我们的 br-A 分支上通过 L' 发送提交,在 GitHub 上提出拉取请求。 PR 将顺利合并,通过“更改”br-B 中的任何内容,只需添加新的合并提交M。所以——除了额外的L' 提交——我们得到了与git merge -s ours 在分支br-B 上运行相同的效果

很难做到这一点

将快照L' 添加到分支br-A 的困难方法是:

git switch br-A
git rm -r .                         # from the top level
git restore -SW --source br-B -- .
git commit -C br-B

例如。第一步将我们置于br-A 并签出提交R。第二个——git rm -r .——从 Git 的 index / staging-area 中删除所有文件,并从我们的工作树中删除相应的文件。 git restore 将所有文件放回,但从--source br-B 或提交L 获取它们,最后一步git commit -C br-B 使用来自提交L 的消息进行新提交。 (使用-C,您可以对其进行编辑。)

这很好用,只是有点慢。为了更快,我们可以使用两种技巧中的任何一种。这是第一个,这可能是我实际使用的:

git switch br-A
git read-tree -u --reset br-B
git commit -C br-B

这消除了有利于git read-tree 的删除和恢复,这可以一举完成。 (您可以使用-m 代替--reset,但需要两个标志之一,而git read-tree 是一个我不太喜欢使用的棘手命令,所以我不记得要使用哪个:幸运的是,这里没关系。)

或者,我们可以这样做:

git switch br-B      # so that we are not on br-A
git branch -f br-A $(git log --no-walk --format=%B br-B | git commit-tree -F - -p br-A br-B^{tree})

如果我没有打错字。但是,这使您没有机会编辑提交消息。你不需要直接检查br-B,你只需要确保你不是on br-A,或者你在提交后使用git merge --ff-only继续前进。

如果 GitHub 能做一个 git merge -s ours 就好了

但它不能,所以就是这样。

【讨论】:

    【解决方案2】:

    测试变基 一个特性分支(包括清理过的代码) B 你的开发者

    第一次保存你的开发

    git 结帐 B 混帐添加 git commit -am "blabla my dev"

    然后更新A

    git checkout A git拉A

    然后将 B 重新设置在 A 之上

    git 结帐 B git rebase A

    此时您可能需要处理一些冲突

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-10-11
      • 2021-08-19
      • 2020-08-09
      • 2016-03-07
      • 1970-01-01
      • 2016-11-20
      • 1970-01-01
      • 2020-07-30
      相关资源
      最近更新 更多