Заощадьте 15% на всіх хостингових послугах

Перевірте свої навички і отримайте Знижку на будь-який план хостингу

Використовуй код: Skills Почати
Рубрики
Linux Віртуальні сервери

Структура 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 reset

Git відновлює індекс з 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 — це простий текстовий файл, що містить або символічне посилання (що вказує на гілку), або необроблений 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 branch

config

Локальний файл конфігурації репозиторію. Він перевизначає глобальні (~/.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>Вказує на кінцевий коміт гілки
Гілка відстеження remoterefs/remotes/<remote>/<name>Локальний кеш кінцевої точки гілки remote
Легковаговий тегrefs/tags/<name>Вказує безпосередньо на об’єкт коміту
Анотований тегrefs/tags/<name>Вказує на об’єкт тегу, який вказує на коміт
Stashrefs/stashВказує на коміт stash

Для підвищення продуктивності Git пакує refs у .git/packed-refs, коли в репозиторії накопичується їх багато. Завжди перевіряйте обидва місця при написанні скриптів для роботи з refs.

Об’єкти Git: незмінне ядро

Все, що зберігається в .git/objects/, є адресованим за вмістом: ім’я файлу — це SHA-1 (або SHA-256 у новіших версіях Git) хеш вмісту об’єкта. Це робить Git за своєю природою захищеним від підробок — зміна будь-якого байта змінює хеш, розриваючи ланцюжок.

Чотири типи об’єктів

Тип об’єктаЩо зберігаєВказує на
BlobНеоброблений вміст файлу (без імені файлу, без прав доступу)Нічого
TreeСписок каталогу: імена файлів, права доступу, SHA blob/treeBlob-и та інші 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.py

Loose-об’єкти та 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 30

Bare-репозиторії: розміщення на стороні сервера

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/Так
Гілка відстеження remoteRefТак (при fetch).git/refs/remotes/Ні (локальний кеш)
Легковаговий тегRefНі.git/refs/tags/Лише з --tags
HEADSymref/хешТак.git/HEADНі
ІндексБінарний файлТак.git/indexНі
ХукиСкриптиТак.git/hooks/Ні
ReflogЖурналТак (автоматично закінчується).git/logs/Ні

Практична матриця рішень та ключові висновки

Використовуйте цей контрольний список при налаштуванні або аудиті Git-репозиторію у вашій інфраструктурі:

Ініціалізація репозиторію

  • Використовуйте git init --bare --shared=group для будь-якого репозиторію, який отримуватиме push від кількох користувачів.
  • Зберігайте bare-репозиторії поза веб-доступними каталогами (ніколи не під /var/www/).

Стан сховища об’єктів

  • Запускайте git fsck --full після будь-якого інциденту зі сховищем або помилки файлової системи.
  • Плануйте git gc periodically 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.

Linux Віртуальні сервери
Віртуальні сервери Хостинг LiteSpeed
Адміністрація Віртуальні сервери