Перебазирование ветки, включая всех ее дочерних элементов


92

У меня следующая топология репозитория Git:

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

Путем перебазирования featureветки я ожидал перебазировать все поддерево (включая дочерние ветви):

$ git rebase feature master

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

Однако это фактический результат:

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

Я знаю, что могу легко исправить это вручную, выполнив:

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

Но есть ли способ автоматически перебазировать ветку, включая всех ее дочерних / потомков?


2
Также посмотрите, как я бы перебазировал целую подисторию - несколько веток, между которыми были некоторые связи в результате слияния . Неприятная часть этого решения - необходимость впоследствии сбросить ссылки ветки темы на новые перемещенные коммиты.
imz - Иван Захарящев

спасибо за упоминание опции --onto для git rebase - это решило мою проблему
jackocnr

7
Не должно $ git rebase feature masterбыть $ git rebase master feature?
hbogert

Возможный дубликат поддерева Git
rebase

Ответы:


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

3
Rebase on требует, чтобы родительский элемент самого старого коммита ограничил начало - следовательно, C ^
Адам Димитрук

3
Разве команда "git branch" не выводит звездочку перед текущей ветвью, что приводит к ошибке этого скрипта, если одна из ветвей для перебазирования в настоящее время проверена?
Марк Лодато

2
Разве git branch не фарфоровая команда? Есть ли способ сделать это более надежным в будущем?
Крис Пфол,

7
Адам: Не уверен, что это правильный путь, вы действительно хотите иметь строки с *, вам просто не нужен сам *. Что-то вроде | tr -d * было бы лучше. Однако мой вопрос: почему здесь используется --onto B? Думал надо перебазировать поверх мастера. И разве C ^ не то же самое, что B? поэтому мы перемещаемся из B (исключая?) в каждую ветвь, содержащую C поверх ... B. Разве результат не будет таким же, как раньше?
Marenz

4
Разве это не должно быть --onto Fвместо того --onto B, чтобы все эти коммиты выполнялись непосредственно в B, а мы перемещаем их в F ?
Объявление N

12

Пару лет назад я написал кое-что, чтобы справиться с подобными вещами. (Комментарии по улучшению, конечно, приветствуются, но не судите слишком много - это было давно! Я еще даже не знал Perl!)

Он предназначен для более статичных ситуаций - вы настраиваете его, задав параметры конфигурации формы branch.<branch>.autorebaseparent. Он не будет касаться веток, для которых не установлен этот параметр конфигурации. Если это не то, что вы хотите, вы, вероятно, могли бы без особых проблем взломать его там, где хотите. Последние год или два я практически не использовал его, но когда я все же использовал, он всегда казался достаточно безопасным и стабильным, насколько это возможно с массовым автоматическим перемещением.

Итак, вот оно. Используйте его, сохранив в файл с именем git-auto-rebaseв вашем PATH. Вероятно, также неплохо использовать -nопцию dry run ( ), прежде чем вы попробуете ее по-настоящему. Это может быть немного больше деталей, чем вы действительно хотите, но он покажет вам, что он собирается попытаться перебазировать и на что. Может избавить тебя от горя.

#!/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=$1
        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]=$2
    }
    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

Одна вещь, которую я обнаружил, с тех пор, как я изначально обратился к этому, заключается в том, что иногда ответ заключается в том, что вы на самом деле вообще не хотите перебазировать! Есть кое-что, что нужно сказать о том, чтобы начинать тематические ветки с правильного общего предка, а не пытаться продвигать их после этого. Но это между вами и вашим рабочим процессом.


Проголосовали за "вместо этого использовать слияние". Я потратил несколько часов, пытаясь переустановить множество веток тем и подтем, прежде чем попробовать вариант слияния, и слияние действительно было намного проще выполнить, даже несмотря на то, что новый мастер сильно отличался от исходного.
davenpcj

3
Меня немного пугает то, что ответ содержит: «Я еще даже не знал Perl» - тем более, что ответ написан не на Perl ... :-)
Питер В. Мёрч

@ PeterV.Mørch, что значит?
Pacerier

0

Если необходимо обновить дату зафиксировавшего , то GIT_COMMITTER_DATEпеременная окружения может быть использована ( ручной ). Также используйте --formatопцию, чтобы получить имя ветки без дополнительного форматирования.

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: необходимо установить либо --committer-date-is-author-dateлибо, GIT_COMMITTER_DATEчтобы гарантировать одинаковую контрольную сумму для C', Ca'и Cb'фиксации (для функции перебазирования , feature-a и feature-b соответственно).

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.