Ответ 1
Вы имеете в виду не то, что сам коммит потерян, а то, что изменения, внесенные (в некоторый файл) в этот коммит, были отменены (un -m ade).
Стоит отметить, что коммиты не "вносят изменения" в файлы. Вместо этого каждый коммит хранит полный набор файлов, целых и неповрежденных. Когда вы просите Git показать вам коммит (скажем, его ID - c2c2c2...
):
$ git show c2c2c2
что делает Git:
- извлечь коммит
c2c2c2...
- извлечь родительский коммит
c2c2c2...
- создать список различий от родителя к ребенку
Вот как Git удается показать вам, что изменилось: он сравнивает "то, что у вас было перед этим коммитом" с "тем, что вы имели на тот коммит". (Git может сделать это довольно быстро, оптимизированно, потому что каждый файл сводится к уникальному хешу "отпечаток", и Git может сначала просто сравнить отпечатки пальцев (хэши). Если хеши совпадают, файлы одинаковы. Извлечение реальных файловых данных может быть затруднительно только в том случае, если хэши различаются.)
Этот процесс - сохранение целых файлов вместо накопления изменений - называется "хранением снимков". (Другие системы управления версиями имеют тенденцию накапливать изменения, которые называются "хранением дельт". Они делают это, потому что сохранение изменений в файлах, очевидно, занимает гораздо меньше места, чем сохранение файлов. Git ловко обходит проблему и заводит, используя в любом случае меньше места на диске, чем в старых системах контроля версий на основе дельты.)
Почему "Git хранит снимки, а не дельты" так важно здесь
Фиксация слияния является особенной, одним конкретным и очевидным способом. Посмотрите на собственную диаграмму и рассмотрите Merge1
. Какой коммит приходит прямо перед ним?
Ответ здесь заключается в том, что и Am
и C2
приходят "прямо перед" Merge1
. То есть commit Merge1
имеет два родительских коммита.
Если вы попросите Git показать, что вы совершаете Merge1
, с каким родителем он должен сравниваться?
Здесь вещи становятся особенно странными. Две команды git log -p
и git show
кажутся очень похожими. На самом деле они очень похожи. Единственным очевидным отличием является то, что git log -p
показывает более одного коммита, в то время как git show
показывает только один коммит, который вы указали ему показать. Вы можете запустить git log -n 1 -p <commit>
чтобы показать только один коммит, и теперь кажется, что они точно такие же.
Они не!
Когда вы используете git show
для фиксации слияния, Git пытается решить проблему "что зафиксировать для сравнения", сравнивая одновременно обоих родителей. Результирующий diff называется комбинированным diff.
Однако, когда вы используете git log -p
слияния, Git просто подбрасывает метафорические руки, говорит: "Я не могу показать патчи против двух родителей", сдается и переходит к следующему коммиту. Другими словами, git log -p
даже не пытается использовать diff для слияния.
Но подождите, там больше
Теперь, в этом случае у вас может возникнуть искушение посмотреть, сможете ли вы выяснить, что случилось с вашим файлом из коммита c2c2c2...
с помощью git show
для двух слияний, в частности, для Merge2
, где изменения были отменены. Но git show
по умолчанию создает комбинированный diff, а комбинированный diff намеренно пропускает большую часть diff. В частности, в комбинированном diff перечислены только файлы, которые были изменены от всех родителей.
Допустим, файл, в котором были отменены ваши изменения из C2
это файл f2
. И, согласно графику, два родителя Merge2
- это An
(у которого f2
так, как вы хотите) и Bn
(которого нет).
На самом деле здесь произошло то, что во время слияния, которое создало Merge2
, вы как-то сказали Git использовать версию f2
из коммита Bn
. То есть файл f2
в Merge2
точно такой же, как файл f2
в Bn
, и отличается от f2
в коммите An
.
Если вы используете git show
для просмотра Merge2
, комбинированный diff пропустит f2
, потому что он такой же, как f2
в Bn
.
То же самое верно, только еще хуже, с git log -p
: он полностью пропускает слияние, потому что слишком сложно показывать различия.
Даже без -p
, когда вы запрашиваете "измененные файлы", git log
завершает свою работу, полностью пропуская слияние. Вот почему вы не можете увидеть это в выводе журнала.
(Кроме того, причина, по которой git log master -- f2
никогда не показывает сам коммит C2
, заключается в том, что добавление имени файла в параметры git log
приводит к "упрощению истории". В том, что я считаю несколько ошибочным поведением, Git завершает работу. упрощая слишком много истории, так что он никогда не показывает коммит C2
. Добавление --full-history
до того, как -- f2
восстанавливает C2
в показанном наборе коммитов. Однако слияние по-прежнему отсутствует, поскольку git log
пропускает его.)
Как увидеть изменения
Есть решение. И git show
и git log
имеют дополнительный флаг -m
, который "разделяет" слияния. То есть вместо того, чтобы рассматривать Merge2
как коммит слияния, они разбивают слияние на два "виртуальных коммита". Один из них будет " Merge2
vs An
", и вы увидите все различия между этими двумя коммитами, а другой будет " Merge2
vs Bn
", и вы увидите все различия между этими двумя коммитами. Это покажет, что файл f2
был переустановлен на тот же, что и в Bn
, потеряв версию из C2
которая появляется в An
но не в Bn
.
(Включите --full-history
а также -m
чтобы убедиться, что коммит C2
обнаруживается.)
Как это произошло в первую очередь
Эта часть не совсем понятна. Вы сказали, что произошел конфликт слияния, что означает, что git merge
остановился и получил ручную помощь от человека. В какой-то момент во время этой помощи человек, вероятно, обновил индекс версией файла f2
из Bn
(или, по крайней мере, версией f2
, в которой не было внесено изменение обратно в C2
).
Это может произойти во время слияний, и это немного коварно, потому что Git показывает слияния с этими сжатыми (комбинированными) различиями, или в случае git log -p
, по git log -p
, по умолчанию. Это то, что нужно остерегаться, особенно если слияние требует ручного разрешения конфликта. В большинстве случаев этот тип ошибки слияния можно найти с помощью автоматических тестов. (Конечно, не все может быть проверено.)