Git rebase 递归分支

14

我正在编写一门编程课程,希望逐步展示如何编写程序。我想使用git来实现这个目的。我的想法是将每一个教训作为一个单独的分支,并随着课程的进行创建新的分支。 normal state

一切都很好,直到我发现在lesson1中犯了一个错误。所以我去那里修复了它。 fix lesson 1

现在问题出现了:我必须重新调整每一个分支。所以:

git checkout lesson2
git rebase lesson1

输入图像描述

然后对于lesson3lesson4同样进行操作。 输入图像描述

每个课程有大约20课,所以每次出错都非常痛苦。是否有一种方法可以自动化或者至少让它变得更容易?

顺便说一句,我用来创建这些图片的工具可以在这里找到。


1
需要的是一种批量多分支变基。总体问题相当困难。您自己的分支是简单和线性的,这减少了问题,易于解决(请参见Gregg的答案),但这也意味着也许分支名称对于您正在做的事情来说有些过度。 :-) - torek
@torek - 我想大规模多分支变基就是filter-branch所做的,或者更多(因此我的回答采用了这种方法)。Git分支足够简单,以至于我不确定是否应该称其为“累赘”,特别是如果他不时需要返回并修复“错误”的课程。但通常情况下标签会起到同样的作用。(除非他使用变基移动它们时会遇到相同的问题...)你有更简单的想法吗? - Mark Adelsberger
@MarkAdelsberger:嗯,不,filter-branch比那个更强大,但对于rebase类型的目的来说也更加笨拙(很难让树发生正确的事情,而重复的cherry-pick=rebase则只是做正确的事情)。我想的是一个直接的提交链,其中“lesson N”不是分支,而只是提交日志消息的一部分,然后您可以使用HEAD^{/lesson 3}来查找提交。 - torek
@torek - 这取决于你要做什么。我在进行一项最初被视为整体变基(在新的根提交上修改整个历史记录中的树)的操作时学习了filter-branch,但事实证明,对于具有非平凡分支拓扑的历史记录,变基是笨拙的方法。(最终我使用了BFG,但这与此无关...)我只能想到很少几个filter-branch操作不能用变基来表示,所以我不确定我是否同意它更强大,而更容易处理复杂操作。 - Mark Adelsberger
@MarkAdelsberger:但是rebase不仅仅是重新设置父级:它是差异和重新应用,而您要应用的新基础可能有实质性的更改。如果您重新设置父级,则只需复制原始树,因此您将无法获得更新的基础。要使用filter-branch,您不仅必须更改每个提交的父级,还可能需要更改其树。 - torek
@torek - 是的,我没有考虑到树在这种情况下会如何结束。显然,许多其他人也没有考虑到这个问题,因为那个答案在我意识到问题并摆脱它之前就得到了几个积极的投票。然而,在许多使用情况下,两个工具都适用,我仍然坚持我的先前评论。 - Mark Adelsberger
5个回答

6

所以我必须重新开始...

之前我提出了一个简单的filter-branch命令,但这有一个重大缺陷。(tl;dr - 我不再建议使用filter-branch --parent-filter;除非您关心为什么,否则可以跳到下一段。)当您使用git filter-branch进行重定向时,它不会重新应用更改以实现有效合并,而是将树保留在重新指定的提交中(创建新的差异)。仍然可以使用filter-branch,但需要使用tree-filterindex-filter,这会变得相当复杂。(如果您可以在脚本中自动修复问题,则使用该脚本作为tree-filter可能有效-可能需要在rev-list参数中进行微调,但让我们假设对于常规情况来说这不是那么容易。我考虑过编写脚本的方法,将"fix"提交中的更改合并到移植中的每个提交中,但这可能会在每次转折处导致冲突,并且也不太容易...)

那么应该怎么办呢?好吧,像Libin Varghese建议的脚本化方法还可以接受,如果没有冲突,并且假设您可以以明智的方式迭代引用名称。但是如果存在冲突,那么还有另一种方法...

所以如果您有

        Bfix <--(lesson1)
       /
A --- B --- C --- D --- E <--(lesson3)(HEAD)
            |
        (lesson2)

您要做的基本上是:
1)将 CDE 重新应用到 Bfix 上,作为 C'D'E'(一个单一的变基操作)
2)将所有引用从被替换的提交(X)移动到其替换(X')上
使用单一的变基可以最小化冲突解决的数量。如果只对 lesson3 进行变基,则会出现以下情况:
      (lesson1)
          |
        Bfix --- C' --- D' --- E' <--(lesson3)(HEAD)
       /
A --- B --- C <--(lesson2)

然后,您只需要重写除第一和最后一课以外的分支的引用。这意味着您需要从“旧提交X”到“替换提交X'”建立映射。
在重新定位完成时,此类映射会通过stdin传递给.git/hooks/post-rewrite(如果存在)。因此,您可以编写一个脚本,使用git show-ref将ref(分支)名称映射到“旧”的SHA1值,然后使用stdin上的映射查找相应的“新”SHA1值,并调用git update-ref
(我计划提供一个示例脚本,但是我的测试repo中出现了一些钩子问题;因此,如果我稍后有时间,我会回到这个问题。但是,如果您熟悉脚本编写和钩子,上述内容概述了需要完成的任务。)

你如何找到相应的新SHA1值?仅通过提交消息进行匹配似乎不够可靠。如果在变基期间解决了冲突,则检查差异将无法起作用。你能否从git-rebase中获取配对信息? - Lionel Henry
假设需要进行直接的变基而不是压缩等操作,给定旧提示和新提示,只需收集所有提交直到根。 - Lionel Henry
现在我看到post-rewrite会给你一个重写提交的列表,甚至可以找出压缩提交。 - Lionel Henry
使用您描述的 post-rewrite 钩子添加了一个答案。 - Lionel Henry

1
start=2
end=10
for i in {$start..$end}
do
        git checkout lesson$i
        git rebase lesson$(($i-1)) || break
done
start=$i

假设没有冲突,这个循环将遍历第2课到第10课,并执行变基操作。
如果变基失败,则起点设置为失败的位置。但是请确保在继续之前解决冲突并执行 rebase --continue

1
这是我尝试解决问题的方法。您需要修复我的语法错误,并完成自动化问题,但这可能是一个开始。
单行。
git rebase lesson1 lesson2

具有与

相同的效果。
git checkout lesson2
git rebase lesson1

你需要将最后一节课的代码变基到新分支中,以便中间的所有提交都同时转移到新分支。你需要解决任何冲突。
git rebase lesson1 lesson4

然后使用类似以下命令将分支转移到新提交(如果课程是连续的)。

git branch lesson2a lesson4^2
git branch lesson3a lesson4^1

如果分支是连续的。'git help revisions' 显示如何使用提交消息从给定分支找到提交。
git branch lesson2a  lesson4^"{/Partial lesson2 commit message}"
git branch lesson3a  lesson4^"{/Partial Lesson3 commit message}"

一旦看起来正确了,就删除旧的提交。
git branch -f lesson2 lesson2a
git branch -D lesson2a

请查看“git help rebase”获取rebase语法的信息,以及“git help revisions”获取指定提交方式的不同方法。

0

简而言之,使用/复制Graphite CLI的实现

这个开源CLI将执行递归分支变基(披露一下,我是贡献者): https://github.com/screenplaydev/graphite-cli

主要的变基递归可以在这里看到:https://github.com/screenplaydev/graphite-cli/blob/dfe4390baf9ce6aeedad0631e41c9f1bdc57ef9a/src/actions/fix.ts#L60

git rebase --onto ${parentBranch.name} ${mergeBase} ${currentBranch.name}

关键见解是在git refs中存储分支的父级,以便在操作过程中递归DAG。如果没有父级元数据,就无法始终确定连续子分支的合并基础。

const metaSha = execSync(`git hash-object -w --stdin`, {input: JSON.stringify(desc)}).toString();

execSync(`git update-ref refs/branch-metadata/${this.name} ${metaSha}`);

https://github.com/screenplaydev/graphite-cli/blob/dfe4390baf9ce6aeedad0631e41c9f1bdc57ef9a/src/wrapper-classes/branch.ts#L102-L109


0
这是一个后重写钩子,如Mark的答案所述。
(我对shell脚本编程相当不熟练,欢迎评论。)
#!/bin/bash

if [ "$1" != "rebase" ]; then
    exit 0
fi


orig=`git rev-parse ORIG_HEAD`

while read line
do
    IFS=' '
    read -ra map <<< "$line"
    old="${map[0]}"
    new="${map[1]}"

    heads=`git show-ref | grep -e " refs/heads" | grep "$old"`

    IFS=$'\n'
    for h in $heads; do

        IFS=' '
        read -ra ref_info <<< "$h"
        ref="${ref_info[1]}"

        # Don't update original branch as this causes rebase to fail
        if [ "$old" != "$orig" ]; then
            echo "Updating '$ref' to $new"
            `git-update-ref $ref $new $old`
        fi
    done
done

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接