git init и устройство .git/
git init — это команда, с которой начинается любой репозиторий. На первый взгляд она тривиальна: вы запустили её, и Git считает, что директория теперь под версионным контролем. На самом деле в этот момент происходит важное: Git создаёт скрытую директорию .git/ со всей служебной инфраструктурой.
В этом уроке откроем .git/ и разберём, что там есть. Это даст вам интуицию: «вот эта команда меняет вот этот файл», и Git перестанет казаться магией. Не будем глубоко в pack format — это для продвинутых. Цель урока — увидеть структуру и понять, какой кусочек за что отвечает.
git init: пять секунд драмы
Создадим репозиторий:
mkdir -p ~/git-sandbox/lesson-03-init
cd ~/git-sandbox/lesson-03-init
git init
# Initialized empty Git repository in /Users/.../lesson-03-init/.git/
После этого:
ls -la
# . .. .git/
Появилась одна директория — .git/. Это весь репозиторий. Удалите .git/ — и эта директория снова перестанет быть Git-репо. Скопируйте .git/ в другое место — и там тоже будет репозиторий.
.git/ — это весь репозиторий, включая ВСЮ историю. Никогда не удаляйте её случайно. Никогда не правьте файлы в ней руками, кроме конфига (мы делали это в уроке 02). Любая порча в objects/ может необратимо повредить репозиторий.
Что внутри .git/
Посмотрим содержимое:
ls -la .git/
Вывод (на чистом репо после git init):
HEAD
config
description
hooks/
info/
objects/
refs/
Разберём каждый.
HEAD: что я сейчас редактирую
cat .git/HEAD
# ref: refs/heads/main
HEAD — это указатель на текущую ветку. Только что после git init ветка ещё не создана (нет коммитов), но HEAD уже «целится» в неё. Когда мы сделаем первый коммит, файл .git/refs/heads/main будет создан, и HEAD будет на него ссылаться.
HEAD — это «вы здесь» в карте репозитория. Когда вы на ветке main, HEAD говорит «main». Когда переключились на feature/x, HEAD говорит «feature/x». Когда сделали git checkout <SHA> напрямую — HEAD говорит сам SHA (это и есть «detached HEAD», урок 5).
config: настройки этого репо
cat .git/config
# [core]
# repositoryformatversion = 0
# filemode = true
# bare = false
# logallrefupdates = true
# ignorecase = true
# precomposeunicode = true
Это local config, перебивающий global. Когда мы делаем `git config —local user.email ”…”, запись добавится сюда. Опции (на пустом repo):
repositoryformatversion = 0— формат репозитория (для совместимости). 0 — classic, 1 — расширения (SHA-256, partial clones)filemode = true— отслеживать ли права файлов. На macOS/Linux — true. На Windows — false (Windows-файловая система не имеет полного набора Unix-прав)bare = false— это не bare-репозиторий (про bare — урок про remotes)logallrefupdates = true— вести reflog (журнал изменений HEAD). Критично для восстановления — модуль 10ignorecase = true— case-insensitive файловая система (типично для macOS HFS+/APFS)precomposeunicode = true— на macOS обрабатывает Unicode нормализацию (для имён файлов с диакритикой)
hooks/: скрипты на события
ls .git/hooks/
# applypatch-msg.sample
# commit-msg.sample
# fsmonitor-watchman.sample
# post-update.sample
# pre-applypatch.sample
# pre-commit.sample
# pre-merge-commit.sample
# pre-push.sample
# pre-rebase.sample
# pre-receive.sample
# prepare-commit-msg.sample
# push-to-checkout.sample
# update.sample
Это примеры hook-скриптов. Все они с расширением .sample — поэтому НЕ активны (Git ищет файлы без .sample). Если хотите активировать, например, pre-commit:
mv .git/hooks/pre-commit.sample .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
После этого скрипт будет запускаться перед каждым коммитом. Подробно — модуль 16.
info/: локальные исключения
ls .git/info/
# exclude
cat .git/info/exclude
# git ls-files --others --exclude-from=.git/info/exclude
# Lines that start with '#' are comments.
# For a project mostly in C, the following would be a good set of
# exclusions (uncomment them if you want to use them):
# *.[oa]
# *.lib
Файл exclude — это локальный .gitignore, который НЕ коммитится. Удобно, когда вы хотите игнорировать что-то только у себя (например, IDE-файлы конкретно вашей машины), не засоряя .gitignore всего проекта. Подробно — модуль 14.
objects/: главное хранилище
ls .git/objects/
# info/
# pack/
На пустом репо objects/ почти пуста: только info/ (метаданные) и pack/ (для packfiles, оптимизированного хранения). Когда мы создадим первый коммит, тут появятся директории с SHA-1 хешами в качестве имён.
Структура: для blob с SHA-1 3b18e512dba79e4c8300dd08aeb37f8e728b8dad путь будет:
.git/objects/3b/18e512dba79e4c8300dd08aeb37f8e728b8dad
Первые 2 hex-символа — поддиректория, остальные 38 — имя файла. Это сделано для производительности файловой системы: если хешировать в одну директорию, при тысячах файлов будет медленно. Разбивка по первым 2 hex-символам даёт 256 поддиректорий — равномерное распределение.
Содержимое файла — сжатое gzip содержимое объекта (blob, tree, commit или tag). Не читается напрямую. Чтобы посмотреть содержимое:
git cat-file -p <SHA>
Подробно про объекты — урок 5 этого модуля. Сейчас просто запомните: objects/ — это сердце хранилища.
refs/: указатели
ls .git/refs/
# heads/
# tags/
Структура:
refs/heads/— локальные веткиrefs/tags/— тегиrefs/remotes/— отслеживающие ветки удалённых репозиториев (появляется послеgit fetch)
После git init refs/heads/ пуста. Когда мы создадим первый коммит на ветке main, появится файл refs/heads/main с одним SHA.
Что появляется после первого коммита
Сделаем первый коммит и посмотрим, что изменилось:
echo "hello" > hello.txt
git add hello.txt
git commit -m "First commit"
# [main (root-commit) a1b2c3d] First commit
# 1 file changed, 1 insertion(+)
# create mode 100644 hello.txt
Теперь посмотрите .git/:
find .git -type f | head -30
Появилось много нового:
.git/HEAD
.git/config
.git/COMMIT_EDITMSG ← последнее сообщение коммита
.git/ORIG_HEAD ← (может появиться после некоторых операций)
.git/index ← бинарный файл индекса
.git/logs/HEAD ← reflog HEAD
.git/logs/refs/heads/main ← reflog ветки main
.git/objects/2e/65efe2a145dda7ee51d1741299f848e5bf752e ← blob "hello\n"
.git/objects/3a/79be... ← tree-объект
.git/objects/a1/b2c3d... ← commit-объект
.git/refs/heads/main ← указатель ветки main на commit
Разберём по порядку.
objects/: появились 3 файла
ls .git/objects/
# 2e/ 3a/ a1/ info/ pack/
Три новых директории с двумя hex-символами. Внутри каждой — один файл. Каждый файл — один объект.
# Blob (содержимое hello.txt)
git cat-file -p 2e65efe2a145dda7ee51d1741299f848e5bf752e
# hello
git cat-file -t 2e65efe2a145dda7ee51d1741299f848e5bf752e
# blob
# Tree (snapshot директории, в которой только hello.txt)
git cat-file -p 3a79be...
# 100644 blob 2e65efe2... hello.txt
# Commit (метаданные + ссылка на tree)
git cat-file -p a1b2c3d...
# tree 3a79be...
# author Ivan Petrov <[email protected]> 1715600000 +0300
# committer Ivan Petrov <[email protected]> 1715600000 +0300
#
# First commit
Видите цепочку: commit -> tree -> blob. Об этом подробно — урок 5.
refs/heads/main: появился
cat .git/refs/heads/main
# a1b2c3d4e5f6...
Один SHA — это коммит, на который теперь указывает main. HEAD по-прежнему ref: refs/heads/main, теперь эта ссылка не пустая.
index: бинарный файл
file .git/index
# .git/index: Git index, version 2, 1 entries
Бинарный файл с записями о файлах в index. Можно посмотреть через git ls-files --stage:
git ls-files --stage
# 100644 2e65efe2a145dda7ee51d1741299f848e5bf752e 0 hello.txt
Формат: <mode> <blob SHA> <stage> <path>. После коммита index == HEAD (синхронизированы).
logs/: reflog появился
cat .git/logs/HEAD
# 0000000000000000000000000000000000000000 a1b2c3d4e5f6... Ivan Petrov <[email protected]> 1715600000 +0300 commit (initial): First commit
Reflog — журнал всех изменений HEAD. Очень важный для восстановления удалённых коммитов. Подробно — модуль 10.
Что НЕ появляется без необходимости
Некоторые директории создаются только когда есть нужда:
refs/remotes/origin/— послеgit remote add origin ...иgit fetchrefs/tags/— послеgit tagobjects/pack/— послеgit gc(gargabe collection) илиgit pushMERGE_HEAD,MERGE_MSG— во время merge с конфликтомREBASE_HEAD,rebase-merge/,rebase-apply/— во время rebase
Все они появляются и исчезают по мере необходимости. Не пугайтесь, если в .git/ вдруг появилась новая директория — это нормально.
Bare vs не-bare репозиторий
Маленькая, но важная деталь: репозиторий, который вы создали через git init в обычной директории, — не bare. У него есть working tree. Файлы лежат рядом с .git/.
Bare-репозиторий — это репозиторий без working tree. Только .git/ структура, причём расположенная не в директории .git, а прямо в корне. Используется на серверах (например, GitHub под капотом — bare-репо).
Создаётся через:
git init --bare repo.git
ls repo.git
# HEAD config description hooks/ info/ objects/ refs/
# (нет .git/ — это сам .git/, развернутый в корне)
В bare-репо вы НЕ можете редактировать файлы. Они существуют только в виде commit/tree/blob объектов. Bare-репо нужны для:
- Удалённых репозиториев (origin на сервере)
- Git-серверов (GitLab self-hosted, Gitea, Forgejo)
Junior работает с bare-репо опосредованно — через git push к ним. Подробно — модуль 6.
Попробуй сам
- Создайте пустой репозиторий и посмотрите, что появилось:
mkdir -p ~/git-sandbox/lesson-03-init
cd ~/git-sandbox/lesson-03-init
git init
find .git -maxdepth 1
- Сделайте первый коммит и посмотрите изменения:
echo "test" > a.txt
git add a.txt
git commit -m "test"
find .git -type f
- Посмотрите содержимое HEAD, refs/heads/main и логов:
cat .git/HEAD
cat .git/refs/heads/main
cat .git/logs/HEAD
- Найдите blob, tree, commit-объекты в objects/:
find .git/objects -type f
- Для каждого — посмотрите тип и содержимое:
git cat-file -t <SHA>
git cat-file -p <SHA>
Это упражнение даст вам пожизненную интуицию: «вот команда меняет вот этот файл». Дальше Git не сможет вас удивить.
ls, tree: навигация по файловой системе cat, less, file: чтение и определение типа файла