如何从已经变基的分支中删除特定的提交?

How to remove specific commit from already rebased branch?

我已经为 develop 分支打开了一个 PR feature/orphans。从那时起,很多重要的分支都被添加到回购协议中,所以我应该重新基于它们。

现在我需要从我的功能分支中删除一个特定的提交,这对我来说有点棘手。我试图遵循类似问题 (4110978 and ) 的几个答案,但不知何故它不起作用 ‍♂️。

我试过这个:

git rebase --onto feature/db-optimization-for-orphans~fcf0c4a feature/db-optimization-for-orphans~2505060 feature/db-optimization-for-orphans

fatal: invalid upstream 'feature/db-optimization-for-orphans~2505060'

我想删除这个提交 #fcf0c4a,这是日志:

* cf83304 - (HEAD -> feature/orphans, origin/feature/orphans) Added orphans collection
* 844eb17 - fix: bugs
* 4f0111f - fix message
* 9093c8d - fix(cat-172): Change data format from object to array
* fcf0c4a - feat(CAT-172): Add new publisher // <----  I WANT TO REMOVE THIS COMMIT
*   2505060 - (origin/develop, develop) Merge branch 'main' into develop
|\
| * 9600c29 - feat(CAT-111) description
* | 878ee2f - feat(cat-196) description
* | 6b53545 - feat(CAT-18) description
* | 1d2ef2e - saving model name into db + test + version
* | 24eea8e - saving ean values into the db + tests
|/
* 10c205c - Add preprod and prod deployers
* 8a8d0f2 - squashed commits
* 15328b6 - initial commit

有人能告诉我删除 fcf0c4a 提交的最佳解决方案是什么吗?

TL;DR

我想你想要git rebase -i origin/develop

我有一个坏消息和一个好消息要告诉你。

有个坏消息

您只能从分支的 end 删除提交:

* cf83304 - (HEAD -> feature/orphans, origin/feature/orphans) Added orphans collection
* 844eb17 - fix: bugs
* 4f0111f - fix message

例如,最后有三个提交,我将把它们的名称缩短为第一个“digit”(最后一个为 c):

... <-4 <-8 <-c

提交 cf83304 向后指向提交 844eb17。这就是 git log 发现 844eb17.

的方式

Git 首先找到 cf83304 因为分支名称 找到它:

cf83304 ... feature/orphans

即名字feature/orphans指向cf83304.

要从分支中删除提交,我们使分支名称指向某个较早的提交:

            c   <-- origin/feature/orphans
           /
... <-4 <-8   <-- feature/orphans

所以提交 cf83304 现在被推到了一边。 (Git 仍然可以找到 cf83304,因为 name origin/feature/orphans 仍然指向 cf83304。)

一旦我们从分支中删除了提交 c...,我们也可以删除提交 8...

        8 <-c   <-- origin/feature/orphans
       /
... <-4   <-- feature/orphans

等等。

所以坏消息是:要删除提交 fcf0c4a——你可以做——你必须删除该分支的所有后续提交

好消息来了

在我们“删除”提交之前——它们并没有真正消失;如果你知道它们的编号,或者它们有其他名称,例如 origin/feature/orphans,我们仍然可以找到它们——我们可以 copy selected commits to new并改进了提交.

每次提交,在 Git:

  • 已编号。那些又大又丑的 random-looking hexadecimal 数字,fcf0c4a 等等,唯一定位 那个特定的提交 。 Git 需要那个数字来找到提交。

  • 包含两个东西:

    • 每个提交都有一个每个文件的完整快照,因为它在您(或任何人)进行提交时出现。这些文件以特殊的、压缩的 de-duplicated、read-only 和 Git-only 形式保存:您无法阅读它们,并且 什么都没有 ——不是甚至 Git 本身 — 可以 覆盖 它们,因此这些不是您使用的文件。它们只是永久存储,以便您以后可以取回它们。

    • 每个提交都包含一些元数据,或者关于提交本身的信息。例如,这包括提交人的姓名和电子邮件地址。它包括一些 date-and-time 标记:当您 运行 git log 并看到带有作者和日期的提交时,这些都来自元数据。它也包括您在此处看到的日志消息。

      对于 Git 本身来说至关重要的是,每个提交中的元数据都存储 previous 的某些列表的原始哈希 ID(s)——提交号(s)提交。大多数提交只存储一个哈希 ID,这就是您在这里得到的;一些,如 2505060,是 merge 提交,存储 two 哈希 ID;每个 non-empty 存储库中至少有一个提交是 有史以来第一次 提交,例如 15328b6。此 first-ever 提交不存储 任何 previous-commit ID,因为没有任何先前的提交。

除了那些奇怪的特殊情况(合并和第一次提交),然后,我们可以画出这样的提交序列:

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

其中每个大写字母如 H 代表一些实际的哈希 ID。 (这就是我在上面所做的,除了我能够使用 真实 哈希 ID 的第一个字符。)字母代表 saved-files-and-metadata,箭头出来每个字母代表存储的 previous-commit 哈希 ID:提交 H 存储较早提交 G 的哈希 ID。我们说H指向G.

现在,再次讨论 bad-news-good-news 主题,坏消息是 提交后无法更改 。 (出于多种原因,这是必要的,包括 Git 相当神奇的哈希 ID 方案——这类似于支持加密货币的技巧——并且还包括提交 share 相同的文件。如果我们能以某种方式更改其中一个文件,那将更改 所有 共享副本。)好消息是我们和 Git —找到这些提交是通过分支和其他名称,我们可以做东西不同提交哈希这些分支和其他名称的 ID。

每个名称仅包含 一个 哈希 ID。对于分支名称,根据定义,该哈希 ID 是 分支上的最后一次提交 。所以当我们有:

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

这意味着根据定义,提交 H 是分支上的 last 提交。这就是我们如何移动名称以删除一个提交或多个提交, 结束:

       G--H
      /
...--F   <-- somebranch

现在 somebranch 指向 F 而不是 HF 自动成为分支上的 最后一次提交 .

每当我们进行 new 提交时,我们都会这样做:

  1. git switch <em>branch</em> or git checkout <em>branch </em>;
  2. 处理/使用文件 Git 从提交中复制选择的分支名称;
  3. 使用git add(原因我不会在这里详述);和
  4. 运行 git commit.

最后一步 - git commit 步骤 - 通过以下方式进行新提交:

  • 收集适当的元数据:例如,它从 user.nameuser.email 获取您的姓名和电子邮件地址;
  • 使用步骤 1 中的 当前分支名称 计算出 当前提交 的哈希 ID:如果它指向 F,那是当前提交
  • 写出新快照和元数据,新提交的箭头指向当前提交;和
  • 最后一招...

但是让我们先画出写出新提交的效果:

       G--H   <-- origin/feature/orphans
      /
...--F   <-- current-branch (HEAD), some-other-branch
      \
       I

我们现在有这个新提交 I,它有一个新的、唯一的、丑陋的大哈希 ID。现在最后一个技巧开始了:Git 将新提交的哈希 ID 写入当前分支名称:

       G--H   <-- origin/feature/orphans
      /
...--F   <-- some-other-branch
      \
       I   <-- current-branch (HEAD)

这就是树枝在 Git 中生长的方式。我们用 git checkoutgit switch 签出一个——在我这里的图中,这意味着我们将特殊名称 HEAD 附加到分支名称;你可以在你自己的 git log 输出中看到那个特殊的名字——检查提交可以让我们得到所有保存的文件 来自 提交。然后我们照常工作并进行 new 提交。 new 提交获得一个新的唯一哈希 ID,并且 Git 将新提交的哈希 ID 填充到当前 name,现在是名称指向新的最后一次提交。

这如何帮助你做你想做的事?

让我们画一些你有的东西,用我喜欢的 one-letter 大写字母名称替换丑陋的大哈希 ID,以我喜欢的形式画它:

...--G--H--I--J--K--L   <-- feature/orphans (HEAD), origin/feature/orphans

这里G代表2505060 - (origin/develop, develop) Merge branch 'main' into developH 代表 fcf0c4a - feat(CAT-172): Add new publisher:您要“删除”的提交。 I 代表 9093c8d - fix(cat-172): Change data format from object to array,您想要保留的提交。 J-K-L 也是您要保留的提交。

坏消息是您将不得不弹出您想要保留的提交。好消息是您可以先将它们复制到新的和改进的提交中。 我们将以:

结束
       H--I--J--K--L   <-- origin/feature/orphans
      /
...--G
      \
       I'-J'-K'-L'  <-- feature/orphans (HEAD)

new commits I'-J'-K'-L' 将被精心安排 copys of the old 承诺。我们将对每个副本进行两项更改:

  1. 每个副本的parent会指向回正确的parent:即I'会直接指向G,不会指向H.
  2. 每个副本的快照文件删除您在提交H.
  3. 中所做的更改

现在,明确但手动且有点慢的方法是手动复制您想要复制的每个提交,一次一个。我们将通过创建一个指向提交 G:

new temporary branch name 来做到这一点
       H--I--J--K--L   <-- feature/orphans, origin/feature/orphans
      /
...--G   <-- temp-branch (HEAD)

我们用:

git switch -c temp-branch 2505060

我们现在在这个新的临时分支上,我们可以看到和使用的文件来自提交 G(或准确地说是 2505060)。

我们现在想要 Git 弄清楚 我们在提交 I 中更改了哪些内容 并且 在这里和现在进行相同的更改并提交它们。 Git 也会从提交 I 复制提交 消息

执行这个简单的“复制一个提交的更改和提交消息”的 Git 命令是 git cherry-pick,所以我们会 运行:

git cherry-pick <hash-of-I>

I 的(缩写)散列是 9093c8d 所以我们可以输入它并按 ENTER 并得到:

       H--I--J--K--L   <-- feature/orphans, origin/feature/orphans
      /
...--G
      \
       I'  <-- temp-branch (HEAD)

然后我们必须用正确的散列 ID 重复另外三个 git cherry-pick 命令。这会将 J 复制到 J',然后将 K 复制到 K',然后将 L 复制到 L'

       H--I--J--K--L   <-- feature/orphans, origin/feature/orphans
      /
...--G
      \
       I'-J'-K'-L'  <-- temp-branch (HEAD)

一旦我们完成了所有 git cherry-pick 步骤,我们只需要告诉 Git:嘿 Git,强制名称 feature/orphans指向当前提交,这需要使用git branch -f。然后我们 git switch feature/orphans 重新开始:

       H--I--J--K--L   <-- origin/feature/orphans
      /
...--G
      \
       I'-J'-K'-L'  <-- feature/orphans (HEAD), temp-branch

然后我们可以完全删除名称 temp-branch,因为我们已经完成了它。

快速方法

执行所有这些单独的步骤——创建一个新的但临时的分支名称,cherry picking 一个接一个地提交,强制旧分支名称到位,切换回 旧分支, 并删除临时分支 — 是 中的一大难题。 我们不必这样做。 我们用 git rebase 命令代替。

git rebase 命令主要是用 一个命令 完成上述操作的一种奇特方式。因为这个命令做了 这么多事情 ,它有很多部分,我认为这就是你 运行 遇到 rebase 问题的地方。

你这里有很多选择——有很多方法可以运行git rebase——但是我自己通常用的是这种案例称为 interactive rebase。你 运行 它是这样的:

git switch feature/orphans     # if you're not already there
git rebase -i origin/develop

这里的名称 origin/develop 可以是任何名称——分支或其他名称——选择你希望提交去的地方。如果愿意,您可以使用原始哈希 ID (git rebase -i 2505060),但我们想要挑选出我一直称之为“提交 G”的提交。 这是副本应该去的地方。

git rebase 命令现在将通过列出您现在拥有的提交来确定要复制的提交,不包括那些 可从 提交 G 访问的提交。无需深入了解这一切意味着什么,简短的版本就是列出提交 H-I-J-K-L这是一个太多的提交,但没关系!列出这些提交哈希 ID 后,git rebase -i 中的 -i 意味着 现在你'列出要复制的提交,在每个哈希 ID 前面用单词 pick 组成指令 sheet。

这条指令 sheet 因此会读作:

pick fcf0c4a feat(CAT-172): Add new publisher
pick 9093c8d fix(cat-172): Change data format from object to array

其余三个提交依此类推。 现在,由于 -igit rebase 根据此指令打开您的编辑器 sheet。 您现在的工作是 调整这些说明 然后写出来并退出你的编辑器。1 在你的特定情况下,你的工作是更改或删除 pick 命令以提交 H—您想要的提交。如果将其更改为 dropd,或者只是删除整行,git rebase 而不是 复制提交 H毕竟

一旦你写出指令sheet,git rebase将继续执行剩余的pick指令,运行宁git cherry-pick每次提交需要复制。这会让你得到 I'-J'-K'-L' 提交。然后 git rebase 通过移动 name feature/orphans 指向最终复制的提交来完成,L':

       H--I--J--K--L   <-- origin/feature/orphans
      /
...--G
      \
       I'-J'-K'-L'  <-- feature/orphans (HEAD)

你现在在你的存储库中有你想要的一组提交,但还有一件事要做。


1一些编辑器并没有真正“退出”:他们需要与 Git 沟通他们已经完成了文件的编写。这是您可能遇到绊脚石的另一个地方。但是 Git 已经和 git commit 有这个问题,如果你不使用 -m 标志,通常你不应该使用 -m 标志。所以如果你有这些棘手的编辑器之一,你应该已经解决了这个问题。


您现在需要使用 git push --force-with-lease

您已发送 提交H-I-J-K-L 到其他Git 存储库,您使用名称origin 调用的存储库。您让其他 Git 存储库创建或更新 他们的 分支名称 feature/orphans。你自己的Git通过记住他们的feature/orphans作为你的origin/feature/orphans.

反映了这一点

您现在需要将 I'-J'-K'-L' 提交的另一个 Git 存储库发送出去——这部分很简单——然后说服他们 他们 应该放弃 他们的 H-I-J-K-L 链以支持您的 new-and-improved I'-J'-K'-L' 提交链。这部分需要使用 force push.

一般来说,Git 非常喜欢向分支添加新提交。它不喜欢 从末尾删除提交: 这通常被认为是坏的或错误的。所以你必须强迫他们 Git 这样做。

使用 git push --force-with-lease origin feature/orphans,你让你的 Git 调用他们的 Git,给他们提交 I'-J'-K'-L',然后发送以下形式的命令:

我认为你的 feature/orphans 成立 cf83304。如果是这样,我命令你将提交 L' 的哈希 ID 填入其中。如果我是对的,请告诉我,你这样做了。

他们要么找到正确的东西并服从,要么告诉你他们为什么不这样做。

您可以使用更简单的 git push --force。这省略了一些安全检查,通过向他们发送命令:

将提交 L' 的哈希 ID 填充到您的 feature/orphans 中!现在做!我命令你!

如果由于某种原因他们选择了 L 之后的 even-newer 提交,这将删除该提交。如果您不知道它的哈希 ID,就永远无法询问它。通过使用“我认为......所以这样做”结构,如果你 错了 你可以在你让他们放弃一个以后没人能找到的提交之前看到到底发生了什么.