简体   繁体   中英

Git rebase recursive branches

I'm writing a programming course with in which I want to show how to write a program step-by-step. I thought I might use git for this purpose. The idea is to keep each lesson as a separate branch and create new branches as the course goes on. 正常状态

It is all fine till I discover I've made a mistake in lesson1 . So I go there and fix it. 修复第 1 课

Now the problem occurs: I have to rebase each and every branch. So:

git checkout lesson2
git rebase lesson1

在此处输入图片说明

Afterwards the same for lesson3 and lesson4 . 在此处输入图片说明

I have about 20 lessons per course so every mistake is very painful. Is there a way to automate it or at least make it easier for me?

btw. The tool I've used to create the images is available here .

So had to go back to the drawing board...

I previously suggested a simple filter-branch command, but this has a significant flaw. ( tl;dr - I no longer suggest this as a use case for filter-branch --parent-filter ; unless you care about why, you can jump to the next paragraph. ) When you re-parent with git filter-branch it doesn't re-apply changes for an effective merge, but rather keeps the tree in the re-parented commit as it was (creating new diffs, essentially). A filter-branch is still possible, but it requires either a tree-filter or an index-filter and this would start to get rather complex. (If you can automate the fix in a script, then using that script as a tree-filter should work - possibly with a little finesse in the rev-list arguments - but let's assume for the general case that this wouldn't be so easy. I thought about scripting a way to merge the changes from the "fix" commit into each commit in the graft, but that could result in a conflict at every turn and also isn't so easy...)

So what to do instead? Well, a scripted approach like Libin Varghese suggests is ok if there are no conflicts, and assuming that you can iterate through the ref names in a sensible way. But supposing there might be conflicts, there is another way...

So if you have

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

what you're trying to do essentially is

1) re-apply C , D , and E over Bfix as C' , D' , and E' (a single rebase operation)

2) move all refs from a replaced commit ( X ) to its replacement ( X' )

Using a single rebase minimizes the amount of conflict resolution. If you just rebase lesson3 then you'll have

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

and then you just need rewrite the refs for branches other than the 1st and last lessons. That means you need a mapping from "old commit X " to "replacement commit X' ".

Just such a mapping is passed on stdin to .git/hooks/post-rewrite (if it exists) at the conclusion of a rebase. So you could write a script that uses git show-ref to map ref (branch) names to "old" SHA1 values, then use the mapping on stdin to find the corresponding "new" SHA1 value, and call git update-ref .

(I am planning to provide an example script, but I'm having some trouble with hooks in my test repo; so if I have a little time later, I'll come back to this. But if you're comfortable with scripting and hooks, the above outlines what needs to be done.)

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

Assuming you don't have conflicts, this loop thru lesson2 thru lesson10, performing the rebase.

If rebase fails, the start is set to the point where it failed. But make sure you resolve conflicts and perform a rebase --continue before proceeding

Here is my attempt at solving the problem. You will have to fix my syntax errors, and complete the automation issues but this might be a start.

the single line

git rebase lesson1 lesson2

has the same effect as

git checkout lesson2
git rebase lesson1

you should rebase the last lesson so all the intermediate commits are transferred to the new branch at the same time. You will have to fix any conflicts that occur.

git rebase lesson1 lesson4

then transfer the branches to new commits (if the lessons are contiguous) with commands that look something like.

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

if the branches are contiguous. 'git help revisions' shows how to find a commit using it's commit message from the given branch.

git branch lesson2a  lesson4^"{/Partial lesson2 commit message}"
git branch lesson3a  lesson4^"{/Partial Lesson3 commit message}"

once this looks right remove the old commits

git branch -f lesson2 lesson2a
git branch -D lesson2a

see 'git help rebase' for the rebase syntax

and 'git help revisions' for different ways to specify commits.

Here is a post-rewrite hook as described in Mark's answer.

(I'm fairly unexperimented with shell scripting so comments welcome.)

#!/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

TL;DR use / copy the implementation of Graphite CLI

This open-source CLI will perform recursive branch rebases (disclosure, I'm a contributor): https://github.com/screenplaydev/graphite-cli

The main rebase-recursion can be seen here: https://github.com/screenplaydev/graphite-cli/blob/dfe4390baf9ce6aeedad0631e41c9f1bdc57ef9a/src/actions/fix.ts#L60

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

The key insight is to store branch parents in git refs, in order to recurse the DAG during operations. Without parent metadata, it would be impossible to always determine the merge-base of successive child branches.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM