Structura Depozitului Git: Un Ghid Tehnic Complet
Git este un sistem distribuit de control al versiunilor care stochează istoricul proiectului ca un graf aciclic direcționat (DAG) de obiecte snapshot imuabile. Fiecare repository Git este construit din trei zone logice — directorul de lucru, indexul de staging și depozitul de obiecte din .git/ — plus un set de pointeri ușori (ramuri, etichete, remote-uri) care navighează prin acel istoric. Înțelegerea modului în care aceste straturi interacționează face diferența dintre utilizarea Git mecanic și utilizarea lui cu precizie chirurgicală.
Dacă îți găzduiești propriile repository-uri pe un VPS, stăpânirea acestei structuri interne îți permite să te recuperezi din dezastre, să proiectezi pipeline-uri CI/CD eficiente și să auditezi fiecare byte din istoricul proiectului tău fără a te baza pe o platformă terță.
Modelul cu Trei Zone: Cum Mișcă Git Datele
Înainte de a analiza componentele individuale, interiorizează modelul de flux de date care guvernează fiecare operațiune Git:
Working Directory --> Staging Area (Index) --> .git/ Object Store
(edit) (git add) (git commit)Modificările călătoresc de la stânga la dreapta când construiești un commit, și de la dreapta la stânga când restaurezi sau resetezi. Fiecare comandă Git este în esență o operațiune de citire sau scriere pe una sau mai multe dintre aceste zone.
Directorul de Lucru
Directorul de lucru (numit și arborele de lucru) este vizualizarea sistemului de fișiere a proiectului tău la o stare specifică de checkout. Când rulezi git clone sau git checkout, Git reconstruiește fișierele din obiectele comprimate din .git/objects/ și le scrie în acest director.
Fișierele din directorul de lucru există în una din patru stări:
- Netracked — Git nu a văzut niciodată acest fișier; există doar pe disc.
- Tracked, nemodificat — fișierul corespunde exact cu ultimul snapshot committed.
- Tracked, modificat — fișierul diferă de ultimul snapshot committed, dar nu a fost staged.
- Tracked, șters — fișierul a fost eliminat de pe disc, dar ștergerea nu a fost staged.
O nuanță critică care îi încurcă pe mulți dezvoltatori: directorul de lucru nu este o simplă copie a repository-ului. Git îl reconstruiește citind obiecte tree și decomprimând obiecte blob. Dacă .git/ este intact, poți regenera întotdeauna directorul de lucru de la zero — inversul nu este adevărat.
Sparse Checkout pentru Monorepo-uri Mari
Pe repository-uri cu zeci de mii de fișiere (comune în arhitecturile monorepo), poți limita căile pe care Git le materializează în directorul de lucru:
git sparse-checkout init --cone
git sparse-checkout set services/api services/authAcest lucru este de neprețuit pe un VPS cu I/O de disc limitat, deoarece Git sare peste decomprimarea blob-urilor pentru căile din afara conului.
Zona de Staging (Index)
Zona de staging, numită intern index, este un fișier binar localizat la .git/index. Acționează ca un commit următor propus — un snapshot mutabil care se află între directorul tău de lucru și depozitul permanent de obiecte.
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 HEADDe Ce Există Indexul
Indexul rezolvă o problemă pe care instrumentele VCS mai simple o ignoră: commit-urile parțiale. Poți fi modificat cinci fișiere, dar vrei doar trei dintre ele în următorul commit. Indexul îți permite să compui exact snapshot-ul pe care intenționezi să îl înregistrezi, independent de ce are editorul tău deschis.
Caz limită — coruperea indexului: Dacă un crash de sistem întrerupe un git add, fișierul index poate deveni corupt. Simptomele includ blocarea git status sau raportarea unor rezultate bizare. Recuperare:
rm .git/index
git resetGit reconstruiește indexul din HEAD fără a atinge directorul tău de lucru.
Indexul ca Registru de Conflicte de Merge
În timpul unui conflict de merge, indexul stochează trei versiuni ale fiecărui fișier conflictual simultan (etapele 1, 2 și 3 — baza, a noastră, a lor). De aceea git diff --cached nu arată nimic util în mijlocul unui conflict; ai nevoie de git diff --cc sau de un instrument de merge pentru a inspecta toate cele trei etape.
Directorul .git/: Anatomia Depozitului de Obiecte
Directorul .git/ este repository-ul. Orice altceva — directorul de lucru, clonele remote — este derivat din el. Ștergerea .git/ transformă un repository într-un director simplu fără istoric.
.git/
├── HEAD
├── config
├── description
├── index
├── COMMIT_EDITMSG
├── hooks/
├── info/
├── logs/
│ ├── HEAD
│ └── refs/
├── objects/
│ ├── info/
│ └── pack/
└── refs/
├── heads/
├── remotes/
└── tags/HEAD
HEAD este un fișier text simplu care conține fie o referință simbolică (care indică spre o ramură) fie un hash SHA-1 brut (starea HEAD detașat).
cat .git/HEAD
# ref: refs/heads/main <-- on a branch
# a3f1c9d... <-- detached HEADHEAD detașat nu este o stare de eroare — este intenționat când faci checkout la un tag sau la un commit specific pentru inspecție. Pericolul este să faci commit-uri în HEAD detașat: acele commit-uri sunt accesibile doar prin reflog până le atașezi la o ramură.
git checkout -b rescue-branch # Attach detached commits to a new branchconfig
Fișierul de configurare al repository-ului local. Suprascrie setările globale (~/.gitconfig) și de sistem (/etc/gitconfig). Intrări comune:
[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/mainPe un server găzduit propriu, vei edita frecvent acest fișier direct când rotești URL-urile remote sau configurezi uploadpack.allowReachableSHA1InWant pentru clone parțiale.
refs/
Directorul refs/ conține fișiere text simple, fiecare conținând un singur hash SHA-1. Aceștia sunt pointerii numiți care fac DAG-ul Git navigabil.
| Tip Ref | Cale | Descriere |
|---|---|---|
| Ramură locală | refs/heads/<name> | Indică spre commit-ul de vârf al unei ramuri |
| Ramură de urmărire remote | refs/remotes/<remote>/<name> | Cache local al vârfului ramurii unui remote |
| Tag ușor | refs/tags/<name> | Indică direct spre un obiect commit |
| Tag adnotat | refs/tags/<name> | Indică spre un obiect tag, care indică spre un commit |
| Stash | refs/stash | Indică spre commit-ul stash |
Pentru performanță, Git împachetează ref-urile în .git/packed-refs odată ce un repository acumulează multe dintre ele. Verifică întotdeauna ambele locații când scrii scripturi care lucrează cu ref-uri.
Obiecte Git: Nucleul Imuabil
Tot ce este stocat în .git/objects/ este adresat prin conținut: numele fișierului este hash-ul SHA-1 (sau SHA-256 în versiunile mai noi de Git) al conținutului obiectului. Acest lucru face Git în mod inerent rezistent la manipulare — schimbarea oricărui byte schimbă hash-ul, rupând lanțul.
Cele Patru Tipuri de Obiecte
| Tip Obiect | Ce Stochează | Indică Spre |
|---|---|---|
| Blob | Conținut brut al fișierului (fără nume de fișier, fără permisiuni) | Nimic |
| Tree | Listarea directorului: nume de fișiere, permisiuni, SHA-uri blob/tree | Blob-uri și alți arbori |
| Commit | Autor, committer, timestamp, mesaj, SHA-uri părinte | Un tree + zero sau mai multe commit-uri părinte |
| Tag | Identitatea tagger-ului, timestamp, mesaj, semnătură GPG | De obicei un commit |
Inspectarea Obiectelor Direct
# 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.pyObiecte Loose vs. Fișiere Pack
Inițial, fiecare obiect este stocat ca un fișier comprimat individual sub .git/objects/<2-char-prefix>/<38-char-suffix>. Acestea sunt obiecte loose. În timp, Git rulează git gc (colectare de gunoi) pentru a grupa obiectele loose în fișiere pack (.git/objects/pack/*.pack) cu un index corespunzător (.pack.idx).
Fișierele pack folosesc compresia delta — stocând diferența dintre obiecte similare în loc de copii complete. Un repository cu mii de fișiere text similare poate scădea dramatic în dimensiune după împachetare. Pe un VPS cu capacitate NVMe limitată, rularea git gc --aggressive pe repository-uri mari înainte de arhivare este o practică standard.
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 packIstoricul Commit-urilor: Graful Aciclic Direcționat
Fiecare obiect commit conține exact un pointer spre un obiect tree (snapshot-ul directorului rădăcină) și zero sau mai mulți pointeri spre commit-uri părinte. Aceasta formează un DAG unde:
- Zero părinți = commit-ul inițial (commit rădăcină)
- Un părinte = un commit normal
- Doi părinți = un commit de merge
- Trei sau mai mulți părinți = un merge octopus (rar, folosit pentru integrarea simultană a multor ramuri de funcționalitate)
git log --oneline --graph --all # Visualize the full DAG
git log --format="%H %P" # Show each commit's SHA and parent SHA(s)Imuabilitatea Commit-urilor și Rescrierea Istoricului
Deoarece SHA-ul unui commit este derivat din conținutul său (inclusiv SHA-urile părinților), orice rescriere creează un nou commit cu un nou SHA. Operațiuni precum git rebase, git commit --amend și git filter-repo nu modifică istoricul — creează un istoric paralel. Commit-urile vechi rămân în depozitul de obiecte până sunt colectate de garbage collector.
De aceea, forțarea push-ului istoricului rescris pe o ramură partajată este distructivă: ramurile locale ale colaboratorilor indică în continuare spre lanțul vechi de commit-uri.
Ramuri: Pointeri Ușori
O ramură nu este altceva decât un fișier de 41 de bytes care conține un hash SHA-1. Crearea unei ramuri este instantanee indiferent de dimensiunea repository-ului, deoarece Git scrie doar un singur fișier mic.
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)Structura Internă a Ramurilor
cat .git/refs/heads/main
# a3f1c9d8e2b1f4c7d9e0a1b2c3d4e5f6a7b8c9d0Când faci un commit pe o ramură, Git scrie noul SHA al commit-ului în acest fișier. Aceasta este totalitatea „avansării unui pointer de ramură.”
Ramuri de Urmărire și Configurarea Upstream
O relație de urmărire îi spune lui Git cu ce ramură remote ar trebui să compare o ramură locală pentru raportarea divergenței git status și comportamentul git pull.
git branch --set-upstream-to=origin/main main
git branch -vv # Show tracking relationships and ahead/behind countsTag-uri: Markeri Permanenți în Istoric
Tag-urile marchează commit-uri specifice ca semnificative — de obicei lansări de software. Spre deosebire de ramuri, tag-urile nu sunt mutate de commit-uri noi.
| Caracteristică | Tag Ușor | Tag Adnotat |
|---|---|---|
| Stocare | Un fișier ref care indică spre un commit | Un obiect tag în depozitul de obiecte |
| Metadate | Niciuna | Numele tagger-ului, email, dată, mesaj |
| Semnare GPG | Nu este posibil | Suportat prin git tag -s |
| Recomandat pentru lansări | Nu | Da |
Transfer cu git push --tags | Da | Da |
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 tagCapcană critică: git push nu trimite tag-urile implicit. Echipele uită frecvent acest lucru și publică note de lansare care fac referire la un tag ce nu există pe remote.
Remote-uri: Colaborare Distribuită
Un remote este un URL cu nume stocat în .git/config. Ramurile de urmărire remote (sub refs/remotes/) sunt snapshot-uri locale read-only ale ramurilor remote-ului, actualizate doar când faci fetch explicit.
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 trackingMultiple Remote-uri
Un singur repository poate urmări mai multe remote-uri — comun când menții un fork alături de upstream:
git remote add upstream git@github.com:original/repo.git
git fetch upstream
git merge upstream/mainCând găzduiești propriile repository-uri bare pe un server dedicat pentru echipa ta, fiecare dezvoltator adaugă serverul ca remote și folosește autentificarea prin cheie SSH pentru acces push.
Hook-uri: Automatizare la Fiecare Eveniment Git
Hook-urile sunt scripturi executabile în .git/hooks/. Git le apelează la puncte definite în fluxul de lucru. Ele nu sunt transferate prin git clone sau git push — fiecare dezvoltator (sau server) trebuie să le instaleze independent. Aceasta este o sursă frecventă de confuzie în mediile de echipă.
Hook-uri pe Partea Clientului
| Hook | Declanșator | Utilizare Comună |
|---|---|---|
pre-commit | Înainte de promptul mesajului de commit | Linting, scanare secrete, execuție teste |
prepare-commit-msg | După crearea mesajului implicit | Injectarea numelui ramurii în mesaj |
commit-msg | După ce utilizatorul scrie mesajul | Impunerea formatului conventional commit |
post-commit | După înregistrarea commit-ului | Notificări locale |
pre-push | Înainte de executarea git push | Rularea suitei complete de teste |
pre-rebase | Înainte de începerea rebase | Prevenirea rebase-ului pe ramuri publicate |
Hook-uri pe Partea Serverului
| Hook | Declanșator | Utilizare Comună |
|---|---|---|
pre-receive | Înainte de actualizarea ref-urilor | Impunerea protecției ramurilor, respingerea force-push |
update | Per-ref în timpul primirii | Impunerea politicii per-ramură |
post-receive | După actualizarea tuturor ref-urilor | Declanșarea CI/CD, trimiterea notificărilor |
Exemplu: Hook Pre-commit pentru Detectarea Secretelor
#!/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 0Fă-l executabil:
chmod +x .git/hooks/pre-commitPentru distribuirea hook-urilor la nivelul echipei, folosește un instrument precum Husky (proiecte Node.js) sau stochează hook-urile într-un director hooks/ la rădăcina repository-ului și creează symlink-uri către ele în timpul configurării proiectului.
Reflog: Plasa de Siguranță
Reflog-ul înregistrează fiecare mișcare a pointerilor HEAD și ai ramurilor, inclusiv operațiunile care par să distrugă istoricul (resetări hard, rebase-uri, commit-uri amendate). Este stocat în .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 entryIntrările din reflog expiră după 90 de zile implicit (gc.reflogExpire). Pe un server de producție, ia în considerare extinderea acestei perioade:
git config gc.reflogExpire 180
git config gc.reflogExpireUnreachable 30Repository-uri Bare: Găzduire pe Partea Serverului
Un repository bare nu are director de lucru. Conține doar conținutul .git/ la nivelul rădăcină. Repository-urile bare sunt formatul corect pentru găzduirea centralizată — acceptă push-uri fără complicațiile unei ramuri checked-out.
git init --bare /srv/repos/myproject.gitCând faci push pe GitHub, GitLab sau un server Git găzduit propriu, faci push pe un repository bare. Dacă îți găzduiești propriul server Git pe un VPS cu cPanel sau un VPS Linux simplu, repository-urile bare sub /srv/repos/ cu acces SSH sunt arhitectura standard.
Inițializarea unui Repository Bare Partajat
# 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 mainStocarea Obiectelor Git: Dimensiune, Integritate și Întreținere
Verificarea Sănătății Repository-ului
git fsck --full # Verify object integrity (finds dangling and corrupt objects)
git fsck --lost-found # Write dangling objects to .git/lost-found/Găsirea și Eliminarea Obiectelor Mari
Fișierele binare mari comise accidental sunt o cauză comună a repository-urilor supradimensionate. Identifică-le înainte de a folosi git filter-repo pentru a le elimina:
# 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-pathsDupă filtrare, toți colaboratorii trebuie să recloneze — repository-urile lor locale fac referire la hash-uri SHA care nu mai există în istoricul rescris.
Comparație: Concepte Cheie ale Repository-ului Git
| Concept | Tip | Mutabil | Stocat În | Transferat prin Push/Fetch |
|---|---|---|---|---|
| Blob | Obiect | Nu | .git/objects/ | Da (când este accesibil) |
| Tree | Obiect | Nu | .git/objects/ | Da (când este accesibil) |
| Commit | Obiect | Nu | .git/objects/ | Da (când este accesibil) |
| Tag Adnotat | Obiect | Nu | .git/objects/ | Doar cu --tags |
| Ramură | Ref | Da | .git/refs/heads/ | Da |
| Ramură de urmărire remote | Ref | Da (la fetch) | .git/refs/remotes/ | Nu (cache local) |
| Tag Ușor | Ref | Nu | .git/refs/tags/ | Doar cu --tags |
| HEAD | Symref/hash | Da | .git/HEAD | Nu |
| Index | Fișier binar | Da | .git/index | Nu |
| Hook-uri | Scripturi | Da | .git/hooks/ | Nu |
| Reflog | Jurnal | Da (expiră automat) | .git/logs/ | Nu |
Matricea de Decizie Practică și Concluzii Cheie
Folosește această listă de verificare când configurezi sau auditezi un repository Git pe infrastructura ta:
Inițializarea repository-ului
- Folosește
git init --bare --shared=grouppentru orice repository care va primi push-uri de la mai mulți utilizatori. - Stochează repository-urile bare în afara directoarelor accesibile web (niciodată sub
/var/www/).
Sănătatea depozitului de obiecte
- Rulează
git fsck --fulldupă orice incident de stocare sau eroare de sistem de fișiere. - Programează
git gcperiodic pe repository-uri cu viață lungă; automatizează-l prin cron pe serverul tău. - Monitorizează dimensiunea fișierelor pack cu
git count-objects -vH; investighează dacă numărul obiectelor loose depășește 1.000.
Igiena ramurilor și ref-urilor
- Șterge ramurile merged prompt; ref-urile vechi se acumulează și încetinesc operațiunile
git fetch --prune. - Folosește
git fetch --pruneîn pipeline-urile CI pentru a evita acțiunile pe ramurile remote șterse.
Implementarea hook-urilor
- Nu te baza niciodată pe
.git/hooks/pentru politici la nivelul echipei — hook-urile nu sunt clonate. Folosește în schimb hook-uripre-receivepe partea serverului sau o poartă CI. - Auditează hook-urile de pe partea serverului după fiecare upgrade al serverului Git; căile interpretorului de hook-uri se pot schimba.
Securitate pe serverele găzduite propriu
- Restricționează accesul SSH la utilizatorul
gitcu comenzi forțate (command=înauthorized_keys). - Folosește
git-shellca shell de autentificare pentru utilizatorulgitpentru a preveni execuția de comenzi arbitrare. - Asociază serverul tău de repository cu un certificat SSL valid dacă expui orice interfață web (Gitea, GitLab, cgit).
Rescrierea istoricului
- Nu rescrie niciodată istoricul pe ramuri partajate cu alții fără un plan de migrare coordonat.
- După
git filter-repo, toți colaboratorii trebuie să recloneze; actualizează imediat URL-urile remote CI/CD.
Recuperare în caz de dezastru
- Extinde expirarea reflog pe serverele de producție (
gc.reflogExpire = 180). - Păstrează o clonă bare secundară pe un host separat ca backup; un simplu
git fetchde la cel primar este suficient.
Întrebări Frecvente
Care este diferența dintre un repository Git bare și unul non-bare?
Un repository non-bare are un director de lucru unde fișierele sunt checked out, plus un subdirector .git/ care conține depozitul de obiecte. Un repository bare conține doar depozitul de obiecte la rădăcina sa (fără director de lucru) și este formatul corect pentru un server partajat care primește push-uri.
Pot recupera commit-urile după rularea git reset --hard?
Da, atât timp cât commit-urile nu au fost colectate de garbage collector. Rulează git reflog pentru a găsi SHA-ul commit-ului pe care vrei să îl recuperezi, apoi git checkout -b recovery-branch <SHA> pentru a-l atașa la o nouă ramură. Intrările din reflog sunt păstrate timp de 90 de zile implicit.
De ce git push nu transferă tag-urile mele?
Prin design, git push transferă doar commit-urile accesibile din ref-urile pe care le trimiți explicit. Tag-urile sunt ref-uri separate și trebuie trimise cu git push origin --tags (toate tag-urile) sau git push origin <tagname> (un tag specific).
Ce se întâmplă cu indexul în timpul unui conflict de merge?
Indexul stochează toate cele trei versiuni ale fiecărui fișier conflictual simultan: etapa 1 (ancestorul comun/baza), etapa 2 (versiunea ta) și etapa 3 (versiunea lor). git add normal scrie doar etapa 0 (rezolvată). Până când toate conflictele sunt rezolvate și staged, git commit va refuza să continue.
Cum diferă hook-urile Git între implementările pe client și pe server?
Hook-urile pe partea clientului rulează pe mașina dezvoltatorului și nu sunt impuse central — orice dezvoltator le poate ocoli ștergând fișierul hook. Hook-urile pe partea serverului (pre-receive, update, post-receive) rulează pe serverul de găzduire și nu pot fi ocolite de client, făcându-le punctul corect de impunere pentru politicile de protecție a ramurilor, cerințele de code review și declanșatoarele CI/CD.
la toate serviciile de găzduire