У нас на проекте существует следующая организация веток в 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, умеющие переносить ветки вместе со всеми их дочерними ветками?
Здравствуйте, 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 для ветки и её подветок одной командой
Здравствуйте, halo, Вы писали:
h> Существуют ли уже готовые инструменты для Git, умеющие переносить ветки вместе со всеми их дочерними ветками?
Лучше так не делать. А как переносить дочерние ветки и дочерние ветки дочерних веток в локальных репах разработчиков? Это даже теоретически невозможно во многих случаях, система-то распределённая.
Действуйте локально — пара веток master/feature и пара feature/task — взаимодействуют одинаково, не надо изобретать глобальный процесс мешая всё со всем.
Делайте регулярно обратный мерж из master в feature-ветки. И, _только если очень хочется_ (любопытно — для чего?) — делайте ребейз feature-ветки один раз только перед мержем её в master, а потом сразу же её удаляйте.
И ровно тот же процесс для task-веткок: в task-ветки делается merge из feature и при желании rebase перед мержем в feature.
Здравствуйте, Буравчик, Вы писали:
Б>Гугл выдает достаточно много статей на тему "git rebase child branches" Б>Там и готовые bash-скрипты есть
Да, я как-то это упустил из виду, и искал то ли по "multiple" вместо "child", то ли ещё как-то. А вот bash-скрипты раньше точно не видел, но пока не уверен, что они (например, git-rebase-all) делают это так, как мне нужно + не уверен, насколько они хорошо работают, если rebase останавливается из-за конфликтов и пустых изменений. Буду разбираться, спасибо!
Re[2]: git rebase для ветки и её подветок одной командой
Здравствуйте, ·, Вы писали:
·>Лучше так не делать. А как переносить дочерние ветки и дочерние ветки дочерних веток в локальных репах разработчиков? Это даже теоретически невозможно во многих случаях, система-то распределённая.
И да, и нет: у нас не такая масштабная разработка и у нас есть договорённость почти эксклюзивного доступа к веткам на этапе разработки. И даже если на апстриме появятся изменения от других разработчиков, их весьма просто слить в локальную ветку (имеется в виду rebase). По крайней мере, нам пока это не мешает.
·>Делайте регулярно обратный мерж из master в feature-ветки. И, _только если очень хочется_ (любопытно — для чего?) — делайте ребейз feature-ветки один раз только перед мержем её в master, а потом сразу же её удаляйте. ·>И ровно тот же процесс для task-веткок: в task-ветки делается merge из feature и при желании rebase перед мержем в feature.
Такого merge мы как раз избегаем по той причине, что было бы очень сложно просматривать граф изменений. Да, rebase привносит некоторые трудности в виде "асинхронности" изменений и более частых конфликтов, разбирать которые никому не нравится (кстати, git-rerere для таких сценариев предназначен?). Но с другой стороны: это терпимо и даёт очень, я бы даже сказал, красивые результаты. Мы довольны.
Re[3]: git rebase для ветки и её подветок одной командой
Здравствуйте, 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 для ветки и её подветок одной командой
Те скрипты, которые мне удалось найти в Сети, делают либо непонятно что, либо делают это не так, как я себе это представлял. Пришлось построить весьма монструозный велосипед. Вдруг пригодится кому-то. Итак, скрипт:
рекурсивно переставляет текущую ветку и все её подветки независимо от уровня вложенности поверх нового коммита, пытаясь сохранить топологию всех дочерних веток;
сохраняет состояние предыдущих ссылок на случай отказа автоматичского git rebase, используемого под капотом, что также позволяет вернуть ветки к изначальному состоянию подобно git rebase --abort;
предлагает несколько подкоманд на случай решения проблем с переносом.
Технически, использует следующие аспекты git:
Оригинальные ссылки сохраняются как refs/rebase-all/* (вроде, так делает git bisect), хотя последовательность переноса сохраняется в отдельному файле (как это делает git rebase, git merge, и т.д.).
Парсинг результата git show-branch для определения топологии и последовательности переноса веток от "самой старой" к "самой новой".
Несколько "plumbing" команд.
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):
Требуется перенести 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