Структура на Git хранилище: Пълно техническо ръководство
Git е разпределена система за контрол на версиите, която съхранява историята на проекта като насочен ацикличен граф (DAG) от неизменяеми обектни снимки. Всяко Git хранилище е изградено от три логически зони — работната директория, индексът за подготовка и хранилището на обекти вътре в .git/ — плюс набор от леки указатели (клонове, тагове, отдалечени хранилища), които навигират тази история. Разбирането как тези слоеве взаимодействат е разликата между механичното използване на Git и използването му с хирургична прецизност.
Ако самостоятелно хоствате хранилищата си на VPS, овладяването на тази вътрешна структура ви позволява да се възстановявате от аварии, да проектирате ефективни CI/CD конвейери и да одитирате всеки байт от историята на проекта си, без да разчитате на платформа на трета страна.
Моделът с три зони: Как Git премества данни
Преди да се потопите в отделните компоненти, усвоете модела на потока от данни, който управлява всяка Git операция:
Working Directory --> Staging Area (Index) --> .git/ Object Store
(edit) (git add) (git commit)Промените се движат отляво надясно, когато изграждате commit, и отдясно наляво, когато възстановявате или нулирате. Всяка Git команда е по същество операция за четене или запис върху една или повече от тези зони.
Работна директория
Работната директория (известна още като работното дърво) е изгледът на файловата система на вашия проект при определено състояние на извличане. Когато изпълните git clone или git checkout, Git възстановява файловете от компресирани обекти в .git/objects/ и ги записва в тази директория.
Файловете в работната директория съществуват в едно от четири състояния:
- Неследени — Git никога не е виждал този файл; той съществува само на диска.
- Следени, непроменени — файлът съвпада точно с последната записана снимка.
- Следени, променени — файлът се различава от последната записана снимка, но не е бил подготвен.
- Следени, изтрити — файлът е премахнат от диска, но изтриването не е било подготвено.
Критичен нюанс, който обърква много разработчици: работната директория не е просто копие на хранилището. Git я възстановява чрез четене на tree обекти и декомпресиране на blob обекти. Ако .git/ е непокътнат, можете винаги да регенерирате работната директория от нулата — обратното не е вярно.
Sparse Checkout за големи монорепозитории
При хранилища с десетки хиляди файлове (характерни за монорепо архитектури), можете да ограничите кои пътища Git материализира в работната директория:
git sparse-checkout init --cone
git sparse-checkout set services/api services/authТова е безценно на VPS с ограничен дисков I/O, тъй като Git пропуска декомпресирането на blob обекти за пътища извън конуса.
Зона за подготовка (индекс)
Зоната за подготовка, вътрешно наречена индекс, е двоичен файл, намиращ се на .git/index. Тя действа като предложен следващ commit — изменяема снимка, която се намира между работната ви директория и постоянното хранилище на обекти.
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 инструменти игнорират: частичните commits. Може да сте променили пет файла, но да искате само три от тях в следващия commit. Индексът ви позволява да съставите точно снимката, която възнамерявате да запишете, независимо от това, което редакторът ви има отворено.
Краен случай — повреда на индекса: Ако системен срив прекъсне 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 не е грешно състояние — то е умишлено, когато извличате таг или конкретен commit за проверка. Опасността е при правене на commits в откачено HEAD: тези commits са достъпни само чрез 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> | Сочи към върховия commit на клон |
| Клон за проследяване на отдалечено хранилище | refs/remotes/<remote>/<name> | Локален кеш на върха на отдалечен клон |
| Лек таг | refs/tags/<name> | Сочи директно към commit обект |
| Анотиран таг | refs/tags/<name> | Сочи към таг обект, който сочи към commit |
| Stash | refs/stash | Сочи към stash commit |
За производителност, Git пакетира референциите в .git/packed-refs, след като хранилището натрупа много от тях. Винаги проверявайте и двете места при скриптиране срещу референции.
Git обекти: Неизменяемото ядро
Всичко съхранено в .git/objects/ е адресирано по съдържание: името на файла е SHA-1 (или SHA-256 в по-новите версии на Git) хешът на съдържанието на обекта. Това прави Git присъщо защитен от подправяне — промяната на всеки байт променя хеша, нарушавайки веригата.
Четирите типа обекти
| Тип обект | Какво съхранява | Сочи към |
|---|---|---|
| Blob | Необработено съдържание на файл (без име на файл, без разрешения) | Нищо |
| Tree | Списък на директория: имена на файлове, разрешения, blob/tree SHA адреси | Blob обекти и други tree обекти |
| Commit | Автор, committer, времева марка, съобщение, родителски SHA(s) | Един tree + нула или повече родителски commits |
| Tag | Самоличност на тагиращия, времева марка, съобщение, GPG подпис | Обикновено commit |
Директна проверка на обекти
# 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Свободни обекти срещу pack файлове
Първоначално всеки обект се съхранява като отделен компресиран файл под .git/objects/<2-char-prefix>/<38-char-suffix>. Това са свободни обекти. С течение на времето, Git изпълнява git gc (събиране на боклук), за да обедини свободните обекти в 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История на commits: Насоченият ацикличен граф
Всеки commit обект съдържа точно един указател към tree обект (снимката на основната директория) и нула или повече указатели към родителски commits. Това формира DAG, където:
- Нула родители = началният commit (корен commit)
- Един родител = обикновен commit
- Два родители = commit от сливане
- Три или повече родители = октопусово сливане (рядко, използвано за интегриране на много функционални клонове едновременно)
git log --oneline --graph --all # Visualize the full DAG
git log --format="%H %P" # Show each commit's SHA and parent SHA(s)Неизменяемост на commits и пренаписване на историята
Тъй като SHA на commit е производен от съдържанието му (включително родителските SHA адреси), всяко пренаписване създава нов commit с нов SHA. Операции като git rebase, git commit --amend и git filter-repo не променят историята — те създават паралелна история. Старите commits остават в хранилището на обекти до събирането на боклука.
Ето защо принудителното изпращане на пренаписана история към споделен клон е разрушително: локалните клонове на сътрудниците все още сочат към старата верига от commits.
Клонове: Леки указатели
Клонът не е нищо повече от 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Когато правите commit върху клон, Git записва новия SHA на commit в този файл. Това е цялото „придвижване на указателя на клона”.
Проследяващи клонове и конфигурация на upstream
Връзката за проследяване казва на Git срещу кой отдалечен клон трябва да сравнява локалния клон за отчитане на git status разминаване и поведение на git pull.
git branch --set-upstream-to=origin/main main
git branch -vv # Show tracking relationships and ahead/behind countsТагове: Постоянни маркери в историята
Таговете маркират конкретни commits като значими — обикновено версии на софтуера. За разлика от клоновете, таговете не се преместват от нови commits.
| Функция | Лек таг | Анотиран таг |
|---|---|---|
| Съхранение | Референтен файл, сочещ към commit | Таг обект в хранилището на обекти |
| Метаданни | Няма | Име на тагиращия, имейл, дата, съобщение |
| 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/) са локални снимки само за четене на клоновете на отдалеченото хранилище, актуализирани само когато изрично извличате.
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Множество отдалечени хранилища
Едно хранилище може да проследява множество отдалечени хранилища — характерно при поддържане на fork заедно с upstream:
git remote add upstream git@github.com:original/repo.git
git fetch upstream
git merge upstream/mainПри самостоятелно хостване на bare хранилища на dedicated server за вашия екип, всеки разработчик добавя сървъра като отдалечено хранилище и използва SSH ключова автентикация за достъп за изпращане.
Hooks: Автоматизирано прилагане при всяко Git събитие
Hooks са изпълними скриптове в .git/hooks/. Git ги извиква в определени точки от работния процес. Те не се прехвърлят чрез git clone или git push — всеки разработчик (или сървър) трябва да ги инсталира независимо. Това е честа причина за объркване в екипни среди.
Hooks от страна на клиента
| Hook | Задействане | Честа употреба |
|---|---|---|
pre-commit | Преди подканата за commit съобщение | Линтиране, сканиране за тайни, изпълнение на тестове |
prepare-commit-msg | След създаване на съобщението по подразбиране | Вмъкване на името на клона в съобщението |
commit-msg | След като потребителят напише съобщението | Прилагане на формат на conventional commit |
post-commit | След записване на commit | Локални известия |
pre-push | Преди изпълнение на git push | Изпълнение на пълен набор от тестове |
pre-rebase | Преди стартиране на rebase | Предотвратяване на rebase на публикувани клонове |
Hooks от страна на сървъра
| Hook | Задействане | Честа употреба |
|---|---|---|
pre-receive | Преди актуализиране на референциите | Прилагане на защита на клонове, отхвърляне на принудително изпращане |
update | За всяка референция по време на получаване | Прилагане на политика за отделни клонове |
post-receive | След актуализиране на всички референции | Задействане на CI/CD, изпращане на известия |
Пример: Pre-commit hook за откриване на тайни
#!/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За разпространение на hooks в целия екип, използвайте инструмент като Husky (Node.js проекти) или съхранявайте hooks в директория hooks/ в корена на хранилището и ги свързвайте символно по време на настройката на проекта.
Reflog: Предпазната мрежа
Reflog записва всяко движение на HEAD и указателите на клонове, включително операции, които изглежда унищожават историята (твърди нулирания, rebase операции, изменени commits). Съхранява се в .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 хранилищата са правилният формат за централизирано хостване — те приемат изпращания без усложненията на извлечен клон.
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 хранилище
| Концепция | Тип | Изменяем | Съхранява се в | Прехвърля се при изпращане/извличане |
|---|---|---|---|---|
| Blob | Обект | Не | .git/objects/ | Да (когато е достъпен) |
| Tree | Обект | Не | .git/objects/ | Да (когато е достъпен) |
| Commit | Обект | Не | .git/objects/ | Да (когато е достъпен) |
| Анотиран таг | Обект | Не | .git/objects/ | Само с --tags |
| Клон | Референция | Да | .git/refs/heads/ | Да |
| Клон за проследяване на отдалечено хранилище | Референция | Да (при извличане) | .git/refs/remotes/ | Не (локален кеш) |
| Лек таг | Референция | Не | .git/refs/tags/ | Само с --tags |
| HEAD | Символна референция/хеш | Да | .git/HEAD | Не |
| Индекс | Двоичен файл | Да | .git/index | Не |
| Hooks | Скриптове | Да | .git/hooks/ | Не |
| Reflog | Журнал | Да (автоматично изтича) | .git/logs/ | Не |
Практическа матрица за вземане на решения и основни изводи
Използвайте този контролен списък при настройване или одитиране на Git хранилище в инфраструктурата ви:
Инициализиране на хранилище
- Използвайте
git init --bare --shared=groupза всяко хранилище, което ще получава изпращания от множество потребители. - Съхранявайте bare хранилища извън уеб-достъпни директории (никога под
/var/www/).
Здраве на хранилището на обекти
- Изпълнявайте
git fsck --fullслед всеки инцидент със съхранението или грешка на файловата система. - Планирайте периодично
git gcза дълготрайни хранилища; автоматизирайте го чрез cron на сървъра ви. - Наблюдавайте размера на pack файловете с
git count-objects -vH; проучете, ако броят на свободните обекти надвиши 1 000.
Хигиена на клонове и референции
- Изтривайте своевременно слетите клонове; остарелите референции се натрупват и забавят операциите
git fetch --prune. - Използвайте
git fetch --pruneв CI конвейерите, за да избегнете действия върху изтрити отдалечени клонове.
Разгръщане на hooks
- Никога не разчитайте на
.git/hooks/за политика в целия екип — hooks не се клонират. Използвайте вместо това hookspre-receiveот страна на сървъра или CI порта. - Одитирайте hooks от страна на сървъра след всяко надграждане на Git сървъра; пътищата на интерпретатора на hooks могат да се променят.
Сигурност на самостоятелно хостваните сървъри
- Ограничете SSH достъпа до потребителя
gitс принудителни команди (command=вauthorized_keys). - Използвайте
git-shellкато login shell за потребителяgit, за да предотвратите произволно изпълнение на команди. - Сдвоете сървъра на хранилището си с валиден SSL сертификат, ако излагате уеб интерфейс (Gitea, GitLab, cgit).
Пренаписване на историята
- Никога не пренаписвайте историята на клонове, споделени с други, без координиран план за миграция.
- След
git filter-repo, всички сътрудници трябва да клонират отново; незабавно актуализирайте URL адресите на отдалечените хранилища в CI/CD.
Възстановяване при авария
- Удължете изтичането на reflog на производствени сървъри (
gc.reflogExpire = 180). - Поддържайте вторичен bare клонинг на отделен хост като резервно копие; прост
git fetchот основния е достатъчен.
ЧЗВ
Каква е разликата между bare и non-bare Git хранилище?
Non-bare хранилището има работна директория, където файловете са извлечени, плюс поддиректория .git/, съдържаща хранилището на обекти. Bare хранилището съдържа само хранилището на обекти в корена си (без работна директория) и е правилният формат за споделен сървър, който получава изпращания.
Мога ли да възстановя commits след изпълнение на git reset --hard?
Да, стига commits да не са събрани от събирача на боклук. Изпълнете git reflog, за да намерите SHA на commit, който искате да възстановите, след това git checkout -b recovery-branch <SHA>, за да го прикачите към нов клон. Записите в reflog се запазват 90 дни по подразбиране.
Защо git push не прехвърля моите тагове?
По дизайн, git push прехвърля само commits, достъпни от референциите, които изрично изпращате. Таговете са отделни референции и трябва да бъдат изпратени с git push origin --tags (всички тагове) или git push origin <tagname> (конкретен таг).
Какво се случва с индекса по време на конфликт при сливане?
Индексът съхранява едновременно и трите версии на всеки конфликтен файл: етап 1 (общ предшественик/база), етап 2 (вашата версия) и етап 3 (тяхната версия). Нормалният git add записва само етап 0 (разрешен). Докато всички конфликти не бъдат разрешени и подготвени, git commit ще откаже да продължи.
Как се различават Git hooks между разгръщания от страна на клиента и от страна на сървъра?
Hooks от страна на клиента се изпълняват на машината на разработчика и не се прилагат централно — всеки разработчик може да ги заобиколи, като изтрие hook файла. Hooks от страна на сървъра (pre-receive, update, post-receive) се изпълняват на хостващия сървър и не могат да бъдат заобиколени от клиента, което ги прави правилната точка за прилагане на политики за защита на клонове, изисквания за преглед на код и CI/CD задействания.
