【问题标题】:Rebase branch preserving commits on another branch based on itRebase 分支保留基于它的另一个分支上的提交
【发布时间】:2020-03-12 01:26:18
【问题描述】:

抱歉,如果标题具有误导性,但我不确定如何描述我所遇到的情况。

我有这样的提交和分支

A --- B --- C --- D (master)
                   \
                    E (another)

我想从master 分支中删除提交BC(保留D),但将它们保留在基于master 的another 分支中。 所以转换后我的树应该是这样的:

A --- D (master)
 \
  B --- C --- E (another)

我想,我可能应该只是重新设置master,但我不确定BC 是否仍会包含在another 中,更不用说从中删除/省略D .

我应该如何进行才能达到上述效果?

【问题讨论】:

    标签: git git-branch git-commit git-rebase


    【解决方案1】:

    假设你想移动变更集,应该没那么难:

    git rebase --onto A C master
    

    这会将分支 master 移动到 A 之上,丢弃直到 C 的修订(所以我只会移动 D 以及分支指针)。那么:

    git rebase --onto C D another
    

    这将使 E 在 C 之上变基,丢弃直到 D 的修订(换句话说,仅将 E 移动到 C 之上...移动分支指针)。

    应该可以。

    【讨论】:

    • 我更新了我当前的树形图,更清楚地表明another 当前基于master。我认为这无论如何都适用于这种情况,对吧?
    • 这似乎是一个很好的答案,但我希望看到解释从我必须滚动查看的 bash 评论转移到 sn-p 之外的一些解释。
    • 没关系,测试它并像魅力一样工作,谢谢。
    【解决方案2】:

    实现它的另一种方法是简单地使用交互式变基:

    https://git-scm.com/book/en/v2/Git-Tools-Rewriting-History

    从 master 创建另一个分支并手动重写它们:

    $ git checkout -b another
    $ git rebase -i 
    

    git rebase 不带参数会给你一个包含该分支上所有提交的列表。然后只需在提交列表中用“d”标记要删除的那些。例如对于您想要“删除” B、C 和 E 的大师。

    d b72a395    E
    pick 5bca8d9 D
    d 15aab26    C
    d 25aab26    B
    pick 35aab26 A
    

    对于“另一个”分支,标记“D”要删除。

    【讨论】:

      【解决方案3】:

      为了得到你想要的结果——嗯,至少可能是你想要的结果——你必须完全停止使用现有的提交 DE,并且原因是没有人——除了你或 Git 本身——可以完全改变 any 关于 any 现有提交的任何事情,以及 提交之间的连接实际上是存储在内部父/子对的子的哈希 ID。

      也就是说,给定第一张图,提交A 是根提交:它没有父提交。没有箭头表示A 之前的提交是_____,因为A 之前没有提交。但是提交B确实有一个箭头,从它指向提交A我面前的提交是提交A。提交C 包含一个指向B 的箭头; D 包含一个指向 C 的箭头;而E 包含一个指向D 的箭头:

      A <-B <-C <-D <-E
      

      commits 不同,分支名称​​可以更改:它们充当指向您选择的任何一个提交的箭头。所以master 当前指向现有提交Danother 指向现有提交E。 Git可以从another开始查找E,使用E查找D,使用D查找C,以此类推;或者Git可以从master开始找到D,找到CBA

      你想要的结果有一个提交 B 指向 AC 指向 B,所以通过 C 的现有提交都可以。但是你想要一个新的和改进的D 变体,它不是指向C,而是直接指向A

      这个新的和改进的D' 大概有一个现有提交没有的快照。要为 D' 创建快照,您希望 Git 获取 CD 中的快照之间的差异,并将该差异应用于 A 中的快照。

      Git 可以自动执行此操作。执行此操作的基本 Git 命令是 git cherry-pick。我们稍后会看到如何使用git rebase运行(正确的一组)git cherry-pick 命令,但让我们从cherry-pick 本身开始。

      同样,您想要一个新的和改进的副本 E,我们可以称之为E',其中的改进是:

      • 指向C,而不是D;和
      • 拥有通过将快照DE 之间的差异应用于C 中的快照而生成的快照。

      同样,这是git cherry-pick 的工作。那么让我们看看如何做到这一点。

      使用git cherry-pick

      要创建父级为A 的新的和改进的D',我们必须首先git checkout 提交A 本身,最好还在那里附加一个临时分支名称以避免混淆。 (在内部,使用git rebase,Git 使用 no 临时分支名称完成所有这些操作。)所以我们将运行:

      git checkout -b temp <hash-of-A>
      

      这给了我们:

      A   <-- temp (HEAD)
       \
        B--C--D   <-- master
               \
                E   <-- another
      

      现在我们像这样使用git cherry-pick

      git cherry-pick <hash-of-D>
      # or: git cherry-pick master
      

      这会将提交 Dmaster 指向的那个——我们可以通过它的哈希 ID 或名称 master 给它——复制到新的提交 D',temp 现在点。 (每当我们进行新的提交时,Git 都会将新提交的哈希 ID 存储在 current 分支中:HEAD 已附加到。所以 temp 现在指向复制 D'。)

      A--D'  <-- temp (HEAD)
       \
        B--C--D   <-- master
               \
                E   <-- another
      

      现在我们需要另一个新的临时分支,指向提交C,所以我们运行git checkout -b temp2 <em>hash-of-C</em>。 (除了原始哈希,我们可以使用 Git 必须查找提交 C 的任何其他方式,例如 master~1,但原始哈希可以剪切和粘贴,只要您剪切正确的哈希即可。 ) 这给了我们:

      A--D'  <-- temp
       \
        B--C   <-- temp2 (HEAD)
            \
             D   <-- master
              \
               E   <-- another
      

      (注意HEAD 现在是如何附加到temp2 的,因为git checkout -b。)现在我们选择提交E 来生成E'

      git cherry-pick another
      

      因为another 指向提交E,所以会成功。如果一切顺利,Git 会自己进行新的提交,我们有:

      A--D'  <-- temp
       \
        B--C--E'  <-- temp2 (HEAD)
            \
             D   <-- master
              \
               E   <-- another
      

      我们现在需要做的是强制名称master 引用提交D',并强制名称another 引用提交E'。现在,我们可以使用git branch -f

      git branch -f master temp
      git branch -f another temp2
      

      这给了我们:

      A--D'  <-- master, temp
       \
        B--C--E'  <-- another, temp2 (HEAD)
            \
             D   [abandoned]
              \
               E   [abandoned]
      

      虽然提交 DE 没有名称——这使得它们很难找到——但它们会在你的 Git 存储库中逗留很长一段时间,通常至少 30天。 (这可以通过各种 reflog 过期设置来控制。)如果你已经将它们的哈希 ID 保存在某个地方(并且你已经 - 或者更确切地说,Git 已经将哈希 ID 保存在一些 reflog 中),你仍然可以获得他们在这段时间内回来。

      您现在可以git checkout 任何一个原始分支名称并删除两个temp 名称。

      使用git rebase 执行此操作

      git rebase 所做的实质上是1 运行 一系列 git cherry-pick 命令,然后通过运行 git branch -f 的等效命令来完成所有操作强制分支名称指向 last 复制的提交,并 git checkout 该分支。 git rebase 将复制的提交集来自 rebase 调用的 upstream 参数。 rebase 将它们复制到的位置,就像 git cherry-pick 一样,来自 rebase 调用的 onto 参数。

      也就是说,你运行:

      git rebase --onto <target> <upstream>
      

      其中 target 是你想要在 第一个复制的提交之前的提交,upstream 告诉 Git 什么提交 不复制。这个“不要复制的东西”起初看起来很奇怪,但你会习惯它。2它还允许你在大多数情况下省略--onto(尽管不是在你的特定情况下)。

      Git 的作用是枚举<em>upstream</em>..HEAD 中的提交,不包括某些通常不受欢迎的提交。3 这提供了应复制/挑选的提交哈希 ID 列表。这个列表被保存到一个临时文件中。4 然后,Git 运行 git checkout 的 HEAD 分离变体来检查 --ontotarget 提交,或 upstream 如果您没有指定 --onto。然后,Git 对保存的哈希 ID 进行挑选。最后,如果一切顺利,Git 会强制将分支及其 HEAD 重新附加到从 rebase 操作中复制的最后一次提交。

      对于您的特殊情况,eftshift0 has already shown the appropriate git rebase commands 比我早了约 20 分钟得到了这个答案。 :-) 这只是对实际情况的详细解释。


      1我在这里说 as if 是因为某些变基方法使用其他方法,而某些变基实际上运行 git cherry-pick,或者——在最现代的Git——直接内置在 Git 内部调用的 sequencer 中,它实现了樱桃采摘。

      2这实际上是很自然的,因为 Git 的 A..B 限制语法。这告诉 Git:找到 可从 B 访问的提交,不包括那些可从 A 访问的提交。 更多关于可达性的信息,见Think Like (a) Git

      3不受欢迎的是现有的合并提交,以及任何已经精心挑选的提交。 Git 使用git patch-id 程序找到后者。正确描述有点棘手,这里不再赘述。

      4它位于.git 下,但在 Git 的开发过程中该位置已移动。如果您好奇,有时可以在 .git/rebase-todo 或类似名称中找到它们。

      【讨论】:

      • 这是一些惊人而宝贵的知识。我非常感谢您花时间详细解释它。我现在可以直观地向您表达我的感激之情的唯一方法是投票,但您的答案是“传递知识”应该是什么样子的定义。再次感谢您!
      猜你喜欢
      • 2023-02-11
      • 1970-01-01
      • 2017-02-21
      • 1970-01-01
      • 2022-10-20
      • 1970-01-01
      • 2018-06-22
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多