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, есть правильный контент только из ветки 'ours', и когда я его сохраняю, файл исчезает из несвязанного списка. Но поскольку у меня есть сотни таких файлов, это не вариант.

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

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

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

Ответ 1

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

Конечным результатом является то, что после 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

или

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) node в графе фиксации. В менее технических терминах это самая последняя фиксация, в которой ваша текущая ветка соединяется с веткой, которую вы объединяете. То есть, записывая каждый идентификатор фиксации родителя слияния, Git может найти последний раз, когда две ветки были вместе, и, следовательно, выяснить, что вы сделали, и что они сделали. Для этого, чтобы вообще работать, Git должен записывать каждое слияние. В частности, он должен написать оба (или все, для так называемых "осьминог" ) сгенерированных родительских идентификаторов в новое слияние.

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


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

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

Для каждого поэтапного файла в вашем дереве, индекс имеет до четырех записей, а не только одну запись. Почти три из них действительно используются, но есть четыре слота, которые пронумерованы, 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 пуст, записывая конфликт изменения/удаления.

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


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

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

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

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


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 's, git checkout должен заменить запись индекса версией от branch -or, если README.txt не существует в tip commit branch, Git должен удалить запись индекса. Более того, если git checkout собирается изменить или удалить запись индекса, ему также необходимо изменить или удалить файл дерева работ. Git гарантирует, что это безопасно, т.е. файл рабочего дерева соответствует файлу фиксации master, прежде чем он позволит вам переключаться между ветвями.

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

Как только все тесты пройдены, и Git решил, что ОК, чтобы переключиться с master на branch -или, если вы указали --force - git checkout, фактически обновляет индекс со всеми измененными (или удаленные) файлы и обновляет дерево работы, чтобы оно соответствовало.

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

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

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

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


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

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 хранит файлы:

  • в commits; 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. Поскольку это commit, Git начинается с записи содержимого файла в индекс. Это записывает слот 0 и стирает 1-3. И теперь файл разрешен!

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


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

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

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

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

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

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


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

Заметьте, однако, что использование --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 commit. Нам нужно git checkout MERGE_HEAD -- doc.txt получить их файл... и это не решит Documentation/doc.txt. Фактически, он просто создал бы ./doc.txt (если бы он был переименован там почти наверняка нет ./doc.txt, поэтому "create" лучше догадываться, чем "перезаписывать" ).

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


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