Что именно делает git "rebase -preserve-merges" делать (и почему?)

Git документация для команды rebase довольно короткая:

--preserve-merges
    Instead of ignoring merges, try to recreate them.

This uses the --interactive machinery internally, but combining it
with the --interactive option explicitly is generally not a good idea
unless you know what you are doing (see BUGS below).

Итак, что на самом деле происходит, когда вы используете --preserve-merges? Чем он отличается от поведения по умолчанию (без этого флага)? Что значит "воссоздать" слияние и т.д.

Ответ 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 не нравится создавать пустые коммиты.

Ответ 2

Git 2.18 (Q2 2018) значительно улучшит опцию --preserve-merge, добавив новую опцию.

"git rebase" выучил "--rebase-merges", чтобы пересадить целое топология графов коммитов в других местах.

(Примечание: Git 2.22, который должен появиться во втором квартале 2019 года, фактически устарел --preserve-merge)

Смотрите commit 25cff9f, commit 7543f6f, commit 1131ec9, commit 7ccdf65, commit 537e7d6, commit a9be29c, commit 8f6aed7 [, commit 1644c73, commit d1e8b01, commit 4c68e7d, commit 9055e40, commit cb5206e, commit a01c2a5, commit 2f6b1d1, commit bf5c057 (25 апреля 2018 г.) от Йоханнеса Шинделина (dscho).
Смотрите коммит f431d73 (25 апреля 2018 г.) от Стефана Беллера (stefanbeller).
См. коммит 2429335 (25 апреля 2018 г.) от Филиппа Вуда (phillipwood).
(Merged by Junio C Hamano -- [TG47] -- in commit 2c18e6a, 23 May 2018)

pull: принять --rebase-merges, чтобы воссоздать топологию ветки

. Аналогично режиму preserve, просто передавая --preserve-merges Опция команды rebase, режим merges просто передает Опция --rebase-merges.

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


Страница man git rebase теперь содержит полный раздел , посвященный перебазированию истории слияниями.

Выдержка:

Есть законные причины, по которым разработчик может захотеть воссоздать коммиты слияния: чтобы сохранить структуру ветки (или "коммитить топология ") при работе с несколькими взаимосвязанными ветвями.

В следующем примере разработчик работает над веткой рефакторинг, способ определения кнопок и другая ветка темы который использует этот рефакторинг для реализации кнопки "Сообщить об ошибке".
Вывод git log --graph --format=%s -5 может выглядеть так:

*   Merge branch 'report-a-bug'
|\
| * Add the feedback button
* | Merge branch 'refactor-button'
|\ \
| |/
| * Use the Button class for all buttons
| * Extract a generic Button class from the DownloadButton one

Разработчик может захотеть переназначить эти коммиты на более новый master сохраняя топологию ветки, например, когда первая тема ожидается, что ветвь будет интегрирована в master намного раньше, чем второй, скажем, для разрешения конфликтов слияния с изменениями в DownloadButton класс, который превратил его в master.

Эта перебазировка может быть выполнена с помощью опции --rebase-merges.


См. commit 1644c73 для небольшого примера:

rebase-helper --make-script: ввести флаг, чтобы перебазировать слияния

Секвенсор только что выучил новые команды, предназначенные для воссоздания ветки структура (похожа по духу на --preserve-merges, но с существенно менее сломанный дизайн).

Позвольте rebase--helper генерировать списки задач, используя эти команды, запускаемые новой опцией --rebase-merges.
Для такой топологии фиксации (где HEAD указывает на C):

- A - B - C (HEAD)
    \   /
      D

сгенерированный список задач будет выглядеть так:

# branch D
pick 0123 A
label branch-point
pick 1234 D
label D

reset branch-point
pick 2345 B
merge -C 3456 D # C

В чем разница с --preserve-merge?
Commit 8f6aed7 объясняет:

Когда-то этот разработчик подумал: не было бы хорошо, если бы скажем, патчи Git для Windows поверх ядра Git можно представить как заросли ветвей и перебазировать поверх ядра Git, чтобы Поддерживаете наборы патчей, подходящие под вишню?

Первоначальная попытка ответить на это была: git rebase --preserve-merges.

Тем не менее, этот эксперимент никогда не был задуман как интерактивный вариант, и он поддерживается только на git rebase --interactive, потому что Реализация команды выглядела уже очень, очень знакомо: разработан тем же человеком, который разработал --preserve-merges: действительно ваш.

А под "вашим по-настоящему" автор ссылается на себя: Йоханнес Шинделин (dscho), который является главной причиной (с несколькими другими героями - Ханнес, Штеффен, Себастьян,...) что у нас есть Git For Windows (хотя в тот день - 2009 - это было нелегко).
Он работает в Microsoft с сентября 2015 года, что имеет смысл, учитывая, что Microsoft сейчас активно использует Git и нуждается в его сервисах.
Эта тенденция началась в 2013 году, фактически с TFS. С тех пор Microsoft управляет крупнейшим Git-хранилищем на планете! И с октября 2018 года Microsoft приобрела GitHub.

Вы можете увидеть Йоханнеса в этом видео для Git Merge 2018 в апреле 2018 года.

Некоторое время спустя, другой разработчик (я смотрю на тебя, Андреас! ;-)) решил, что было бы неплохо разрешить --preserve-merges быть объединенным с --interactive (с оговорками!) и сопровождающим Git (ну, временный сопровождающий Git во время отсутствия Junio, то есть) согласились, и именно тогда гламур дизайна --preserve-merges начал распадаться довольно быстро и неутешительно.

Здесь Джонатан говорит об Андреасе Швабе из Suse.
Вы можете увидеть некоторые из их обсуждений еще в 2012 году.

Причина? В режиме --preserve-merges родители коммитов слияния (или в этом отношении, любого коммита) не были указаны явно, но были подразумевается именем коммита, переданным команде pick.

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

Увы, эти недостатки также помешали этому режиму (чей оригинальный цель состояла в том, чтобы служить Git для нужд Windows, с дополнительной надеждой что это может быть полезно и другим) от обслуживания Git для Windows ' необходимо.

Пять лет спустя, когда стало действительно невыносимо иметь одного громоздкого, большая серия патчей для "совок" частично связанных, частично не связанных в Git для Windows, который был перебазирован на основные теги Git время от времени время (зарабатывание незаслуженного гнева разработчика злосчастного git-remote-hg серия, которая первой устарела, конкурируя с Git for Windows подход, только чтобы быть оставленным без сопровождающего позже) был действительно несостоятельно, "садовые ножницы Git" родились: сценарий, спекуляция поверх интерактивной перебазировки, которая сначала определить топологию веток исправлений, которые необходимо перебазировать, создать псевдо-список задач для дальнейшего редактирования, превратить результат в настоящий список задач (интенсивное использование команды exec для "реализации" отсутствует список задач) и, наконец, воссоздать серию патчей на начало нового базового коммита.

(На скрипт Git Garden Shears в этом патче ссылаются в commit 9055e40)

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

С этим патчем совершенство садовых ножниц Git приходит к самому git rebase -i.
Передача опции --rebase-merges сгенерирует список задач, которые можно легко понять, и где это очевидно как изменить порядок коммитов.
Новые ветки можно ввести, вставив команды label и вызвав merge <label>.
И как только этот режим станет стабильным и общепринятым, мы можем отказаться от ошибки проектирования, которая была --preserve-merges.


Git 2.19 (Q3 2018) улучшает новую опцию --rebase-merges, заставляя ее работать с --exec.

Опция "--exec" для "git rebase --rebase-merges" помещает exec команды в неправильных местах, что было исправлено.

См. коммит 1ace63b (09 августа 2018 г.) и коммит f0880f7 (06 августа 2018 г.) от Йоханнеса Шинделина (dscho).
(Merged by Junio C Hamano -- [TG452] -- in commit 750eb11, 20 Aug 2018)

rebase --exec: заставить работать с --rebase-merges

Идея --exec заключается в добавлении вызова exec после каждого pick.

После введения коммитов fixup!/s quash! эта идея была расширена применить к "выбору, возможно с последующей цепью исправлений/сквоша", т.е. exec не будет вставлен между pick и любым из его соответствующих fixup или squash строки.

Текущая реализация использует подвох, чтобы добиться этого: предполагает, что есть только команды pick/fixup/squash, а затем вставляет строки exec перед любым pick, кроме первой, и добавляет последний.

Со списками задач, сгенерированными git rebase --rebase-merges, это простая реализация показывает свои проблемы: она выдает точную ошибку вещь, когда есть команды label, reset и merge.

Давайте изменим реализацию, чтобы сделать именно то, что мы хотим: ищем Линии pick, пропустите все цепочки фиксации/сквоша, а затем вставьте exec линия. Вспенить, промыть, повторить.

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

При этом также добавьте строки exec после команд merge, потому что они По духу похожи на команды pick: они добавляют новые коммиты.


Git 2.22 (Q2 2019) исправляет использование refs/rewritten/иерархии для хранения промежуточных состояний перебазирования, что по своей сути делает иерархию worktree.

См. commit b9317d5, commit 90d31ff, commit 09e6564 (07 марта 2019) Нгуен Тай Тай Нгук Дуй (pclouds).
(Merged by Junio C Hamano -- [TG475] -- in commit 917f2cd, 09 Apr 2019)

Убедитесь, что refs/rewritten/per-worktree

a9be29c (sequencer: сделать ссылки, сгенерированные командой label worktree-local, 2018-04-25, Git 2.19) добавляет refs/rewritten/ в качестве рабочего дерева ссылочное пространство.
К сожалению (мой плохой) есть пара мест, которые Нужно обновить, чтобы убедиться, что это действительно для каждого рабочего дерева.

- add_per_worktree_entries_to_dir() обновлен, чтобы удостовериться в правильности перечисления    посмотрите на каждое дерево работ refs/rewritten/ вместо одного на репо.

  • common_list[] обновляется, так что git_path() возвращает правильный место нахождения. Это включает в себя "rev-parse --git-path".

Этот беспорядок создан мной.
Я начал пытаться исправить это с введением refs/worktree,, где все ссылки будут на дерево без специальной обработки.
Неудачные ссылки/переписанные были до ссылки/рабочая ветка, так что это все, что мы можем сделать.


С Git 2.24 (Q4 2019), "git rebase --rebase-merges" научился управлять различными стратегиями слияния и передавать им специфические для стратегии опции.

См. коммит 476998d (04 сентября 2019 г.) от Элайджи Ньюрена (newren).
Смотрите commit e1fac53, commit a63f990, commit 5dcdd74, commit e145d99, commit 4e6023b, commit f67336d, commit a9c7107 [, commit b8c6f24, commit d51b771, commit c248d32, commit 8c1e240, commit 5efed0e, commit 68b54f6, commit 2e7bbac, commit 6180b20, commit d5b581f (31 июля 2019) от Йоханнес Шинделин (dscho).
(Merged by Junio C Hamano -- [TG487] -- in commit 917a319, 18 Sep 2019)