Ответ 1
Как и в случае нормальной git rebase, git с --preserve-merges
сначала идентифицирует список коммитов, сделанных в одной части графика фиксации, а затем повторяет эти коммиты поверх другой части. Различия с --preserve-merges
касаются того, какие коммиты выбраны для повтора и как это воспроизведение выполняется для слияния.
Чтобы быть более явным в отношении основных различий между нормальным и сохраняющим слияние ребазом:
- Сохраняющаяся слиянием перезагрузка готова повторить (некоторые) слияния, тогда как нормальная rebase полностью игнорирует коммит.
- Поскольку он желает перезаписать слияние коммитов, сохранение баланса слиянием должно определить, что значит повторить фиксацию слияния, и иметь дело с некоторыми дополнительными морщинами
- Самая интересная часть, концептуально, возможно, заключается в выборе того, что должно быть новым браком слияния родителей.
- Выполнение повторного выполнения слияния также требует явной проверки конкретных коммитов (
git checkout <desired first parent>
), тогда как нормальная перестановка не должна беспокоиться об этом.
- Сохранение с сохранением слияния учитывает более мелкий набор коммитов для повтора:
- В частности, он будет рассматривать только повторные коммиты, сделанные с момента последней базы (-ов) слияния, т.е. самое последнее время, когда две ветки расходились - в то время как нормальная перебаза может повториться, возвращается к первому ветки расходятся.
- Чтобы быть временным и неясным, я считаю, что это, в конечном счете, средство, чтобы исключить повторение "старых коммитов", которые уже были "включены в" коммитирование.
Сначала я попытаюсь описать "достаточно точно", что делает rebase --preserve-merges
, и тогда будут некоторые примеры. Можно, конечно, начать с примеров, если это кажется более полезным.
Алгоритм в "Кратком"
Если вы действительно хотите попасть в сорняки, загрузите источник git и исследуйте файл git-rebase--interactive.sh
. (Rebase не является частью ядра git C, а написана в bash. И за кулисами он разделяет код с "интерактивной перестановкой".)
Но здесь я нарисую то, что, по моему мнению, является его сутью. Чтобы уменьшить количество вещей, о которых нужно подумать, я взял несколько свобод. (например, я не пытаюсь зафиксировать с 100% точностью точный порядок, в котором происходят вычисления, и игнорировать некоторые менее центрально-показательные темы, например, что делать с фиксациями, которые уже были выбраны вишневыми между ветвями).
Во-первых, обратите внимание, что резервная перезагрузка, не связанная с слиянием, довольно проста. Это более или менее:
Find all commits on B but not on A ("git log A..B")
Reset B to A ("git reset --hard A")
Replay all those commits onto B one at a time in order.
Rebase --preserve-merges
является сравнительно сложным. Здесь так просто, как я смог сделать это, не теряя при этом вещей, которые кажутся довольно важными:
Find the commits to replay:
First find the merge-base(s) of A and B (i.e. the most recent common ancestor(s))
This (these) merge base(s) will serve as a root/boundary for the rebase.
In particular, we'll take its (their) descendants and replay them on top of new parents
Now we can define C, the set of commits to replay. In particular, it those commits:
1) reachable from B but not A (as in a normal rebase), and ALSO
2) descendants of the merge base(s)
If we ignore cherry-picks and other cleverness preserve-merges does, it more or less:
git log A..B --not $(git merge-base --all A B)
Replay the commits:
Create a branch B_new, on which to replay our commits.
Switch to B_new (i.e. "git checkout B_new")
Proceeding parents-before-children (--topo-order), replay each commit c in C on top of B_new:
If it a non-merge commit, cherry-pick as usual (i.e. "git cherry-pick c")
Otherwise it a merge commit, and we'll construct an "equivalent" merge commit c':
To create a merge commit, its parents must exist and we must know what they are.
So first, figure out which parents to use for c', by reference to the parents of c:
For each parent p_i in parents_of(c):
If p_i is one of the merge bases mentioned above:
# p_i is one of the "boundary commits" that we no longer want to use as parents
For the new commit ith parent (p_i'), use the HEAD of B_new.
Else if p_i is one of the commits being rewritten (i.e. if p_i is in R):
# Note: Because we're moving parents-before-children, a rewritten version
# of p_i must already exist. So reuse it:
For the new commit ith parent (p_i'), use the rewritten version of p_i.
Otherwise:
# p_i is one of the commits that *not* slated for rewrite. So don't rewrite it
For the new commit ith parent (p_i'), use p_i, i.e. the old commit ith parent.
Second, actually create the new commit c':
Go to p_1'. (i.e. "git checkout p_1'", p_1' being the "first parent" we want for our new commit)
Merge in the other parent(s):
For a typical two-parent merge, it just "git merge p_2'".
For an octopus merge, it "git merge p_2' p_3' p_4' ...".
Switch (i.e. "git reset") B_new to the current commit (i.e. HEAD), if it not already there
Change the label B to apply to this new branch, rather than the old one. (i.e. "git reset --hard B")
Rebase с аргументом --onto C
должен быть очень похож. Вместо того, чтобы начинать комментировать воспроизведение в HEAD из B, вы начинаете записывать воспроизведение в HEAD of C вместо этого. (И используйте C_new вместо B_new.)
Пример 1
Например, возьмите граф фиксации
B---C <-- master
/
A-------D------E----m----H <-- topic
\ /
F-------G
m - объединение слияния с родителями E и G.
Предположим, что мы перепутали тему (H) поверх мастера (C), используя обычное, не сохраняющее слияние сохранение перебазироваться. (Например, тема проверки, мастер переадресации.) В этом случае git будет выбирать следующие записи для повтора:
- выбрать D
- выберите E
- выбрать F
- выберите G
- выберите H
а затем обновить граф фиксации следующим образом:
B---C <-- master
/ \
A D'---E'---F'---G'---H' <-- topic
(D '- повторный эквивалент D и т.д.)
Обратите внимание, что команда merge commit m не выбрана для воспроизведения.
Если мы вместо этого использовали --preserve-merges
rebase H поверх C. (Например, тема проверки, rebase --preserve-merges master.) В этом новом случае git будет выбирать следующие коммиты для повтора
- выбрать D
- выберите E
- выберите F (на D 'в ветке "subtopic" )
- выберите G (на F 'в ветке "subtopic" )
- выбрать Объединить ветку 'subtopic' в тему
- выберите H
Теперь m выбрано для повтора. Также обратите внимание, что слияния родителей E и G были выбрано для включения перед слиянием commit m.
Вот результат построения графика:
B---C <-- master
/ \
A D'-----E'----m'----H' <-- topic
\ /
F'-------G'
Опять же, D '- это вишня-подобранная (т.е. воссозданная) версия D. То же самое для E' и т.д. Каждое перехват не на хозяине было воспроизведено. И E и G (слияния родителей m) были воссозданы как E 'и G', чтобы служить родителями m '(после rebase история дерева все еще остается той же).
Пример 2
В отличие от обычной rebase, сохранение с сохранением слияния может создавать множественные дети восходящей головы.
Например, рассмотрим:
B---C <-- master
/
A-------D------E---m----H <-- topic
\ |
------- F-----G--/
Если мы переустановим H (тему) поверх C (master), то фиксации, выбранные для rebase, следующие:
- выбрать D
- выберите E
- выбрать F
- выберите G
- выбрать m
- выберите H
И результат будет таким:
B---C <-- master
/ | \
A | D'----E'---m'----H' <-- topic
\ |
F'----G'---/
Пример 3
В приведенных выше примерах как фиксация слияния, так и ее два родителя переигрываются, а не исходные родители, которые имеют первоначальное слияние. Тем не менее, в других флагах повторное объединение слияния может закончиться с родителями, которые уже были на графике фиксации до слияния.
Например, рассмотрим:
B--C---D <-- master
/ \
A---E--m------F <-- topic
Если мы переустанавливаем тему на master (сохраняем слияние), тогда фиксации для воспроизведения будут
- pick merge commit m
- выбрать F
Переписанный график фиксации будет выглядеть так:
B--C--D <-- master
/ \
A-----E---m'--F'; <-- topic
Здесь replayed merge commit m 'получает родителей, которые уже существовали в графе фиксации, а именно D (HEAD мастера) и E (один из родителей исходного слияния m).
Пример 4
Сохранение слияния может привести к путанице в некоторых случаях "пустой фиксации". По крайней мере, это верно только в некоторых более старых версиях git (например, 1.7.8.)
Возьмите этот график фиксации:
A--------B-----C-----m2---D <-- master
\ \ /
E--- F--\--G----/
\ \
---m1--H <--topic
Обратите внимание, что как commit m1, так и m2 должны включать все изменения из B и F.
Если мы попытаемся сделать git rebase --preserve-merges
из H (темы) на D (master), то для повторного воспроизведения выбираются следующие коммиты:
- выбрать m1
- выберите H
Обратите внимание, что изменения (B, F), объединенные в m1, должны быть уже включены в D. (Эти изменения должны быть уже включены в m2, потому что m2 объединяет детей из B и F.) Поэтому, концептуально, воспроизведение m1 на вершине D, вероятно, должен быть либо не-op, либо создать пустую фиксацию (то есть, где diff между последовательными версиями пуст).
Вместо этого, однако, git может отклонить попытку воспроизведения m1 поверх D. Вы можете получить такую ошибку:
error: Commit 90caf85 is a merge but no -m option was given.
fatal: cherry-pick failed
Похоже, что забыл передать флаг git, но основная проблема заключается в том, что git не нравится создавать пустые коммиты.