Git 从分支中减去推送的提交

Git subtract pushed commits from a branch

我在同一分支上为一个模块共同开发了一些代码,同时我也在另一个分支上与同事的模块进行了集成。基本上我最终同时修改了两个模块的代码,因为我让这两个模块相互通信。现在我想将所有代码一起提交,但不希望将分支中的内容混为一谈。

所以基本上我在 A 上进行了开发,从 B 中挑选了我想要的更改,并且已经进行了测试集成。现在我想分离提交,让分支 A 只包含 "Apple",分支 B 只包含 "Banana",分支 C 包含集成代码。我不想重命名分支,所以我已经通过 cherry-pick 将所有更改从分支 A 复制到 C。另外,我已经挑选了从分支 A 到 B 的相关更改。

现在剩下要做的就是从分支 A 中减去提交,这样就只剩下 "Apple" 了。我正在寻找最好的方法来做到这一点。我不相信 git revert 是正确的选择,我想我可以做一个交互式 rebase 并删除我不想要的 A 提交,但我正在寻找验证这是正确的方向,或者如果需要其他东西。

分支名称并不重要,除了对人类而言。 (你是人类吗?如果是的话,我们可能会遇到一些障碍。)

对 Git 重要的是 提交 。提交 存储库中的历史记录。每个提交都有一个唯一的哈希 ID,这些哈希 ID 是 Git 查找提交的方式(我们稍后会看到一个重要的例外)。散列 ID 又大又丑,看起来 随机,人类基本上不可能使用,这就是我们使用分支名称的原因。

每个提交都包含所有文件的完整快照(好吧,所有出现在该提交中的文件)。而且,每个提交都包含一些元数据——关于提交的信息——例如提交者和时间,以及对 Git 本身非常重要的直接父提交的原始哈希 ID。所以这让 Git 从 last 提交开始并向后工作。

也就是说,假设哈希为 H 的提交是某个链中的 last 提交,标记为 branch-A 之类的分支名称:

... <-F <-G <-H   <--branch-A

name branch-A 保存链中最后一次提交的哈希 ID,即提交 H。提交 H 本身持有早期提交 G 的哈希 ID,后者持有早期提交 F 的哈希 ID,依此类推。

诀窍是 name 包含 last 提交的哈希 ID。没有其他简单的方法可以找到最后一个!最后一个找到倒数第二个,找到倒数第三个,依此类推,但不只是人类需要这个名字:Git 也需要它,找到 最后提交。

So basically I did my development on A, cherry-picked the changes I wanted from B, and already did the tested integration.

当您使用 git cherry-pick 时,您是在告诉 Git 复制 (效果和一些元数据)提交。所以你和任何在 B 上工作的人都是从一些共同的起点开始的,比如提交 H:

          I--J   <-- branch-A
         /
...--G--H   <-- common-starting-point
         \
          K   <-- branch-B

您提到您使用 git cherry-pick 从其他分支 复制 一些提交。将 K 复制到一个新的提交,我将其称为 K' 以表明它与 K 的相似程度,给出:

          I--J--K'  <-- branch-A
         /
...--G--H   <-- common-starting-point
         \
          K   <-- branch-B

请注意 name branch-A 现在如何指向新的 last 提交,现在是 K' .提交 K' 具有所有内容的完整快照,但是将 K'J 进行比较,您会看到与比较 更改 相同=33=] 到 HK' 的作者和日志消息也将与 H 的作者和日志消息匹配,除非您告诉 Git 更改它们。当然,K'的父哈希是J,而K的父哈希是H

您也可以添加更多提交,就像您可能所做的那样:

          I--J--K'--L   <-- branch-A
         /
...--G--H   <-- common-starting-point
         \
          K   <-- branch-B

Now all that's left to do is subtract commits from branch A so that only "Apple" is left. I'm looking for the best way to do this.

不一定有最好的方法。但是 Git 非常适合 add 提交到一个分支,更不用说 remove 从一个分支提交。如果你确实想删除一个提交,你可以告诉 Git 强制 name 向后移动。不再是 last 的提交现在无法找到。例如,如果我们强制名称 branch-A 向后移动以指向提交 I,我们将得到:

            J--K'--L   [abandoned]
           /
          I   <-- branch-A
         /
...--G--H   <-- common-starting-point
         \
          K   <-- branch-B

使用 Git 的常规提交查看工具,例如 git log,我们不会 看到 JK'L,所以看起来它们已经消失了。1 查看者从 last 提交开始,由分支名称找到,并向后工作。

无论如何,这里的大问题是 Git 是为 add 提交构建的。你可以让你自己的 Git 向后移动你自己的分支名称,例如使用 git resetgit branch -f,但这不会使任何 other Git,您已向其发送提交,将 名称向后移动。


1如果我们让他们在这个 unreachable 状态停留足够长的时间,提交 JK'最终会被垃圾收集git gc 命令,它 Git 运行 时不时地自己执行,一般来说,一旦它们已经这样运行了至少 30 天,就会执行此操作——所以你至少得到 30天,再加上 Git 到 运行 git gc 需要多长时间才能改变你的想法并让他们回来。


一个简单的解决方案

处理此问题的一个简单方法是使用新名称。由于 Git 不关心名称,我们可以只调用它 neo-A 而不是 branch-A。我们将 neo-A 设置为指向现有提交 H,您最初从哪里开始:

          I--J--K'--L   <-- branch-A
         /
...--G--H   <-- common-starting-point, neo-A
         \
          K   <-- branch-B

现在,对于 neo-A,我们将 添加 提交,一次一个。我们查看现有的提交。 I是我们喜欢的,所以我们可以复制它,或者直接使用它,因为它很好。让我们做后者——直接使用它——通过使 neo-aL 的方向前进一步,Git "likes" 使分支名称移动的方式。 (当然,向 L 的方向移动很重要,而不是 K,但这很容易,因为我们处于控制之中。)

            J--K'--L   <-- branch-A
           /
          I   <-- neo-A
         /
...--G--H   <-- common-starting-point
         \
          K   <-- branch-B

我们 HL 方向的下一个提交是提交 J。那一个也很好:我们可以按原样使用名称 neo-AJ 前进一步,给出:

               K'--L   <-- branch-A
              /
          I--J   <-- neo-A
         /
...--G--H   <-- common-starting-point
         \
          K   <-- branch-B

K' 是个问题:我们不想要它。所以我们只是不在这里前进。不过我们确实想要 L,现在我们 必须 复制它,因为现有的 L 指向 K',我们想要一个新的指向 K'改为 J。所以这次我们需要使用git cherry-pick,产生:

               K'--L   <-- branch-A
              /
          I--J--L'  <-- neo-A
         /
...--G--H   <-- common-starting-point
         \
          K   <-- branch-B

我们会继续,但此时我们已经完成了所有提交,也就是说,我们完成了。

我们现在可以安全地 git push neo-A name。那么我们只需要说服大家停止使用名称即可。

让这件事更容易发生

上述方法的缺点是我们必须 "move the name forward" 一次一个步骤。如果我们可以创建 neo-A 并让我们的 Git 完成所有工作,那就太好了。事实证明,我们可以git rebase 命令拥有我们喜欢的所有机制。

我们只需要:

  • 创建 neo-A 指向 branch-A
  • 相同的 提交
  • 运行git rebase -i <hash of common starting point H>
  • 对于我们想要
  • 的提交,将pick更改为drop

和 Git 将自动倒回 neo-A 然后向前跳过或复制提交。最后的结果就是我们画的:

               K'--L   <-- branch-A
              /
          I--J--L'  <-- neo-A (HEAD)
         /
...--G--H   <-- common-starting-point
         \
          K   <-- branch-B

这是有效的,因为 Git 强制 name neo-A 在复制完成后移动到最后复制的提交,在这种情况下, L'.

这会在名称为 neo-A 的任何 other Git 中产生问题,但我们只是编造了它,所以没有其他 Git 有它。所以现在 git push 是安全的。我们将发送新的提交,比如 L',给另一个 Git,然后要求他们设置他们的名字 neo-A 指向链中的最后一个提交——L'——就像我们的名字一样。

我们不需要一个新名字,只要没有人参与

如果我们愿意,我们可以 git rebase -i 直接用 branch-A 本身来做。结果将是:

               K'--L   [abandoned]
              /
          I--J--L'  <-- branch-A (HEAD)
         /
...--G--H   <-- common-starting-point
         \
          K   <-- branch-B

然后我们可以使用 git push --force 或等效于 要求 另一个 Git 放弃其提交 LK'并使 its 名称 branch-A 指向提交 L'。假设他们服从——他们可以拒绝——我们现在让他们的branch-A搬家了。

这里唯一真正的问题是一些讨厌的人可能已经将 他们的 branch-A 复制到另一个 Git 存储库。该人可能希望他们的 branch-A 副本仅在正常的向前添加新提交方向上移动。这就是使用不同分支名称的真正原因:避免混淆对分支名称有期望的人。

如果这里没有人可以混淆,或者如果所有其他人事先知道这可能会发生,请随时倒带并强制推送现有分支名称。

新名称方法还有一个优点

假设,在复制你的提交同时删除其他人的提交的过程中,你犯了一个错误。

如果您使用自己的名字 branch-A 执行此操作,则很难 找到 您的原始提交系列。 Git 有一些技巧 (git reflog) 可以帮助解决这个问题,但它们更多地用于紧急情况而不是日常使用。我发现创建新名称然后进行变基要好得多。如果出现问题,我仍然有 old 名称,从中我可以轻松地 find 所有旧提交以正确的顺序。

对于可以强制推送的私有分支名称,我有时会稍微改变一下顺序。而不是:

git checkout neo-A branch-A
git rebase -i <start-point>

我愿意:

git branch branch-A.0
git rebase -i <start-point>

现在如果出现问题,我有名字 branch-A.0 来记住原始提交。有时我会保留多个 "old versions of branch":

git branch branch-A.1
git rebase -i ...
git branch branch-A.2
git rebase -i ...

直到我有 "right" 提交集合。每个带编号的名称都会跟踪 "right commits".

的每个连续近似值