Восстановление потерянных изменений после возврата неудачного слияния - PullRequest
0 голосов
/ 24 октября 2018

Я относительно новичок в Git и допустил (глупую) ошибку после недоразумения справочной статьи, и я не уверен, как полностью решить проблему с помощью Git, а не вводить изменения вручную в целевую ветку вручную.

S---sc1---sc2---sc3-----sc4----M4---R1---M5---sc5
 \                         \  /         /
  T1-------------------M2---M3--------R2
   \               \  /
    F---fc1---fc2---M1

Некоторые примечания: S - это основная ветвь в этом сценарии, T1 - ветвь команды, полученная из S, а F - моя ветвь функций, извлеченная из T1.

*.1010 * У меня настроено автоматическое слияние, поэтому, когда коммиты выполняются в ветке T1, они проходят непрерывную интеграцию, а затем автоматически объединяются до S.В ветке T1 был один файл, в котором возникали конфликты слияния с S из коммита другого члена команды, поэтому я решил исправить это, как только закончил свою работу над F.

Iобъединены T1 в F (M1), затем F в T1 (M2).Учитывая проблемы, с которыми я сталкивался в прошлом, с разрешениями конфликтов слияния, не ведущими себя так, как я ожидал, я подумал, что попробую что-то новое: объединение только конфликтующего файла из S в T1, решение конфликта слияния там, удалениевсе остальные файлы из слияния, а затем позволяя непрерывной интеграции объединить все до S

Я начал слияние с S до T1 (M3) без фиксации, обратился кконфликт, удалил другие (~ 200) файлы из слияния, затем зафиксировал.Это автоматически объединено в S (M4).

Я сразу заметил, что исключая эти ~ 200 файлов, казалось, полностью уничтожили изменения, что равнялось примерно месяцу работы в 2 командах,Я (неправильно) решил, что лучший способ действий - действовать быстро и отменить коммит слияния M4 и M3 до того, как моя ошибка попала в чьи-либо локальные репозитории.Сначала я вернул M4 (R1), а когда это было зафиксировано, я вернул M3 (R2).Я полагал, что это был правильный порядок, потому что я не был уверен, что если бы сработало автоматическое слияние, возникли бы другие проблемы. В конечном итоге R2 было зафиксировано и автоматически слилось с S (M5).

Это решило проблему уничтожения всех остальных изменений, но все мои изменения в F плюс файл, который изначально имел конфликт слияния, были удалены из S.Мне удалось зафиксировать изменения одного файла непосредственно в S (sc5), но изменения в F намного сложнее.Они все еще живут в T1, но так как они были возвращены из S как часть R1, я не могу просто отправить их обратно.

Я провел большую часть дня, пытаясь понятьКак лучше получить эти изменения до S, но git rebase и git cherry-pick не похоже, что они будут делать то, что мне нужно, хотя я прекрасно понимаю, что могу ошибаться в этом.Если кто-то намного лучше в Git, чем я, может предложить хотя бы отправную точку, это было бы удивительно.Спасибо!

Редактировать: Удалены бесполезные / запутанные точки из графика.M2 не удалось автоматически объединить до S из-за конфликта слияния, который я пытался разрешить с помощью M3.

Редактировать 2: После прочтения фантастического объяснения от ТорекаЯ начал пытаться ребазировать.Я забыл, что объединял ветку T1 в ветку F несколько раз за всю историю F из-за того, сколько времени занимала эта ветвь функций.Это означало, что нужно было разрешить множество конфликтов слияния.

Когда Торек ответил на это, я предпринял попытку сквоша слияния.Моя первоначальная мысль: мне нужно объединить новую ветку от сквоша слияния до ветви T1, а затем слить ветку T1 до S, но я столкнулся с той же проблемой, когда он не видитизменения.Я предполагаю, что это связано с тем, что изменения уже существуют в T1, поэтому в основном они просто возвращали те же, ранее отозванные изменения обратно в S, что им не нужно.

Редактировать 3: Благодаря очень хорошо объясненному, подробному ответу от Торека (большое спасибо!), Я прохожу сквош слияния и затем объединяю результат этого доS ветка после разрешения конфликтов.

1 Ответ

0 голосов
/ 24 октября 2018

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

Знакомство с битами

Позвольте мне начать с перерисовки этого графика (который я считаюэто своего рода частичный граф, но он содержит ключевые фиксации, которые нам нужны), как я предпочитаю:

S0--sc1---sc2---sc3-----sc4----M4---R1---M5---sc5   <-- branch-S
  \                        \  /         /
   T0-------------o----M2---M3--------R2   <---- branch-T1
    \              \  /
    F0--fc1---fc2---M1   <------------------- branch-F

Здесь имена ветвей равны branch-S, branch-T1branch-F, и эти имена в настоящее время идентифицируют коммиты, чьи идентификаторы хеша являются чем-то непроизносимым и недоступным для запоминания людьми, но мы называем sc5, R2 и M1 соответственно.Любые узлы o являются коммитами, которые особо не различаются и могут фактически представлять произвольное количество коммитов.Именованные fc<number> представляют собой некоторый набор коммитов в ветви функций, а коммиты M<number> являются слияниями.Я переименовал первые коммиты S0, T0 и F0, чтобы отличать их от названий ветвей.

Некоторые слияния выполняются вручную:

$ git checkout <branch-name>
$ git merge [options] <other-branch>
... fix up conflicts if necessary, and git commit (or git merge --continue)

Другие слияниясделаны программным обеспечением и происходят только при отсутствии конфликтов.Коммиты R запускаются:

git checkout <branch>
git revert -m 1 <hash ID of some M commit>

, где <branch> было T1 или S, а -m 1 потому, что вы всегда должны указывать git revert, какого родителя использоватьпри возврате слияния, и он почти всегда является родительским # 1.

Создание коммитов перемещает имя ветки

Самый простой граф коммитов Git - это прямая линия с одним именем ветки, обычно master:

A--B--C   <-- master (HEAD)

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

Затем вы изменяете файлы в рабочем дереве, используя git add, чтобы скопировать их обратно виндекс, используйте git add, чтобы скопировать новые файлы в индекс, если это необходимо, и выполните git commit.Создание нового коммита работает путем замораживания этих индексных копий в моментальный снимок.Затем Git добавляет метаданные моментального снимка - ваше имя и адрес электронной почты, ваше сообщение журнала и т. Д. - вместе с идентификатором хэша текущего коммита, чтобы новый коммит снова указывал на существующий коммит.Результат:

A--B--C   <-- master (HEAD)
       \
        D

с новым коммитом, с его новым уникальным идентификатором хеша, просто зависающим в воздухе, и ничего не запоминающим.Итак, последний шаг создания нового коммита заключается в записи хеш-идентификатора нового коммита в имя ветви:

A--B--C--D   <-- master (HEAD)

, и теперь текущий коммит равен D, аИндекс и текущее соответствие.Если вы git add добавили все файлы в рабочем дереве, это также соответствует текущему коммиту и индексу.Если нет, вы можете git add больше файлов и зафиксировать снова, заставив имя master указать на новый коммит E и так далее.В любом случае (единственным) родителем нового коммита будет то, что было текущим коммитом .

О слияниях

Позвольте мне описать, как на самом деле работает git merge.В некоторых случаях и в некоторых случаях это очень просто, и давайте начнем с самого простого случая истинного слияния.Рассмотрим график, который выглядит следующим образом:

          o--...--L   <-- mainline (HEAD)
         /
...--o--*
         \
          o--...--R   <-- feature

Мы запустили git checkout mainline; git merge feature, поэтому мы говорим Git объединить ветку feature / commit R в ветку mainline / commit L,Для этого Git сначала должен найти коммит merge base .Грубо говоря, база слияния - это «ближайший» коммит, общий для , достижимый из , обоих ветвей.В этом простом случае мы начинаем с L и возвращаемся к более старым коммитам, начинаем с R и идем назад, и первое место, с которым мы встречаемся, это коммит *, так что это база слияния.

(Подробнее о достижимости см. Думай как (а) Git .)

ХаВ поисках базы слияния Git необходимо превратить снимки L (слева / локально / --ours) и R (справа / удаленно / --theirs) в наборы изменений.Эти наборы изменений сообщают Git, что мы сделали на mainline с базы слияния * и что они сделали на feature с базы слияния.Все эти три коммита имеют идентификаторы хеша, которые являются реальными именами трех коммитов, поэтому Git может выполнить внутренний эквивалент:

git diff --find-renames <hash-of-*> <hash-of-L>   # what we changed
git diff --find-renames <hash-of-*> <hash-of-R>   # what they changed

Слияние просто объединяет два набораизменяет и применяет объединенный набор к файлам в снимке в *.

Когда все идет хорошо, Git делает новый коммит обычным способом, за исключением того, что новый коммит имеет two родители.Это заставляет текущую ветку указывать на новый коммит слияния:

          o--...--L
         /         \
...--o--*           M   <-- mainline (HEAD)
         \         /
          o--...--R   <-- feature

Первый родительский элемент M равен L, а второй - R.Вот почему при возврате почти всегда используется родитель № 1, и поэтому git log --first-parent только «видит» ветвь магистрали, проходя от M до L, полностью игнорируя ветвь R.(Обратите внимание, что слово ответвление здесь относится к структуре графа, а не к ответвлению ИМЯ подобно feature: на этом этапе мы можем удалить name feature полностью. См. Также Что именно мы подразумеваем под «ветвью»? )

Когда что-то пойдет не так

Слияние будет остановлено с конфликт слияния , если два набора изменений перекрываются "плохим способом".В частности, предположим, что base-vs-L говорит об изменении строки 75 файла F, а base-vs-R также говорит об изменении строки 75 файла F.Если оба набора изменений говорят о том, что они делают то же самое изменение , Git согласен с этим: комбинация двух изменений состоит в том, чтобы сделать изменение один раз.Но если они говорят, чтобы сделать различные изменения, Git объявляет конфликт слияния.В этом случае Git остановится после того, как сделает все возможное, и заставит вас убрать беспорядок.

Поскольку имеется три входа, Git на этом этапе оставит все три версий файла F в указателе.Обычно индекс содержит одну копию каждого файла для фиксации, но на этом этапе разрешения конфликтов он может содержать до трех копий.(Часть «до» заключается в том, что у вас могут быть другие виды конфликтов, в которые я не буду вдаваться здесь из-за нехватки места.) Между тем, в рабочем дереве копия файла F,Git оставляет свое приближение для слияния с двумя или всеми тремя наборами строк в файле рабочего дерева с маркерами <<<<<<< / >>>>>>> вокруг них.(Чтобы получить все три, установите merge.conflictStyle на diff3. Я предпочитаю этот режим для разрешения конфликтов.)

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

Что бы вы ни делали,хотя окончательное слияние - при условии, что вы не прерываете его и не используете один из вариантов слияния без слияния-y - все равно дает тот же результат на графике и независимо от того, что вы добавили в индекс, путем разрешенияконфликты, является результатом слияния.Это новый снимок в коммите слияния.

Более сложные базы слияния

Когда график очень прост, как показано выше, базу слияния легко увидеть.Но графики не остаются простыми, а ваши нет.База слияния для графа, в котором есть некоторые слияния, сложнее.Рассмотрим, например, только следующий фрагмент:

 ...--sc4----M4---R1
         \  /
...--M2---M3--------R2

Если R1 и R2 являются двумя коммитами, то какова их база слияния?Ответ M3, а не sc4.Причина в том, что, хотя M3 и sc4 являются обоими коммитами, которые достижимы, начиная с R1 и R2 и работая в обратном направлении, M3 "ближе" к R2 (один шаг назад).Расстояние от R1 до M3 или sc4 составляет два прыжка - перейдите к M4, затем вернитесь еще на один шаг, но расстояние от R2 до M3 составляет один прыжок, а расстояние отR2 до sc4 - это два прыжка.Таким образом, M3 «ниже» (в терминах графа) и поэтому выигрывает соревнование.

(К счастью, в вашем графе нет случаев, когда есть ничья. Если равен ничьюПодход Git по умолчанию - объединение всех связанных коммитов, по два за один раз, для создания «виртуальной базы слияния», которая фактически является фактической, хотя и временной, коммитом, а затем использует этот временный коммит, созданный путем слияния баз слияния.Это стратегия рекурсивная , которая получила свое название от того факта, что Git рекурсивно объединяет базы слияния для получения базы слияния. Вместо этого вы можете выбрать стратегию resol * , которая просто выбираетодна из баз, на первый взгляд, случайная - какая бы база ни появлялась в начале алгоритма. Преимущества в этом нет редко: рекурсивный метод обычно либо делает то же самое, либо является улучшением по сравнению со случайным выбором победителя.)

Ключевым выводом здесь является то, что внесение изменений коммита слияния, которые фиксируют future merges, выберет какбаза слияния эйр .Это важно даже при выполнении простых слияний, поэтому я выделил их жирным шрифтом.Вот почему мы делаем коммиты слияния, в отличие от сквош-операций слияния, которые не являются слияниями.(Но слияние сквоша все еще полезно, как мы увидим чуть позже.)

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

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

Я слил branch-T1 в branch-F (M1), затем branch-F в branch-T1 (M2).

Я предполагаю, что слияние fc2 (как тогдашний конец branch-F) и o (как тогдашний конец branch-T1) прошло хорошои Git смог сделать M1 самостоятельно.Как мы видели ранее, слияние действительно основано не на ветвях , а на коммитах .Это создание нового коммита, который корректирует имена веток.Итак, это создало M1, так что branch-F указывало на M1.M1 сам указывал на существующую подсказку branch-T1 - коммита, который я сейчас пометил o - как своего второго родителя, с fc2 в качестве первого родителя.Git вычисляет правильное содержимое для этого коммита, git diff -ing содержимое T0, базы слияния, против o и против fc2:

T0-------------o   <-- branch-T1
 \
 F0--fc1---fc2   <--- branch-F (HEAD)

Когда все идет хорошо, Git теперь делает M1 самостоятельно:

T0-------------o   <-- branch-T1
 \              \
 F0--fc1---fc2---M1   <--- branch-F (HEAD)

Теперь вы git checkout branch-T1 и git merge --no-ff branch-F (без --no-ff Git просто сделает ускоренную перемотку вперед, чтоне то, что на картинке), поэтому Git находит базу слияния o и M1, что само по себе o.Это объединение легко: разница от o до o - ничто, и ничто плюс разница от o до M1 равно содержанию M1.Так что M2, как снимок, точно такой же, как M1, и Git легко его создает:

T0-------------o----M2   <-- branch-T1 (HEAD)
 \              \  /
 F0--fc1---fc2---M1   <--- branch-F

Пока все хорошо, но теперь все начинает идти не так:

В ветке T1 был один файл, который имел конфликты слияния с S ... Учитывая проблемы, с которыми я сталкивался в прошлом, с разрешениями конфликтов слияния не вел себя так, как я ожидал, ядумал, что попробую что-то новое: объединить только конфликтующий файл из S в T1, решить конфликт слияния там, удалить все остальные файлы из объединения, а затем позволить непрерывной интеграции объединить все до S.

Итак, что вы сделали на этом этапе:

git checkout branch-T1
git merge branch-S

which остановлен конфликтом слияния.График в этой точке такой же, как приведенный выше, но с некоторым дополнительным контекстом:

S0--sc1---sc2---sc3-----sc4   <-- branch-S
  \
   T0-------------o----M2   <-- branch-T1 (HEAD)
    \              \  /
    F0--fc1---fc2---M1   <-- branch-F

Операция слияния находит базу слияния (S0), которая отличается от двух коммитов с наконечником (M2 и sc4), объединяет полученные изменения и применяет их к содержимому S0.Один конфликтующий файл теперь находится в индексе как три входные копии и в рабочем дереве как усилие Git по слиянию, но с маркерами конфликта.Между тем все не конфликтующие файлы находятся в индексе и готовы к замораживанию.

Увы, теперь вы удаляете некоторые файлы (git rm) во время конфликтующего объединения.Это удаляет файлы из индекса и рабочего дерева.Результирующий коммит, M3, скажет, что правильный способ объединения коммитов M2 и sc4 на основе merge-base S0 - удалить эти файлы.(Это, конечно, было ошибкой.)

Это автоматически объединено с S (M4).

Здесь, я предполагаю, это означает, что системаиспользуя любое предварительно запрограммированное правило, оно имело эквивалент:

git checkout branch-S
git merge --no-ff branch-T1

, который нашел базу слияния коммитов sc4 (наконечник branch-S) и M3, то есть M3, так же, как база слияния o и M1 была M1 ранее.Таким образом, новый коммит, M4, соответствует M3 по содержанию, и в этот момент мы имеем:

S0--sc1---sc2---sc3-----sc4----M4   <-- branch-S
  \                        \  /
   T0-------------o----M2---M3   <-- branch-T1
    \              \  /
    F0--fc1---fc2---M1   <-- branch-F

Я сразу заметил, что исключая эти ~ 200 файлов, похоже, стерлиполностью меняется, что равняется примерно месяцу работы между двумя командами.Я (неправильно) решил, что лучший способ действий - действовать быстро и отменить коммит слияния M4 и M3 до того, как моя ошибка попала в чьи-либо локальные репозитории.Сначала я вернул M4 (R1), и как только это было совершено, я вернулся M3 (R2).

На самом деле, это было прекрасно!Он получает правильный контент , что очень полезно, когда вы делаете это немедленно.Использование git checkout branch-s && git revert -m 1 branch-S (или git revert -m 1 <hash-of-M4>) для создания R1 из M4 в основном отменяет слияние с точки зрения содержимого, так что:

git diff <hash-of-sc4> <hash-of-R1>

вообще ничего не должно производить.Аналогично, используя git checkout branch-T1 && git revert -m 1 branch-T1 (или то же самое с хэшем) для создания R2 из M3 отмен, которые объединяются с точки зрения содержимого: сравнивая M2 и R2, вы должны увидеть идентичное содержимое.

Отмена слияния отменяет содержимое , но не history

Теперь проблема в том, что Git считает, что все изменения в вашей ветви функций включены правильно.Любой git checkout branch-T1 или git checkout branch-S, за которым следует git merge <any commit within branch-F>, будет смотреть на график, следуя обратным ссылкам от коммита к коммиту, и увидит, что этот коммит в branch-F - например, fc2 или M1 - уже объединено .

Хитрость для их получения заключается в создании нового коммита, который делает то же самое, что последовательность коммитов от F0 до M1 делает, это не уже объединено.Самый простой - хотя и самый уродливый - способ сделать это - использовать git merge --squash.Более сложный и, возможно, лучший способ сделать это - использовать git rebase --force-rebase для создания новой ветви функций.(Примечание: у этой опции есть три варианта написания, и самый простой для ввода - -f, но в описании Линуса Торвальдса - --no-ff. Я думаю, что наиболее запоминающейся является версия --force-rebase,но я бы на самом деле использовал -f сам.)

Давайте кратко рассмотрим оба, а затем рассмотрим, что использовать и почему.В любом случае, как только вы закончите, на этот раз вы должны будете правильно объединить новые коммиты без удаления файлов;но теперь, когда вы знаете, что на самом деле делает git merge, сделать это будет намного проще.

Мы начнем с создания нового имени ветви.Мы можем повторно использовать branch-F, но я думаю, что будет понятнее, если мы этого не сделаем.Если мы хотим использовать git merge --squash, мы создаем это новое имя ветви, указывающее на фиксацию T0 (игнорируя тот факт, что после T0 есть коммиты - помните, что любое имя ветви может указывать на любой коммит):

T0   <-- revised-F (HEAD)
 \
  F0--fc1--fc2--M1   <-- branch-F

Если шмы хотим использовать git rebase -f, мы создаем это новое имя, указывающее на коммит fc2:

T0-----....
 \
  F0--fc1--fc2--M1   <-- branch-F, revised-F (HEAD)

Мы делаем это с:

git checkout -b revised-F <hash of T0>   # for merge --squash method

или:

git checkout -b revised-f branch-F^1      # for rebase -f method

в зависимости от того, какой метод мы хотим использовать.(Суффикс ^1 или ~1 - вы можете использовать любой из них - исключает сам M1, возвращаясь на шаг первого родителя к fc2. Идея здесь состоит в том, чтобы исключить коммит o и любые другие достижимые коммитыс o. Не должно быть никаких других слияний в branch-F вдоль этого нижнего ряда коммитов, здесь.)

Теперь, если мы хотим использовать «слияние в сквош» (которое использует механизм слияния в Gitбез слияния commit ), мы запускаем:

git merge --squash branch-F

При этом используется наш текущий коммит плюс подсказка branch-F (commit M1) в качестве левого и правогосторон слияния, находя их общий коммит в качестве базы слияния.Обычный коммит, конечно, просто F0, поэтому слияние result является снимком в M1.Тем не менее, новая сделанная фиксация имеет только одного родителя: это вообще не коммит слияния, и он выглядит так:

   fc1--fc2--M1   <-- branch-F
  /
F0-------------F3   <-- revised-F (HEAD)

Снимок вF3 соответствует этому в M1, но сам коммит является новым.Он получает новое сообщение о коммите (которое вы можете редактировать), и его эффект, когда Git смотрит на F3 как коммит, заключается в том, чтобы сделать тот же набор изменений, сделанных с F0 до M1.

Если мы выберем метод rebase, мы теперь запустим:

git rebase -f <hash-of-T0>

(Вместо этого можно использовать хеш o, то есть branch-F^2, то есть второй родительский элемент M1. Вв этом случае вы можете начать с revised-F, указывающего на M1. Вероятно, это то, что я бы сделал, чтобы избежать необходимости вырезать и вставлять много хеш-идентификаторов с потенциальными опечатками, но не очевидно, как это работает, если вывыполнил много упражнений по манипулированию графами.)

То есть мы хотим скопировать коммиты F0 - fc2 включительно в новые коммиты с новыми идентификаторами хэшей.Это то, что будет делать git rebase (см. Другие ответы StackOverflow и / или описание Линуса выше): мы получаем:

  F0'-fc1'-fc2'   <-- revised-F (HEAD)
 /
T0-----....
 \
  F0--fc1--fc2--M1   <-- branch-F

Теперь, когда у нас есть revised-F, указывающая либо на один коммит (F3) или цепочку коммитов (цепочка, оканчивающаяся на fc2', копия fc2), мы можем git checkout какую-то другую ветку и git merge revised-F.

Основываясь на комментариях, вот двапути для повторного слияния

На этом этапе я предполагаю, что у вас есть результат слияния сквоша (коммит с одним родителем, который не является слиянием, но содержит нужный снимок, который я называю F3 здесь).Нам также нужно немного пересмотреть перерисованный график, основываясь на комментариях, которые указывают, что было больше слияний в branch-F:

S0--sc1---sc2---sc3-----sc4----M4---R1---M5---sc5   <-- branch-S
  \                        \  /         /
   T0-----o-------o----M2---M3--------R2   <---- branch-T1
    \      \       \  /
    F0--fc1-o-fc2---M1   <--------------- branch-F

Теперь мы добавим ветку revised-F, которая должна иметьодин коммит, который является потомком либо F0, либо T0.Не важно, какой.Поскольку я использовал F0 ранее, давайте рассмотрим это здесь:

S0--sc1---sc2---sc3-----sc4----M4---R1---M5---sc5   <-- branch-S
  \                        \  /         /
   T0-----o-------o----M2---M3--------R2   <---- branch-T1
    \      \       \  /
    F0--fc1-o-fc2---M1   <--------------- branch-F
      \
       ---------------------------------F3   <-- revised-F

Содержимое коммита F3 соответствует содержимому M1 (поэтому git diff branch-F revised-F ничего не говорит), но родительский элемент F3 здесь F0.(Примечание: существуют быстрые способы создания F3 с использованием git commit-tree, но пока оно уже существует и соответствует M1 по содержанию, мы можем просто использовать его.)

Если мы сейчас это сделаем:

git checkout branch-T1
git merge revised-F

Git найдет базу слияния между коммитом R2 (вершина branch-T1) и F3 (вершина revised-F).Если мы перейдем по всем обратным (левым) ссылкам с R2, мы сможем добраться до T0 через M3, затем M2, затем до некоторого числа o с и, наконец, T0, или мы сможем добраться до F0 через M3, затем M2, затем M1, затем fc2 и обратно до F0.Между тем мы можем перейти от F3 прямо к F0 всего за один прыжок, поэтому база слияния, вероятно, равна F0.

(Чтобы подтвердить это, используйте git merge-base:

git merge-base --all branch-T1 revised-F

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

Git теперь будет запускать два git diff с, чтобы сравнить содержимое F0 с F3 - то есть все, что мы сделали для реализации этой функции - и сравнить содержимое F0 с содержимым R2, на кончике branch-T1.Мы получим конфликты, когда оба различия изменяют одни и те же строки одних и тех же файлов.В другом месте Git возьмет содержимое F0, применит объединенные изменения и оставит результат готовым к фиксации (в индексе).

Разрешение этих конфликтов и фиксация даст вам новый коммит, который приведет кв:

S0--sc1---sc2---sc3-----sc4----M4---R1---M5---sc5   <-- branch-S
  \                        \  /         /
   T0-----o-------o----M2---M3--------R2-----M6   <---- branch-T1
    \      \       \  /                     /
    F0--fc1-o-fc2---M1   <-- branch-F      /
      \                                   /
       ---------------------------------F3   <-- revised-F

Теперь M6, возможно, может объединяться в branch-S.


В качестве альтернативы, мы можем объединиться непосредственно в branch-S.Менее очевидно, какой коммит является базой слияния, но, вероятно, снова F0.Вот снова тот же рисунок:

S0--sc1---sc2---sc3-----sc4----M4---R1---M5---sc5   <-- branch-S
  \                        \  /         /
   T0-----o-------o----M2---M3--------R2   <---- branch-T1
    \      \       \  /
    F0--fc1-o-fc2---M1   <--------------- branch-F
      \
       ---------------------------------F3   <-- revised-F

Начиная с коммита sc5, мы работаем в обратном направлении до M5 до R2, и мы сейчас находимся в той же ситуации, в которой были раньше.Таким образом, мы можем git checkout branch-S и выполнить то же слияние, разрешить похожие конфликты - на этот раз мы сравниваем F0 с sc5, а не с R2, поэтому конфликты могут немного отличаться - и в конечном итоге фиксируем:

S0--sc1---sc2---sc3-----sc4----M4---R1---M5---sc5----M6   <-- branch-S
  \                        \  /         /           /
   T0-----o-------o----M2---M3--------R2   <------ / -- branch-T1
    \      \       \  /                           /
    F0--fc1-o-fc2---M1   <-- branch-F            /
      \                                         /
       ---------------------------------------F3   <-- revised-F

Чтобы убедиться, что F0 является базой для слияния, используйте git merge-base как раньше:

git merge-base --all branch-S revised-F

и, чтобы увидеть, что вам нужно слить, выполните два git diffs от базы слияния до двух советов.

(Что делать слияние - решать вам.)

...