Mert Özen Her Satırda Daha İleri
DevOps Git & GitHub — #04

Git Commit'in İçi: Snapshot, Object'ler ve Hash

Mert Özen 6 Kas 2026 10 dk 1 görüntülenme
Git Commit'in İçi: Snapshot, Object'ler ve Hash

Commit'in diff değil snapshot olduğunu git cat-file ile commit, tree ve blob object'lerini açarak görüyor, hash'i shasum ile elle hesaplıyor, değişmeyen dosyanın neden ikinci kez saklanmadığını buluyoruz. İyi bir commit mesajının kurallarını ve git commit --amend'in neden yeni bir commit ürettiğini öğreniyoruz.

Sürüm notu: Bu yazıdaki komutlar Git 2.54 ile denenmiştir.

Önce Tahmin Et

Bir önceki yazıda bookstore repo'suna iki commit attık. İkinci commit'te src/books.js'e hiç dokunmadık. Sence Git, ikinci commit için bu dosyayı ikinci kez saklıyor mu? Bir de şunu düşün: son commit'in mesajındaki tek bir harfi düzeltirsen commit'in hash'i aynı kalır mı?

İki sorunun cevabı da commit'in içinde ne olduğuna bağlı. Bu yazıda .git/objects klasörünün kapağını açıp commit'i parçalarına ayıracağız. Sonra iyi bir commit mesajının nasıl yazıldığını ve son commit'i --amend ile nasıl düzelteceğini göreceğiz.

Diff Değil, Snapshot

Pek çok sürüm kontrolü aracı geçmişi değişiklik listesi olarak düşünür: ilk sürüm, üstüne şu satırlar eklendi, şu satırlar silindi. Git farklı düşünür. Her commit, projenin o anki hâlinin tam bir fotoğrafıdır, yani bir snapshot. Commit'te "README'ye bir satır eklendi" yazmaz; "README'nin bu andaki içeriği şu" yazar.

Bu her commit'te bütün dosyaların kopyalandığı anlamına gelmiyor. Değişmeyen bir dosya için Git yeni bir kopya oluşturmaz, bir önceki commit'teki kopyayı gösterir. Fotoğraf albümü benzetmesiyle: her sayfada sınıfın tamamı var, ama aynı kalan yüzler için yeni baskı yapılmamış, aynı fotoğraf tekrar yapıştırılmış. git log -p ya da git diff ile gördüğün değişiklikler ise saklanan bir şey değil; Git onları iki snapshot'ı karşılaştırarak o anda hesaplar.

Commit Object'ini Açmak

Git'in sakladığı her şey bir object'tir ve her object'in bir hash'i vardır. git cat-file bu object'leri okumak için kullanılan alt seviye bir komut: -t tipini, -p içeriğini gösterir.

$ git log --oneline
fae4454 (HEAD -> main) Add shopping cart
29f27c8 Add README and book list
$ git cat-file -t HEAD
commit
$ git cat-file -p HEAD
tree c93a32e8f9f704b6eda3f36f76f4e561e3371ff9
parent 29f27c8b6b3bb1a9abea102641c7bfb23f90d575
author Ada Lovelace <ada@example.com> 1794034860 +0300
committer Ada Lovelace <ada@example.com> 1794034860 +0300

Add shopping cart

Commit dediğimiz şey bu kadar küçük bir metin. tree satırı projenin kök klasörünün snapshot'ını gösterir. parent bir önceki commit'in tam hash'i; geçmişi zincir hâline getiren bu satır. author değişikliği yazan, committer commit'i oluşturan kişi. Çoğu zaman ikisi aynıdır; başkasının patch'ini uyguladığında ya da commit'i yeniden yazdığında ayrışırlar. Adın yanındaki sayı tarih: 1 Ocak 1970'ten bu yana geçen saniye sayısı (Unix zamanı) ve saat dilimi. En altta, boş bir satırdan sonra commit mesajı.

Dikkat et, commit'in içinde hiçbir dosya içeriği ya da diff yok. Commit yalnızca bir tree'yi, bir parent'ı ve kim, ne zaman, neden bilgisini birbirine bağlar.

Tree ve Blob

Commit'in gösterdiği tree'yi açalım. Kısa hash yeterli; Git, repo'da tek bir object'e karşılık geldiği sürece ilk birkaç karakteri kabul eder:

$ git cat-file -t c93a32e
tree
$ git cat-file -p c93a32e
100644 blob 62ada8e0411d44b007ad18b3bb6ce007b86766ab	README.md
040000 tree c0ab695ca8c78ad42a7555e7b0b13426040d070b	src
$ git cat-file -p c0ab695
100644 blob 4426e0f1682977f267f15efbfa866d3ce583ad6f	books.js
100644 blob 4dd8f15e0a1d5ecb79a472f73d915bbbfd78d84f	cart.js

Tree bir klasörün snapshot'ıdır. Her satır bir girdi: mod, tip, hash ve isim. 100644 normal bir dosya, 040000 bir alt klasör demek; çalıştırılabilir bir script olsaydı 100755 görürdün. Kök tree, README.md için bir blob ve src için başka bir tree gösteriyor. src tree'si de iki dosyanın blob'larını. Klasör yapısı, tree'lerin iç içe geçmesiyle kurulur.

$ git cat-file -t 4426e0f
blob
$ git cat-file -p 4426e0f
export const books = [
  { id: 1, title: "Dune", price: 12.5 },
  { id: 2, title: "Neuromancer", price: 9.99 },
];
$ git hash-object src/books.js
4426e0f1682977f267f15efbfa866d3ce583ad6f

Blob, dosyanın içeriğidir; sadece içerik. Dosyanın adı blob'da yazmaz, onu bir üstündeki tree tutar. git hash-object, bir dosyanın Git'te hangi hash'i alacağını hesaplar. Diskteki src/books.js için çıkan hash, tree'deki blob'un hash'iyle birebir aynı.

Hash Nereden Geliyor?

Hash rastgele bir kimlik numarası değil, içeriğin parmak izidir. Git, object'in tipini ve boyutunu küçük bir başlık olarak içeriğin önüne ekler ve sonucun SHA-1 özetini alır:

# İçeriğin bayt cinsinden boyutu
$ git cat-file -s 4426e0f
115
# Aynı hash'i Git'siz, elle hesaplayalım
$ (printf 'blob 115\0'; cat src/books.js) | shasum
4426e0f1682977f267f15efbfa866d3ce583ad6f  -

Git'e hiç sormadan, sadece shasum ile aynı 4426e0f... değerine ulaştık. Bunun iki önemli sonucu var. Birincisi, aynı içerik dünyanın her yerinde aynı hash'i alır; Git'e "content-addressable" denmesinin sebebi bu. İçeriği bilen, adresini de bilir. İkincisi, tek bir bayt değişirse hash tamamen değişir. Bir dosya değişince blob'u değişir, blob değişince onu gösteren tree'nin içeriği değişir, tree değişince commit'in içeriği değişir. Bir commit'in hash'i, o commit'e kadar olan bütün geçmişin parmak izidir. Geçmişi kimse fark ettirmeden değiştiremez.

Gündelik işte hash'in tamamına nadiren ihtiyaç duyarsın:

$ git rev-parse HEAD
fae445405fbc78d83b054ec05f64c00d259a3ee0
$ git rev-parse --short HEAD
fae4454

git rev-parse, HEAD gibi bir ismi tam hash'e çevirir; --short da repo'da benzersiz kalan en kısa hâlini verir. Bugün Git varsayılan olarak SHA-1 kullanıyor. SHA-256 ile repo oluşturmak da mümkün ve Git 3.0'da yeni repo'lar için varsayılan olması planlanıyor; bunu serinin sonundaki sürüm yazılarında konuşacağız.

Parent Zinciri ve Açılıştaki Soru

HEAD~1, HEAD'in bir önceki commit'i demek. İlk commit'e bakalım:

$ git cat-file -p HEAD~1
tree b269fa4063053158c295b245671c0c8b9785ddb9
author Ada Lovelace <ada@example.com> 1794034800 +0300
committer Ada Lovelace <ada@example.com> 1794034800 +0300

Add README and book list
$ git cat-file -p b269fa4
100644 blob 0c0f2aa13b8a7f7fdc76bdcdaa5ced0014200f01	README.md
040000 tree 8610f9abc29a16233d17d31ba7aa4646ec6be446	src
$ git cat-file -p 8610f9a
100644 blob 4426e0f1682977f267f15efbfa866d3ce583ad6f	books.js

İlk commit'te parent satırı yok; bir önceki yazıda gördüğümüz root-commit bu demek. Şimdi books.js satırına bak: 4426e0f.... İkinci commit'in src tree'sindeki hash'in aynısı. Açılıştaki ilk sorunun cevabı bu: değişmeyen dosya ikinci kez saklanmadı, iki commit'in tree'si aynı blob'u gösteriyor. README.md değiştiği için onun blob'u iki commit'te farklı.

Repo'da kaç object olduğunu sayalım:

$ git count-objects
11 objects, 44 kilobytes
$ git fsck --dangling
dangling blob 1db6c00f7df85da2ae0c0b97dc7e98c7bdab3e81
$ git cat-file -p 1db6c00
# Bookstore

A tiny bookstore app for the Git & GitHub series.

Books live in src/books.js.

İki commit'te beşer object var: commit, kök tree, src tree'si ve iki blob. Ama sayı 10 değil 11. Fazladan olan, bir önceki yazıdaki MM anından kalma: README'nin ilk satırı eklenmiş ara hâli. git add, dosyayı staging area'ya eklerken içeriği hemen blob olarak objects'e yazar. Biz ardından dosyayı tekrar ekleyince bu blob'u hiçbir tree göstermez oldu; git fsck ona "dangling", yani sahipsiz diyor. Bu tür object'leri Git'in temizlik işlemi zamanla siler.

İkinci sorunun cevabı da artık belli. Commit mesajı commit object'inin içinde. Tek bir harfini değiştirirsen içerik değişir, içerik değişirse hash değişir. Yani bir commit'i "düzeltemezsin"; ancak yerine yeni bir commit koyabilirsin. Birazdan --amend'in tam olarak bunu yaptığını göreceğiz.

İyi Bir Commit Mesajı

Hash'ler makineler için, mesajlar insanlar içindir. Altı ay sonra git log'a bakan kişi çoğu zaman sensin. Önce kötü ve iyi birkaç örnek:

# Kötü: neyi, neden değiştirdiğini anlatmıyor
fix
update files
WIP
Added some stuff to cart and also fixed README typo
# İyi: kısa, emir kipinde, tek bir işi anlatıyor
Fix rounding in cartTotal
Add The Hobbit to the book list
Remove unused leftPad dependency

Yerleşik kurallar şunlar. İlk satır bir özet satırıdır ve kısa tutulur; yaklaşık 50 karakter iyi bir hedef. git log --oneline, GitHub'ın commit listesi ve birçok araç yalnızca bu satırı gösterir. Özet satırı emir kipinde yazılır: "Added" ya da "Adds" değil, "Add". Özet satırını İngilizcedeki "If applied, this commit will ..." cümlesinin devamı gibi düşün: "If applied, this commit will Fix rounding in cartTotal" düzgün bir cümle olur. Git'in kendi ürettiği mesajlar da bu biçimde: Merge branch ..., Revert ....

Ayrıntı gerekiyorsa özet satırından sonra bir boş satır bırakılır ve gövde yazılır. Gövde "ne"den çok "neden"i anlatır; neyin değiştiğini diff zaten gösteriyor. Gövde satırlarını 72 karakter civarında bölmek terminalde okumayı kolaylaştırır. Mesajları İngilizce yazıyoruz: ekipler, açık kaynak projeler ve araçlar bu dili bekliyor, kod da zaten İngilizce. Her commit tek bir işi anlatmalı; mesajında "and also" geçiyorsa muhtemelen iki commit'tir.

Sepete toplam fiyatı hesaplayan bir fonksiyon ekleyelim:

export function cartTotal(cart) {
  return cart.reduce((sum, book) => sum + book.price, 0);
}

Kısa bir gövdeyi -m'i iki kez vererek yazabilirsin; her -m ayrı bir paragraf olur. Daha uzun mesajlar için -m vermeden git commit deyip bir önceki yazıda ayarladığımız editörde yazmak daha rahat.

# src/cart.js'e cartTotal fonksiyonunu ekledikten sonra
$ git add src/cart.js
$ git commit -m "Add cart total" -m "Sum the book prices in the cart. Rounding is left to the UI for now."
[main dd0de64] Add cart total
 1 file changed, 4 insertions(+)
$ git log -1
commit dd0de649e7e2f53b206fcae4cfdfda7528d0a3e9 (HEAD -> main)
Author: Ada Lovelace <ada@example.com>
Date:   Sat Nov 7 10:02:00 2026 +0300

    Add cart total
    
    Sum the book prices in the cart. Rounding is left to the UI for now.

Son Commit'i Düzeltmek: --amend

Commit'i attık ve README'ye cartTotal()'ı yazmayı unuttuğumuzu fark ettik. Bunun için ayrı bir "Update README" commit'i atmak yerine son commit'i düzeltebiliriz:

$ git log --oneline
dd0de64 (HEAD -> main) Add cart total
fae4454 Add shopping cart
29f27c8 Add README and book list
# README'yi güncellemeyi unutmuşuz; satırı ekleyip aynı commit'e katıyoruz
$ git status -s
 M README.md
$ git add README.md
$ git commit --amend --no-edit
[main e3ef418] Add cart total
 Date: Sat Nov 7 10:02:00 2026 +0300
 2 files changed, 5 insertions(+)
$ git log --oneline
e3ef418 (HEAD -> main) Add cart total
fae4454 Add shopping cart
29f27c8 Add README and book list

--amend, staging area'daki son hâli alır ve son commit'in yerine yeni bir commit koyar. --no-edit mesajı aynen korur; mesajı da değiştirmek istersen git commit --amend -m "..." dersin, staging area boş olsa bile yalnızca mesaj değişir. Hash'e bak: dd0de64 gitti, yerine e3ef418 geldi. Bu bir "düzenleme" değil, yeni bir commit:

$ git log -1 --format=fuller
commit e3ef4186cc77d8cba596c0d4a7a5b7b0fc430768 (HEAD -> main)
Author:     Ada Lovelace <ada@example.com>
AuthorDate: Sat Nov 7 10:02:00 2026 +0300
Commit:     Ada Lovelace <ada@example.com>
CommitDate: Sat Nov 7 10:03:00 2026 +0300

    Add cart total
    
    Sum the book prices in the cart. Rounding is left to the UI for now.
# Eski commit silinmedi, sadece artık hiçbir branch onu göstermiyor
$ git cat-file -t dd0de64
commit

--format=fuller iki tarihi ayrı gösteriyor. AuthorDate, değişikliğin ilk yazıldığı an olarak korundu; CommitDate ise amend'in yapıldığı an. Eski dd0de64 hâlâ repo'da duruyor ama main artık onu göstermiyor. Yanlış bir amend yaptığında onu nasıl geri bulacağını reflog yazısında göreceğiz.

Kural basit: --amend'i yalnızca henüz kimseyle paylaşmadığın, yani push etmediğin commit'lerde kullan. Push ettiğin bir commit'i amend edersen senin geçmişin ile ekip arkadaşlarının geçmişi ayrışır, çünkü onların elindeki commit artık seninkinde yok.

Sık Düşülen Tuzaklar

İlk tuzak, Git'in diff sakladığını sanmak. Bu yanlış model, ileride rebase ya da cherry-pick'in neden "yeni" commit'ler ürettiğini anlamayı zorlaştırır. Aklında tut: commit bir snapshot'tır ve parent'ı değişen her commit yeni bir hash alır.

İkincisi, push edilmiş bir commit'i amend etmek. Yerelde her şey düzgün görünür, ama bir sonraki push reddedilir ve ekip arkadaşının elindeki geçmişle seninki çatallanır. Paylaşılmış bir hatayı düzeltmenin güvenli yolu yeni bir commit atmaktır.

Üçüncüsü, --amend'in yalnızca mesajı değiştirdiğini sanmak. Staging area'da bekleyen ne varsa commit'e katılır. Amend'den önce git status'a bakmazsan, ayrı bir commit'e gidecek bir değişikliği yanlışlıkla bu commit'e gömebilirsin.

Dördüncüsü, mesajı "fix", "update", "WIP" gibi tek kelimelerle geçiştirmek. Bunlar o an zaman kazandırır, ama bir hatanın hangi commit'le geldiğini ararken git log'u işe yaramaz hâle getirir.

Küçük Bir Deneme

★☆☆ Kendi repo'nda git cat-file -p HEAD ile başla ve tree, alt tree ve blob'ları takip ederek src/cart.js'in içeriğine ulaş. Kaç adımda vardın?

★★☆ git cat-file -s ile README.md blob'unun boyutunu bul ve printf 'blob SIZE\0' yöntemiyle hash'ini elle hesapla (SIZE yerine bulduğun sayıyı yaz). git hash-object README.md ile karşılaştır. Sonra README'ye tek bir boşluk ekleyip hash'i tekrar hesapla.

★★★ Son commit'in mesajını git commit --amend -m "..." ile değiştir. Önceki ve sonraki hash'leri not et, git cat-file -p ile iki commit object'ini karşılaştır. Hangi satırlar aynı kaldı, hangileri değişti? tree satırının neden aynı kaldığını açıklayabiliyor musun?

Bir sonraki yazıda repo'ya girmemesi gereken dosyalarla uğraşacağız: .gitignore pattern'leri, ! ile istisna, global excludes, git check-ignore -v, zaten commit'lenmiş bir dosyayı git rm --cached ile takipten çıkarmak, git mv ve .gitattributes ile satır sonları.