git rebase для ветки и её подветок одной командой
От: halo Украина  
Дата: 28.05.18 11:53
Оценка: 7 (2)
У нас на проекте существует следующая организация веток в Git-репозитории:

master
\- dev
  \- feature-####
    \- task-####


Время от времени в dev-ветку мы сливаем feature-ветки, работу над которыми считаем завершённой. По такому же принципу в feature-ветки мы сливаем task-ветки. После таких слияний над остальными ветками, которые ещё не объединены со своими родительскими, мы делаем rebase (обязательно с опцией -p) поверх новых изменений в родительской ветке. Это довольно утомительно и затратно по времени, если у ветки, которой нужен rebase, есть ещё дочерние ветки, потому как для каждой из них нужно повторить rebase отдельно, не забыв ни одной ветки.

В принципе, можно реализовать такую стратегию rebase в полуавтоматическом режиме с помощью скриптов, но кажется, что это будет велосипед непроверенного качества. Некоторое время назад у меня получилось написать устанавливаемый в git-директорию Python-скрипт, но в его стабильности и удобности я до конца не уверен + не хочу таскать его с собой, равно как и сам Python. Думаю, можно было бы даже как-то реализовать задумку с помощью более-менее простого скрипта на bash (git-rev-parse, git-rev-list, git-merge-base), и чуть ли не в алиасы его прописать, но и тут не уверен. Например, что делать если одну из дочерних веток нельзя объединить автоматически.

Существуют ли уже готовые инструменты для Git, умеющие переносить ветки вместе со всеми их дочерними ветками?
Отредактировано 28.05.2018 11:58 halo . Предыдущая версия .
git rebase
Re: git rebase для ветки и её подветок одной командой
От: netch80 Украина http://netch80.dreamwidth.org/
Дата: 28.05.18 13:00
Оценка: +1
Здравствуйте, halo, Вы писали:

H>У нас на проекте существует следующая организация веток в Git-репозитории:


H>
H>master
H>\- dev
H>  \- feature-####
H>    \- task-####
H>


H>Время от времени в dev-ветку мы сливаем feature-ветки, работу над которыми считаем завершённой. По такому же принципу в feature-ветки мы сливаем task-ветки. После таких слияний над остальными ветками, которые ещё не объединены со своими родительскими, мы делаем rebase (обязательно с опцией -p) поверх новых изменений в родительской ветке. Это довольно утомительно и затратно по времени, если у ветки, которой нужен rebase, есть ещё дочерние ветки, потому как для каждой из них нужно повторить rebase отдельно, не забыв ни одной ветки.


А зачем вообще это, чем вам это лучше merge в такой многоветвистой обстановке?
The God is real, unless declared integer.
Re: git rebase для ветки и её подветок одной командой
От: Буравчик Россия  
Дата: 31.05.18 20:05
Оценка: 2 (1)
Здравствуйте, halo, Вы писали:

H>Существуют ли уже готовые инструменты для Git, умеющие переносить ветки вместе со всеми их дочерними ветками?


Гугл выдает достаточно много статей на тему "git rebase child branches"
Там и готовые bash-скрипты есть
Best regards, Буравчик
Re: git rebase для ветки и её подветок одной командой
От: · Великобритания  
Дата: 01.06.18 20:51
Оценка:
Здравствуйте, halo, Вы писали:

h> Существуют ли уже готовые инструменты для Git, умеющие переносить ветки вместе со всеми их дочерними ветками?

Лучше так не делать. А как переносить дочерние ветки и дочерние ветки дочерних веток в локальных репах разработчиков? Это даже теоретически невозможно во многих случаях, система-то распределённая.
Действуйте локально — пара веток master/feature и пара feature/task — взаимодействуют одинаково, не надо изобретать глобальный процесс мешая всё со всем.
Делайте регулярно обратный мерж из master в feature-ветки. И, _только если очень хочется_ (любопытно — для чего?) — делайте ребейз feature-ветки один раз только перед мержем её в master, а потом сразу же её удаляйте.
И ровно тот же процесс для task-веткок: в task-ветки делается merge из feature и при желании rebase перед мержем в feature.
avalon/2.0.6
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re[2]: git rebase для ветки и её подветок одной командой
От: halo Украина  
Дата: 02.06.18 06:49
Оценка:
Здравствуйте, netch80, Вы писали:

N>А зачем вообще это, чем вам это лучше merge в такой многоветвистой обстановке?


merge сам по себе приводит к засорению графа изменений. Поэтому мы оставили такую возможность только для слияния изменений в родительские ветки.
Re[2]: git rebase для ветки и её подветок одной командой
От: halo Украина  
Дата: 02.06.18 06:53
Оценка:
Здравствуйте, Буравчик, Вы писали:

Б>Гугл выдает достаточно много статей на тему "git rebase child branches"

Б>Там и готовые bash-скрипты есть

Да, я как-то это упустил из виду, и искал то ли по "multiple" вместо "child", то ли ещё как-то. А вот bash-скрипты раньше точно не видел, но пока не уверен, что они (например, git-rebase-all) делают это так, как мне нужно + не уверен, насколько они хорошо работают, если rebase останавливается из-за конфликтов и пустых изменений. Буду разбираться, спасибо!
Re[2]: git rebase для ветки и её подветок одной командой
От: halo Украина  
Дата: 02.06.18 07:01
Оценка:
Здравствуйте, ·, Вы писали:

·>Лучше так не делать. А как переносить дочерние ветки и дочерние ветки дочерних веток в локальных репах разработчиков? Это даже теоретически невозможно во многих случаях, система-то распределённая.


И да, и нет: у нас не такая масштабная разработка и у нас есть договорённость почти эксклюзивного доступа к веткам на этапе разработки. И даже если на апстриме появятся изменения от других разработчиков, их весьма просто слить в локальную ветку (имеется в виду rebase). По крайней мере, нам пока это не мешает.

·>Делайте регулярно обратный мерж из master в feature-ветки. И, _только если очень хочется_ (любопытно — для чего?) — делайте ребейз feature-ветки один раз только перед мержем её в master, а потом сразу же её удаляйте.

·>И ровно тот же процесс для task-веткок: в task-ветки делается merge из feature и при желании rebase перед мержем в feature.

Такого merge мы как раз избегаем по той причине, что было бы очень сложно просматривать граф изменений. Да, rebase привносит некоторые трудности в виде "асинхронности" изменений и более частых конфликтов, разбирать которые никому не нравится (кстати, git-rerere для таких сценариев предназначен?). Но с другой стороны: это терпимо и даёт очень, я бы даже сказал, красивые результаты. Мы довольны.
Re[3]: git rebase для ветки и её подветок одной командой
От: · Великобритания  
Дата: 02.06.18 17:04
Оценка:
Здравствуйте, halo, Вы писали:

H>·>Лучше так не делать. А как переносить дочерние ветки и дочерние ветки дочерних веток в локальных репах разработчиков? Это даже теоретически невозможно во многих случаях, система-то распределённая.

H>И да, и нет: у нас не такая масштабная разработка и у нас есть договорённость почти эксклюзивного доступа к веткам на этапе разработки. И даже если на апстриме появятся изменения от других разработчиков, их весьма просто слить в локальную ветку (имеется в виду rebase). По крайней мере, нам пока это не мешает.
Если у вас всё такое централизованное, то попробуйте gerrit, он позволяет этим управлять проще, с помощью changesets и topics.

H>·>Делайте регулярно обратный мерж из master в feature-ветки. И, _только если очень хочется_ (любопытно — для чего?) — делайте ребейз feature-ветки один раз только перед мержем её в master, а потом сразу же её удаляйте.

H>·>И ровно тот же процесс для task-веткок: в task-ветки делается merge из feature и при желании rebase перед мержем в feature.
H>Такого merge мы как раз избегаем по той причине, что было бы очень сложно просматривать граф изменений.
А конкретно что там сложно? Неужели это сложнее резолва частых конфликтов? Ветки по идее не должны жить долго, поэтому в ветках сильно хитрого графа получаться не должно.

H>Да, rebase привносит некоторые трудности в виде "асинхронности" изменений и более частых конфликтов, разбирать которые никому не нравится

H>(кстати, git-rerere для таких сценариев предназначен?).
Он скроее для другого. Когда у тебя есть некий продукт со множеством поддерживаемых текущих версий (иначе говоря, множество веток master) и примерно одни и те же изменения надо вливать в разные ветки.

H>Но с другой стороны: это терпимо и даёт очень, я бы даже сказал, красивые результаты. Мы довольны.

В смысле линейная история в master? Ну не знаю, она не бесплатно даётся, поэтому не самоцель.
но это не зря, хотя, может быть, невзначай
гÅрмония мира не знает границ — сейчас мы будем пить чай
Re: git rebase для ветки и её подветок одной командой
От: halo Украина  
Дата: 04.07.18 12:54
Оценка:
Те скрипты, которые мне удалось найти в Сети, делают либо непонятно что, либо делают это не так, как я себе это представлял. Пришлось построить весьма монструозный велосипед. Вдруг пригодится кому-то. Итак, скрипт:


Технически, использует следующие аспекты git:


  git-rebase-all.sh — реализация на bash
  пролог
#!/bin/bash

set +x
set -e


  "ядро"
############
### core ###
############

function die {
    echo_stderr "fatal: $1"
    exit 1
}

function echo_stderr {
    >&2 echo "$1"
}

function require {
    if [ -z "$1" ]; then
        die "$2 is not specified"
    fi
}

function trim {
    local trimmed="$1"
    trimmed="${trimmed## }"
    trimmed="${trimmed%% }"
    echo -n "$trimmed"
}


  git
###########
### git ###
###########

GIT_REPO_DIRECTORY="$(git rev-parse --git-dir 2> /dev/null)"
GIT_REPO_SEQUENCE_STATE_FILE=$GIT_REPO_DIRECTORY/REBASE-ALL-SEQUENCE.LST

function git.require_repo {
    if [ -z "$GIT_REPO_DIRECTORY" ]; then
        die 'must run in a git repo'
    fi
}

function git.list_rebase_branch_refs {
    local upstream="$1"
    local branch="$2"
    require "$upstream" 'upstream'
    require "$branch" 'branch'
    git for-each-ref --contains "$branch" --no-merged "$upstream" refs/heads --format='%(refname)'
}

function git.list_rebase_all_refs {
    git for-each-ref refs/rebase-all --format='%(refname)'
}

function git.get_descendant_refs_or_self {
    local UPSTREAM=$1
    local DOWNSTREAM=$2
    git for-each-ref --no-merged "$UPSTREAM" --contains "$DOWNSTREAM" 'refs/heads' --format='%(refname)'
}

function git.get_branch_name {
    local object="$1"
    require "$object"
    git rev-parse --abbrev-ref "$object"
}

function git.group_refs_by_object {
    git show-ref "$@" \
        | sort
}

function git.extract_head_refs {
    local -A KNOWN_REFS
    local ARG_COUNT=$#
    for ((i = 0; i < $ARG_COUNT; i += 2)); do
        local OBJECT=$1; shift
        local REF=$1; shift
        if [[ ! ${KNOWN_REFS[$OBJECT]} ]]; then
            KNOWN_REFS[$OBJECT]="$REF"
            echo "$REF"
        fi
    done
}

function git.extract_tail_refs {
    local -A KNOWN_REFS
    local ARG_COUNT=$#
    for ((i = 0; i < $ARG_COUNT; i += 2)); do
        local OBJECT=$1; shift
        local REF=$1; shift
        if [[ ! ${KNOWN_REFS[$OBJECT]} ]]; then
            KNOWN_REFS[$OBJECT]="$REF"
        else
            echo "${KNOWN_REFS[$OBJECT]} $REF"
        fi
    done
}

function git.show_branches_histogram_output {
    local FOUND=
    while IFS= read -r LINE; do
        if [[ $FOUND ]]; then
            # leading whitespace chars are trimmed so \b is a non-null but unprintable character
            echo -e "\b $LINE"
        else
            if [[ "$LINE" == -* ]]; then
                FOUND=1
            fi
        fi
    done < <(git show-branch --no-color $@)
}

function git.convert_histogram_output_to_rebase_sequence {
    local UPSTREAM=$1
    local HISTOGRAM_OUTPUT=$2
    local HEADS=$(echo "$HISTOGRAM_OUTPUT" \
        | sed -E 's/^(.+) \[(.+)\].*$/\1 \2/g' \
        | sed -E '/\^$/d' \
        | sed -E '/~[0-9]+$/d' \
        | tac)
    local HEAD_COUNT=$(echo "$HEADS" \
        | wc -l)
    local -a TREE_HEADS
    local SORTED_TREE_HEADS
    for ((i = 0; i <= $HEAD_COUNT - 1; i++)); do
        TREE_HEADS[$i]=$(echo "$HEADS" \
            | grep -P '^...{'$((i))'}[^ ].{'$((HEAD_COUNT - i))'}.' \
            | sed -E 's/^...{'$((HEAD_COUNT))'} //')
            # destroy the new lines
        TREE_HEADS[$i]=$(echo ${TREE_HEADS[$i]})
    done
    function printTreeHeads {
        for ((i = 0; i <= $HEAD_COUNT - 1; i++)); do
            echo "${TREE_HEADS[i]}"
        done
    }
    SORTED_TREE_HEADS=$(printTreeHeads \
        | sort)
    echo "$SORTED_TREE_HEADS" \
        | sed 's/^/'"$UPSTREAM"' /' \
        | rev \
        | cut -d' ' -f1,2 \
        | rev
}


  состояние
#############
### state ###
#############

function state.require_no_state {
    local any_refs=$(git.list_rebase_all_refs | head -1)
    if [ ! -z "$any_refs" ]; then
        die 'refs/rebase-all/* not empty. rebase-all in progress?'
    fi
}

function state.require_state {
    local any_refs=$(git.list_rebase_all_refs | head -1)
    if [ -z "$any_refs" ]; then
        die 'refs/rebase-all/* empty. no rebase-all in progress?'
    fi
}


  основная работа
############
### main ###
############

function main.abort {
    git.require_repo
    state.require_state
    git.list_rebase_all_refs \
        | while read OLD_REF; do
            if [[ $OLD_REF != refs/rebase-all/* ]]; then
                die "must never happen, unexpected ref $OLD_REF"
            fi
            local ref="${OLD_REF:16}"
            git update-ref "$ref" "$OLD_REF"
            git update-ref -d "$OLD_REF"
            echo_stderr "$ref := $(git rev-parse $ref)"
        done
    git rebase --abort 2> /dev/null
    rm -f "$GIT_REPO_SEQUENCE_STATE_FILE"
}

function main.continue {
    git.require_repo
    state.require_state
    while true; do
        IFS='' read -r line < "$GIT_REPO_SEQUENCE_STATE_FILE"
        IFS=' ' read -ra refs <<< "$line"
        local upstream="${refs[0]}"
        local downstream="${refs[1]}"
        if [[ -z "$upstream" || -z "$downstream" ]]; then
            break
        fi
        local branch="$(git rev-parse --abbrev-ref $downstream)"
        echo_stderr "$upstream + $downstream ($branch)"
        git checkout "$branch" 2> /dev/null
        git rebase -p --keep-empty --allow-empty-message "$upstream"
        local lastError="$?"
        if [[ "$lastError" != '0' ]]; then
            exit "$lastError"
        fi
        echo "$(sed -e 1,1d < "$GIT_REPO_SEQUENCE_STATE_FILE")" > "$GIT_REPO_SEQUENCE_STATE_FILE"
    done
    main.quit
}

function main.help {
    echo 'usage: rebase-all.sh <upstream>'
    echo '   or: rebase-all.sh --status   - show current status'
    echo '   or: rebase-all.sh --continue - go to the next rebase operation'
    echo '   or: rebase-all.sh --abort    - revert all refs'
    echo '   or: rebase-all.sh --quit     - clear rebase-all state but do not restore old refs'
    echo '   or: rebase-all.sh --help     - show this help'
}

function main.quit {
    git.require_repo
    state.require_state
    git.list_rebase_all_refs \
        | while read OLD_REF; do
            if [[ $OLD_REF != refs/rebase-all/* ]]; then
                die "must never happen, unexpected ref $OLD_REF"
            fi
            git update-ref -d "$OLD_REF"
        done
    rm -f "$GIT_REPO_SEQUENCE_STATE_FILE"
}

function main.rebase {
    local upstream="$1"
    require "$upstream" 'upstream'
    git.require_repo
    state.require_no_state
    local branch="$(git.get_branch_name @)"
    if [[ "$branch" == 'HEAD' ]]; then
        die 'cannot rebase detached HEAD'
    fi
    git.list_rebase_branch_refs "$upstream" "$branch" \
        | while read REF; do
            git update-ref "refs/rebase-all/$REF" "$REF"
            echo_stderr "$REF = $(git rev-parse $REF)"
        done
    local allRefsToRebase=$(git.get_descendant_refs_or_self $upstream $branch)
    local refsGroupedByObject=$(git.group_refs_by_object $allRefsToRebase)
    local refsToRebase=$(git.extract_head_refs $refsGroupedByObject)
    local refsToReset=$(git.extract_tail_refs $refsGroupedByObject)
    local histogramOutput=$(git.show_branches_histogram_output $refsToRebase)
    local rebaseSequence=$(git.convert_histogram_output_to_rebase_sequence "$upstream" "$histogramOutput")
    echo_stderr "$rebaseSequence"
    echo "$rebaseSequence" > "$GIT_REPO_SEQUENCE_STATE_FILE"
    main.continue
}

function main.status {
    git.require_repo
    state.require_state
    git.list_rebase_all_refs \
        | while read OLD_REF; do
            if [[ $OLD_REF != refs/rebase-all/* ]]; then
                die "must never happen, unexpected ref $OLD_REF"
            fi
            local ref="${OLD_REF:16}"
            echo_stderr "$ref = $(git rev-parse $ref)"
        done
    cat "$GIT_REPO_SEQUENCE_STATE_FILE"
}


  запуск приложения
if [ -z "$1" ]; then
    main.status
else
    COMMAND=
    case "$1" in
    --abort) COMMAND='ABORT'; shift;;
    --continue) COMMAND='CONTINUE'; shift;;
    --help) COMMAND='HELP'; shift;;
    --quit) COMMAND='QUIT'; shift;;
    --status) COMMAND='STATUS'; shift;;
    *) COMMAND='REBASE';;
    esac
    case "$COMMAND" in
    ABORT) main.abort;;
    CONTINUE) main.continue;;
    HELP) main.help;;
    QUIT) main.quit;;
    REBASE) main.rebase $1;;
    STATUS) main.status;;
    *) die "unknown command: $COMMAND";;
    esac
fi


Скрипт требует доработки (как минимум: возврата на оригинальную ветку), поэтому использовать его в такой реализации следует только на свой страх, риск и понимание того, как можно восстановить ветки в случае сбоя скрипта. Также следует учесть, что, если несколько ссылок указывает на один и тот же коммит, скрипт не в состоянии их отличить и пробует обработать их в алфавитном порядке. Я проверял работоспособность только в git 2.18.0, что, возможно, не даёт переносить пустые коммиты (хотя они сохраняются при git rebase -i -- баг 2.18.0?), и только под Linux.

Пример

Допустим, есть следующая топология ветвей в упрощённом варианте (E=epic, US=user-story, T=task):

  * dev
  * dev
  |       * T-521-candidate-B
  |      /
  |     | * T-521-candidate-A
  |     |/
  |     * T-521
  |    /
  |   * US-474
  |  /
  | * E-128
  |/
  * dev
 /
* master


Требуется перенести E-128 на dev вместе cо всеми дочерними ветками: US-474, T-521, T-521-candidate-A и T-521-candidate-B:

git checkout E-128
./git-rebase-all.sh dev


  пример вывода при благоприятном исходе
refs/heads/E-128 = 7dae7b3f713be9fa966b1b621ffcdc74ad8f0b1c
refs/heads/US-474 = 8621e6506a8a55b9d4ecef4c1c4d1b7d22e514fa
refs/heads/T-521 = 80cd2878c076a2cb83c99d6ee0e44de86bffea6d
refs/heads/T-521-candidate-B = a14cfd68f41372e5cdbbc067c53d9c38a8ebaae8
refs/heads/T-521-candidate-A = a97700a43c4dd54d2789c766309a69932714e4a1
dev refs/heads/E-128
refs/heads/E-128 refs/heads/US-474
refs/heads/US-474 refs/heads/T-521
refs/heads/T-521 refs/heads/T-521-candidate-B
refs/heads/T-521 refs/heads/T-521-candidate-A
dev + refs/heads/E-128 (E-128)
Your branch is up to date with 'origin/E-128'.
Successfully rebased and updated refs/heads/E-128.
refs/heads/E-128 + refs/heads/US-474 (US-474)
Your branch is up to date with 'origin/US-474'.
Successfully rebased and updated refs/heads/US-474.
refs/heads/US-474 + refs/heads/T-521 (T-521)
Your branch is up to date with 'origin/T-521'.
Successfully rebased and updated refs/heads/T-521.
refs/heads/T-521 + refs/heads/T-521-candidate-B (T-521-candidate-B)
Your branch is up to date with 'origin/T-521-candidate-B'.
Successfully rebased and updated refs/heads/T-521-candidate-B.
refs/heads/T-521 + refs/heads/T-521-candidate-A (T-521-candidate-A)
Your branch is up to date with 'origin/T-521-candidate-A'.
Successfully rebased and updated refs/heads/T-521-candidate-A.


После этого ветки будут в следующем состоянии:

        * T-521-candidate-B
       /
      | * T-521-candidate-A
      |/
      * T-521
     /
     * US-474
     /
    * E-128
   /
  * dev
  * dev
  * dev
 /
* master
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.