git checkout --ours не удаляет файлы из списка несмешанных файлов

Привет, мне нужно объединить две ветки вот так.

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

git merge branch1
...conflicts...
git status
....
# Unmerged paths:
#   (use "git add/rm <file>..." as appropriate to mark resolution)
#
#   both added:   file1
#   both added:   file2
#   both added:   file3
#   both added:   file4
git checkout --ours file1
git chechout --theirs file2
git checkout --ours file3
git chechout --theirs file4
git commit -a -m "this should work"
U   file1
fatal: 'commit' is not possible because you have unmerged files.
Please, fix them up in the work tree, and then use 'git add/rm <file>' as
appropriate to mark resolution and make a commit, or use 'git commit -a'.

Когда я делаю git merge tool, есть правильный контент только из «нашей» ветки, и когда я сохраняю его, файл исчезает из несмешанного списка. Но поскольку у меня есть сотни таких файлов, это не вариант.

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

Но мне кажется, я неправильно понял концепцию git checkout --ours/theirs команд после слияния.

Не могли бы вы предоставить мне некоторую информацию, как справиться с этой ситуацией? Я использую git 1.7.1


person Charlestone    schedule 11.09.2016    source источник


Ответы (1)


В основном это причуда того, как git checkout работает внутри. У разработчиков Git есть тенденция позволять реализации диктовать интерфейс.

Конечным результатом является то, что после git checkout с --ours или --theirs, если вы хотите разрешить конфликт, вы также должны git add те же пути:

git checkout --ours -- path/to/file
git add path/to/file

Но это не случай с другими формами git checkout:

git checkout HEAD -- path/to/file

or:

git checkout MERGE_HEAD -- path/to/file

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

Чтобы увидеть, как все это работает внутри, и почему вам нужен отдельный git add, кроме тех случаев, когда вы этого не делаете, читайте дальше. :-) Для начала давайте кратко рассмотрим процесс слияния.

Слияние, часть 1: как начинается слияние

Когда вы бежите:

$ git merge commit-or-branch

первое, что делает Git, - это находит базу слияния между названным коммитом и текущим (HEAD) коммитом. (Обратите внимание, что если вы укажете здесь имя ветки, как в git merge otherbranch, Git преобразует это в идентификатор фиксации, а именно в конец ветки. Он сохраняет аргумент имени ветки для сообщения журнала слияния, но требует идентификатор фиксации для найти базу слияния.)

Найдя подходящую базу слияния, 1 Git затем создает два git diff листинга: один от базы слияния до HEAD, а другой от базы слияния до указанного вами коммита. Это дает «что вы изменили» и «что они изменили», которые Git теперь должен объединить.

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

Для файлов, в которых они внесли изменения, а вы - нет, Git может просто взять их версию.

Для файлов, в которые вы оба внесли изменения, Git должен выполнить настоящую работу по слиянию. Он сравнивает изменения построчно, чтобы увидеть, можно ли их объединить. Если он может объединить их, он это сделает. Если слияния кажутся - опять же, основанными на чисто построчном сравнении - конфликтующими, Git объявляет «конфликт слияния» для этого файла (и продолжает и все равно пытается выполнить слияние, но оставляет маркеры конфликта на месте).

После того, как Git объединил все, что мог, он либо завершает слияние - поскольку конфликтов не было, - либо останавливается с конфликтом слияния.


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

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

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


Слияние, часть 2: остановка с конфликтом и "индекс" Git

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

Для каждого поэтапного файла в вашем рабочем дереве индекс содержит до четырех записей, а не только одну запись. Максимум три из них фактически используются, но есть четыре слота, пронумерованные с 0 по 3.

Нулевой слот используется для разрешенных файлов. Когда вы работаете с Git и не выполняете слияния, используется только нулевой слот. Когда вы редактируете файл в рабочем дереве, он имеет «неустановленные изменения», а затем вы git add файл, и изменения записываются в репозиторий, обновляя нулевой слот; ваши изменения теперь «поэтапно».

Слоты 1-3 используются для неразрешенных файлов. Когда git merge должен остановиться из-за конфликта слияния, он оставляет нулевой слот пустым и записывает все в слоты 1, 2 и 3. Версия файла базовой слияния записывается в слот 1, --ours версия записывается в слоте 2, а версия --theirs записывается в слот 3. Эти ненулевые записи слота позволяют Git узнать, что файл не разрешен. 2

При разрешении файлов вы git add их, что стирает все записи слотов 1-3 и записывает запись с нулевым слотом для поэтапной фиксации. Таким образом Git узнает, что файл разрешен и готов к новой фиксации. (Или, в некоторых случаях, вы git rm файл, и в этом случае Git записывает специальное «удаленное» значение в нулевой слот, снова стирая слоты 1–3.)


2 В некоторых случаях один из этих трех слотов также пуст. Предположим, что файл new не существует в базе слияния и добавлен как в нашу, так и в их. Затем :1:new остается пустым, а :2:new и :3:new записывают конфликт добавления / добавления. Или предположим, что файл f действительно существует в базе, изменен в нашей ветке HEAD и удален в их ветке. Затем :1:f записывает базовый файл, :2:f записывает нашу версию файла, а :3:f пусто, записывая конфликт изменения / удаления.

В случае конфликтов модификации / модификации все три слота заняты; только при отсутствии одного файла одно из этих слотов становится пустым. Логически невозможно иметь два пустых слота: нет ни конфликта удаления / удаления, ни конфликта создания / добавления. Но есть некоторая странность с конфликтами переименования, которые я здесь опустил, поскольку этот ответ достаточно длинный! В любом случае, именно наличие некоторых значений в слотах 1, 2 и / или 3 помечает файл как неразрешенный.


Слияние, часть 3: завершение слияния

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

Команда фиксации работает так же, как и всегда: она превращает содержимое индекса в объекты tree и записывает новую фиксацию. Единственная особенность коммита слияния - это то, что у него более одного идентификатора родительского коммита. 3 Дополнительные родительские элементы берутся из файла, который git merge оставляет после себя. Сообщение слияния по умолчанию также исходит из файла (на практике это отдельный файл, хотя в принципе их можно было объединить).

Обратите внимание, что во всех случаях содержимое новой фиксации определяется содержимым индекса. Более того, после выполнения новой фиксации индекс все еще заполнен: он по-прежнему содержит то же содержимое. По умолчанию git commit не будет делать еще одну новую фиксацию на этом этапе, потому что видит, что индекс соответствует HEAD фиксации. Он называет это «пустым» и требует --allow-empty сделать дополнительную фиксацию, но индекс вообще не пуст. Он все еще довольно полон - он просто полон того же самого, что и коммит HEAD.


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

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


Как git checkout branch использует индекс

После всего этого мы должны посмотреть, как git checkout также использует индекс Git. Помните, что при нормальном использовании занят только нулевой слот, а в индексе есть одна запись для каждого поэтапного файла. Более того, эта запись соответствует текущей (HEAD) фиксации, если вы не изменили файл и git add не изменили результат. Он также соответствует файлу в рабочем дереве, если вы не изменили файл. 4

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

Скажем, для конкретности, что вы используете master, а делаете git checkout branch. Git будет сравнивать каждую текущую запись индекса с записью индекса, которая должна быть в самой последней фиксации ветки branch. То есть для файла README.txt содержимое master такое же, что и для branch, или они разные?

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

В частности, в случае, когда файл branch отличается от файла master, git checkout должен заменить запись индекса версией из branch - или, если README.txt не существует в подсказке фиксации branch, Git должен удалить запись индекса. Более того, если git checkout собирается изменить или удалить запись индекса, ему также необходимо изменить или удалить файл рабочего дерева. Git проверяет, что это безопасно, т.е. что файл рабочего дерева совпадает с файлом коммита master, прежде чем он позволит вам переключать ветви.

Другими словами, именно так (и почему) Git определяет, можно ли менять ветки - есть ли у вас модификации, которые могут быть нарушены при переключении с master на branch. Если у вас есть модификации в вашем рабочем дереве, но измененные файлы одинаковы в обеих ветвях, Git может просто оставить изменения в индексе и рабочем дереве. Он может и будет предупреждать вас об этих измененных файлах, «перенесенных» в новую ветку: easy, так как он все равно должен был это проверить.

После того, как все тесты пройдены и Git решил, что можно переключиться с master на branch - или, если вы указали _71 _, _ 72_ фактически обновляет индекс со всеми измененными (или удаленными) файлами и обновляет дерево работы для соответствия.

Обратите внимание, что все это действие использовало нулевой слот. В слотах 1–3 вообще нет записей, так что git checkout не нужно удалять такие вещи. Вы не находитесь в середине конфликтующего слияния, и вы запустили git checkout branch не только для извлечения одного файла, но и для всего набора файлов и переключения ветвей.

Также обратите внимание, что вы можете вместо проверки ветки проверить конкретную фиксацию. Например, вот как вы можете посмотреть на предыдущую фиксацию:

$ git log
... peruse log output ...
$ git checkout f17c393 # let's see what's in this commit

Действие здесь такое же, как и при проверке ветки, за исключением того, что вместо использования tip фиксации ветки Git проверяет произвольную фиксацию. Вместо того, чтобы находиться «в» новой ветке, вы теперь находитесь в no ветке: 5 Git дает вам «отдельную HEAD». Чтобы снова прикрепить голову, вы должны git checkout master или git checkout branch вернуться "на" ветку.


4 Запись индекса может не соответствовать версии рабочего дерева, если Git выполняет специальные модификации завершения CR-LF или применяет фильтры смазывания. Это становится довольно продвинутым, и лучше всего пока проигнорировать этот случай. :-)

5 Точнее, это помещает вас в анонимную (безымянную) ветку, которая будет расти из текущего коммита. Вы останетесь в режиме отсоединенной HEAD, если сделаете новые коммиты, и как только вы git checkout сделаете другой коммит или ветку, вы переключитесь туда, и Git «откажется» от сделанных вами коммитов. Смысл этого отдельного режима HEAD состоит в том, чтобы вы могли осмотреться и, чтобы вы могли делать новые коммиты, которые просто исчезнут, если вы не предпримете специальных действий для сохранения. их. Тем не менее, для тех, кто относительно плохо знаком с Git, наличие коммитов «просто уходи» не так хорошо, поэтому убедитесь, что вы находитесь в этом режиме «отдельной HEAD», когда бы вы ни находились в нем.

Команда git status сообщит вам, находитесь ли вы в режиме отсоединенной HEAD. Используйте его часто. 6 Если у вас старый Git (OP - 1.7.1, который сейчас очень старый), git status не так полезен, как в современных версиях Git, но все же лучше чем ничего.

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


Извлечение определенных файлов и почему иногда это разрешает конфликты слияния

Однако у команды git checkout есть и другие режимы работы. В частности, вы можете запустить git checkout [flags etc] -- path [path ...] для проверки определенных файлов. Здесь все становится странно. Обратите внимание, что когда вы используете эту форму команды, Git не проверяет, не перезаписываете ли вы свои файлы. 7

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

Вообще говоря, Git хранит файлы в трех местах:

  • в коммитах; 8
  • в индексе;
  • и в дереве работы.

Команда checkout может считывать данные из любого из первых двух мест и всегда записывает результат в дерево работы.

Когда git checkout получает файл из фиксации, он сначала копирует его в индекс. Каждый раз, когда он это делает, он записывает файл в нулевой слот. Запись в нулевой слот стирает слоты 1-3, если они заняты. Когда git checkout получает файл из индекса, ему не нужно копировать его в индекс. (Конечно, нет: он уже там!) Вот как git checkout работает, когда вы не в середине слияния: вы можете git checkout -- path/to/file вернуть индексную версию. 9

Однако предположим, что вы находитесь в середине конфликтного слияния и собираетесь git checkout какой-то путь, возможно, с --ours. (Если вы не находитесь в середине слияния, в слотах 1-3 ничего нет, и --ours не имеет смысла.) Итак, вы запускаете git checkout --ours -- path/to/file.

Этот git checkout получает файл из индекса - в данном случае из слота индекса 2. Поскольку он уже находится в индексе, Git не записывает в индекс, а только в рабочее дерево. Итак, файл не разрешен!

То же самое и с git checkout --theirs: он получает файл из индекса (слот 3) и ничего не решает.

Но: если вы git checkout HEAD -- path/to/file, вы говорите git checkout извлечь из HEAD фиксации. Поскольку это фиксация, Git начинает с записи содержимого файла в индекс. Это записывает слот 0 и стирает 1-3. И теперь файл разрешен!

Поскольку во время конфликтующего слияния Git записывает идентификатор объединяемого коммита в MERGE_HEAD, вы также можете git checkout MERGE_HEAD -- path/to/file получить файл из другого коммита. Он также извлекается из фиксации, поэтому записывает в индекс, разрешая файл.


7 Мне часто хочется, чтобы Git использовал для этого другую команду внешнего интерфейса, поскольку тогда мы могли бы однозначно сказать, что git checkout безопасен, что он не будет перезаписывать файлы без --force. Но этот вид git checkout действительно перезаписывает файлы специально!

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

То же самое, собственно, и с индексом. Каждый слот индекса содержит не фактическое содержимое файла, а скорее хеш-идентификаторы объектов BLOB-объектов в репозитории.

Однако для наших целей это не имеет большого значения: мы просто просим Git получить commit:path, и он находит для нас деревья и идентификатор большого двоичного объекта. Или мы просим Git получить :n:path, и он находит идентификатор большого двоичного объекта в записи индекса для path для слота n. Затем он получает содержимое файла, и все готово.

Этот синтаксис двоеточия и числа работает везде в Git, тогда как флаги --ours и --theirs работают только в git checkout. Забавный синтаксис двоеточия описан в gitrevisions.

9 Пример использования git checkout -- path таков: предположим, независимо от того, выполняете ли вы слияние, вы внесли некоторые изменения в файл, протестировали, обнаружили, что эти изменения сработали, а затем запустили git add для файла. Затем вы решили внести дополнительные изменения, но снова не запускали git add. Вы тестируете второй набор изменений и обнаруживаете, что они неверны. Если бы только вы могли восстановить версию файла в виде дерева работ, установленную на ту версию, которую вы git add редактировали только что… Ага, вы можете: вы git checkout -- path и Git копирует индексную версию из нулевого слота обратно в дерево работы.


Предупреждение о незаметном поведении

Однако обратите внимание, что использование --ours или --theirs имеет еще одно небольшое различие, помимо поведения «извлечь из индекса и, следовательно, не разрешать». Предположим, что во время нашего конфликтующего слияния Git обнаружил, что какой-то файл был переименован. То есть в базе слияния у нас был файл doc.txt, но теперь в HEAD у нас есть Documentation/doc.txt. Путь, который нам нужен для git checkout --ours, - Documentation/doc.txt. Это также путь в коммите HEAD, так что git checkout HEAD -- Documentation/doc.txt нормально.

Но что, если в объединяемом коммите doc.txt не переименовали? В этом случае мы должны 10 иметь возможность git checkout --theirs -- Documentation/doc.txt получить свои doc.txt из индекса. Но если мы попытаемся git checkout MERGE_HEAD -- Documentation/doc.txt, Git не сможет найти файл: его нет в Documentation, в MERGE_HEAD фиксации. Мы должны git checkout MERGE_HEAD -- doc.txt получить их файл ... и это не решит Documentation/doc.txt. Фактически, он просто создал бы ./doc.txt (если бы он был переименован, почти наверняка не было бы ./doc.txt, поэтому «создать» лучше, чем «перезаписать»).

Поскольку при слиянии используются имена HEAD, обычно достаточно git checkout HEAD -- path извлекать и разрешать за один шаг. И если вы работаете над разрешением файлов и работаете с git status, вы должны знать, есть ли у них переименованный файл, и, следовательно, безопасно ли git checkout MERGE_HEAD -- path извлечь и разрешить за один шаг, отменив свои собственные изменения. Но вы все равно должны знать об этом и знать, что делать, если нужно переименовать, о чем следует позаботиться.


10 Я говорю «следует», а не «можно», потому что Git в настоящее время забывает переименование слишком рано. Поэтому, если вы используете --theirs для получения файла, который вы переименовали в HEAD, вы должны использовать здесь старое имя, а затем переименовать файл в рабочем дереве.

person torek    schedule 11.09.2016
comment
Это, наверное, один из самых недооцененных постов, которые я когда-либо видел. Это должна быть вики! - person Nicolas D; 27.04.2017
comment
Я нашел этот пост неделю назад и уже трижды к нему возвращался. «недооценен» - слишком слабое слово для такого ответа! - person lucidbrot; 25.11.2017
comment
Мне не нравится, что я должен это знать, но, по крайней мере, этот ответ отлично объясняет это. Еще лучше было бы добавить примечания (больше примечаний!) Относительно новых команд git switch и git restore. Они снимают часть веса с git checkout. - person Andrew Keeton; 22.10.2019
comment
@AndrewKeeton: Я на самом деле не пробовал новый git restore (версии Git моих основных машин на данный момент отстают на одну или несколько версий), но, судя по документации, теперь вы можете читать отдельно в индексе и / или рабочем дереве, так что, по-видимому, вы можете получить любое поведение. Но, как обычно, документация немного скупа на мелкие детали, поэтому я хотел бы сначала ее протестировать. :-) - person torek; 22.10.2019