Как увидеть трехсторонний git diff даже после разрешения конфликтов - PullRequest
1 голос
/ 12 апреля 2020

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

  line1
++<<<<<<< HEAD
 +line1a
++=======
+ line1b
++>>>>>>> b


  line2
++<<<<<<< HEAD
 +line2a
++=======
+ line2b
++>>>>>>> b

Частично через Слияние git diff по-прежнему показывает трехстороннюю разницу

  line1
 +line1a
+ line1b


  line2
++<<<<<<< HEAD
 +line2a
++=======
+ line2b
++>>>>>>> b

, но как только я разрешаю все конфликты и добавляю их, git diff ничего не показывает. Как я могу увидеть трехсторонний дифференциал? В частности, я хочу видеть что-то вроде

  line1
 +line1a
+ line1b


  line2
 +line2a
+ line2b

1 Ответ

1 голос
/ 13 апреля 2020

TL; DR

Попробуйте использовать git checkout -m, но будьте очень осторожны с этим, поскольку это разрушительная команда. Обратите внимание, что это только иногда работает.

Long

Они на самом деле отличаются: то, что вы видите в файле рабочего дерева во время конфликтующего слияния - от всего, что использует слияние Git двигатель, включая сбор вишни, который происходит во время перебазирования - это то, что осталось от Git низкоуровневого драйвера слияния , и то, что вы видите при запуске git diff, создается Git '* комбинированный код различий .

Первый тип вывода, который не имеет формального имени, может быть воспроизведен в любое время , если у вас есть все три ввода файлы доступны . Второй тип вывода, комбинированный diff, ... сложнее.

Сам драйвер низкоуровневого слияния имеет значение , доступное как отдельная программа, git merge-file.

Как я могу увидеть [комбинированный] diff?

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

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

    git checkout -m -- path/to/file.ext
    

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

  • Если вы выполнили ребазирование или аналогичное, вам придется повторить конкретную операцию, чтобы снова вызвать конфликт.

  • Слияния немного отличаются, как мы увидим через мгновение.

В Git конфликты возникают при выполнении "трехстороннего слияния" , Трехстороннее слияние подразумевает три входных файла . Когда вы используете простой git merge, источники для этих трех файлов легче увидеть, поэтому давайте рассмотрим этот случай, прежде чем мы перейдем к ребазингу и черри-выбору. Вот еще больше информации, которую вы должны знать в первую очередь, чтобы понять, что здесь происходит.

Что нужно знать об индексе Git

Мы начнем с серии коммитов, которые начинаются с некоторой общей общей истории, например:

...--G--H   <-- master

Теперь мы создадим два новых имени ветви, branch1 и branch2, оба указывают на существующий коммит, чей ha sh равен H:

...--G--H   <-- master, branch1, branch2

, так что все коммиты находятся на всех ветвях. , Затем в каждой из этих двух новых веток мы делаем несколько новых коммитов. Неважно, сколько, по крайней мере, один на каждой ветви; Я нарисую по два на каждом здесь, как только мы туда доберемся.

Что-то, что нужно знать о коммитах, заключается в том, что каждый из них содержит снимок всех ваших файлов в специальном, читаемом только, Git -только, сжатый формат. Это замораживает копию файлов на все времена, так что Git может получить их позже, с любого коммита, в любое время. Замороженная копия может использоваться только по Git, поэтому для незамерзшей обычной копии нужно go где-то еще. Вы сообщаете git checkout, какой коммит вы хотите, и он извлекает файлов, превращая их обратно в обычные и полезные файлы, помещая полезные копии в вашу рабочую область, что Git вызывает ваше рабочее дерево или рабочее дерево .

Если вы git checkout сделаете коммит по его sh ID, Git извлечет все это Зафиксируйте замороженные файлы в вашем рабочем дереве, чтобы вы могли видеть и использовать эту версию c. Это не совсем то, как вы обычно работаете с Git.

Что нужно знать о новых коммитах, так это то, что Git делает их из Git индекс , не из вашего рабочего дерева. То есть: мы используем git checkout, чтобы выбрать имя ветви , которая, в свою очередь, выбирает последний коммит, содержащийся в этой ветви. Теперь у нас есть текущее имя - Git, которое присоединяет специальное имя HEAD к одному из имен ветвей - и текущее commit . Git копирует каждый подтвержденный файл из коммита в ваше рабочее дерево ... но также копирует каждый зафиксированный файл в * 169 * index .

Другими словами индекс содержит копию каждого файла из текущего коммита. 1 Поначалу эта копия кажется бессмысленной: она есть в вашем рабочем дереве. Почему бы не использовать это? Другие системы контроля версий на самом деле делают это, но Git этого не делает. Точно, почему, ну, это до Git авторов, но мы можем заметить это: копия index находится в замороженном формате. Это означает, что нет необходимости повторно сжимать копию рабочего дерева. Команда git add может взять обновленную копию рабочего дерева и сжать ее, и теперь индексная копия обновляется и готова к фиксации. Когда вы запускаете git commit, копия index каждого файла является той, которая входит в новый коммит.

Поэтому мы можем сказать, что индекс содержит ваш предложенный следующий коммит . Это будет немного сложнее в данный момент, но сейчас давайте git checkout branch и сделаем один новый коммит. Начнем с этого:

...--G--H   <-- master, branch1 (HEAD), branch2

Текущая ветвь равна branch1. Текущий коммит равен H (что означает некоторый фактический идентификатор га sh). И индекс Git, и ваше рабочее дерево заполнены снимком из коммита H.

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

          I
         /
...--G--H   <-- master, branch1 (HEAD), branch2

и теперь происходит волшебный шаг c: Git записывает идентификатор ha sh нового коммита в текущее имя, так что branch1 теперь указывает на I:

          I   <-- branch1 (HEAD)
         /
...--G--H   <-- master, branch2

Таким образом, ветви растут по одному коммиту за раз, когда мы используем git checkout, чтобы получить их, изменить файлы рабочего дерева, использовать git add скопировать обновленные файлы обратно в индекс, чтобы быть готовым к созданию снимка, а затем запустить git commit, чтобы сделать снимок. Новый снимок указывает на тот, который был текущим - был HEAD - и теперь новый является текущим. Новый был только что сделан из индекса, так что индекс и фиксация совпадают, как они это сделали, когда мы безошибочно извлекли коммит H ранее, и мы готовы изменить и зафиксировать еще немного.


1 Технически, индекс содержит ссылку на внутренний Git blob ha sh ID, а не на фактическую копию файла. Но если вы не начнете копаться в деталях индекса - как мы это сделаем через мгновение - вы не сможете понять разницу между этим и наличием полной копии файла.


Слияние, обычное - style

Итак, допустим, мы сделали два коммита в каждой ветви и получили branch1 прямо сейчас, например:

          I--J   <-- branch1 (HEAD)
         /
...--G--H
         \
          K--L   <-- branch2

(имя master по-прежнему указывает на H но я буду ленивый и перестану рисовать это сейчас). Теперь мы запускаем git merge branch2.

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

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

  • Копия каждого файла в базе слияния , фиксация H, помещается в слот 1.

  • Копия каждого файла из текущего коммита J попадает в слот 2. На практике уже есть копия в нулевом слоте - нормальная неконфликтующий полностью разрешенный слот - так что Git может просто переместить его на один шаг. Здесь есть несколько сложных случаев, которые вы обычно не видите сами, если ваш индекс и / или рабочее дерево грязные, потому что команда git merge не позволит вам начать, если ваш индекс и / или рабочее дерево грязные . 2

  • Копия каждого файла из другого коммита, L здесь, попадает в слот 3.

Теперь есть три копии каждого файла , по крайней мере для каждого файла, который входит во все три коммита, что является интересным случаем здесь.

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

Если базовая копия слияния совпадает с их копия - слот 1 = слот 3 - но наша нет , тогда мы должны были изменить файл. Правильный результат слияния: , возьмите наш файл , поэтому Git перемещает копию слота 2 в нулевой слот, стирая два других слота и снова оставляя файл рабочего дерева в покое. Файл решен: мы использовали наш.

Базовая копия слияния соответствует наша копия - слот 1 = слот 2, но их нет, тогда они должны были изменить файл. Правильный результат слияния: взять их файл , поэтому Git перемещает копию слота-3 в нулевой слот и на этот раз также извлекает копию слота-3 в рабочее дерево. Файл решен: мы использовали их.

Только для случая всех трех слотов Git должна выполнять какую-либо реальную работу. Git теперь вызывает свой трехуровневый однофайловый драйвер слияния для трех файлов.

Низкоуровневый драйвер записывает копию файла рабочего дерева в качестве вывода. Он также смотрит на каждое фактическое изменение строки источника, то есть на то, что мы увидим, если запустим git diff. Он сравнивает базовую копию слияния (слот 1) файла с нашей копией (слот 2), чтобы увидеть, что мы изменили, и сравнивает базу слияния с их (слот 1 против слота 3), чтобы увидеть, что они изменили. Там, где изменения не перекрываются или примыкают (касаются), стандартный драйвер слияния низкого уровня заменяет линии слота-1 линиями других слотов. Если изменения do перекрываются или примыкают, стандартный низкоуровневый драйвер слияния записывает конфликт слияния в копию файла рабочего дерева.

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

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

Команда git add скопирует все, что у вас есть в вашем файле рабочего дерева в нулевой слот и удалите остальные три слота. Таким образом, обновив файл рабочего дерева, вы запускаете для него git add, и это означает, что файл разрешен.

После разрешения всех конфликтов вы запускаете git merge --continue или git commit, чтобы сообщить Git до финиша sh работа. Git использует файлы, которые все находятся в нулевом слоте, для создания нового коммита. Поэтому он имеет снимок из индекса, как обычно. Единственное, что есть специальные о новом слиянии совершить то, что он имеет не только обычные один родитель, но два

1295

первый родительский объект слияния - это тот же коммит, которым он всегда будет, в данном случае - J, а второй родительский элемент - другой коммит: в данном случае L.


2 Грязный здесь означает копия некоторого файла в индексе и / или рабочем дереве не соответствует HEAD -коммиту копия файла . Пока все три копии do совпадают, так что команда git status сообщает nothing to commit, working tree clean, не имеет значения, откуда взялась эта копия слота-2: все три совпадают.


Cherry-picking объединяется

Давайте рассмотрим более простую серию коммитов. Вместо двух веток, которые мы хотим объединить , давайте предположим, что у нас просто есть это:

        tag:v1.0
           |
           v
...--E--F--G   <-- release/1
            \
             H--I--J   <-- develop (HEAD)

Мы сделали какой-то фактический выпуск программного обеспечения с коммитом G, являющимся версия выпуска 1.0 (и помеченная и разветвленная). Мы пошли дальше и начали добавлять новые функции в ветку разработки и сделали новые коммиты H-I-J. Теперь мы понимаем: эй, в коммите J, единственное изменение , которое мы сделали, это исправило неприятную ошибку, существующую и в коммите G (возможно, введенную еще в коммите E или F поэтому он есть в G и H и I).

Мы бы хотели обновить наш выпуск до v1.1 с исправлением, которое мы добавили из J. То есть мы хотим скопировать коммит J в новый коммит, подобный J - который исправляет ошибку - но это происходит после G. 3 Мы позвоним этот новый коммит J':

        tag:v1.0
           |
           v
...--E--F--G--J'  <-- release/1
            \
             H--I--J   <-- develop

(Как только все это будет сделано, мы пометим коммит J' как v1.1 и перезапустим.)

Итак, мы run:

git checkout release/1
git cherry-pick develop

Сам по себе вишневый выбор работает просто:

  • Предположим, что каждый коммит имеет один родительский коммит. В этом случае J имеет одного родителя, I.
  • Обрабатывает текущий коммит - который будет G после git checkout - как слот-2.
  • Обрабатывайте родителя как базу слияния , а сам коммит как другой - или слот 3 - коммит.

Так Git теперь будет отличать файлы в I от файлов в G, чтобы увидеть, что мы изменили, т. Е. На go в обратном направлении с I до G отступая от того, что мы сделали в H. Он будет отличать файлы в I от файлов в J, чтобы увидеть, что они изменились, чтобы исправить ошибку. Затем он будет объединять наши изменения с их изменениями, как обычно.

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

Как только все конфликты разрешены, Git делает новый коммит как обычный, с одним родителем коммит, а не как коммит слияния. Его единственным родителем является коммит, который был HEAD раньше, а новый коммит теперь HEAD как обычно.


3 На самом деле может быть лучше найти оригинал зафиксировать, что представил ошибку, и создать ветку и исправить ее в ветке. Затем мы можем объединить это исправление с каждым выпуском вместо выбора вишни. Разница не имеет значения на иллюстрации выше - на самом деле, выбор вишни становится все проще и проще - но со временем разница в конечном итоге имеет значение с точки зрения управления выпусками. См. Серию Рэймонда Чена об этом .


Сама ребазировка в основном представляет собой серию операций по подбору вишни

Если мы начнем с:

...--G--H   <-- master
         \
          I--J   <-- feature (HEAD)

и кто-то добавит master коммитов, чтобы у нас было:

...--G--H--K--L   <-- master
         \
          I--J   <-- feature (HEAD)

, мы могли бы скопировать I в новый и улучшенный I', затем скопируйте J в новый и улучшенный J', чтобы получить:

                I'-J'  <-- HEAD (detached HEAD)
               /
...--G--H--K--L   <-- master
         \
          I--J   <-- feature

Как только это будет сделано, мы бы хотели Git очистить имя feature off commit J и сделайте так, чтобы вместо него было зафиксировано J', и заново прикрепите HEAD:

                I'-J'  <-- feature (HEAD)
               /
...--G--H--K--L   <-- master
         \
          I--J   [abandoned]

Копирование из I в I' и из J до J', это именно то, что git cherry-pick делает. Таким образом, rebase может:

  • перечислить коммиты для копирования в правильном порядке (I, затем J); 4
  • отсоединить HEAD, проверив целевой коммит L по номеру га sh, эквивалентному git checkout --detach, и исторически один вид перебазирования буквально выполнял эту команду;
  • выполнить два git cherry-pick команды; 5 и
  • принудительно перемещают ветвь и повторно присоединяют HEAD. 6

(я выиграл ' Мы не можем понять, как работает new-i sh --rebase-merges, что сильно усложняет это.)


4 Получение этого списка из права совершает копирование, на самом деле довольно сложно. Мы не будем go детализировать здесь.

5 Некоторые операции перебазирования буквально делают это, по одной: интерактивное перебазирование, в частности, превращает каждую команду pick в отдельную git cherry-pick шаг. Другие стараются быть более эффективными и / или немного отличаться внутренне, особенно внутренняя часть старого стиля git-rebase--am. Git 2.26, наконец, отходит от использования этого перебазирования старого стиля по умолчанию, поскольку оно пропускает некоторые случаи переименования.

6 Этот последний шаг вы можете сделать вручную с помощью git checkout -B или git switch -C, если по какой-то причине вы хотите выполнить все четыре шага вручную.


Наконец, вернемся к исходному вопросу

Как я могу увидеть трехсторонняя разница?

Очевидно, нам нужны три входа : версия с слиянием и две другие версии. Допустим, имя файла здесь F .

Если вы только что запустили что-либо, использующее Git механизм слияния , и находитесь в середине конфликтующего слияния три входа находятся в Git index прямо сейчас. Вот где их получил драйвер слияния Git. Написана его собственная попытка слияния с файлом рабочего дерева, и вы можете увидеть это, посмотрев на него.

Или вы можете запустить git diff сейчас. git diff замечает, что для файла F существует три индексные копии. Он различает три и объединяет различия в комбинированный дифференциал . 7

Эти индексные копии можно назвать для определенных команд Git, используя :1:<em>F</em>, :2:<em>F</em> и :3:<em>F</em>. Одна из наиболее полезных Git команд здесь, например, git show:

git show :1:path/to/file > file.BASE
git show :2:path/to/file > file.OURS
git show :3:path/to/file > file.THEIRS

. * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *.. Git уничтожил три копии с более высоким номером, заменив их одной копией с нулевым интервалом. Вы можете видеть с git show, используя имя :path/to/file или :0:path/to/file, но на самом деле это уже тот, который уже находится в вашем рабочем дереве, так зачем беспокоиться?

Если хотите, вы можете Git до реконструировать конфликт слияния:

 git checkout -m -- path/to/file

Git помещает три копии обратно в три слота и повторно запускает драйвер слияния, перезаписывая рабочее дерево копия. 8

Чтобы получить git diff, чтобы дать вам комбинированный дифференциал на этом этапе, вы должны поместить три копии в указатель. Если вы действительно хотите, есть способ загрузить произвольное содержимое файла в индекс с любым номером промежуточного слота, используя git update-index, но это сложно: вы должны превратить их в Git объекты BLOB-объектов сначала и получите их га sh идентификаторы. Я не рекомендую делать это, так как трудно понять правильно:

git hash-object -w -t blob --stdin < contents

создает соответствующий BLOB-объект ha sh, после чего git update-index --index-info может читать строки из стандартного ввода, чтобы поместить объекты в индексные слоты. Формат потока stdin, заданный для git update-index --index-info, довольно жесткий и предназначен только для использования другими программами. (Обратите внимание, что --cacheinfo, который проще в использовании, не позволяет записывать в ненулевые номера слотов.)

Как только вы фиксируете результат слияния - как слияние, или выбранный вишней коммит, или что угодно - все данные git checkout -m исчезли, и вы не можете восстановить состояние слияния таким образом. Однако при слиянии commit записываются оба его родительских коммита, и выполнение git show при коммите слиянием вызывает код комбинированного diff.

Здесь есть большое предостережение: git show при фиксации слияния по умолчанию используется комбинированный diff в стиле --cc (two-da sh, two- c). Это отличается от вывода git diff во время конфликтующего слияния, когда конфликты находятся в ненулевых временных интервалах индекса. Использование git show -c заставляет Git использовать стиль -c one-da sh one- c, который ближе (но не совпадает с) к выводу git diff во время конфликтующего объединения.


7 Это не совсем верно, поскольку при изменении копии рабочего дерева вы увидите, что выходные данные из git diff изменяются. Git знает, что это не то, о чем мы заботимся: мы действительно хотим видеть дерево слотов-2-vs-work-tree и слот-3-vs-work-tree. Вот что здесь используется и объединяется.

8 Вы можете сделать это git checkout -m без предварительного git add -ing файла, чтобы пометить его как разрешенный. В этом случае три слота уже заполнены и готовы к go. Однако копия рабочего дерева по-прежнему заточена, и это, пожалуй, самая важная часть здесь.


Связанная работа

Это совсем не то же самое, но вы можете быть заинтересованным в интердиффах и диапазонах . См. Что interdiff делает, что diff не может? и Как мне получить interdiff между этими двумя git коммитами? для получения дополнительной информации.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...