30 апреля 2012 в 20:45

Что скрывает от нас директория .git из песочницы

Вот и мне посчастливилось познакомиться с git. Каюсь, пользуясь Subversion, я знал, как в IDEA или TortoiseSVN сделать то, что мне надо, но даже не представлял, что происходит за сценой. В данном случае я решил подойти к git более ответственно и хорошенько изучить его перед использованием. Сейчас я знаю какие команды надо использовать для выполнения задуманного, но не знаю, как это сделать в IDEA или TortoiseSVN.
Но я решил пойти еще дальше и узнать, что происходит в самой директории .git. Там оказалось все настолько интересно и просто, что я решил поделиться этим с вами.

Вот так выглядит .git после команды git init.



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



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

Самыми важными элементами являются objects, refs, HEAD, index. Для того чтобы понять, что это и с чем их едят, мы создадим несколько файлов, каталог, и добавим их в наш репозиторий.

Объекты


Первоначально каталог оbjects содержит пустые подкаталоги pack и info и никаких файлов.

Создадим в рабочей директории файл test.txt с содержимым «test file version 1».
$ echo ‘test file version 1’ > test.txt

Добавим этот файл в индекс.
$ git add test.txt

Теперь посмотрим что изменилось у нас в репозитории.
$ find .git/objects
.git/objects/27/703ec79a98c1d097d5b1cd320befffa376e826

В директорию objects добавился файл. Следует сказать, что все файлы в данной директории являются git объектами определенного типа.
Давайте посмотрим содержимое и тип данного объекта с помощью команды cat-file.
$ git cat-file -p 2770
test file version 1
$ git cat-file -t 2770
blob

Данный объект имеет тип blob. Это и есть начальное представление данных в Git — один файл на единицу хранения с именем, которое вычисляется как SHA1 хеш содержимого и заголовка объекта. Первые два символа SHA определяют подкаталог файла, остальные 38 — имя. Данный объект просто хранит снимок содержимого файла test.txt.

Далее видим, что появился файл index. Это бинарный файл, содержащий список индексированных файлов и связанных с ними blob объектов.
$ git ls-files --stage
100644 27703ec79a98c1d097d5b1cd320befffa376e826 0       test.txt

То есть индекс содержит всю необходимую информацию для создания tree объекта при последующем коммите. Tree объект — это еще один тип объекта в git. Чуть позже мы с ним познакомимся.
А теперь добавим каталог new и файл new/new.txt
$ mkdir new
$ echo "new file" > new/new.txt
$ git add .
$ find .git/objects -type f
.git/objects/27/703ec79a98c1d097d5b1cd320befffa376e826
.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92

Давайте узнаем тип нового объекта и его содержимое.
$ git cat-file -p fa49
new file
$ git cat-file -t fa49
blob

И заглянем снова в index.
$ git ls-files --stage
100644 fa49b077972391ad58037050f2a75f74e3671e92 0       new/new.txt
100644 27703ec79a98c1d097d5b1cd320befffa376e826  0       test.txt

А теперь все это закоммитим.
$ git commit -m "first commit"
[master (root-commit) cae1990] first commit
2 files changed, 2 insertions(+), 0 deletions(-)
create mode 100644 new/new.txt
create mode 100644 test.txt

Теперь наш репозиторий содержит 5 объектов.
$ find .git/objects -type f
.git/objects/27/703ec79a98c1d097d5b1cd320befffa376e826
.git/objects/49/66bf4e5c88c5f9d149b45bb2f3099644701d93
.git/objects/ca/e19909974ee9e64f5787fe4ee89b9b8fe94ccf
.git/objects/eb/85079ce7fd354ffc630f4a8e2991196cb3807f
.git/objects/fa/49b077972391ad58037050f2a75f74e3671e92

Добавилось еще три файла. Посмотрим что это за файлы.
$ git cat-file -t 4966
tree
$ git cat-file -p 4966
040000 tree eb85079ce7fd354ffc630f4a8e2991196cb3807f    new
100644 blob 27703ec79a98c1d097d5b1cd320befffa376e826    test.txt

$ git cat-file -t eb85
tree
$ git cat-file -p eb85
100644 blob fa49b077972391ad58037050f2a75f74e3671e92    new.txt

Это другой тип объектов git — tree объект. Объект данного типа содержит одну или более записей, указывающей на другое дерево или на blob объект.
И наконец, последний тип объекта — объект-коммит.
$ git cat-file -p cae1
tree 4966bf4e5c88c5f9d149b45bb2f3099644701d93
author Ivan Ivanov <i_ivanov@adam.net> 1335783964 +0300
committer Ivan Ivanov <i_ivanov@adam.net> 1335783964 +0300

first commit

Мы видим дерево верхнего уровня, о котором я уже упоминал ранее, имя автора и коммитера и сообщение коммита.
Сделаем новое изменение(в test.txt изменим текст на «test file version 2»)и закоммитим. Кроме ссылки на дерево появилась еще ссылка на предшествующий коммит.
$ git cat-file -p b303
tree 42e998096f18d4249dc00ec89eaaadc44a8bf3cb
parent cae19909974ee9e64f5787fe4ee89b9b8fe94ccf
author Ivan Ivanov <i_ivanov@adam.net> 1335786789 +0300
committer Ivan Ivanov <i_ivanov@adam.net> 1335786789 +0300

second commit


Чтобы все стало на свои места, нарисуем граф объектов


Ссылки


В git ссылка — это файл-указатель с простым именем, который содержит значение хеша SHA-1. Данные файлы размещаются в каталоге .git/refs/
$ find .git/refs
.git/refs
.git/refs/heads
.git/refs/heads/master
.git/refs/tags

Так как у нас только одна ветка master, то и ссылка тоже только одна, которая указывает на последний коммит.
Давайте сделаем бранч release, указывающий на первый коммит.
$ git branch release cae1990

$ find .git/refs
.git/refs
.git/refs/heads
.git/refs/heads/master
.git/refs/heads/release
.git/refs/tags

$ cat .git/refs/heads/release
cae19909974ee9e64f5787fe4ee89b9b8fe94ccfa

Вот по сути что такое ветка в git — простой указатель на определенный коммит.
Посмотрим более наглядно


HEAD


Данный файл содержит ссылку не на хеш, а на текущую ветку.
$ cat .git/HEAD
ref: refs/heads/master

Если перейти на другую ветку, то и содержание данного файла изменится.
$ git co release
Switched to branch 'release'

$ cat .git/HEAD
ref: refs/heads/release


Вместо итога


А есть еще метки, удаленные ссылки, директория info и много много вкусного.
Андрей Марков @andreich3
карма
41,0
рейтинг 0,0
Самое читаемое Разработка

Комментарии (22)

  • +22
    Больше статей про гит, хороших и разных! Ловите +1.
  • +1
    + еще странная вещь как заметки к комитам
  • 0
    Достаточно интересное чтиво на ночь. Спасибо.
    • +1
      Если честно, опередили. Хотел послезавтра где-то об этом же написать.
      • +2
        Про .git-овые файлы еще много интересного можно рассказать.

        Например, что использование SHA для идентификации содержимого файлов и деревьев естественным образом исключает дублирование данных в репозитории. У git'а нет записи изменений как таковых — что произошло в конкретном commit'е определяется исходя из того, что контрольная сумма у файла (или директории) с данным именем поменялась, а если после commit'а появился один или больше файлов с той же sha, что и до коммита, то это копирование или перемещение. Все очень просто, но с таким подходом отследить переименование файла с одновременным изменением содержимого не получится. Если вам важно прослеживать историю изменений, придется переименовывать и изменять файл в разных коммитах. Кстати, если не ошибаюсь, данные нескольких репозиторий можно хранить совместно, что делает git еще более эффективным для больших проектов.

        Еще можно написать как ставятся хуки, описать revwalk, сравнить аннотированые и обычные тэги, ну или алгоритмы вычисления истории изменений, diff, blame… Так что не сдерживайте себя ;)
        • +3
          git умный, если файл изменился не сильно, он поймёт, что его переименовали. Естественно ему можно это подсказать командой git mv

          Хранить много проектов в одном репозитории можно, но не рекомендуется. Это называется submodules. Работает не особо, так что лучше хранить в разных репозиториях, благо это ничего не стоит.
          • +1
            По поводу git mv git wiki говорит что это полный аналог удаления файла и добавления с новым именем; хотя то что вы говорите про «если файл изменился не сильно, он поймёт» я уже несколько раз встречал в обсуждениях git'а — надо будет разобраться. Тут только надо понимать, поправьте, если я ошибаюсь, что ядру git'а это без разницы — git может дать полный snapshot в точке #N и в точке #N-1, а что конкретно произошло между этими моментами — это уже проблема интерпретации клиента или дополнительных утилит.

            По поводу нескольких проектов в репозитории — согласен, я бы старался избегать. Зачем смешивать отслеживание изменений в нескольких несвязных проектах? Submodules — это все-таки больше «внешние» проекты, тоже не совсем то.

            Я говорил про объединение данных репозиторий — кажется, это делается через .git/objects/info/alternates. Это не связано с подпроектами, скорее с оптимизацией крупных хранилищ исходного кода. Например, если вы хотите сделать свой github с поддержкой fork'ов и при этом реально копировать репозитории, то проблемы с местом на жестом диске будут вас преследовать в ночных кошмарах. В случае если папку objects можно расшарить между несколькими репозиториями, такой проблемы не будет. Очевидно, что практический интерес это представляет в основном крупным компаниям с множеством команд и продуктов, ну или хостерам кода типа github, bitbucket, kiln и т.д., но с точки зрения возможностей git'а, все же полезно знать.
            • +1
              Вот только что закоммитил один зип архивчик с его перепаковкой и переименованием — git все распознал без дополнительных пинков и даже показал процент совпадения файлов, а вот если его переименовать и директорию сменить — уже сам не может, надо таки пинать.
            • +1
              идея хранения всех форков в одном репозитории весьма интересная, вполне может быть что именно для этих целей она и создавалась. потому как большая часть обьектов в .git/objects будут общими.

              Что касается submodules, я с вами не соглашусь. Весьма бывает удобно вынести общий модуль для нескольких проектов в отдельный репозиторий. Например если над ними работают 2 разные комманды и основной проект будет хранить только ссылку на коммит с которым всё работает хорошо. Общий модуль обновился — ничего не поломалось. Когда нужны будут новые функции — обновили коммит в submodule протестировали, подпилили, то что отвалилось и уже стабильную связку закоммитили в репозиторий.
              Или например есть 5 проектов с общим ядром. 3 проекта просто на поддержке, их лучше трогать чем реже тем лучше, только баги править. Еще 2 в активной разработке и под них в ядре делаются изменения. Общий submodule будет отличным решением в этой ситуации. Вобщем примеров можно много привести когда submodule жить помогает.
        • 0
          Ок. Попытаюсь раскрыть тему подробнее. Спасибо. :)
  • +4
    Цикл статей на эту тему: Git guts (на русском).

    То же самое одним большим постом: Git guts all-in-one
    • 0
      Отмечу, кстати, что с момента написания статьи многое поменялось, поэтому для тех, кто дойдет до четвертой главы — что бы коммиты появились в git log, нужно прописать соответствующие «ссылки» на комиты в .git/refs/heads/master
  • –1
    А зачем вам знать как работает Git?
    • 0
      Имею ввиду, что уже не в первый раз натыкаюсь на такие статьи-раскопки Git, но ни разу не видел подобного про Svn и остальные SCM-системы.
      Академический интерес?
    • 0
      Интересно, как там всё внутри устроено.
    • 0
      Лично мне удобней знать все о том, чем я пользуюсь (в разумных количествах, конечно). Становятся более понятны принципы работы и возможности системы. Всегда проще понять как работает, чем запоминать, как ею пользоваться.
    • 0
      чтобы осознанно им пользоваться
      Правда, в случае с остальными популярными VCS для этого достаточно почитать и понять документацию. Впрочем, мир уже пару лет как сошёл с ума и эти нудные подробности уже давно никому не интересны.
  • 0
    А ещё бывают packed-refs, object/pack — собранные в один файл несколько ссылок, объектов.
  • +1
    Как Mercurial устроен внутри вот тут неплохо написано www.aosabook.org/en/mercurial.html
  • 0
    Картинки приказали долго жить.
    • 0
      Прошу прощения, обновил картинки.

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.