git 重新定位一个分支,包括它的所有子节点

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/5600659/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-19 05:18:51  来源:igfitidea点击:

Rebasing a branch including all its children

gitversion-controlbranchrebasegit-rebase

提问by Tomasz Nurkiewicz

I have the following Git repository topology:

我有以下 Git 存储库拓扑:

A-B-F (master)
   \   D (feature-a)
    \ /
     C (feature)
      \
       E (feature-b)

By rebasing featurebranch I expected to rebase the whole subtree (including child branches):

通过重新定位feature分支,我希望重新定位整个子树(包括子分支):

$ git rebase feature master

A-B-F (master)
     \   D (feature-a)
      \ /
       C (feature)
        \
         E (feature-b)

However, this is the actual result:

然而,这是实际结果:

      C' (feature)
     /
A-B-F (master)
   \   D (feature-a)
    \ /
     C
      \
       E (feature-b)

I know I can easily fix it manually by executing:

我知道我可以通过执行以下操作轻松手动修复它:

$ git rebase --onto feature C feature-a
$ git rebase --onto feature C feature-b

But is there a way to automatically rebase branch including all its children/descendants?

但是有没有办法自动重新设置分支,包括它的所有孩子/后代?

采纳答案by Adam Dymitruk

git branch --contains C | \
xargs -n 1 \
git rebase --committer-date-is-author-date --preserve-merges --onto B C^

回答by Cascabel

A couple years ago I wrote something to handle this sort of thing. (Comments for improvement are of course welcome, but don't judge too much - it was a long time ago! I didn't even know Perl yet!)

几年前,我写了一些东西来处理这种事情。(当然欢迎提出改进意见,但不要过多评价——那是很久以前的事了!我什至不知道 Perl!)

It's meant for more static situations - you configure it by setting config parameters of the form branch.<branch>.autorebaseparent. It won't touch any branches which don't have that config parameter set. If that's not what you want, you could probably hack it to where you want it without too much trouble. I haven't really used it much in the last year or two, but when I did use it, it always seemed to be quite safe and stable, insofar as that's possible with mass automated rebasing.

它适用于更静态的情况 - 您可以通过设置表单的配置参数来配置它branch.<branch>.autorebaseparent。它不会触及任何没有该配置参数集的分支。如果这不是您想要的,您可能可以轻松将其破解到您想要的位置。在过去的一两年里,我并没有真正使用它,但是当我使用它时,它似乎总是非常安全和稳定,就大规模自动变基而言,这是可能的。

So here it is. Use it by saving it into a file called git-auto-rebasein your PATH. It's probably also a good idea to use the dry run (-n) option before you try it for real. It may be a little more detail than you really want, but it will show you what it's going to try to rebase, and onto what. Might save you some grief.

所以在这里。通过保存成一个名为使用它git-auto-rebase在你的PATH-n在真正尝试之前使用试运行 ( ) 选项可能也是一个好主意。它可能比您真正想要的要详细一些,但它会向您展示它将尝试重新定位的内容以及重新定位的内容。可能会为你省去一些悲伤。

#!/bin/bash

CACHE_DIR=.git/auto-rebase
TODO=$CACHE_DIR/todo
TODO_BACKUP=$CACHE_DIR/todo.backup
COMPLETED=$CACHE_DIR/completed
ORIGINAL_BRANCH=$CACHE_DIR/original_branch
REF_NAMESPACE=refs/pre-auto-rebase

print_help() {
    echo "Usage:  git auto-rebase [opts]"
    echo "Options:"
    echo "    -n   dry run"
    echo "    -c   continue previous auto-rebase"
    echo "    -a   abort previous auto-rebase"
    echo "         (leaves completed rebases intact)"
}

cleanup_autorebase() {
    rm -rf $CACHE_DIR
    if [ -n "$dry_run" ]; then
        # The dry run should do nothing here. It doesn't create refs, and won't
        # run unless auto-rebase is empty. Leave this here to catch programming
        # errors, and for possible future -f option.
        git for-each-ref --format="%(refname)" $REF_NAMESPACE |
        while read ref; do
            echo git update-ref -d $ref
        done
    else
        git for-each-ref --format="%(refname)" $REF_NAMESPACE |
        while read ref; do
            git update-ref -d $ref
        done
    fi
}

# Get the rebase relationships from branch.*.autorebaseparent
get_config_relationships() {
    mkdir -p .git/auto-rebase
    # We cannot simply read the indicated parents and blindly follow their
    # instructions; they must form a directed acyclic graph (like git!) which
    # furthermore has no sources with two sinks (i.e. a branch may not be
    # rebased onto two others).
    # 
    # The awk script checks for cycles and double-parents, then sorts first by
    # depth of hierarchy (how many parents it takes to get to a top-level
    # parent), then by parent name. This means that all rebasing onto a given
    # parent happens in a row - convenient for removal of cached refs.
    IFS=$'\n'
    git config --get-regexp 'branch\..+\.autorebaseparent' | \
    awk '{
        child=
        sub("^branch[.]","",child)
        sub("[.]autorebaseparent$","",child)
        if (parent[child] != 0) {
            print "Error: branch "child" has more than one parent specified."
            error=1
            exit 1
        }
        parent[child]=
    }
    END {
        if ( error != 0 )
            exit error
        # check for cycles
        for (child in parent) {
            delete cache
            depth=0
            cache[child]=1
            cur=child
            while ( parent[cur] != 0 ) {
                depth++
                cur=parent[cur]
                if ( cache[cur] != 0 ) {
                    print "Error: cycle in branch."child".autorebaseparent hierarchy detected"
                    exit 1
                } else {
                    cache[cur]=1
                }
            }
            depths[child]=depth" "parent[child]" "child
        }
        n=asort(depths, children)
        for (i=1; i<=n; i++) {
            sub(".* ","",children[i])
        }
        for (i=1; i<=n; i++) {
            if (parent[children[i]] != 0)
                print parent[children[i]],children[i]
        }
    }' > $TODO

    # Check for any errors. If the awk script's good, this should really check
    # exit codes.
    if grep -q '^Error:' $TODO; then
        cat $TODO
        rm -rf $CACHE_DIR
        exit 1
    fi

    cp $TODO $TODO_BACKUP
}

# Get relationships from config, or if continuing, verify validity of cache
get_relationships() {
    if [ -n "$continue" ]; then
        if [ ! -d $CACHE_DIR ]; then
            echo "Error: You requested to continue a previous auto-rebase, but"
            echo "$CACHE_DIR does not exist."
            exit 1
        fi
        if [ -f $TODO -a -f $TODO_BACKUP -a -f $ORIGINAL_BRANCH ]; then
            if ! cat $COMPLETED $TODO | diff - $TODO_BACKUP; then
                echo "Error: You requested to continue a previous auto-rebase, but the cache appears"
                echo "to be invalid (completed rebases + todo rebases != planned rebases)."
                echo "You may attempt to manually continue from what is stored in $CACHE_DIR"
                echo "or remove it with \"git auto-rebase -a\""
                exit 1
            fi
        else
            echo "Error: You requested to continue a previous auto-rebase, but some cached files"
            echo "are missing."
            echo "You may attempt to manually continue from what is stored in $CACHE_DIR"
            echo "or remove it with \"git auto-rebase -a\""
            exit 1
        fi
    elif [ -d $CACHE_DIR ]; then
        echo "A previous auto-rebase appears to have been left unfinished."
        echo "Either continue it with \"git auto-rebase -c\" or remove the cache with"
        echo "\"git auto-rebase -a\""
        exit 1
    else
        get_config_relationships
    fi
}

# Verify that desired branches exist, and pre-refs do not.
check_ref_existence() {
    local parent child
    for pair in "${pairs[@]}"; do
        parent="${pair% *}"
        if ! git show-ref -q --verify "refs/heads/$parent" > /dev/null ; then
            if ! git show-ref -q --verify "refs/remotes/$parent" > /dev/null; then
                child="${pair#* }"
                echo "Error: specified parent branch $parent of branch $child does not exist"
                exit 1
            fi
        fi
        if [ -z "$continue" ]; then
            if git show-ref -q --verify "$REF_NAMESPACE/$parent" > /dev/null; then
                echo "Error: ref $REF_NAMESPACE/$parent already exists"
                echo "Most likely a previous git-auto-rebase did not complete; if you have fixed all"
                echo "necessary rebases, you may try again after removing it with:"
                echo
                echo "git update-ref -d $REF_NAMESPACE/$parent"
                echo
                exit 1
            fi
        else
            if ! git show-ref -q --verify "$REF_NAMESPACE/$parent" > /dev/null; then
                echo "Error: You requested to continue a previous auto-rebase, but the required"
                echo "cached ref $REF_NAMESPACE/$parent is missing."
                echo "You may attempt to manually continue from the contents of $CACHE_DIR"
                echo "and whatever refs in refs/$REF_NAMESPACE still exist, or abort the previous"
                echo "auto-rebase with \"git auto-rebase -a\""
                exit 1
            fi
        fi
    done
}

# Create the pre-refs, storing original position of rebased parents
create_pre_refs() {
    local parent prev_parent
    for pair in "${pairs[@]}"; do
        parent="${pair% *}"
        if [ "$prev_parent" != "$parent" ]; then
            if [ -n "$dry_run" ]; then
                echo git update-ref "$REF_NAMESPACE/$parent" "$parent" \"\"
            else
                if ! git update-ref "$REF_NAMESPACE/$parent" "$parent" ""; then
                    echo "Error: cannot create ref $REF_NAMESPACE/$parent"
                    exit 1
                fi
            fi
        fi

        prev_parent="$parent"
    done
}

# Perform the rebases, updating todo/completed as we go
perform_rebases() {
    local prev_parent parent child
    for pair in "${pairs[@]}"; do
        parent="${pair% *}"
        child="${pair#* }"

        # We do this *before* rebasing, assuming most likely any failures will be
        # fixed with rebase --continue, and therefore should not be attempted again
        head -n 1 $TODO >> $COMPLETED
        sed -i '1d' $TODO

        if [ -n "$dry_run" ]; then
            echo git rebase --onto "$parent" "$REF_NAMESPACE/$parent" "$child"
            echo "Successfully rebased $child onto $parent"
        else
            echo git rebase --onto "$parent" "$REF_NAMESPACE/$parent" "$child"
            if ( git merge-ff -q "$child" "$parent" 2> /dev/null && echo "Fast-forwarded $child to $parent." ) || \
                git rebase --onto "$parent" "$REF_NAMESPACE/$parent" "$child"; then
                echo "Successfully rebased $child onto $parent"
            else
                echo "Error rebasing $child onto $parent."
                echo 'You should either fix it (end with git rebase --continue) or abort it, then use'
                echo '"git auto-rebase -c" to continue. You may also use "git auto-rebase -a" to'
                echo 'abort the auto-rebase. Note that this will not undo already-completed rebases.'
                exit 1
            fi
        fi

        prev_parent="$parent"
    done
}

rebase_all_intelligent() {
    if ! git rev-parse --show-git-dir &> /dev/null; then
        echo "Error: git-auto-rebase must be run from inside a git repository"
        exit 1
    fi

    SUBDIRECTORY_OK=1
    . "$(git --exec-path | sed 's/:/\n/' | grep -m 1 git-core)"/git-sh-setup
    cd_to_toplevel


    # Figure out what we need to do (continue, or read from config)
    get_relationships

    # Read the resulting todo list
    OLDIFS="$IFS"
    IFS=$'\n'
    pairs=($(cat $TODO))
    IFS="$OLDIFS"

    # Store the original branch
    if [ -z "$continue" ]; then
        git symbolic-ref HEAD | sed 's@refs/heads/@@' > $ORIGINAL_BRANCH
    fi

    check_ref_existence
    # These three depend on the pairs array
    if [ -z "$continue" ]; then
        create_pre_refs
    fi
    perform_rebases

    echo "Returning to original branch"
    if [ -n "$dry_run" ]; then
        echo git checkout $(cat $ORIGINAL_BRANCH)
    else
        git checkout $(cat $ORIGINAL_BRANCH) > /dev/null
    fi

    if diff -q $COMPLETED $TODO_BACKUP ; then
        if [ "$(wc -l $TODO | cut -d" " -f1)" -eq 0 ]; then
            cleanup_autorebase
            echo "Auto-rebase complete"
        else
            echo "Error: todo-rebases not empty, but completed and planned rebases match."
            echo "This should not be possible, unless you hand-edited a cached file."
            echo "Examine $TODO, $TODO_BACKUP, and $COMPLETED to determine what went wrong."
            exit 1
        fi
    else
        echo "Error: completed rebases don't match planned rebases."
        echo "Examine $TODO_BACKUP and $COMPLETED to determine what went wrong."
        exit 1
    fi
}


while getopts "nca" opt; do
    case $opt in
        n ) dry_run=1;;
        c ) continue=1;;
        a ) abort=1;;
        * )
            echo "git-auto-rebase is too dangerous to run with invalid options; exiting"
            print_help
            exit 1
    esac
done
shift $((OPTIND-1))


case $# in
    0 )
        if [ -n "$abort" ]; then
            cleanup_autorebase
        else
            rebase_all_intelligent
        fi
        ;;

    * )
        print_help
        exit 1
        ;;
esac

One thing that I've found, since I originally addressed this, is that sometimes the answer is that you didn't actually want to rebase at all! There's something to be said for starting topic branches at the right common ancestor in the first place, and not trying to move them forward after that. But that's between you and your workflow.

自从我最初解决这个问题以来,我发现的一件事是,有时答案是您实际上根本不想变基!首先要在正确的共同祖先处启动主题分支,然后再不尝试将它们向前移动,这有什么要说的。但这取决于您和您的工作流程。

回答by ruvim

If it is need to update a committer date, the GIT_COMMITTER_DATEenvironment variable can be used (manual). Also use --formatoption to get a branch name without additional formatting.

如果需要更新提交者日期GIT_COMMITTER_DATE可以使用环境变量(手动)。还可以使用--formatoption 来获取分支名称而无需其他格式。

export GIT_COMMITTER_DATE=$( date -Iseconds )
git branch --format='%(refname)' --contains C | xargs -n 1 | git rebase -p --onto master C^
unset GIT_COMMITTER_DATE
# don't forget to unset this variable to avoid effect for the further work

NB: it is required to set either --committer-date-is-author-dateor GIT_COMMITTER_DATEto guarantee the same checksum for C', Ca'and Cb'commits (on rebasing feature, feature-aand feature-bcorrespondingly).

注意:需要为,和commits设置--committer-date-is-author-dateGIT_COMMITTER_DATE保证相同的校验和(相应地在 rebase特性特性-a和特性-b 上)。C'Ca'Cb'