Структура Git-репозитория: Полное техническое руководство
Git — это распределённая система контроля версий, которая хранит историю проекта в виде направленного ациклического графа (DAG) неизменяемых объектов-снимков. Каждый Git-репозиторий построен из трёх логических зон — рабочего каталога, индекса промежуточной области и хранилища объектов внутри .git/ — а также набора лёгких указателей (ветки, теги, удалённые репозитории), которые обеспечивают навигацию по истории. Понимание того, как эти уровни взаимодействуют, — это разница между механическим использованием Git и его применением с хирургической точностью.
Если вы самостоятельно размещаете свои репозитории на VPS, знание этой внутренней структуры позволяет восстанавливаться после сбоев, проектировать эффективные CI/CD-пайплайны и проверять каждый байт истории вашего проекта без использования сторонних платформ.
Модель трёх зон: как Git перемещает данные
Прежде чем углубляться в отдельные компоненты, усвойте модель потока данных, которая управляет каждой операцией Git:
Working Directory --> Staging Area (Index) --> .git/ Object Store
(edit) (git add) (git commit)Изменения перемещаются слева направо при создании коммита и справа налево при восстановлении или сбросе. Каждая команда Git — это по сути операция чтения или записи в одной или нескольких из этих зон.
Рабочий каталог
Рабочий каталог (также называемый рабочим деревом) — это представление вашего проекта в файловой системе в определённом состоянии после переключения. Когда вы выполняете git clone или git checkout, Git восстанавливает файлы из сжатых объектов в .git/objects/ и записывает их в этот каталог.
Файлы в рабочем каталоге существуют в одном из четырёх состояний:
- Неотслеживаемые — Git никогда не видел этот файл; он существует только на диске.
- Отслеживаемые, неизменённые — файл точно соответствует последнему зафиксированному снимку.
- Отслеживаемые, изменённые — файл отличается от последнего зафиксированного снимка, но не был добавлен в индекс.
- Отслеживаемые, удалённые — файл был удалён с диска, но удаление не было добавлено в индекс.
Важный нюанс, который сбивает с толку многих разработчиков: рабочий каталог — это не простая копия репозитория. Git восстанавливает его, читая объекты-деревья и распаковывая объекты-блобы. Если .git/ не повреждён, вы всегда можете заново сгенерировать рабочий каталог с нуля — обратное неверно.
Разреженное переключение для больших монорепозиториев
В репозиториях с десятками тысяч файлов (характерных для архитектур монорепозиториев) можно ограничить пути, которые Git материализует в рабочем каталоге:
git sparse-checkout init --cone
git sparse-checkout set services/api services/authЭто незаменимо на VPS с ограниченным дисковым I/O, поскольку Git пропускает распаковку блобов для путей за пределами конуса.
Область подготовки (индекс)
Область подготовки, внутренне называемая индексом, — это бинарный файл, расположенный по адресу .git/index. Он выступает в роли предлагаемого следующего коммита — изменяемого снимка, находящегося между рабочим каталогом и постоянным хранилищем объектов.
git add <file> # Stage a specific file
git add -p # Interactively stage hunks within a file
git add -u # Stage all tracked modifications and deletions
git status # Compare working directory and index against HEAD
git diff --cached # Show diff between index and HEADЗачем нужен индекс
Индекс решает проблему, которую более простые инструменты VCS игнорируют: частичные коммиты. Вы можете изменить пять файлов, но хотите включить в следующий коммит только три из них. Индекс позволяет составить именно тот снимок, который вы намерены зафиксировать, независимо от того, что открыто в вашем редакторе.
Граничный случай — повреждение индекса: если системный сбой прерывает git add, файл индекса может быть повреждён. Симптомы включают зависание git status или вывод странных результатов. Восстановление:
rm .git/index
git resetGit перестраивает индекс из HEAD, не затрагивая рабочий каталог.
Индекс как регистр конфликтов слияния
Во время конфликта слияния индекс одновременно хранит три версии каждого конфликтующего файла (стадии 1, 2 и 3 — базовая, наша, их). Именно поэтому git diff --cached не показывает ничего полезного в середине конфликта; для просмотра всех трёх стадий вам нужен git diff --cc или инструмент слияния.
Каталог .git/: анатомия хранилища объектов
Каталог .git/ и есть репозиторий. Всё остальное — рабочий каталог, удалённые клоны — является производным от него. Удаление .git/ превращает репозиторий в обычный каталог без истории.
.git/
├── HEAD
├── config
├── description
├── index
├── COMMIT_EDITMSG
├── hooks/
├── info/
├── logs/
│ ├── HEAD
│ └── refs/
├── objects/
│ ├── info/
│ └── pack/
└── refs/
├── heads/
├── remotes/
└── tags/HEAD
HEAD — это обычный текстовый файл, содержащий либо символическую ссылку (указывающую на ветку), либо необработанный SHA-1-хэш (состояние отсоединённого HEAD).
cat .git/HEAD
# ref: refs/heads/main <-- on a branch
# a3f1c9d... <-- detached HEADОтсоединённый HEAD — это не состояние ошибки; оно является намеренным, когда вы переключаетесь на тег или конкретный коммит для проверки. Опасность заключается в создании коммитов в состоянии отсоединённого HEAD: эти коммиты доступны только через reflog до тех пор, пока вы не прикрепите их к ветке.
git checkout -b rescue-branch # Attach detached commits to a new branchconfig
Файл конфигурации локального репозитория. Он переопределяет глобальные (~/.gitconfig) и системные (/etc/gitconfig) настройки. Распространённые записи:
[core]
repositoryformatversion = 0
filemode = true
bare = false
[remote "origin"]
url = git@github.com:user/repo.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/mainНа самостоятельно размещённом сервере вы будете часто редактировать этот файл напрямую при смене URL удалённых репозиториев или настройке uploadpack.allowReachableSHA1InWant для частичных клонов.
refs/
Каталог refs/ содержит обычные текстовые файлы, каждый из которых хранит один SHA-1-хэш. Это именованные указатели, которые делают DAG Git доступным для навигации.
| Тип ссылки | Путь | Описание |
|---|---|---|
| Локальная ветка | refs/heads/<name> | Указывает на последний коммит ветки |
| Ветка отслеживания удалённого репозитория | refs/remotes/<remote>/<name> | Локальный кэш последнего коммита удалённой ветки |
| Лёгкий тег | refs/tags/<name> | Указывает непосредственно на объект-коммит |
| Аннотированный тег | refs/tags/<name> | Указывает на объект-тег, который указывает на коммит |
| Stash | refs/stash | Указывает на коммит stash |
Для повышения производительности Git упаковывает ссылки в .git/packed-refs после того, как в репозитории накапливается их большое количество. При написании скриптов для работы со ссылками всегда проверяйте оба места.
Объекты Git: неизменяемое ядро
Всё, что хранится в .git/objects/, является адресуемым по содержимому: имя файла — это SHA-1-хэш (или SHA-256 в новых версиях Git) содержимого объекта. Это делает Git изначально защищённым от несанкционированного изменения — изменение любого байта меняет хэш, нарушая цепочку.
Четыре типа объектов
| Тип объекта | Что хранит | Указывает на |
|---|---|---|
| Блоб | Необработанное содержимое файла (без имени файла, без прав доступа) | Ничего |
| Дерево | Список каталога: имена файлов, права доступа, SHA блобов/деревьев | Блобы и другие деревья |
| Коммит | Автор, коммиттер, временная метка, сообщение, SHA родителя(ей) | Одно дерево + ноль или более родительских коммитов |
| Тег | Идентификатор создателя тега, временная метка, сообщение, GPG-подпись | Обычно коммит |
Прямая проверка объектов
# Show the type of any object
git cat-file -t a3f1c9d
# Show the content of any object
git cat-file -p a3f1c9d
# Show the tree of the current HEAD commit
git ls-tree HEAD
# Show a specific blob's content
git show HEAD:src/main.pyОтдельные объекты и файлы пакетов
Изначально каждый объект хранится как отдельный сжатый файл в .git/objects/<2-char-prefix>/<38-char-suffix>. Это отдельные объекты. Со временем Git запускает git gc (сборку мусора), чтобы объединить отдельные объекты в файлы пакетов (.git/objects/pack/*.pack) с соответствующим индексом (.pack.idx).
Файлы пакетов используют дельта-сжатие — хранение разницы между похожими объектами вместо полных копий. Репозиторий с тысячами похожих текстовых файлов может значительно уменьшиться после упаковки. На VPS с ограниченной ёмкостью NVMe запуск git gc --aggressive на больших репозиториях перед архивированием является стандартной практикой.
git count-objects -vH # Show loose object count and disk usage
git gc --aggressive # Repack aggressively (CPU-intensive)
git verify-pack -v .git/objects/pack/*.idx | sort -k3 -n | tail -20
# Find the 20 largest objects in the packИстория коммитов: направленный ациклический граф
Каждый объект-коммит содержит ровно один указатель на объект-дерево (снимок корневого каталога) и ноль или более указателей на родительские коммиты. Это формирует DAG, где:
- Ноль родителей = начальный коммит (корневой коммит)
- Один родитель = обычный коммит
- Два родителя = коммит слияния
- Три и более родителей = осьминожье слияние (редко, используется для одновременной интеграции многих функциональных веток)
git log --oneline --graph --all # Visualize the full DAG
git log --format="%H %P" # Show each commit's SHA and parent SHA(s)Неизменяемость коммитов и перезапись истории
Поскольку SHA коммита является производным от его содержимого (включая SHA родителей), любая перезапись создаёт новый коммит с новым SHA. Операции git rebase, git commit --amend и git filter-repo не изменяют историю — они создают параллельную историю. Старые коммиты остаются в хранилище объектов до сборки мусора.
Именно поэтому принудительная отправка переписанной истории в общую ветку является деструктивной: локальные ветки коллег по-прежнему указывают на старую цепочку коммитов.
Ветки: лёгкие указатели
Ветка — это не что иное, как файл размером 41 байт, содержащий SHA-1-хэш. Создание ветки происходит мгновенно независимо от размера репозитория, поскольку Git записывает только один небольшой файл.
git branch feature/auth # Create branch at current HEAD
git checkout -b feature/auth # Create and switch in one step
git switch -c feature/auth # Modern equivalent (Git 2.23+)
git branch -d feature/auth # Delete (safe: refuses if unmerged)
git branch -D feature/auth # Delete (force: regardless of merge status)Внутреннее устройство веток
cat .git/refs/heads/main
# a3f1c9d8e2b1f4c7d9e0a1b2c3d4e5f6a7b8c9d0Когда вы создаёте коммит в ветке, Git записывает SHA нового коммита в этот файл. Это и есть суть «продвижения указателя ветки».
Отслеживающие ветки и конфигурация upstream
Отношение отслеживания сообщает Git, с какой удалённой веткой следует сравнивать локальную ветку для отчётности о расхождении git status и поведения git pull.
git branch --set-upstream-to=origin/main main
git branch -vv # Show tracking relationships and ahead/behind countsТеги: постоянные маркеры в истории
Теги отмечают конкретные коммиты как значимые — как правило, выпуски программного обеспечения. В отличие от веток, теги не перемещаются при новых коммитах.
| Характеристика | Лёгкий тег | Аннотированный тег |
|---|---|---|
| Хранение | Файл ссылки, указывающий на коммит | Объект-тег в хранилище объектов |
| Метаданные | Отсутствуют | Имя создателя тега, email, дата, сообщение |
| GPG-подпись | Невозможна | Поддерживается через git tag -s |
| Рекомендуется для релизов | Нет | Да |
Передача с git push --tags | Да | Да |
git tag v2.1.0 # Lightweight tag at HEAD
git tag -a v2.1.0 -m "Release 2.1.0" # Annotated tag
git tag -s v2.1.0 -m "Signed release" # GPG-signed annotated tag
git push origin --tags # Push all tags to remote
git push origin v2.1.0 # Push a specific tagКритическая ошибка: git push по умолчанию не отправляет теги. Команды часто забывают об этом и публикуют примечания к релизу со ссылкой на тег, которого не существует на удалённом репозитории.
Удалённые репозитории: распределённое сотрудничество
Удалённый репозиторий — это именованный URL, хранящийся в .git/config. Ветки отслеживания удалённого репозитория (в refs/remotes/) — это локальные снимки веток удалённого репозитория только для чтения, обновляемые только при явном выполнении fetch.
git remote add origin git@github.com:user/repo.git
git remote -v # List remotes with URLs
git remote set-url origin <new-url> # Change a remote URL
git fetch origin # Update remote-tracking branches
git fetch --prune # Remove stale remote-tracking branches
git push origin main # Push local main to remote
git push -u origin feature/auth # Push and set upstream trackingНесколько удалённых репозиториев
Один репозиторий может отслеживать несколько удалённых репозиториев — это распространено при поддержке форка наряду с основным репозиторием:
git remote add upstream git@github.com:original/repo.git
git fetch upstream
git merge upstream/mainПри самостоятельном размещении голых репозиториев на выделенном сервере для вашей команды каждый разработчик добавляет сервер как удалённый репозиторий и использует аутентификацию по SSH-ключу для доступа на запись.
Хуки: автоматизированное применение политик при каждом событии Git
Хуки — это исполняемые скрипты в .git/hooks/. Git вызывает их в определённых точках рабочего процесса. Они не передаются при git clone или git push — каждый разработчик (или сервер) должен устанавливать их самостоятельно. Это частый источник путаницы в командных средах.
Клиентские хуки
| Хук | Триггер | Распространённое использование |
|---|---|---|
pre-commit | Перед запросом сообщения коммита | Линтинг, сканирование секретов, выполнение тестов |
prepare-commit-msg | После создания сообщения по умолчанию | Вставка имени ветки в сообщение |
commit-msg | После того как пользователь написал сообщение | Применение формата conventional commit |
post-commit | После записи коммита | Локальные уведомления |
pre-push | Перед выполнением git push | Запуск полного набора тестов |
pre-rebase | Перед началом rebase | Предотвращение rebase опубликованных веток |
Серверные хуки
| Хук | Триггер | Распространённое использование |
|---|---|---|
pre-receive | Перед обновлением ссылок | Применение защиты веток, отклонение принудительной отправки |
update | Для каждой ссылки при получении | Применение политик для отдельных веток |
post-receive | После обновления всех ссылок | Запуск CI/CD, отправка уведомлений |
Пример: хук pre-commit для обнаружения секретов
#!/usr/bin/env bash
# .git/hooks/pre-commit
if git diff --cached --name-only | xargs grep -lE '(AKIA|passwords*=|api_keys*=)' 2>/dev/null; then
echo "ERROR: Potential secret detected in staged files. Commit aborted."
exit 1
fi
exit 0Сделайте его исполняемым:
chmod +x .git/hooks/pre-commitДля распространения хуков в команде используйте инструмент наподобие Husky (для проектов на Node.js) или храните хуки в каталоге hooks/ в корне репозитория и создавайте символические ссылки на них при настройке проекта.
Reflog: защитная сеть
Reflog записывает каждое перемещение HEAD и указателей веток, включая операции, которые внешне уничтожают историю (жёсткие сбросы, rebase, изменённые коммиты). Он хранится в .git/logs/.
git reflog # Show HEAD movement history
git reflog show main # Show movement history for a specific branch
git checkout HEAD@{3} # Check out the state HEAD was in 3 moves ago
git branch recovered HEAD@{5} # Recover commits by branching from a reflog entryЗаписи reflog истекают через 90 дней по умолчанию (gc.reflogExpire). На производственном сервере рассмотрите возможность увеличения этого срока:
git config gc.reflogExpire 180
git config gc.reflogExpireUnreachable 30Голые репозитории: размещение на стороне сервера
Голый репозиторий не имеет рабочего каталога. Он содержит только содержимое .git/ на корневом уровне. Голые репозитории — это правильный формат для централизованного размещения: они принимают отправки без осложнений, связанных с переключённой веткой.
git init --bare /srv/repos/myproject.gitКогда вы отправляете данные на GitHub, GitLab или самостоятельно размещённый Git-сервер, вы отправляете их в голый репозиторий. Если вы размещаете собственный Git-сервер на VPS с cPanel или обычном Linux VPS, голые репозитории в /srv/repos/ с SSH-доступом являются стандартной архитектурой.
Инициализация общего голого репозитория
# On the server
git init --bare --shared=group /srv/repos/project.git
chown -R git:developers /srv/repos/project.git
# On a developer's machine
git remote add origin git@yourserver.com:/srv/repos/project.git
git push -u origin mainХранение объектов Git: размер, целостность и обслуживание
Проверка работоспособности репозитория
git fsck --full # Verify object integrity (finds dangling and corrupt objects)
git fsck --lost-found # Write dangling objects to .git/lost-found/Поиск и удаление больших объектов
Случайно зафиксированные большие бинарные файлы — распространённая причина раздутых репозиториев. Определите их перед использованием git filter-repo для их удаления:
# Find the 10 largest objects by compressed size
git verify-pack -v .git/objects/pack/*.idx
| sort -k3 -rn
| head -10
| awk '{print $1}'
| xargs -I{} git cat-file -p {}# Remove a file from all history (requires git-filter-repo)
git filter-repo --path path/to/large-file.bin --invert-pathsПосле фильтрации все участники должны повторно клонировать репозиторий — их локальные репозитории ссылаются на SHA-хэши, которых больше не существует в переписанной истории.
Сравнение: ключевые концепции репозитория Git
| Концепция | Тип | Изменяемый | Хранится в | Передаётся при push/fetch |
|---|---|---|---|---|
| Блоб | Объект | Нет | .git/objects/ | Да (если достижим) |
| Дерево | Объект | Нет | .git/objects/ | Да (если достижим) |
| Коммит | Объект | Нет | .git/objects/ | Да (если достижим) |
| Аннотированный тег | Объект | Нет | .git/objects/ | Только с --tags |
| Ветка | Ссылка | Да | .git/refs/heads/ | Да |
| Ветка отслеживания удалённого репозитория | Ссылка | Да (при fetch) | .git/refs/remotes/ | Нет (локальный кэш) |
| Лёгкий тег | Ссылка | Нет | .git/refs/tags/ | Только с --tags |
| HEAD | Символическая ссылка/хэш | Да | .git/HEAD | Нет |
| Индекс | Бинарный файл | Да | .git/index | Нет |
| Хуки | Скрипты | Да | .git/hooks/ | Нет |
| Reflog | Журнал | Да (автоматически истекает) | .git/logs/ | Нет |
Практическая матрица решений и ключевые выводы
Используйте этот контрольный список при настройке или аудите репозитория Git на вашей инфраструктуре:
Инициализация репозитория
- Используйте
git init --bare --shared=groupдля любого репозитория, который будет получать отправки от нескольких пользователей. - Храните голые репозитории за пределами веб-доступных каталогов (никогда не в
/var/www/).
Работоспособность хранилища объектов
- Запускайте
git fsck --fullпосле любого инцидента с хранилищем или ошибки файловой системы. - Планируйте периодический запуск
git gcна долгоживущих репозиториях; автоматизируйте его через cron на вашем сервере. - Отслеживайте размер файлов пакетов с помощью
git count-objects -vH; проводите расследование, если количество отдельных объектов превышает 1 000.
Гигиена веток и ссылок
- Своевременно удаляйте слитые ветки; устаревшие ссылки накапливаются и замедляют операции
git fetch --prune. - Используйте
git fetch --pruneв CI-пайплайнах, чтобы избежать действий с удалёнными удалёнными ветками.
Развёртывание хуков
- Никогда не полагайтесь на
.git/hooks/для применения общекомандных политик — хуки не клонируются. Вместо этого используйте серверные хукиpre-receiveили CI-шлюз. - Проверяйте серверные хуки после каждого обновления Git-сервера; пути интерпретаторов хуков могут измениться.
Безопасность на самостоятельно размещённых серверах
- Ограничьте SSH-доступ для пользователя
gitс помощью принудительных команд (command=вauthorized_keys). - Используйте
git-shellв качестве оболочки входа для пользователяgit, чтобы предотвратить выполнение произвольных команд. - Обеспечьте ваш сервер репозиториев действующим SSL-сертификатом, если вы открываете какой-либо веб-интерфейс (Gitea, GitLab, cgit).
Перезапись истории
- Никогда не переписывайте историю в ветках, общих с другими, без скоординированного плана миграции.
- После
git filter-repoвсе участники должны повторно клонировать репозиторий; немедленно обновите URL удалённых репозиториев в CI/CD.
Аварийное восстановление
- Увеличьте срок хранения reflog на производственных серверах (
gc.reflogExpire = 180). - Храните вторичный голый клон на отдельном хосте в качестве резервной копии; простого
git fetchс основного сервера достаточно.
Часто задаваемые вопросы
В чём разница между голым и неголым репозиторием Git?
Неголый репозиторий имеет рабочий каталог, в котором извлечены файлы, плюс подкаталог .git/, содержащий хранилище объектов. Голый репозиторий содержит только хранилище объектов в своём корне (без рабочего каталога) и является правильным форматом для общего сервера, принимающего отправки.
Можно ли восстановить коммиты после выполнения git reset --hard?
Да, при условии, что коммиты не были удалены сборщиком мусора. Запустите git reflog, чтобы найти SHA нужного коммита, затем git checkout -b recovery-branch <SHA>, чтобы прикрепить его к новой ветке. Записи reflog хранятся 90 дней по умолчанию.
Почему git push не передаёт мои теги?
По замыслу git push передаёт только коммиты, достижимые из явно указанных вами ссылок. Теги — это отдельные ссылки, и их нужно отправлять с помощью git push origin --tags (все теги) или git push origin <tagname> (конкретный тег).
Что происходит с индексом во время конфликта слияния?
Индекс одновременно хранит все три версии каждого конфликтующего файла: стадия 1 (общий предок/база), стадия 2 (ваша версия) и стадия 3 (их версия). Обычный git add записывает только стадию 0 (разрешённую). До тех пор, пока все конфликты не будут разрешены и добавлены в индекс, git commit откажется продолжать.
Чем клиентские хуки Git отличаются от серверных?
Клиентские хуки выполняются на машине разработчика и не применяются централизованно — любой разработчик может обойти их, удалив файл хука. Серверные хуки (pre-receive, update, post-receive) выполняются на сервере размещения и не могут быть обойдены клиентом, что делает их правильной точкой применения политик защиты веток, требований к проверке кода и триггеров CI/CD.
на всех хостинговых услугах