คืนสาขารวมถึงลูก ๆ ทุกคน


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
ดูเพิ่มเติมวิธีการที่ฉันต้องการ rebase subhistory ทั้ง - หลายสาขาด้วยการเชื่อมโยงบางอย่างระหว่างพวกเขาที่เกิดจากการผสาน ส่วนที่ไม่พึงประสงค์ของการแก้ปัญหานั้นคือความจำเป็นในการรีเซ็ตสาขาของหัวข้อที่อ้างถึงการกระทำที่ rebased ใหม่ในภายหลัง
imz - Ivan Zakharyaschev

ขอบคุณที่พูดถึงตัวเลือก --onto สำหรับ git rebase - มันแก้ไขปัญหาของฉัน
jackocnr

7
ไม่ควร$ git rebase feature masterจะเป็น$ git rebase master feature?
hbogert

อาจซ้ำกันของทรีย่อย Git rebase
carnicer

คำตอบ:


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

3
Rebase ต้องใช้พาเรนต์ของการกระทำที่เก่าแก่ที่สุดเพื่อคั่นการเริ่มต้นดังนั้น C ^
Adam Dymitruk

3
คำสั่ง "git branch" ไม่แสดงผลดาวก่อนสาขาปัจจุบันหรือไม่ทำให้สคริปต์นี้เสียหายหากมีการเช็คเอาต์สาขาใดสาขาหนึ่งไปยัง rebase
Mark Lodato

2
git branch ไม่ใช่คำสั่งพอร์ซเลนหรือไม่? มีวิธีทำสิ่งนี้เพื่อพิสูจน์อนาคตอีกเล็กน้อยหรือไม่?
Chris Pfohl

7
อดัม: ไม่แน่ใจว่านั่นคือหนทางที่จะไปคุณต้องการมีเส้นกับ * คุณไม่ต้องการ * เอง สิ่งที่ชอบ | tr -d * จะเหมาะกว่า คำถามของฉันคือเหตุใดจึงใช้ - ถึง B ฉันคิดว่ามันควรจะได้รับการปรับปรุงใหม่ให้เป็นหลัก C ^ ไม่เหมือนกับ B หรือไม่? ดังนั้นเราจึงเปลี่ยนจาก B (ไม่รวม?) ไปยังแต่ละสาขาที่มี C อยู่ด้านบนของ ... B. ผลลัพธ์จะไม่เหมือนเดิมทุกประการหรือไม่?
Marenz

4
ไม่ควรที่จะ--onto Fแทนการ--onto Bเป็นกระทำทั้งหมดเหล่านี้เป็น aready บน B และเราย้ายพวกเขาไปยังF ?
Ad 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 ... :-)
Peter V. Mørch

@ 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

หมายเหตุ: จะต้องตั้งค่าอย่างใดอย่างหนึ่ง--committer-date-is-author-dateหรือGIT_COMMITTER_DATEที่จะรับประกันการตรวจสอบเช่นเดียวกันสำหรับC', Ca'และCb'กระทำ (บน rebasing คุณลักษณะ , คุณลักษณะแบบและคุณลักษณะ-Bตามลําดับ)

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.