Git Object Modeli
Giriş — Git'in Motorunun Kapağını Açıyoruz
Şimdiye kadar Git'i bir araç olarak kullandık: git add, git commit, git push... Ama hiç "Git bunu nasıl yapıyor?" diye merak ettin mi? Commit dediğimiz şey aslında ne? Dosyalar nasıl saklanıyor? Branch gerçekten ne?
Bu bölümde Git'in iç yapısını (internals) keşfedeceksin. Bu bilgi sadece merak gidermek için değil — sorun çıktığında ne olduğunu gerçekten anlamak için kritik. Arabanın motorunu tanıyan sürücü, garip bir ses duyduğunda panik yapmaz — neye bakacağını bilir.
Bu derste Git'in dört temel nesne türünü öğreneceksin: blob, tree, commit ve tag. Ve Git'in aslında bir content-addressable filesystem (içerik adreslenebilir dosya sistemi) olduğunu anlayacaksın.
Content-Addressable Storage
Analoji — Kütüphanedeki Parmak İzi Sistemi
Normal bir dosya sistemi düşün: dosyayı adıyla bulursun. documents/report.txt — ismine göre ararsın, bulursun. Ama dosyanın adını değiştirirsen? Aynı dosya, farklı isim — sistem artık onu bulamaz.
Git farklı çalışır. Git'te her şey içeriğine göre adreslenir. Dosyanın adı ne olursa olsun, içeriği aynıysa aynı adrese sahip olur. İçeriğin parmak izi (hash) adresi belirler.
Normal dosya sistemi: İsime göre adresleme
documents/report.txt → Dosya içeriği
Git: İçeriğe göre adresleme
SHA-1 hash → Dosya içeriği
abc1234... → "Hello World\n"
Aynı içerik = Aynı hash = Aynı nesne
(Dosya adı, konum önemsiz)SHA-1 Hash
Git, her nesnenin içeriğinden SHA-1 hash üretir. Bu hash:
40 karakter hexadecimal (160 bit)
Deterministik: aynı içerik her zaman aynı hash'i üretir
Çarpışma olasılığı astronomik düzeyde düşük
İçerik değişirse hash tamamen farklı olur
# Git'in hash hesaplama yöntemi:
echo -n "blob 12\0Hello World\n" | sha1sum
# SHA-1("blob <boyut>\0<içerik>") = hash
# Git'le hash hesapla:
echo "Hello World" | git hash-object --stdin
# 557db03de997c86a4a028e1ebd3a1ceb225be238💡 Bilgi: Git, SHA-1'den SHA-256'ya geçiş sürecinde. SHA-1'de teorik çarpışma saldırıları gösterildi (2017, Google SHAttered). Git'in yeni sürümleri SHA-256 desteği ekliyor. Ama mevcut ekosistemde hâlâ SHA-1 kullanılıyor.
Dört Nesne Türü
Git'in tüm veri modeli dört nesne türüne dayanır:
┌─────────────────────────────────────────────────────────┐
│ GIT NESNE TÜRLERİ │
├─────────────────────────────────────────────────────────┤
│ │
│ 📄 blob (Binary Large Object) │
│ Dosya içeriği (sadece veri, isim yok) │
│ │
│ 📁 tree │
│ Dizin yapısı (dosya adları + blob referansları) │
│ │
│ 📝 commit │
│ Anlık görüntü (tree + yazar + mesaj + parent) │
│ │
│ 🏷️ tag (annotated) │
│ Etiketli referans (commit + tagger + mesaj) │
│ │
└─────────────────────────────────────────────────────────┘İlişki diyagramı:
tag ──────► commit ──────► tree ──────► blob
"v1.0" "abc123" "/" "README.md içeriği"
│ │
│ ├──► blob "src/app.js içeriği"
│ │
│ └──► tree "src/"
│ │
▼ └──► blob "src/index.js içeriği"
commit (parent)
"def456"Blob — Dosya İçeriği
Blob, Git'in en temel nesnesidir. Bir dosyanın ham içeriğini saklar. Dosya adı, izinler, konum bilgisi blob'da yok — sadece saf veri.
Blob Oluşturma ve İnceleme
# Bir dosya oluştur ve Git'e ekle
echo "Hello Git Internals" > hello.txt
git add hello.txt
# Git bu dosyanın blob nesnesini oluşturdu
# Hash'ini öğren:
git hash-object hello.txt
# d3b5a4d7f8e2c1a9b0f6e8d7c5b4a3d2e1f0a9b8
# Blob'u incele:
git cat-file -t d3b5a4d # Tür
# blob
git cat-file -p d3b5a4d # İçerik
# Hello Git Internals
git cat-file -s d3b5a4d # Boyut (byte)
# 20Blob Depolama Mekanizması
# Git, blob'u nereye sakladı?
# Hash'in ilk 2 karakteri = dizin adı
# Geri kalanı = dosya adı
ls .git/objects/d3/b5a4d7f8e2c1a9b0f6e8d7c5b4a3d2e1f0a9b8
# Dosya mevcut!
# Bu dosya zlib ile sıkıştırılmış:
# header: "blob 20\0"
# content: "Hello Git Internals\n".git/objects/
├── d3/
│ └── b5a4d7f8e2c1a9b0f6e8d7c5b4a3d2e1f0a9b8 ← blob
├── a1/
│ └── ...
└── info/
└── packsAynı İçerik = Aynı Blob
# İki farklı dosya, aynı içerik
echo "Hello" > file1.txt
echo "Hello" > file2.txt
git hash-object file1.txt
# ce013625030ba8dba906f756967f9e9ca394464a
git hash-object file2.txt
# ce013625030ba8dba906f756967f9e9ca394464a ← Aynı hash!
# Git bu içeriği SADECE BİR KEZ saklar!
# Bu, Git'in disk alanını verimli kullanmasının sırrı💡 İpucu: Bu deduplication (yinelenen verileri kaldırma) mekanizması otomatiktir. 100 dosyanın aynı lisans metnini içerdiğini düşün — Git tek bir blob saklar, 100 tree entry'si ona işaret eder.
Tree — Dizin Yapısı
Tree nesnesi, bir dizinin (folder) anlık görüntüsünü temsil eder. İçeriğinde:
Dosya modları (izinler)
Nesne türleri (blob veya tree)
Hash referansları
Dosya/dizin adları
Tree Yapısı
# Proje yapısı:
# my-project/
# ├── README.md
# ├── src/
# │ ├── index.js
# │ └── utils.js
# └── package.json
# Root tree'yi incele:
git cat-file -p HEAD^{tree}
# 100644 blob abc1234... README.md
# 040000 tree def5678... src
# 100644 blob ghi9012... package.jsonÇıktının açıklaması:
100644 blob abc1234... README.md
│ │ │ │
│ │ │ └── Dosya/dizin adı
│ │ └────────────── Nesne hash'i
│ └────────────────────── Nesne türü (blob=dosya, tree=dizin)
└───────────────────────────── Dosya modu (izinler)Dosya Modları
100644 → Normal dosya (rw-r--r--)
100755 → Çalıştırılabilir dosya (rwxr-xr-x)
120000 → Symbolic link
040000 → Dizin (tree)
160000 → Submodule (gitlink)Alt Tree'yi İnceleme
# src/ dizininin tree'si:
git cat-file -p def5678
# 100644 blob jkl3456... index.js
# 100644 blob mno7890... utils.jsTree Hiyerarşisi Diyagramı
Root Tree (abc...)
├── 100644 blob (def...) README.md → "# My Project\n..."
├── 040000 tree (ghi...) src/
│ ├── 100644 blob (jkl...) index.js → "console.log(...);\n"
│ └── 100644 blob (mno...) utils.js → "function helper()...\n"
└── 100644 blob (pqr...) package.json → "{ \"name\": ... }\n"Tree'nin Önemli Özelliği
Tree, anlık bir snapshot (anlık görüntü) tutar. Dosya değişmemişse, yeni tree eski blob'a işaret eder — kopya oluşturmaz:
Commit 1 Tree: Commit 2 Tree:
├── blob-A README.md ├── blob-A README.md (aynı!)
├── blob-B index.js ├── blob-C index.js (değişti!)
└── blob-D package.json └── blob-D package.json (aynı!)
Sadece index.js değişti → sadece 1 yeni blob oluştu.
README.md ve package.json'un blob'ları tekrar kullanıldı.Commit — Anlık Görüntü
Commit nesnesi, projenin belirli bir andaki tam durumunu kaydeder. İçeriğinde:
Tree: Kök dizinin tree hash'i (projenin tüm dosya yapısı)
Parent: Önceki commit(ler)in hash'i
Author: Değişikliği yapan kişi + tarih
Committer: Commit'i oluşturan kişi + tarih
Message: Commit mesajı
Commit Anatomisi
git cat-file -p HEADtree abc1234def5678ghi9012jkl3456mno78901
parent def5678ghi9012jkl3456mno78901pqr12345
author Ahmet <ahmet@email.com> 1709305800 +0300
committer Ahmet <ahmet@email.com> 1709305800 +0300
feat: Add search functionality
Implemented full-text search using Elasticsearch.
- Added search endpoint
- Added search indexing
- Added search UI componentAuthor vs Committer
Author: Kodu yazan kişi + tarih
Committer: Commit'i Git'e kaydeden kişi + tarih
Genellikle aynı kişi. Farklı olma durumları:
- cherry-pick: Author orijinal yazar, committer cherry-pick yapan
- rebase: Author orijinal yazar, committer rebase yapan
- git commit --amend: Committer tarihi değişir, author aynı kalır
- git format-patch + git am: Başkasının patch'ini uygulamaCommit Zinciri
Her commit parent'ına referans verir. Bu, bir linked list oluşturur:
Commit C → Parent: Commit B → Parent: Commit A → Parent: (yok, ilk commit)
git cat-file -p HEAD
# tree ...
# parent abc1234 ← Bir önceki commit
git cat-file -p abc1234
# tree ...
# parent def5678 ← Ondan önceki commit
git cat-file -p def5678
# tree ...
# (parent yok — bu ilk commit!)Merge Commit — İki Parent
Merge commit'in iki parent'ı vardır:
git cat-file -p MERGE_COMMIT_HASH
# tree abc1234...
# parent def5678... ← main'deki önceki commit
# parent ghi9012... ← feature branch'teki son commit
# author ...
# committer ...
#
# Merge branch 'feature/search' into main parent1 parent2
│ │
▼ ▼
main: ──A───B───C───M─── (M = merge commit, 2 parent)
\ /
feature: D───ETag Nesnesi — Etiketli Referans
Annotated tag, Git'te ayrı bir nesne olarak saklanır:
git cat-file -p v1.0.0object abc1234def5678ghi9012jkl3456mno78901
type commit
tag v1.0.0
tagger Ahmet <ahmet@email.com> 1709305800 +0300
Release v1.0.0 — İlk Stabil SürümTag nesnesi:
object → commit hash'i (tag'in işaret ettiği commit)
type → nesne türü (genellikle "commit")
tag → tag adı
tagger → oluşturan kişi + tarih
mesaj → tag açıklaması💡 İpucu: Lightweight tag'ler Git nesnesi oluşturmaz — sadece bir ref dosyasıdır. Annotated tag'ler ise ayrı bir Git nesnesi olarak saklanır ve bu yüzden daha zengin bilgi içerir.
git cat-file — Git'in Röntgen Cihazı
git cat-file komutu, Git nesnelerini incelemek için kullanılır:
# Nesne türünü öğren
git cat-file -t abc1234
# blob / tree / commit / tag
# Nesne içeriğini göster
git cat-file -p abc1234
# (içerik türe göre değişir)
# Nesne boyutunu göster
git cat-file -s abc1234
# 42 (byte)
# Nesnenin var olup olmadığını kontrol et
git cat-file -e abc1234
# (çıktı yok = var, hata = yok)git cat-file ile Proje Keşfi
# Tüm nesne zincirini takip et:
# 1. HEAD'in işaret ettiği commit'i bul
git rev-parse HEAD
# abc1234...
# 2. Commit'in tree'sini bul
git cat-file -p abc1234 | head -1
# tree def5678...
# 3. Tree'nin içeriğini gör (dosya listesi)
git cat-file -p def5678
# 100644 blob ghi9012... README.md
# 040000 tree jkl3456... src/
# 100644 blob mno7890... package.json
# 4. Bir dosyanın içeriğini oku
git cat-file -p ghi9012
# # My Awesome Project
# ...Tüm Nesne Akışı
# Bir dosyanın commit'ten blob'a kadar izlenmesi:
HEAD
│
▼
refs/heads/main → commit (abc1234)
│
├── tree: def5678 (kök dizin)
│ │
│ ├── blob: ghi9012 (README.md)
│ ├── tree: jkl3456 (src/)
│ │ ├── blob: mno7890 (index.js)
│ │ └── blob: pqr1234 (utils.js)
│ └── blob: stu5678 (package.json)
│
├── parent: commit (vwx9012)
├── author: Ahmet <ahmet@email.com>
└── message: "feat: Add search".git Dizini Anatomisi
ls -la .git/.git/
├── HEAD # Şu anki branch'i gösteren symbolic ref
├── config # Repo'ya özel yapılandırma
├── description # GitWeb için açıklama (nadiren kullanılır)
├── index # Staging area (binary format)
├── packed-refs # Sıkıştırılmış referanslar
│
├── hooks/ # Git hook scriptleri
│ ├── pre-commit.sample
│ ├── commit-msg.sample
│ └── ...
│
├── info/ # Ek bilgiler
│ └── exclude # Repo-local gitignore
│
├── logs/ # Reflog kayıtları
│ ├── HEAD
│ └── refs/
│ └── heads/
│ └── main
│
├── objects/ # TÜM Git nesneleri (blob, tree, commit, tag)
│ ├── d3/ # Hash'in ilk 2 karakteri = dizin adı
│ │ └── b5a4d7... # Geri kalanı = dosya adı
│ ├── a1/
│ │ └── ...
│ ├── info/
│ └── pack/ # Sıkıştırılmış nesne paketleri
│ ├── pack-xxx.idx
│ └── pack-xxx.pack
│
└── refs/ # Branch ve tag referansları
├── heads/ # Branch'ler
│ ├── main # main branch'in son commit hash'i
│ └── feature/
├── tags/ # Tag'ler
│ └── v1.0.0
└── remotes/ # Remote tracking branch'ler
└── origin/
├── main
└── feature/Kendi Nesneni Oluşturma
Git'in plumbing (altyapı) komutlarıyla sıfırdan nesne oluşturalım:
# 1. Boş bir repo oluştur
mkdir git-internals-demo && cd git-internals-demo
git init
# 2. Elle blob oluştur
echo "Merhaba Git Internals!" | git hash-object -w --stdin
# abc1234... (-w = write, nesneyi .git/objects'e yaz)
# Blob oluştu mu?
git cat-file -p abc1234
# Merhaba Git Internals!
# 3. Tree oluştur (blob'u bir dosya adıyla eşle)
# Önce staging area'ya ekle:
git update-index --add --cacheinfo 100644 abc1234 merhaba.txt
# Tree'yi yaz:
git write-tree
# def5678...
# Tree'yi incele:
git cat-file -p def5678
# 100644 blob abc1234... merhaba.txt
# 4. Commit oluştur
echo "İlk commit (elle oluşturuldu)" | git commit-tree def5678
# ghi9012...
# Commit'i incele:
git cat-file -p ghi9012
# tree def5678...
# author Ahmet <ahmet@email.com> 1709305800 +0300
# committer Ahmet <ahmet@email.com> 1709305800 +0300
#
# İlk commit (elle oluşturuldu)
# 5. Branch'i bu commit'e yönlendir
git update-ref refs/heads/main ghi9012
# 6. Çalıştı mı?
git log --oneline
# ghi9012 İlk commit (elle oluşturuldu)Bu egzersiz, git add + git commit komutlarının arka planda ne yaptığını gösterir:
git add file.txt
= git hash-object -w file.txt (blob oluştur)
+ git update-index (staging area güncelle)
git commit -m "mesaj"
= git write-tree (tree oluştur)
+ git commit-tree (commit oluştur)
+ git update-ref (branch'i güncelle)Hash Çarpışması ve Güvenlik
SHA-1 Ne Kadar Güvenli?
SHA-1 hash: 160 bit = 2^160 olası değer
= 1.461.501.637.330.902.918.203.684.832.716.283.019.655.932.542.976
Çarpışma olasılığı (birthday paradox ile):
2^80 ≈ 1.2 × 10^24 nesne sonrasında %50 olasılık
Perspektif:
Saniyede 1 milyon nesne oluştursan:
Çarpışma olasılığının %50 olması için: ~38 milyon yıl
Ama: 2017'de Google, SHA-1'de kasıtlı çarpışma gösterdi (SHAttered)
→ Git SHA-256 geçişi başladı (henüz yaygın değil)💡 İpucu: Pratikte SHA-1 çarpışması Git'te gerçek bir sorun değil. Kasıtlı saldırılar için bile çok büyük hesaplama gücü gerekiyor. Git, ek korumalar da uyguluyor. Ama uzun vadede SHA-256'ya geçiş olacak.
Porcelain vs Plumbing Komutları
Git komutları iki kategoriye ayrılır:
Porcelain (Üst Seviye) — Kullanıcının günlük kullandığı:
git add, git commit, git push, git pull, git log,
git branch, git merge, git rebase, git stash...
Plumbing (Alt Seviye) — Git'in iç mekanizması:
git hash-object, git cat-file, git update-index,
git write-tree, git commit-tree, git update-ref,
git rev-parse, git ls-tree, git ls-files...Porcelain = Lavabonun dışı (güzel, kullanıcı dostu)
Plumbing = Lavabonun altındaki borular (iç mekanizma)
Günlük işlerinde porcelain yeterli.
Sorun çıktığında, script yazarken veya Git'i anlamak istediğinde
plumbing'e dalarsın.Pratik: Nesne Grafiğini Görselleştirme
# Proje oluştur
mkdir object-demo && cd object-demo
git init
# İlk commit
echo "# My Project" > README.md
mkdir src
echo "console.log('hello');" > src/index.js
git add .
git commit -m "Initial commit"
# İkinci commit
echo "console.log('world');" >> src/index.js
git add .
git commit -m "Update index.js"
# Nesne grafiğini çiz:
echo "=== Son commit ==="
COMMIT=$(git rev-parse HEAD)
echo "Commit: $COMMIT"
echo ""
echo "=== Commit içeriği ==="
git cat-file -p $COMMIT
echo ""
TREE=$(git cat-file -p $COMMIT | head -1 | awk '{print $2}')
echo "=== Root tree: $TREE ==="
git cat-file -p $TREE
echo ""
echo "=== Tüm nesneler ==="
git rev-list --objects --allÇıktı şuna benzer:
=== Son commit ===
Commit: abc1234...
=== Commit içeriği ===
tree def5678...
parent ghi9012...
author Ahmet <ahmet@email.com> 1709305800 +0300
committer Ahmet <ahmet@email.com> 1709305800 +0300
Update index.js
=== Root tree: def5678... ===
100644 blob jkl3456... README.md
040000 tree mno7890... src
=== Tüm nesneler ===
abc1234... (commit 2)
def5678... (tree: root, commit 2)
ghi9012... (commit 1)
jkl3456... (blob: README.md — değişmedi, paylaşıldı!)
mno7890... (tree: src/, commit 2)
pqr1234... (blob: index.js, commit 2)
stu5678... (tree: root, commit 1)
vwx9012... (tree: src/, commit 1)
yza3456... (blob: index.js, commit 1)Commit 2 (abc1234)
├── tree: def5678 (root)
│ ├── blob: jkl3456 (README.md) ◄── Paylaşılan nesne!
│ └── tree: mno7890 (src/)
│ └── blob: pqr1234 (index.js) ← Yeni blob
└── parent: Commit 1 (ghi9012)
└── tree: stu5678 (root)
├── blob: jkl3456 (README.md) ◄── Aynı blob!
└── tree: vwx9012 (src/)
└── blob: yza3456 (index.js) ← Eski blobREADME.md değişmediği için aynı blob her iki commit'in tree'sinde de kullanılıyor. Git akıllıca paylaşıyor!
Özet
Bu derste Git'in iç yapısını derinlemesine öğrendik:
Git bir content-addressable filesystem'dır — her nesne SHA-1 hash'iyle adreslenir, aynı içerik = aynı hash = tek kopya
Blob dosya içeriğini saklar (ad/konum bilgisi yok), tree dizin yapısını temsil eder (mod + tür + hash + ad), commit projenin anlık görüntüsüdür (tree + parent + author + mesaj), tag etiketli referanstır
`git cat-file` Git'in röntgen cihazıdır —
-ttür,-piçerik,-sboyut gösterirPorcelain komutları (add, commit, push) kullanıcı dostu üst katmandır; plumbing komutları (hash-object, cat-file, write-tree) altyapıdır
.git/objects/dizininde tüm nesneler hash'lerinin ilk 2 karakteriyle klasörlenir ve zlib ile sıkıştırılırGit'in veri modeli aslında bir DAG (Directed Acyclic Graph — Yönlü Çevrimsiz Çizge) oluşturur
Bir sonraki derste bu nesne modelinin üzerindeki referans katmanını göreceğiz: refs, HEAD ve reflog.
AI Asistan
Sorularını yanıtlamaya hazır