Что именно делает 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? Чем он отличается от поведения по умолчанию (без этого флага)? Что значит "воссоздать" слияние и т.д.

301
10 апр. '13 в 4:29
источник поделиться
2 ответов

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

414
10 апр. '13 в 4:29
источник

Связанные вопросы


Похожие вопросы

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

" git rebase " научился " --rebase-merges " для трансплантации всей топологии графа коммитов в другом месте.

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

См. Коммит 25cff9f, коммит 7543f6f, коммит 1131ec9, коммит 7ccdf65, коммит 537e7d6, коммит a9be29c, коммит 8f6aed7, коммит 1644c73, коммит d1e8b01, коммит 4c68e7d, коммит 9055e40, коммит cb5206e, коммит a01c2a5 b1, комм. F1, коммит 1, cd5b5f5, f1, комм. Йоханнес dscho (dscho).
См. Коммит f431d73 (25 апреля 2018 г.) Штефана Беллера (stefanbeller).
См. Фиксацию 2429335 (25 апреля 2018 г.) Филиппа Вуда (phillipwood).
(Объединено Junio C Hamano - gitster - в коммите 2c18e6a, 23 мая 2018 г.)

pull: accept --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.


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

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

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

Позвольте позволить rebase--helper генерировать списки --rebase-merges используя эти команды, запускаемые новой --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?
Коммит 8f6aed7 объясняет:

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

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

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

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

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

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

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

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

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

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

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

(Сценарий садовых ножниц Git упоминается в этом патче в коммите 9055e40)

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

С этим патчем совершенство садовых ножниц Git сводится к самой git rebase -i.
Передача параметра --rebase-merges сгенерирует список --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 (dscho).
(Объединено Юнио К Хамано - gitster - в коммите 750eb11, 20 августа 2018 г.)

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

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

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

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

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

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

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

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


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

См. Коммит b9317d5, коммит 90d31ff, коммит 09e6564 (07 марта 2019 г.) Нгуен Тай pclouds Дуй (pclouds).
(Объединено Junio C Hamano - gitster - в коммите 917f2cd, 09 апреля 2019 г.)

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

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

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

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

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

40
27 мая '18 в 22:26
источник

Посмотрите другие вопросы по меткам или Задайте вопрос