Как git сопоставляет BLOB-объекты с файлами в деревьях коммитов? - PullRequest
5 голосов
/ 10 апреля 2019

Глава 3.1 книги Git четко гласит, что только промежуточные файлы будут храниться в виде BLOB-объектов в дереве фиксации.

Если, как и объект фиксации, BLOB-объект получаетхэш-идентификатор, который уникален для его содержимого, как Git удается отслеживать соответствие между BLOB-объектами и файлами между коммитами?Хэш-идентификаторы тех же файловых BLOB-объектов в разных коммитах могут не совпадать, поскольку их содержимое различается.


Простой пример:

Предположим, я только что создал пустое хранилищебез коммитов.Я создаю файл README.md, ставлю его и фиксирую.Git хранит объект дерева, у которого есть блоб, идентифицируемый хэшем содержимого README.md.

Давайте предположим, что я изменяю README.md, stage и commit.Git хранит объект дерева, у которого есть большой двоичный объект, идентифицированный по хэшу измененного содержимого README.md.Естественно, мы можем ожидать, что этот второй хеш будет отличаться от хеша, идентифицирующего BLOB-объект README.md в первом дереве коммитов.

Как бы Git ответил на запрос об истории README.md?

git log README.md

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


Ответы [ 2 ]

13 голосов
/ 10 апреля 2019

Это на самом деле довольно хороший вопрос.

Форма коммита для внутреннего хранения отчасти актуальна, поэтому давайте рассмотрим ее на минутку. Индивидуальный коммит на самом деле довольно маленький. Вот один из репозитория 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 - это не имеет значения, один бит. В некоторых случаях, таких как различие двух коммитов, это имеет значение, но только для нас, людей, пытающихся понять, что произошло. Но в некоторых случаях, таких как слияние, это очень важно .

0 голосов
/ 10 апреля 2019

Вы имеете в виду, изменился ли файл? Ну, на самом деле не имеет значения, изменился файл или нет. Каждая ревизия указывает на дерево , то есть на корневой каталог проекта, который ревизия представляет в этот момент времени . Дерево - это рекурсивная структура, которая содержит имена большего количества деревьев (та же концепция корневого дерева) или файлов. Итак, вы получаете имя дерева (каталога) или файла .... и идентификатор для содержимого . Если объект является файлом, вы получаете содержимое, прямо ... если объект является деревом, хорошо ... вы получаете другое дерево с другой структурой и содержимым ... и так далее, и так далее, и так далее. Теперь ... каждая ревизия указывает также , поэтому его родительская ревизия (или родительская, если это коммит слияния). И эта ревизия также содержит дерево, которое, конечно, отображается на содержание проекта в тот момент времени и т. Д. И вуаля! никаких трюков.

Итак, что происходит, если файл меняет содержимое? Хорошо ... у вас будут деревья с такими же "именами" в структуре деревьев, которые составляют ревизии, о которых вы говорите ... но тогда идентификаторы изменятся, потому что содержимое файла изменится. Таким образом, имена будут одинаковыми, идентификаторы будут изменены. Я думаю, что вы должны использовать немного git cat-file -p, начиная с ваших ревизий, а затем идентификаторы объектов (деревья, капли), чтобы вы полностью поняли, что происходит.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...