во время слияния git с meld, почему я могу изменить LOCAL и REMOTE, если будет сохранен только MERGED?

согласно одному из ответов на этот вопрос https://stackoverflow.com/a/18011273/5238559, LOCAL, BASE и файлы REMOTE не будут изменены в процессе слияния, а только результирующий файл MERGED.

во время слияния в meld я бы изменил среднюю панель (BASE), перемещая код слева (LOCAL) и справа (REMOTE). Я понял, что BASE будет своего рода предварительным просмотром того, как будет выглядеть окончательно объединенный файл, но он не будет сохранен напрямую, что кажется логичным шагом безопасности.

однако я также могу переместить код из БАЗОВОГО в ЛОКАЛЬНЫЙ или УДАЛЕННЫЙ, и, когда я закрою объединение, мне будет предложено сохранить изменения во всех трех файлах. почему я могу это сделать, если только BASE (т.е. MERGED) имеет отношение к процессу слияния? что происходит с модификациями в LOCAL и REMOTE?


person mluerig    schedule 14.03.2021    source источник


Ответы (1)


TL;DR из TL;DR

Git не использует файлы вашего рабочего дерева, за исключением случаев, когда вы (или что-то еще) запускаете git add. Обратите внимание, что git mergetool запускает git add только для одного из файлов, с которыми работает meld. Таким образом, вы можете написать столько дополнительных файлов, сколько захотите. Гит это не волнует. Он заботится только об этом конкретном файле, когда meld выполнено.

TL;DR

Предположительно, вы используете этот инструмент слияния meld через git mergetool. Принцип работы git mergetool до смешного прост, если вы понимаете, как работает само слияние, и именно поэтому вы можете изменять все эти файлы: потому что они все просто файлы.

Чтобы все это имело смысл, вам нужно знать, как работает git merge. Это подводит нас к различиям между:

  • коммиты, которые Git фактически хранит;
  • Git's index, который имеет три имени; он участвует в совершении коммитов и берет на себя расширенную роль во время слияния; и
  • ваше рабочее дерево или рабочее дерево (оба названия относятся к одному и тому же), в котором хранятся файлы, которые вы и такие программы, как meld или vim, можете видеть и редактировать.

Третье из них — ваше рабочее дерево — это единственное место, где хранятся файлы, которые вы можете видеть. Но — и это очень важно — ваше рабочее дерево совсем не в Git. Это просто место, куда Git втыкает файлы, чтобы вы могли их видеть и работать над/с ними. Позже git add скопирует один из этих файлов обратно в индекс Git. Если вы используете git mergetool для запуска инструмента слияния, код git mergetool запустит git add для вас.

Сценарий mergetool запускает git add в объединенном файле (по имени), поэтому все, что в этом файле, получает git added. Любые оставшиеся файлы являются просто мусором для Git: они просто неотслеживаемые файлы. Я считаю, что mergetool должен очищать ненужные файлы (но следует не означает, что всегда будет, и мнения также могут различаться по части следует; есть сохранить здесь опцию резервного копирования, которую я никогда не использовал).

Длинная

Вы можете пропустить некоторые разделы ниже, в зависимости от того, насколько вы знакомы с Git. Я постараюсь сделать их короткими (опустив многое), но они все равно будут длинными.

Дополнительная информация о коммитах

Каждому коммиту Git присваивается уникальный номер. Эти числа не являются простым счетом — у нас нет коммита № 1, за которым следует № 2, затем № 3 и так далее. Вместо этого числа выглядят случайными, большими и уродливыми хэш-идентификаторами, вычисленными с помощью криптографической хэш-функции. Эти номера уникальны для всех репозиториев Git повсюду (именно так Git управляет распределенным характером коммитов), но все, что нам нужно знать, это то, что коммиты пронумерованы.

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

  • Каждый коммит имеет полный снимок каждого файла, хранящийся в специальном архивном формате, который может прочитать только Git. (Этот формат сжат, часто очень сильно, и удаляет дубликаты содержимого файлов. В нем могут храниться файлы, которые ваша ОС не может эффективно использовать или даже извлекать в некоторых случаях; в этих случаях слияние будет затруднено или невозможно. ) Файлы, которые находятся в коммите, определяются тем, что находится в индексе Git, как описано в следующем разделе, в то время, когда кто-то запускает git commit.

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

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

Индекс Git и то, как он расширяется во время слияний

У индекса в Git есть три названия: Git называет его индексом (как я делаю здесь), индексом (по крайней мере, для обычных коммитов) и — редко в наши дни, в основном во флагах типа --cachedкэш. Для обычных коммитов без слияния я предпочитаю описывать индекс как содержащий предлагаемый следующий коммит.

Индекс обычно представляет собой список кортежей: имя, режим и хэш-идентификатор:

  • Имя представляет собой имя файла, дополненное косой чертой, например top/sub/file.ext. На этом уровне Git не думает о каталогах, содержащих файлы: он просто имеет файлы с длинными именами, содержащими косую черту. Даже в Windows эти косые черты идут вперед, хотя Git должен поместить такой файл в файл с именем file.ext внутри папки с именем top, содержащей подпапку sub, которую Windows предпочитает обозначать как top\sub\file.ext. Индекс настаивает на использовании косой черты внутри. (Обычно это не отображается для пользователей, это просто способ понять проблему Git, которая не позволяет ему хранить пустую папку. Такого просто не может быть в индексе Git: индекс содержит только файлы .)

  • Режим для обычного файла действительно просто запоминает, +x или -x: исполняемый файл или неисполняемый файл. Для истерических причин это значение сохраняется как 100755 или 100644 соответственно. .

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

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

При запуске git merge он расширяет индекс. Он заменяет все записи нулевого этапа, представляющие текущую фиксацию (индекс должен совпадать с текущей фиксацией в начале операции слияния), на записи стадии 2. Это также открывает места для записей этапа 1 и этапа 3. Мы вернемся к этому ниже.

Ваше рабочее дерево

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

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

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

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

В остальном ваше рабочее дерево — это просто обычный старый набор файлов и каталогов/папок (в зависимости от того, какой термин вы предпочитаете). Все, что работает на вашем компьютере, работает и здесь. Помимо записи в него, когда вы говорите — например, с помощью git checkout — Git просто позволяет вам играть с ним в свое удовольствие. Затем вы можете запустить git status, который только просматривает его, или git add, который копирует его в индекс Git. Однако, пока вы не сделаете что-либо из этого, Git будет полностью автономным.

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

Внутренности трехстороннего слияния

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

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

Я добавил два имени ветки, branch1, которую мы сейчас проверили, т. е. мы используем коммит J для заполнения индекса Git и нашего рабочего дерева, и branch2, который выбирает коммит L. Обозначение (HEAD) показывает, что мы извлекли branch1. Все шесть перечисленных коммитов являются обычными коммитами с одним родителем, поэтому, если смотреть на коммит J — то есть, git log, если бы мы запускали его прямо сейчас, — мы видим, как история, сначала коммит J, затем коммит I, затем коммит H, затем совершить G и так далее. Как видно из фиксации L — если мы запустим git log branch2 — мы сначала увидим фиксацию L, затем K, затем H, затем G и так далее, как и раньше.

Эти две истории коммитов встретятся, когда мы идем назад, как здесь, в коммите H. Таким образом, коммит H является базой слияния в этом трехстороннем слиянии.

Цель слияния — объединить работу. Мы хотим, чтобы Git самостоятельно выяснил, что мы изменили с момента коммита H. Это наши изменения. Мы хотим, чтобы Git выяснили, что они изменили с момента коммита H. Это их изменения. На самом деле Git может сделать это, используя git diff:

git diff --find-renames <hash-of-H> <hash-of-J>

Это создаст список всех файлов, которые мы изменили, и какие строки нужно удалить и добавить в каждый из этих файлов, чтобы превратить копии тех файлов, которые существуют в коммите H, в копии тех же файлов, которые существуют в J.

Так же:

git diff --find-renames <hash-of-H> <hash-of-L>

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

Если Git просто (просто?) объединит эти два списка и применит оба наборы изменений к файлам, взятым из коммита H, Git получит набор файлов, который сохраняет наши изменения (H-to-J ) и добавляет свои изменения (от H до L). Во многих случаях некоторые файлы, которые мы изменили, не будут иметь никаких изменений на их стороне, и наоборот. Это будет легко для Git. В некоторых случаях некоторые файлы будут иметь изменения обе стороны. Если эти изменения затрагивают разные строки, Git может объединить эти изменения самостоятельно.

Во всяком случае, это правила, которые использует Git. Это просто:

  • Извлекает (в индекс Git) каждый файл в H: они входят в записи slot-1.
  • Извлекает (в индекс Git) каждый файл в J: они входят в записи слота-2. Конечно, они уже были в слоте 0, поэтому извлечение не требуется; Git может просто переместить записи слота 0 в слот 2. (При использовании git cherry-pick -n или аналогичного Git действительно нужно просто перемещать записи слотов, потому что в этих случаях не требуется, чтобы индекс соответствовал чему-либо. Но это особый случай, который git merge обычно не допускает.)
  • Извлекает (в индекс Git) каждый файл в L: они входят в записи слота-3.

Теперь в индексе есть три копии каждого файла из базового коммита слияния (BASE), --ours коммита (LOCAL) и их (REMOTE). Каждый из них на самом деле является просто хэш-идентификатором для внутреннего объекта большого двоичного объекта Git (ну, плюс имя и режим, а промежуточный номер представляет слот).1

Из-за трюка с дедупликацией, если никто не внес никаких изменений в файл, все три промежуточных слота будут содержать один и тот же хэш-идентификатор (и режим), и Git может просто свернуть все три элемента индекса обратно в одна запись с нулевым слотом. Если мы изменили файл, а они нет, база и их слот будут иметь одинаковый хэш-идентификатор (и режим), а наши будут отличаться, и Git просто возьмет нашу версию файла, переместив слот 2 в нулевой слот и удалив слоты 1 и 3. Если они изменили файл, а мы нет, база и наш слот будут имеют одинаковый хэш-идентификатор, а их идентификаторы будут отличаться, и Git просто возьмет их версию файла, переместив слот 3 в нулевой слот и т. д.

Это означает, что нам приходится усердно работать только с файлами, в которых обе стороны внесли изменения (ну, или для конфликтов высокого уровня/дерева, которые я здесь пропущу) . В этом случае различные стратегии слияния, которые сегодня есть в Git, работают следующим образом:

  • вызов драйвера слияния, если он есть: эта программа должна выполнить эту работу; или
  • вызов встроенного низкоуровневого драйвера слияния, в противном случае.

Встроенный низкоуровневый драйвер слияния работает построчно, используя git diff для отдельных файлов.2 Для каждого фрагмента различий, который вы видите в выводе git diff, он ищет посмотрите, коснулась ли другая сторона тех же строк или строк, которые касаются другого изменения (например, если наш diff добавляет строку в конце, а их diff также добавляет строку в конце, Git не имеет идея, какой порядок использовать при добавлении обоих наборов строк).3 Он записывает в нашу копию рабочего дерева рассматриваемого файла Git’s лучше всего угадать правильное слияние. Если все пойдет хорошо — если Git сможет объединить два набора изменений без конфликтов — Git затем выполнит внутреннюю git add над файлом. Если нет, Git оставляет конфликты в копии рабочего дерева файла с маркерами конфликтов и не выполняет внутреннюю проверку файла git add.

Когда низкоуровневый драйвер сталкивается с чем-то, что считается конфликтом, если действует расширенный аргумент -X ours или -X theirs, он просто примет наше изменение (от 1-против-2) или их изменение (1-против-3) в соответствии со значением -X и не ставить маркеры конфликта. Таким образом, низкоуровневые конфликты могут автоматически разрешаться в программном обеспечении с помощью этих флагов. Обратите внимание, однако, что Git не делает здесь ничего умного. Он просто выбирает разницу между файлами 1 и 2 или разницу между файлами 1 и 3 на основе фрагмента построчного сравнения. Но это позволяет Git запускать внутренний git add самостоятельно.

Когда Git запускает внутренний git add, он просто берет копию рабочего дерева файла и копирует ее в нулевой слот, стирая слоты с 1 по 3 для этого файла. Это помечает файл как разрешенный. Индекс возвращается к нормальному состоянию для этого одного набора файловых записей. После того, как все файлы были обработаны, либо в индексе Git все еще отображаются некоторые конфликты (поскольку какой-то файл не был предварительно свернут и не получил git add-ed), либо их нет (все файлы подверглись легкому сворачиванию индекса). , или получил git add-ed после того, как низкоуровневый драйвер сделал свое дело).


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

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

3Низкоуровневое объединение, которое Git не поддерживает напрямую, но которое вы можете получить с помощью git merge-file, используемого в качестве низкоуровневого драйвера слияния, который вы пишете, предполагает этот порядок строк не имеет значения и может справиться с этим, не вызывая конфликта.


Итог всего этого

Описание того, что слияние делает с индексом Git, довольно длинное, но если вы полностью следовали логике, то увидите следующее:

  • Любой файл, который не мог иметь конфликт, теперь находится на нулевой стадии.
  • Любой файл, который мог иметь конфликт, но драйвер (из .gitattributes) или встроенное по умолчанию низкоуровневое слияние файлов смог разрешить его самостоятельно (возможно, с помощью -X ours или -X theirs), также этап нулевой.
  • Следовательно, только файлы, которые имели неразрешимые низкоуровневые конфликты или конфликты высокого уровня/уровня дерева (которые я опускаю здесь из соображений экономии места), имеют ненулевые записи стадии индекса.

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

Если конфликт был конфликтом низкого уровня, и мы использовали встроенный в Git низкоуровневый драйвер слияния для какого-либо файла, копия рабочего дерева этого файла имеет маркеры конфликта в Это. Эти маркеры получаются при прогоне трех исходных входных файлов через тот же код, что и git merge-file (так что вы можете таким образом реконструировать конфликты слияния, но на данный момент есть более простой способ с git checkout -m или git restore -m). Независимо от того, что находится в копии рабочего дерева файла, три входных файла существуют в индексе.

Если мы сейчас запустим git mergetool, этот код будет рыться в индексе (используя git ls-files --stage или аналогичный), чтобы найти конфликтующие файлы. Затем он использует git checkout-index для извлечения трех файлов, которые были входными данными для низкоуровневого драйвера слияния. Они получают причудливые имена в стиле .gittemporary, которые git mergetool переименовываются в file_BASE, file_LOCAL и file_REMOTE соответственно (ну, точный шаблон именования сложен, и это всего лишь приближение). Для внутренних целей он копирует file в file_BACKUP. Затем он запускает выбранный вами инструмент слияния для этих файлов (за исключением резервной копии).

Ваш инструмент слияния теперь работает с файлами рабочего дерева. Ни один из этих файлов не находится в Git. Вы делаете с ними все, что хотите, используя инструмент слияния. Что бы ни было в file, git mergetool предполагает, что это результат, полученный с помощью инструмента слияния.

Здесь есть еще одна особенная хитрость:

  • Некоторые инструменты слияния имеют доверенные коды выхода, а некоторые нет.

  • Если ваш инструмент слияния помечен как доверенный и завершается со статусом слияние выполнено, используйте результат, Git git add сделает это. Это стирает три слота и помечает файл разрешенным.

  • Если вашему слиянию не доверяют, Git сравнит файл _BACKUP с выходными данными инструмента. Если файл не изменился, git mergetool спросит вас, считаете ли вы, что слияние сработало. Только если вы скажете «да», это будет git add результатом.

Когда git merge останавливается посередине, ваша задача состоит в том, чтобы навести порядок, записав в индекс Git в нулевой слот правильный результат слияния. Вы можете сделать это любым удобным для вас способом. Обычно я предпочитаю просто открывать file в vim после того, как Git запишет его с merge.conflictStyle установленным на diff3. Я считаю, что большинство конфликтов легко решить таким образом. В некоторых случаях я действительно хочу получить три версии, и для этих случаев git mergetool является способом сделать это, но, поиграв с git mergetool, я не нашел его особенно < em>хороший способ сделать это. Тем не менее, это одна из тех сделок с предпочтениями пользователей.

В любом случае, как только вы разрешите все конфликты и запустите git add для обновления индекса Git, вы должны запустить:

git merge --continue

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

В старые добрые времена вам приходилось бежать:

git commit

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

person torek    schedule 14.03.2021
comment
Я очень ценю, что вы дали такой подробный ответ, с большим количеством справочной информации и всего остального. Я несколько раз читал то, что вы написали, но прямого ответа на свой вопрос там не нашел (или он был погребен с другой инфой). Извините... - person mluerig; 18.03.2021
comment
@mluerig: раздел TL; DR является первым, перед длинной частью. Чтобы уменьшить его еще больше: Git не использует ваши файлы рабочего дерева, за исключением случаев, когда вы запускаете git add. git mergetool запускает git add только для одного из файлов, с которыми работает meld. Таким образом, вы можете написать столько дополнительных файлов, сколько захотите. Гит это не волнует. Он заботится только об этом одном конкретном файле, когда объединение завершено. - person torek; 18.03.2021
comment
хорошо спасибо! меня все еще сбивает с толку то, что мне разрешено редактировать эти файлы, хотя они нигде не сохраняются - но я думаю, что это особенность слияния? - person mluerig; 18.03.2021
comment
Это своего рода особенность Git. Я предполагаю, что вы запустили git mergetool, и mergetool создает эти файлы, а затем снова удаляет их позже (иногда, не всегда!) и больше их не использует. - person torek; 18.03.2021
comment
хорошо - еще раз спасибо за все эти усилия. если бы вы могли добавить эту информацию в tl; dr часть вашего ответа, я могу принять это :-) - person mluerig; 18.03.2021