Почему git не автоматически повторно использует разрешения конфликтов при выполнении rebase-merges rebase? - PullRequest
0 голосов
/ 25 января 2019

(аналогично этому вопросу , но с некоторым контекстом и демонстрацией того, почему rerere не является ответом.)

Для данной истории:

                /...o      origin/master
o...o...o...o...o...o...o  master
    \...o........../       topic

У меня есть ветка темы, которую я слил в master и сделал еще один коммит.Между тем, кто-то из апстрима сделал еще один коммит на origin / master, поэтому я больше не могу выдвинуть своего мастера как есть.

Я хочу переместить мой master на origin / master без изменения SHA коммитов по теме и безпотеря разрешения конфликта уже выполнена на мастере.(Это самый распространенный случай, когда я хочу сохранить коммиты слияния, поэтому я удивлен, что это, по-видимому, так сложно.)

С включенным rerere, git rebase -p почти работает - для любых конфликтов в исходном слиянии, он запоминает, что я сделал, чтобы исправить их, и повторно применяет это (хотя он оставляет файл помеченным как конфликтующий, поэтому я должен помнить, чтобы пометить каждый как уже разрешенный, без перезапуска разрешения конфликта нафайл, который слегка раздражает из внешнего интерфейса TortoiseGit).Но если были какие-либо другие изменения в файлах, которые также были исправлены в коммите слияния (например, строки, которые были просто добавлены в слияние без конфликтов, но все еще нуждались в исправлении из-за изменений в другом месте), они теряются.

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

Почему он не повторно применяет набор изменений из исходного коммита слияния?Это сделало бы rerere излишним и не потеряло бы эти дополнительные изменения.Он мог оставить затронутые файлы помеченными как конфликтные, если требовал подтверждения от человека, но во многих случаях этого автоматического разрешения было бы вполне достаточно.

Другими словами, пометить некоторые коммиты выше:

                /...N      origin/master
o...o...o...o...B...M...A  master
    \...T........../       topic

T - the commit on topic
B - the merge-base of origin/master and master
N - the new commit on origin/master
M - the merge between B and T
A - the extra post-merge commit

M имеет родителей B и T и уникальный набор изменений Mc.При создании M 'git выполняет новое слияние между родителями N и T и отбрасывает Mc.Почему git не может просто повторно применить Mc вместо того, чтобы выбросить его?

В конце я хочу, чтобы история выглядела так:

o...o...o...o...B...N...M'...A'  master
    \...T............../

Где M 'и A' меняют SHA1 сребаз, но M 'включает в себя ревизию Mc, а T не менял SHA1 или родителя.И теперь я могу быстро переместиться из оригинала / мастера к A '.


Я также заметил, что есть новая опция --rebase-merges, которая сначала звучит хорошо, а потом приводит к правильному графику -но точно так же, как --preserve-merges по-прежнему останавливается с конфликтами на M 'и теряет любые уникальные изменения в Mc, не сохраняемые иначе как rerere.


Альтернативная формулировка вопроса, которая может быть более полезной:

С учетом начального состояния, указанного выше, и только что начавшегося интерактивного перебазирования, которое теперь находится в состояниях HEAD1 или HEAD2:

        /...........(T)
       /               \
      /             /...M'  HEAD2
     /              /...    HEAD1
    /           /...N       origin/master
o...o...o...o...B...M...A   master
    \...T........../        topic

(HEAD1 извлек N, но больше ничего не сделал; HEAD2 создалновое слияние с N в качестве родителя 1 и T в качестве родителя 2, но еще не зафиксировано из-за неразрешенных конфликтов)

Существует ли некоторая последовательность команд rebase и / или git, которая будет:

  1. Рассчитать разность Mc между M и B (выбирая B, потому что другой родительский T не меняется)
  2. Примените это к конфликтующему дереву M '(которое должно завершитьсяy разрешать все конфликты, если только N не вводит новые) ИЛИ Просто примените это поверх N (без предварительного слияния) - они должны быть эквивалентны;вторая может быть проще
  3. Пауза для человека, чтобы разрешить любые оставшиеся конфликты, введенные N, если таковые имеются.
  4. Передать M 'как слияние между N и T
  5. Продолжитькак обычно (в этом случае перестановка A на A 'поверх M')

И почему git не делает это по умолчанию?

1 Ответ

0 голосов
/ 25 января 2019

Фундаментальная причина, по которой git rerere не может записать отсутствие конфликтов, заключается в том, что git rerere реализован дешевым и грязным способом: Git принимает каждый начальный конфликт, отбрасывает его из некоторых данных, чтобы сделать его более применимым (таким же образом git patch-id удаляет номера строк и некоторые пробелы), а затем сохраняет конфликт как объект BLOB-объекта в базе данных, получая идентификатор хеша, который хранится в каталоге rerere. Позже, когда вы git commit получите результат, Git объединяет один конкретный конфликтующий объект и меняет его с разрешением. Таким образом, он только «знает» конфликты, а не любые другие изменения.

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

Возможно, Git может сэкономить больше, но это не так.

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

Это неверно. Все коммиты - это просто снимки состояния. Слияния здесь не особенные - как и коммиты без слияния, они имеют полное дерево исходников. Особенность в том, что у них есть два (или более) родителя.

Копирование без слияния, как это делает git cherry-pickgit rebase делает многократно, многократно вызывая git cherry-pick, или делая что-то не очень хорошее, но похожее), работает с использованием коммита (один-единственный) parent как основа слияния для операции merge-as-a-verb. Копирование слияния вообще невозможно, и rebase не пытается: он просто повторно выполняет слияние.

(С другой стороны, git cherry-pick позволит вам выбрать слияние с помощью опции -m, чтобы выбрать одного конкретного родителя. Git просто делает вид, что это единственный родитель на время действия трехсторонней связи Операция слияния. Теоретически, код rebase мог бы сделать то же самое: -m 1 почти всегда является правильным родителем, и всегда можно использовать низкоуровневый git commit-tree, чтобы сделать фактический коммит, чтобы сделать его коммитом слияния Но git rebase этого не делает.)

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

Да (по причине, описанной выше). Это, возможно, одна из причин, по которой люди называют такие вещи «злым слиянием» (хотя другая возможная причина этой фразы заключается в том, что такие изменения были, по крайней мере, для всех доказательств, имеющихся у человека в будущем, на самом деле никто не запрашивал). Хотя это не поможет вашей цели с существующими слияниями, я бы посоветовал не вносить такие изменения: вместо этого вносите эти изменения до или после слияния, в обычном коммите без слияния, который поступает в слияние или из него, так что позже rebase -p или rebase --rebase-merges может сохранить их.

...