Cho thuê lại một chi nhánh bao gồm tất cả các chi nhánh của nó


91

Tôi có cấu trúc liên kết kho lưu trữ Git sau:

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

Bằng cách khôi phục featurenhánh, tôi dự kiến ​​sẽ căn cứ lại toàn bộ cây con (bao gồm cả các nhánh con):

$ git rebase feature master

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

Tuy nhiên, đây là kết quả thực tế:

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

Tôi biết tôi có thể dễ dàng sửa nó theo cách thủ công bằng cách thực hiện:

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

Nhưng có cách nào để tự động rebase branch bao gồm tất cả các con / con của nó không?


2
Xem thêm cách tôi căn cứ lại toàn bộ lịch sử con - một số nhánh, với một số liên kết giữa chúng là kết quả của việc hợp nhất . Phần khó chịu của giải pháp đó là cần phải đặt lại các tham chiếu của nhánh chủ đề về các cam kết được phục hồi mới sau đó.
imz - Ivan Zakharyaschev

nhờ đề cập đến tùy chọn --onto cho rebase git - nó giải quyết vấn đề của tôi
jackocnr

6
Không nên $ git rebase feature master$ git rebase master feature?
hbogert

Có thể trùng lặp của Git rebase cây con
carnicer

Câu trả lời:


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

3
Rebase lên đòi hỏi phụ huynh những lâu đời nhất cam kết để phân định sự khởi đầu - do đó C ^
Adam Dymitruk

3
Lệnh "git branch" không xuất ra một dấu sao trước nhánh hiện tại, làm hỏng tập lệnh này nếu một trong các nhánh để rebase hiện đã được kiểm tra?
Mark Lodato

2
Nhánh git không phải là lệnh sứ sao? Có cách nào để làm điều này, đó là bằng chứng tương lai hơn một chút không?
Chris Pfohl

7
Adam: Không chắc đó là con đường để đi, bạn muốn có các dòng với dấu *, bạn chỉ không muốn bản thân dấu *. Một cái gì đó như | tr -d * sẽ phù hợp hơn. Tuy nhiên, câu hỏi của tôi là: Tại sao điều này lại sử dụng --onto B? Tôi nghĩ nó nên được phục hồi trên đầu của chủ nhân. Cũng không phải C ^ không giống với B? vì vậy chúng ta sẽ giảm từ B (không bao gồm?) đến mỗi nhánh chứa C ở trên ... B. Kết quả sẽ không giống hệt như trước?
Marenz

4
Không nên --onto Fthay vì điều đó --onto B, vì tất cả những cam kết này đều dồn vào B, và chúng ta chuyển chúng vào F ?
Quảng cáo N

12

Một vài năm trước, tôi đã viết một cái gì đó để xử lý những việc như thế này. (Những ý kiến ​​đóng góp để cải thiện tất nhiên là được hoan nghênh, nhưng đừng đánh giá quá nhiều - nó đã lâu rồi! Tôi thậm chí còn chưa biết Perl!)

Nó dành cho các tình huống tĩnh hơn - bạn định cấu hình nó bằng cách đặt các thông số cấu hình của biểu mẫu branch.<branch>.autorebaseparent. Nó sẽ không chạm vào bất kỳ nhánh nào không có thông số cấu hình đó. Nếu đó không phải là những gì bạn muốn, bạn có thể hack nó đến nơi bạn muốn mà không gặp quá nhiều khó khăn. Tôi đã không thực sự sử dụng nó nhiều trong một hoặc hai năm qua, nhưng khi tôi sử dụng nó, nó luôn có vẻ khá an toàn và ổn định, trong chừng mực có thể với tính năng khôi phục tự động hàng loạt.

Vì vậy, nó đây. Sử dụng nó bằng cách lưu nó vào một tệp có tên git-auto-rebasetrong của bạn PATH. Bạn cũng nên sử dụng -ntùy chọn dry run ( ) trước khi thử thực tế. Nó có thể chi tiết hơn một chút so với những gì bạn thực sự muốn, nhưng nó sẽ cho bạn thấy những gì nó sẽ cố gắng căn cứ lại và những gì. Có thể giúp bạn bớt đau buồ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=$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

Một điều mà tôi đã tìm thấy, kể từ khi tôi giải quyết vấn đề này ban đầu, là đôi khi câu trả lời là bạn không thực sự muốn rebase chút nào! Có điều cần nói là hãy bắt đầu các nhánh chủ đề ở đúng tổ tiên chung ngay từ đầu và không cố gắng chuyển chúng về phía trước sau đó. Nhưng đó là giữa bạn và quy trình làm việc của bạn.


Đã ủng hộ "sử dụng hợp nhất thay thế". Tôi đã dành vài giờ để cố gắng lấy lại nhiều nhánh chủ đề và chủ đề phụ trước khi thử tùy chọn hợp nhất, và hợp nhất thực sự dễ thực hiện hơn nhiều, mặc dù chủ đề mới khác xa so với chủ đề ban đầu.
davenpcj

3
Nó làm tôi sợ một chút rằng câu trả lời bao gồm: "Tôi thậm chí không biết Perl chưa" - đặc biệt là kể từ khi câu trả lời là không được viết bằng Perl ... :-)
Peter V. Mørch

@ PeterV.Mørch, nghĩa là gì?
Pacerier

0

Nếu cần cập nhật ngày cam kết , GIT_COMMITTER_DATEcó thể sử dụng biến môi trường ( thủ công ). Cũng sử dụng --formattùy chọn để lấy tên chi nhánh mà không cần định dạng bổ sung.

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: nó là cần thiết để thiết lập một trong hai --committer-date-is-author-datehoặc GIT_COMMITTER_DATEđể đảm bảo kiểm tra tương tự cho C', Ca'Cb'cam kết (trên rebasing tính năng , tính năng mộttính năng b tương ứng).

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.