Структура Git-репозиторію: Повний технічний посібник
Git — це розподілена система контролю версій, яка зберігає історію проєкту у вигляді спрямованого ациклічного графа (DAG) незмінних об’єктів-знімків. Кожен Git-репозиторій побудований з трьох логічних зон — робочого каталогу, індексу (staging index) та сховища об’єктів всередині .git/ — а також набору легковагих покажчиків (гілки, теги, remote), що навігують цю історію. Розуміння того, як ці шари взаємодіють, — це різниця між механічним використанням Git та його використанням з хірургічною точністю.
Якщо ви самостійно розміщуєте свої репозиторії на VPS, опанування цієї внутрішньої структури дозволяє відновлюватися після збоїв, проєктувати ефективні CI/CD-пайплайни та перевіряти кожен байт історії вашого проєкту без залежності від сторонніх платформ.
Модель трьох зон: як Git переміщує дані
Перш ніж заглиблюватися в окремі компоненти, засвойте модель потоку даних, яка керує кожною операцією Git:
Working Directory --> Staging Area (Index) --> .git/ Object Store
(edit) (git add) (git commit)Зміни рухаються зліва направо, коли ви створюєте коміт, і справа наліво, коли ви відновлюєте або скидаєте. Кожна команда Git — це по суті операція читання або запису в одній або кількох із цих зон.
Робочий каталог
Робочий каталог (також називається робочим деревом) — це файлова система вашого проєкту в певному стані checkout. Коли ви виконуєте git clone або git checkout, Git відновлює файли зі стиснених об’єктів у .git/objects/ та записує їх у цей каталог.
Файли в робочому каталозі існують в одному з чотирьох станів:
- Невідстежувані (Untracked) — Git ніколи не бачив цей файл; він існує лише на диску.
- Відстежувані, незмінені (Tracked, unmodified) — файл точно відповідає останньому збереженому знімку.
- Відстежувані, змінені (Tracked, modified) — файл відрізняється від останнього збереженого знімка, але не був доданий до індексу.
- Відстежувані, видалені (Tracked, deleted) — файл був видалений з диска, але видалення не було додано до індексу.
Критичний нюанс, який збиває з пантелику багатьох розробників: робочий каталог — це не проста копія репозиторію. Git відновлює його, зчитуючи об’єкти дерева та розпаковуючи blob-об’єкти. Якщо .git/ цілий, ви завжди можете відновити робочий каталог з нуля — зворотне неможливо.
Sparse Checkout для великих монорепозиторіїв
У репозиторіях з десятками тисяч файлів (характерно для архітектур монорепозиторіїв) можна обмежити, які шляхи Git матеріалізує в робочому каталозі:
git sparse-checkout init --cone
git sparse-checkout set services/api services/authЦе надзвичайно корисно на VPS з обмеженим дисковим I/O, оскільки Git пропускає розпакування blob-об’єктів для шляхів поза конусом.
Область підготовки (Index)
Область підготовки, яка внутрішньо називається індексом, — це бінарний файл, розташований за адресою .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На самостійно розміщеному сервері ви часто будете редагувати цей файл безпосередньо при зміні remote URL або налаштуванні uploadpack.allowReachableSHA1InWant для часткових клонів.
refs/
Каталог refs/ містить прості текстові файли, кожен з яких зберігає один SHA-1 хеш. Вони є іменованими покажчиками, що роблять DAG Git придатним для навігації.
| Тип ref | Шлях | Опис |
|---|---|---|
| Локальна гілка | refs/heads/<name> | Вказує на кінцевий коміт гілки |
| Гілка відстеження remote | refs/remotes/<remote>/<name> | Локальний кеш кінцевої точки гілки remote |
| Легковаговий тег | refs/tags/<name> | Вказує безпосередньо на об’єкт коміту |
| Анотований тег | refs/tags/<name> | Вказує на об’єкт тегу, який вказує на коміт |
| Stash | refs/stash | Вказує на коміт stash |
Для підвищення продуктивності Git пакує refs у .git/packed-refs, коли в репозиторії накопичується їх багато. Завжди перевіряйте обидва місця при написанні скриптів для роботи з refs.
Об’єкти Git: незмінне ядро
Все, що зберігається в .git/objects/, є адресованим за вмістом: ім’я файлу — це SHA-1 (або SHA-256 у новіших версіях Git) хеш вмісту об’єкта. Це робить Git за своєю природою захищеним від підробок — зміна будь-якого байта змінює хеш, розриваючи ланцюжок.
Чотири типи об’єктів
| Тип об’єкта | Що зберігає | Вказує на |
|---|---|---|
| Blob | Необроблений вміст файлу (без імені файлу, без прав доступу) | Нічого |
| Tree | Список каталогу: імена файлів, права доступу, SHA blob/tree | Blob-и та інші tree |
| Commit | Автор, committer, мітка часу, повідомлення, SHA батьківського коміту | Один tree + нуль або більше батьківських комітів |
| Tag | Ідентифікатор того, хто поставив тег, мітка часу, повідомлення, 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.pyLoose-об’єкти та pack-файли
Спочатку кожен об’єкт зберігається як окремий стиснений файл у .git/objects/<2-char-prefix>/<38-char-suffix>. Це loose-об’єкти. З часом Git запускає git gc (збирання сміття), щоб об’єднати loose-об’єкти в pack-файли (.git/objects/pack/*.pack) з відповідним індексом (.pack.idx).
Pack-файли використовують дельта-стиснення — зберігаючи різницю між схожими об’єктами, а не повні копії. Репозиторій з тисячами схожих текстових файлів може значно зменшитися після пакування. На 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, де:
- Нуль батьків = початковий коміт (кореневий коміт)
- Один батько = звичайний коміт
- Два батьки = коміт злиття
- Три або більше батьків = octopus merge (рідкісний, використовується для одночасного злиття багатьох feature-гілок)
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, з якою remote-гілкою локальна гілка повинна порівнюватися для звітування про розбіжності git status та поведінки git pull.
git branch --set-upstream-to=origin/main main
git branch -vv # Show tracking relationships and ahead/behind countsТеги: постійні маркери в історії
Теги позначають конкретні коміти як значущі — зазвичай випуски програмного забезпечення. На відміну від гілок, теги не переміщуються новими комітами.
| Функція | Легковаговий тег | Анотований тег |
|---|---|---|
| Зберігання | Файл ref, що вказує на коміт | Об’єкт тегу в сховищі об’єктів |
| Метадані | Відсутні | Ім’я того, хто поставив тег, 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 за замовчуванням не відправляє теги. Команди часто забувають про це і публікують примітки до релізу з посиланням на тег, якого не існує на remote.
Remote: розподілена співпраця
Remote — це іменований URL, що зберігається в .git/config. Гілки відстеження remote (у refs/remotes/) — це локальні знімки гілок remote лише для читання, що оновлюються лише тоді, коли ви явно виконуєте 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Кілька remote
Один репозиторій може відстежувати кілька remote — це поширено при підтримці форку разом з upstream:
git remote add upstream git@github.com:original/repo.git
git fetch upstream
git merge upstream/mainПри самостійному розміщенні bare-репозиторіїв на виділеному сервері для вашої команди кожен розробник додає сервер як remote та використовує автентифікацію за 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 | Перед оновленням refs | Захист гілок, відхилення force-push |
update | Для кожного ref під час отримання | Застосування політики для окремих гілок |
post-receive | Після оновлення всіх refs | Запуск 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 30Bare-репозиторії: розміщення на стороні сервера
Bare-репозиторій не має робочого каталогу. Він містить лише вміст .git/ на кореневому рівні. Bare-репозиторії є правильним форматом для централізованого розміщення — вони приймають push без ускладнень, пов’язаних з перевіреною гілкою.
git init --bare /srv/repos/myproject.gitКоли ви відправляєте на GitHub, GitLab або самостійно розміщений Git-сервер, ви відправляєте в bare-репозиторій. Якщо ви розміщуєте власний Git-сервер на VPS з cPanel або звичайному Linux VPS, bare-репозиторії в /srv/repos/ з доступом по SSH є стандартною архітектурою.
Ініціалізація спільного bare-репозиторію
# 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 |
|---|---|---|---|---|
| Blob | Об’єкт | Ні | .git/objects/ | Так (якщо досяжний) |
| Tree | Об’єкт | Ні | .git/objects/ | Так (якщо досяжний) |
| Commit | Об’єкт | Ні | .git/objects/ | Так (якщо досяжний) |
| Анотований тег | Об’єкт | Ні | .git/objects/ | Лише з --tags |
| Гілка | Ref | Так | .git/refs/heads/ | Так |
| Гілка відстеження remote | Ref | Так (при fetch) | .git/refs/remotes/ | Ні (локальний кеш) |
| Легковаговий тег | Ref | Ні | .git/refs/tags/ | Лише з --tags |
| HEAD | Symref/хеш | Так | .git/HEAD | Ні |
| Індекс | Бінарний файл | Так | .git/index | Ні |
| Хуки | Скрипти | Так | .git/hooks/ | Ні |
| Reflog | Журнал | Так (автоматично закінчується) | .git/logs/ | Ні |
Практична матриця рішень та ключові висновки
Використовуйте цей контрольний список при налаштуванні або аудиті Git-репозиторію у вашій інфраструктурі:
Ініціалізація репозиторію
- Використовуйте
git init --bare --shared=groupдля будь-якого репозиторію, який отримуватиме push від кількох користувачів. - Зберігайте bare-репозиторії поза веб-доступними каталогами (ніколи не під
/var/www/).
Стан сховища об’єктів
- Запускайте
git fsck --fullпісля будь-якого інциденту зі сховищем або помилки файлової системи. - Плануйте
git gcperiodically on long-lived repositories; automate it via cron on your server. - Відстежуйте розмір pack-файлів за допомогою
git count-objects -vH; досліджуйте, якщо кількість loose-об’єктів перевищує 1 000.
Гігієна гілок та refs
- Своєчасно видаляйте злиті гілки; застарілі refs накопичуються та уповільнюють операції
git fetch --prune. - Використовуйте
git fetch --pruneв CI-пайплайнах, щоб уникнути дій на видалених remote-гілках.
Розгортання хуків
- Ніколи не покладайтеся на
.git/hooks/для загальнокомандної політики — хуки не клонуються. Натомість використовуйте серверні хукиpre-receiveабо CI-шлюз. - Перевіряйте серверні хуки після кожного оновлення Git-сервера; шляхи до інтерпретаторів хуків можуть змінюватися.
Безпека на самостійно розміщених серверах
- Обмежте SSH-доступ до користувача
gitза допомогою примусових команд (command=вauthorized_keys). - Використовуйте
git-shellяк оболонку входу для користувачаgit, щоб запобігти виконанню довільних команд. - Поєднайте ваш сервер репозиторію з дійсним SSL-сертифікатом, якщо ви відкриваєте будь-який веб-інтерфейс (Gitea, GitLab, cgit).
Перезапис історії
- Ніколи не переписуйте історію на гілках, спільних з іншими, без узгодженого плану міграції.
- Після
git filter-repoвсі співробітники повинні повторно клонувати; негайно оновіть remote URL в CI/CD.
Аварійне відновлення
- Збільшуйте термін зберігання reflog на виробничих серверах (
gc.reflogExpire = 180). - Зберігайте вторинний bare-клон на окремому хості як резервну копію; простого
git fetchз основного достатньо.
FAQ
У чому різниця між bare та non-bare Git-репозиторієм?
Non-bare репозиторій має робочий каталог, де файли перевіряються, плюс підкаталог .git/, що містить сховище об’єктів. Bare-репозиторій містить лише сховище об’єктів у своєму корені (без робочого каталогу) і є правильним форматом для спільного сервера, що приймає push.
Чи можна відновити коміти після виконання git reset --hard?
Так, якщо коміти ще не були зібрані збирачем сміття. Запустіть git reflog, щоб знайти SHA коміту, який ви хочете відновити, потім git checkout -b recovery-branch <SHA>, щоб прикріпити його до нової гілки. Записи reflog зберігаються за замовчуванням 90 днів.
Чому git push не передає мої теги?
За задумом, git push передає лише коміти, досяжні з refs, які ви явно відправляєте. Теги є окремими refs і повинні відправлятися за допомогою git push origin --tags (всі теги) або git push origin <tagname> (конкретний тег).
Що відбувається з індексом під час конфлікту злиття?
Індекс одночасно зберігає всі три версії кожного конфліктного файлу: стадія 1 (спільний предок/база), стадія 2 (ваша версія) та стадія 3 (їхня версія). Звичайний git add записує лише стадію 0 (вирішену). Доки всі конфлікти не будуть вирішені та додані до індексу, git commit відмовиться продовжувати.
Чим відрізняються Git-хуки при клієнтському та серверному розгортанні?
Клієнтські хуки запускаються на машині розробника і не застосовуються централізовано — будь-який розробник може обійти їх, видаливши файл хука. Серверні хуки (pre-receive, update, post-receive) запускаються на сервері розміщення і не можуть бути обійдені клієнтом, що робить їх правильною точкою застосування політик захисту гілок, вимог до перегляду коду та тригерів CI/CD.
на всіх хостингових послугах