Сброс дерева (фиксация/ветвь и все его дочерние элементы)

Это мое текущее дерево git:

A - H (master)
|
\- B - C - D - E (feature)
           |
           \- F (new)
           |
           \- G (other)

И я бы хотел перегрузить боковую ветвь так, чтобы она зависела от H, а не от A:

A - H (master)
    |
    \- B'- C'- D'- E'(feature)
               |
               \- F'(new)
               |
               \- G'(other)

Легкая концепция, трудно сделать автоматически, похоже. Это уже было задано здесь и here, но предлагаемые решения не работаю для меня.

Во-первых, как указано в первом, вывод git branch не является тривиальным для синтаксического анализа, когда существует текущая ветвь (имеется предварительный набор *). Но это не пробник, в моем случае я могу легко указать имена feature, new и other вручную или убедиться, что текущая ветка master.

Затем я попытался с этими командами:

git rebase --committer-date-is-author-date --preserve-merges --onto master feature^ feature
git rebase --committer-date-is-author-date --preserve-merges --onto master feature^ new
git rebase --committer-date-is-author-date --preserve-merges --onto master feature^ other

и я получаю:

A - H (master)
    |
    \- E'(feature)
    |
    \- B' - C' - D' - F'(new)
    |
    \- B" - C" - D" - G'(other)

Определенно не то, что я хочу! Или, если я использую B^ вместо feature^, тогда я также получаю историю B - C - D в ветки feature.

Итак, любое дальнейшее предложение о том, как сделать это более или менее автоматически?

РЕДАКТОР: Это похоже на работу с этим:

git checkout feature
git merge other
git merge new
git rebase -p master feature

Теперь, по крайней мере, дерево выглядит корректно, мне просто нужно переместить головки ветвей вправо, чтобы они добрались до слияний... что можно сделать с помощью

git checkout master
git branch -f new feature^2
git branch -f feature feature^1
git branch -f other feature^2
git branch -f feature feature^1

Ответ 1

А, я не заметил, что ваши команды были основаны на принятом ответе на ваш первый связанный вопрос. В любом случае, вы совершенно явно задали вопрос о том, что вы получили: каждый из E, F и G переустановлен на master.

Я думаю, что вы хотели:

git rebase ... --onto master A feature

для изменения

A - H (master)
|
\- B - C - D - E (feature)

к

A - H (master)
    |
    \- B'- C'- D'- E'(feature)

(мастер - это новый корень, A - старый корень. Использование функции ^ как старого корня означает, что вы только пересадили последнюю фиксацию функции, как вы видели)

И затем:

git rebase ... --onto D' D new
git rebase ... --onto D' D other

чтобы отделить новое и другое от D и пересадить их на D '. Обратите внимание, что после того, как вы сбросили функцию, feature^ означает D', а не D.


Что касается автоматизации процесса, я могу показать вам что-то подобное, но сложной частью является обработка ошибок и восстановление.

transplant_tree.sh

#!/bin/bash
trap "rm -f /tmp/$$.*" EXIT

function transplant() # <from> <to> <branch>
{
    OLD_TRUNK=$1
    NEW_TRUNK=$2
    BRANCH=$3

    # 1. get branch revisions
    REV_FILE="/tmp/$$.rev-list.$BRANCH"
    git rev-list $BRANCH ^$OLD_TRUNK > "$REV_FILE" || exit $?
    OLD_BRANCH_FORK=$(tail -1 "$REV_FILE")
    OLD_BRANCH_HEAD=$(head -1 "$REV_FILE")
    COMMON_ANCESTOR="${OLD_BRANCH_FORK}^"

    # 2. transplant this branch
    git rebase --onto $NEW_TRUNK $COMMON_ANCESTOR $BRANCH

    # 3. find other sub-branches:
    git branch --contains $OLD_BRANCH_FORK | while read sub;
    do
        # 4. figure out where the sub-branch diverges,
        # relative to the (old) branch head
        DISTANCE=$(git rev-list $OLD_BRANCH_HEAD ^$sub | wc -l)

        # 5. transplant sub-branch from old branch to new branch, attaching at
        # same number of commits before new HEAD
        transplant $OLD_BRANCH_HEAD ${BRANCH}~$DISTANCE  $sub
    done
}

transplant $1 $2 $3

для вашего использования, transplant_tree.sh master master feature должен работать, предполагая, что все пересоединения успешны. Это выглядело бы так:

  • OLD_TRUNK = NEW_TRUNK = мастер, функция BRANCH =
    • получить изменения в ветке
      • OLD_BRANCH_FORK = B
      • OLD_BRANCH_HEAD = Е
      • COMMON_ANCESTOR = B ^ == A
    • пересадить эту ветку
      • git rebase --onto master B^ feature
    • найти другие подразделы
      • к югу = новый
        • DISTANCE = $(git rev-list E ^new | wc -l) == 1
        • recurse with OLD_TRUNK = E, NEW_TRUNK = feature ~ 1, BRANCH = new
      • югу = другие
        • и др.

Если одна из неудач отказов, если она позволяет вам исправить ее вручную и как-то возобновить? Должна ли она вернуть все это?

Ответ 2

Это моя первая попытка использования "сантехнических" команд, поэтому не стесняйтесь предлагать улучшения.

Две мощные команды, которые вы можете использовать здесь, чтобы понять, как перемещать это дерево, - это git for-each-ref и git rev-list:

  • git for-each-ref refs/heads --contains B дает вам все (локальные) ссылки, которые вы хотите переместить, то есть feature, new, other
  • git rev-list ^B^ feature new other дает вам все фиксации, которые вы хотите переместить.

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

Предположим, что для согласованности с rebase вы приводите аргументы как:
git transplant-onto H A <list of branches>, тогда вы можете использовать следующее (и помещать его под git-transplant-onto в свой путь):

#!/usr/bin/env bash

function usage() {
    echo
    echo "Usage: $0 [-n] [-v] <onto> <from> <ref-list>"
    echo "    Transplants the tree from <from> to <onto>"
    echo "    i.e. performs git rebase --onto <onto> <from> <ref>, for each <ref> in <ref-list>"
    echo "    while maintaining the tree structure inside <ref-list>"
    exit $1
}

dry_run=0
verbose=0
while [[ "${1::1}" == "-" ]]; do
    case $1 in
        -n) dry_run=1 ;;
        -v) verbose=1 ;;
        *) echo Unrecognized option $1; usage -1 ;;
    esac
    shift
done

# verifications
if (( $# < 2 )) || ! onto=$(git rev-parse --verify $1) || ! from=$(git rev-parse --verify $2); then usage $#; fi
git diff-index --quiet HEAD || {echo "Please stash changes before transplanting"; exit 3}

# get refs to move
shift 2
if (( $# == 0 )); then
    refs=$(git for-each-ref --format "%(refname)" refs/heads --contains $from)
else
    refs=$(git show-ref --heads --tags [email protected] | cut -d' ' -f2)
    if (( $# != $(echo "$refs" | wc -l) )); then echo Some refs passed as arguments were wrong; exit 4; fi
fi

# confirm
echo "Following branches will be moved: "$refs
REPLY=invalid
while [[ ! $REPLY =~ ^[nNyY]?$ ]]; do read -p "OK? [Y/n]"; done
if [[ $REPLY =~ [nN] ]]; then exit 2; fi

# only work with non-redundant refs
independent=$(git merge-base --independent $refs)

if (( verbose )); then
    echo INFO:
    echo independent refs:;git --no-pager show -s --oneline --decorate $independent
    echo redundant refs:;git --no-pager show -s --oneline --decorate $(git show-ref $refs | grep -Fwvf <(echo "$independent") )
fi

# list all commits, keep those that have 0 or 2+ children
# so we rebase only forks or leaves in our tree
tree_nodes=$(git rev-list --topo-order --children ^$from $independent | sed -rn 's/^([0-9a-f]{40})(( [0-9a-f]{40}){2,})?$/\1/p')

# find out ancestry in this node list (taking advantage of topo-order)
declare -A parents
for f in $tree_nodes; do
    for p in ${tree_nodes#*$h} $from; do
        if git merge-base --is-ancestor $p $h ; then
            parents[$h]=$p
            break
        fi
    done
    if [[ ${parents[$h]:-unset} = unset ]]; then echo Failed at finding an ancestor for the following commit; git --no-pager show -s --oneline --decorate $h; exit 2; fi
done

# prepare to rebase, remember mappings
declare -A map
map[$from]=$onto

# IMPORTANT! this time go over in chronological order, so when rebasing a node its ancestor will be already moved
while read h; do
    old_base=${parents[$h]}
    new_base=${map[$old_base]}
    git rebase --preserve-merges --onto $new_base $old_base $h || {
        git rebase --abort
        git for-each-ref --format "%(refname:strip=2)" --contains $old_base refs/heads/ | \
            xargs echo ERROR: Failed a rebase in $old_base..$h, depending branches are:
        exit 1
    }
    map[$h]=$(git rev-parse HEAD)
done < <(echo "$tree_nodes" | tac)

# from here on, all went well, all branches were rebased.
# update refs if no dry-run, otherwise show how

ref_dests=
for ref in $refs; do
    # find current and future hash for each ref we wanted to move
    # all independent tags are in map, maybe by chance some redundant ones as well
    orig=$(git show-ref --heads --tags -s $ref)
    dest=${map[$orig]:-unset}

    # otherwise look for a child in the independents, use map[child]~distance as target
    if [[ $dest = unset ]]; then
        for child in $independent; do
            if git merge-base --is-ancestor $ref $child ; then
                dest=$(git rev-parse ${map[$child]}~$(git rev-list $ref..$child | wc -l) )
                break
            fi
        done
    fi

    # finally update ref
    ref_dests+=" $dest"
    if (( dry_run )); then
        echo git update-ref $ref $dest
    else
        git update-ref $ref $dest
    fi
done

if (( dry_run )); then
    echo
    echo If you apply the update-refs listed above, the tree will be:
    git show-branch $onto $ref_dests
else
    echo The tree now is:
    git show-branch $onto $refs
fi

Другим способом является получение всех индивидуальных коммитов со своим родителем в порядке, который вы можете перенести (скажем git rev-list --topo-order --reverse --parents), а затем использовать git am для каждого отдельного коммита.

Ответ 3

В этих случаях приятным трюком является объединение (объединение) всех ветвей переместился в окончательную фиксацию node. После этого используйте rebase с --preserve-merges для перемещения полученного заключенного поддерева (множество ветвей).

Создание закрытого поддерева, содержащего все ветки, раскрывает 2 узла (начало и конец), которые используются в качестве входных параметров для rebase команда.

Конец закрытого поддерева - это искусственный node, который может удаляются после перемещения дерева, а также другие узлы, которые возможно, были созданы для объединения других ветвей.

Посмотрим на следующий случай.

Разработчик хочет вставить новый фиксатор (мастер) в другие 3 ветки развития (b11, b2, b3). Один из них (b11) слияние ветки признаков b12, основанной на b1. Другие 2 ветки (b2, b3) расходятся.

Конечно, разработчик мог бы забрать это новое коммитирование в каждая из этих ветвей, но разработчик может предпочесть не имеют одну и ту же фиксацию в трех разных ветвях, но только 1 commit до того, как ветки расходятся.

* baa687d (HEAD -> master) new common commit
| * b507c23 (b11) b11
| *   41849d9 Merge branch 'b12' into b11
| |\
| | * 20459a3 (b12) b12
| * | 1f74dd9 b11
| * | 554afac b11
| * | 67d80ab b11
| |/
| * b1cbb4e b11
| * 18c8802 (b1) b1
|/
| * 7b4e404 (b2) b2
| | * 6ec272b (b3) b3
| | * c363c43 b2 h
| |/
| * eabe01f header
|/
* 9b4a890 (mirror/master) initial
* 887d11b init

Для этого первым шагом является создание общего слияния, которое включает 3 ветки. Для этого временная ветка называется пакетом используется.

Слияние в пакет может создавать конфликты, но это не важно поскольку эти слияния впоследствии будут отброшены. Просто проинструктируйте git автоматически их решить, добавив опции -s recursive -Xours.

$ git checkout -b pack b11 # create new branch at 'b11' to avoid losing original refs
$ git merge -s recursive -Xours b2 # merges b2 into pack
$ git merge -s recursive -Xours b3 # merges b3 into pack

Это все дерево после слияния всего в ветку пакета:

*   b35a4a7 (HEAD -> pack) Merge branch 'b3' into pack
|\
| * 6ec272b (b3) b3
| * c363c43 b2 h
* |   60c9b7c Merge branch 'b2' into pack
|\ \
| * | 7b4e404 (b2) b2
| |/
| * eabe01f header
* | b507c23 (b11) b11
* |   41849d9 Merge branch 'b12' into b11
|\ \
| * | 20459a3 (b12) b12
* | | 1f74dd9 b11
* | | 554afac b11
* | | 67d80ab b11
|/ /
* | b1cbb4e b11
* | 18c8802 (b1) b1
|/
| * baa687d (master) new common commit
|/
* 9b4a890 initial
* 887d11b init

Теперь пришло время переместить созданное поддерево. Для этого используется следующая команда:

$ git rebase --preserve-merges --onto master master^ pack

Мастер ссылок ^ означает фиксацию перед мастером (мастерская родительский), 9b4a890 в этом случае. Эта фиксация НЕ переустанавливается, она происхождение 3 переустановленных ветвей. И, конечно, пакет является окончательной ссылкой на целое поддерево.

При перезагрузке могут возникнуть конфликты слияния. Если бы уже были конфликты, прежде чем делать слияние, они снова возникнут. Быть обязательно решить их одинаково. Для искусственных коммитов, созданных для слияния во временный пакет node, не беспокойтесь и решать их автоматически.

После rebase это будет результирующее дерево:

*   95c8d3d (HEAD -> pack) Merge branch 'b3' into pack
|\
| * d304281 b3
| * ed66668 b2 h
* |   b8756ee Merge branch 'b2' into pack
|\ \
| * | 8d82257 b2
| |/
| * e133de9 header
* | f2176e2 b11
* |   321356e Merge branch 'b12' into b11
|\ \
| * | c919951 b12
* | | 8b3055f b11
* | | 743fac2 b11
* | | a14be49 b11
|/ /
* | 3fad600 b11
* | c7d72d6 b1
|/
* baa687d (master) new common commit
|
* 9b4a890 initial
* 887d11b init

Иногда старые ссылки ветвей не могут быть перемещены (даже если дерево перемещается без них). В этом случае вы можете восстановить или изменить ссылку вручную.

Это также время, чтобы отменить дозаболочные слияния, которые сделали возможным сбрасывая все дерево. После некоторого удаления reset/checkout, это это дерево:

* f2176e2 (HEAD -> b11) b11
*   321356e Merge branch 'b12' into b11
|\
| * c919951 (b12) b12
* | 8b3055f b11
* | 743fac2 b11
* | a14be49 b11
|/
* 3fad600 b11
* c7d72d6 (b1) b1
| * d304281 (b3) b3
| * ed66668 b2 h
| | * 8d82257 (b2) b2
| |/
| * e133de9 header
|/
* baa687d (mirror/master, mirror/HEAD, master) new common commit
* 9b4a890 initial
* 887d11b init

Это именно то, чего хотел достичь разработчик: фиксация разделяется тремя ветвями.

Ответ 4

Я не думаю, что вы должны быть в состоянии действительно хотеть автоматизма по делу. Для восстановления даже простых ветвей обычно требуется справедливое внимание. Если сделано в возрасте ветки и/или больше, чем несколько коммитов, шансы на успех очень малы. И у вас, кажется, есть полный лес.

В то время как если на самом деле ситуация не в том, что в большинстве случаев нечеткие и rebase работают, то только выдача интерактивной команды rebase и сокращение вручную todo займет всего несколько секунд.

Также из природы я не вижу полного авто решения.

Но чтобы быть конструктивным, я бы попытался решить следующее:

  • сначала создайте программу, которая может анализировать историю источников и дает мне ветки дерева. Для примера выше это даст B..D на A; E..E на D; F..G на D.
  • отметьте A 'как H (от входного сигнала)
  • Примените самый нижний кусок, который основан на A: вишневый выбор (или перестановка на) B..D на A ';
  • отметьте новый верх как D '
  • применить оставшиеся куски, для которых уже выполнено сопоставление