Это на самом деле довольно хороший вопрос.
Форма коммита для внутреннего хранения отчасти актуальна, поэтому давайте рассмотрим ее на минутку. Индивидуальный коммит на самом деле довольно маленький. Вот один из репозитория Git для Git, а именно commit b5101f929789889c2e536d915698f58d5c5c6b7a
:
$ git cat-file -p b5101f929789889c2e536d915698f58d5c5c6b7a | sed 's/@/ /'
tree 3f109f9d1abd310a06dc7409176a4380f16aa5f2
parent a562a119833b7202d5c9b9069d1abb40c1f9b59a
author Junio C Hamano <gitster pobox.com> 1548795295 -0800
committer Junio C Hamano <gitster pobox.com> 1548795295 -0800
Fourth batch after 2.20
Signed-off-by: Junio C Hamano <gitster pobox.com>
(sed 's/@/ /'
- это, возможно, возможно, сокращение количества спама в электронной почте, которое должен получить Junio Hamano :-)). Как вы можете видеть здесь, объект фиксации ссылается на родительский объект фиксации по хеш-идентификатору другого коммита a562a11983...
. Он также ссылается на объект tree по хеш-идентификатору, а хеш-идентификатор объекта дерева начинается с 3f109f9d1a
. Мы также можем посмотреть на этот объект дерева, используя git cat-file -p
:
$ git cat-file -p 3f109f9d1a | head
100644 blob de1c8b5c77f7566d9e41949e5e397db3cc1b487c .clang-format
100644 blob 42cdc4bbfb05934bb9c3ed2fe0e0d45212c32d7a .editorconfig
100644 blob 9fa72ad4503031528e24e7c69f24ca92bcc99914 .gitattributes
040000 tree 7ba15927519648dbc42b15e61739cbf5aeebf48b .github
100644 blob 0d77ea5894274c43c4b348c8b52b8e665a1a339e .gitignore
100644 blob cbeebdab7a5e2c6afec338c3534930f569c90f63 .gitmodules
100644 blob 247a3deb7e1418f0fdcfd9719cb7f609775d2804 .mailmap
100644 blob 03c8e4c613015476fffe3f1e071c0c9d6609df0e .travis.yml
100644 blob 8c85014a0a936892f6832c68e3db646b6f9d2ea2 .tsan-suppressions
100644 blob 536e55524db72bd2acf175208aef4f3dfc148d42 COPYING
(в дереве достаточно много данных, поэтому я скопировал здесь только первые десять строк).
Внутри дерева вы видите режим (100644
), тип (blob
- это подразумевается режимом и также записывается во внутренний объект Git; на самом деле он не хранится в объекте дерева), хеш ID (de1c8b5c77f...
) и имя (.clang-format
) большого двоичного объекта. Вы также можете видеть, что tree
может ссылаться на дополнительные tree
объекты, как в случае с поддеревом .github
.
Если мы возьмем этот конкретный хеш-идентификатор объекта BLOB-объекта, мы можем просмотреть содержимое этого объекта также по хеш-идентификатору:
$ git cat-file -p de1c8b5c77f | head
# This file is an example configuration for clang-format 5.0.
#
# Note that this style definition should only be understood as a hint
# for writing new code. The rules are still work-in-progress and does
# not yet exactly match the style we have in the existing code.
# Use tabs whenever we need to fill whitespace that spans at least from one tab
# stop to the next one.
#
# These settings are mirrored in .editorconfig. Keep them in sync.
(снова я обрезал копию на 10 строк, так как файл довольно длинный).
Только для иллюстрации, давайте посмотрим и на поддерево .github
:
$ git cat-file -p 7ba15927519648dbc42b15e61739cbf5aeebf48b
100644 blob 64e605a02b71c51e9f59c429b28961c3152039b9 CONTRIBUTING.md
100644 blob adba13e5baf4603de72341068532e2c7d7d05f75 PULL_REQUEST_TEMPLATE.md
Что делает с ними Git, так это рекурсивно читает по мере необходимости объект tree из коммита. Git считывает их в структуру данных, которую он называет index или cache . (Технически говоря, версия в памяти - это структура данных cache , хотя в документации Git обычно немного неясно, какие имена использовать, когда.) Таким образом, кеш, созданный чтением commit b5101f929789889c2e536d915698f58d5c5c6b7a
скажет, например, что имя .clang-format
имеет режим 100644
и blob-hash de1c8b5c77f7566d9e41949e5e397db3cc1b487c
, а имя .github/CONTRIBUTING.md
имеет режим 100644
и blob-hash 64e605a02b71c51e9f59c429b28961c3152039b9
.
Обратите внимание, что различные компоненты имен (.github
плюс CONTRIBUTING.md
) фактически объединены в кэш-памяти в памяти. (В формате на диске они сжимаются с помощью алгоритмического трюка.)
Кэш в памяти, который помогает Git сопоставлять имена файлов
В конце концов, это внутренний (в памяти) кэш, в котором хранятся кортежи . Если вы попросите Git сравнить коммит b5101f929789889c2e536d915698f58d5c5c6b7a
с другим коммитом, Git также прочитает другой коммит в кэш в памяти. Этот другой кеш либо имеет запись с именем .github/CONTRIBUTING.md
, либо его нет.
Если у обоих коммитов есть файлы с одинаковыми именами , Git предполагает - для целей этого одного сравнения, которое Git сейчас делает, и смотрите ниже - что это тот же файл . Это верно независимо от того, являются ли хэши BLOB-объектов одинаковыми или нет.
Реальный вопрос, на который мы здесь отвечаем, связан с identity . Идентификация файла в системе контроля версий определяет, является ли этот файл «одним и тем же» файлом в двух разных версиях (однако сама система контроля версий определяет версии). Это относится к фундаментальному философскому вопросу идентичности, как изложено в этой статье в Википедии об мыслительном эксперименте о Корабле Тесуса : как мы узнаем что-то, или даже какое-то одно , Кто или что мы думаем, что они? Если вы встретили своего двоюродного брата Боба, когда вы оба были очень молоды, и вы снова встретили кого-то по имени Боб, он ваш двоюродный брат? Ты и он были крошечными тогда; теперь ты больше и старше, с другим опытом. В реальном мире мы ищем подсказки из нашего окружения: Боб - дитя людей, которые являются братьями и сестрами ваших родителей? Если это так, то Боб, вероятно, является тем же двоюродным братом Бобом, которого вы встречали давно, даже если он (и вы) выглядят совсем иначе.
Git,конечно, не делает ничего из этого. В большинстве случаев простого факта, что оба файла имеют имя .github/CONTRIBUTING.md
, достаточно, чтобы идентифицировать их как «один и тот же файл». Имена совпадают, поэтому мы закончили.
git diff
предлагает дополнительные услуги
В нашей повседневной разработке иногда приходится переименовывать файл. По некоторым причинам файл с именем a/b.c
может быть переименован в d/e.f
или d/e.c
.
Предположим, мы на коммите a123456
и файл с именем a/b.c
. Затем мы переходим к фиксации f789abc
. Этот второй коммит не имеет a/b.c
, но имеет d/e.f
. Git просто удалит a/b.c
из нашего индекса (форма кэша на диске) и рабочего дерева, и заполнит новый d/e.f
в нашем индексе и рабочем дереве, и все будет хорошо.
Но предположим, мы просим Git сравнить a123456
с f789abc
. Git может просто скажите нам: Чтобы изменить a123456
на f789abc
, удалите a/b.c
и создайте новый d/e.f
с этим содержимым. То, что равно что git checkout
сделал и этого достаточно. Но что, если содержимое точно совпадает? Для Git гораздо эффективнее , чтобы сказать нам: Чтобы изменить a123456
на f789abc
, переименовать a/b.c
в d/e.f
. И на самом деле, с правильными опциями, git diff
будет делать именно это:
git diff --find-renames a123456 f789abc
Как Git справился с этим трюком? Ответ заключается в вычислении идентификатора файла .
Поиск идентификатора файла
Предположим, что в коммите L (для левой стороны) есть файл (a/b.c
), которого нет в коммите R (для правой стороны). Предположим далее, что коммит R имеет некоторый файл (d/e.f
), которого нет в коммите L . Вместо того, чтобы просто сказать нам: вы должны удалить файл L и использовать файл R , теперь Git может сравнивать содержимое двух файлов.
Из-за природы хэшей объектов Git - они полностью детерминированы, основаны на содержимом файлов - действительно просто для Git обнаружить, что a/b.c
в L равно 100% идентично d/e.f
в R . В этом конкретном случае они будут иметь точно такой же хэш-идентификатор! Так Git делает это: если какой-то файл исчез из L и какой-то другой файл, появившийся в R , и Git попросили найти переименования, Git проверяет совпадения хеш-идентификаторов. Если он находит некоторые из них, он объединяет эти файлы (и выводит их из очереди несопоставленных файлов - эта очередь, содержащая файлы из L и R , является "очередью обнаружения переименования" «).
Эти файлы с разными именами были идентифицированы как один и тот же файл. В конце концов, маленький двоюродный брат Боб такой же, как большой двоюродный брат Боб - за исключением того, что вам обоим нужно быть маленьким.
Итак, если это обнаружение переименования еще не еще сопоставило файл в L с файлом в R , Git будет стараться изо всех сил. Теперь он будет извлекать реальные двоичные объекты и вычислять своего рода «процент совпадения». Это использует сложный маленький алгоритм, который я не буду здесь описывать, но если достаточное количество подстрок в этих двух файлах совпадает, Git объявит файлы равными 50, 60, 75 или более процентам похожих .
Найдя одну пару файлов в очереди переименования, которые, скажем, на 72% похожи друг на друга, Git продолжает сравнивать файлы и со всеми остальными файлами. Если выясняется, что одно из этих двух на 94% похоже на другое, то такое сходство превосходит 72% -ное сходство. Если нет, то сходство 72% является достаточным - по крайней мере, 50% - поэтому Git объединит эти два файла и объявит, что они имеют одинаковую идентичность.
В любом случае, если совпадение достаточно хорошее и является лучшим из всех непарных файлов, то это конкретное совпадение берется. Еще раз, маленький двоюродный брат Боб все тот же, что и большой двоюродный брат Боб.
Послевыполняя этот тест на всех несопоставленных файловых парах, git diff
берет сопоставленные результаты и вызывает эти файлы переименованный . Опять же, это происходит только в том случае, если вы используете --find-renames
(или -M
), и вы можете установить для порога значение, отличное от 50%, если хотите.
Неверный матч
Команда git diff
предлагает другой сервис. Обратите внимание, что мы начали с , предполагая , что если коммиты L и R имели файлы с одинаковым именем , эти файлы были одинаковыми файл , даже если содержимое отличается. Но что, если они не? Что если file
в L переименовать в bettername
в R , и , то кто-то создал новый file
в R?
Для этого git diff
предлагает параметр -B
(или «разрыв соединения»). При действии -B
для файлов, которые начинаются с идентификации по имени, их спаривание будет нарушено, если они слишком dis -подобны То есть Git проверит, совпадают ли два хэша больших двоичных объектов, и если нет, Git вычислит индекс сходства. Если индекс упадет ниже некоторого порога, Git прервет сопряжение и поместит оба файла в очередь обнаружения переименования, прежде чем запускать детектор переименования в стиле --find-renames
.
Как особый поворот, Git будет повторно соединять разорванные пары, если они не настолько сильно отличаются, что вы не хотите, чтобы это было сделано. Следовательно, для -B
вы на самом деле указываете два порога сходства: первое число - это когда предварительно нарушить спаривание, а второе - когда навсегда его разорвать.
git merge
использует git diff --find-renames
Когда вы используете git merge
для выполнения трехстороннего слияния, есть три входа:
- базовый коммит слияния, который является предком обоих концевых коммитов; и
- левый и правый коммит,
--ours
и --theirs
.
Git запускает две git diff
команды внутри. Один сравнивает базу с L , а другой сравнивает базу с R .
Оба эти дифференциала работают с включенным --find-renames
. Если diff из базы в L находит переименование, Git знает, как использовать изменения , показанные для этого переименования Аналогично, если diff из base в R находит переименование, Git знает, как использовать эти изменения. Он объединит оба набора изменений и попытается (но, как правило, не удастся) объединить оба переименования, если оба различия показывают переименование.
git log --follow
также использует детектор переименования
При использовании git log --follow
Git просматривает историю коммитов, по одной коммит-паре - child-and-parent - за раз, выполняя сравнения от родителя к потомку. Включается ограниченная форма кода обнаружения переименования, чтобы увидеть, был ли один файл, который вы --follow
-ing, переименовали в этой паре коммитов. Если это так, как только git log
переходит к родителю, он меняет имя, которое ищет . Этот метод работает довольно хорошо, но имеет некоторые проблемы при слияниях (поскольку коммиты слияний имеют более одного родителя).
Заключение
Идентификатор файла - это то, что все это значит. Поскольку Git не знает, априори, этот файл a/b.c
в коммите L является или не является "тем же" файлом, что и файл d/e.f
в коммите R , Git может используйте переименовать обнаружение , чтобы принять решение. В некоторых случаях - например, при проверке коммита L или R - это не имеет значения, один бит. В некоторых случаях, таких как различие двух коммитов, это имеет значение, но только для нас, людей, пытающихся понять, что произошло. Но в некоторых случаях, таких как слияние, это очень важно .