← Kursa Dön
📄 Text · 30 min

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)
# 20

Blob 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/
    └── packs

Aynı İç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.js

Tree 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 HEAD
tree 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 component

Author 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 uygulama

Commit 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───E

Tag Nesnesi — Etiketli Referans

Annotated tag, Git'te ayrı bir nesne olarak saklanır:

git cat-file -p v1.0.0
object abc1234def5678ghi9012jkl3456mno78901
type commit
tag v1.0.0
tagger Ahmet <ahmet@email.com> 1709305800 +0300

Release v1.0.0 — İlk Stabil Sürüm
Tag 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 blob

README.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 — -t tür, -p içerik, -s boyut gösterir

  • Porcelain 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ır

  • Git'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.