Bir Commit'in Anatomisi

Commit'ler bir git repository'sinin yapıtaşıdır. Bir repository'nin kilometre taşı olarak isimlendirebiliriz. Repository'mizin geçmişte hangi aşamalardan geçtiğini gösterir. Repository geçmişinde kayıt altına alınan bu değişiklikleri bir kişiye atfeder. Repository'miz üzerinde farklı durumlara geçip farklı çalışmalar yapmamıza olanak tanır. Henüz ana akıma almaya hazır olmadığımız değişiklikleri ayrı tutmamıza olanak tanır. Ve tüm bu özellikleri git kullanıcıdan soyutlanmış bir şekilde sağlar. Commit'ler kalıcı halde saklanmalarına rağmen repository'mizin dosyaları arasında yer tutmazlar. Bize görünmez bir şekilde varlıklarını sürdürürler. Peki kilometre taşlarımız olan bu commit'ler nerede?

Yazımızın girişinde bir repository dizininde bulunan .git dizinin içeriğine bakmıştık ve bu dizinin içinde bulunduğu dizine git repository'si olma niteliği kazandırdığını dile getirmiştik. Üzerinde çalıştığımız repository'mize yazımız boyunca farklı değişikliklerde bulunduk. Bu işlemlerin sonucunda repository'mizin .git klasörüne tekrar bakarak, yazının başındaki haline kıyasla ne gibi değişikliklerin olduğunu gözlemleyelim.

$ tree .git
.git
├── config
├── description
├── HEAD
├── hooks
│   ├── applypatch-msg.sample
│   ├── commit-msg.sample
│   ├── fsmonitor-watchman.sample
│   ├── post-update.sample
│   ├── pre-applypatch.sample
│   ├── pre-commit.sample
│   ├── pre-merge-commit.sample
│   ├── pre-push.sample
│   ├── pre-rebase.sample
│   ├── pre-receive.sample
│   ├── prepare-commit-msg.sample
│   ├── push-to-checkout.sample
│   ├── sendemail-validate.sample
│   └── update.sample
├── index
├── info
│   └── exclude
├── logs
│   ├── HEAD
│   └── refs
│       ├── heads
│          ├── dal-A
│          ├── dal-B
│          ├── main
│          ├── yeni-bir-branch
│          ├── yeni-branch
│          └── yeni-branch-2
│       └── remotes
├── objects
│   ├── 0d
│      └── 3ca3b82801797f1e43b13e13ff848745f2508e
│   ├── 13      ├── 549709c8a35740ffd8e8807d8d3316f9954ef6
│      └── bb9e0ba6d7bd9e4087f7cdb110080c10a8249d
│   ├── 2c
│      └── 6d14409ca7bd4bedf69b1fa2b7df7e673c3c8b
│   ├── 2f
│      └── 7e211bb943c48fa21d1cd73a0a0ba8ca871f39
│   ├── 31      └── f3baeb6bda14f8f02d304999e16df9de956377
│   ├── 36      └── 7ea1ad26695c83c96a6632563f1118344897c0
│   ├── 3b
│      └── e11c69355948412925fa5e073d76d58ff3afd2
│   ├── 40      └── 1ce7dbd55d28ea49c1c2f1c1439eb7d2b92427
│   ├── 51      └── ea971882184ee5986f440bb9e0ed2baea702ae
│   ├── 52      └── ac9288adff03447ba51676ca78a830d8b69ccb
│   ├── 54      └── 8d693ac499dffbba69340bfb0b25593473883a
│   ├── 5f
│      └── a69300068f328394fc9765d3a3843d0d6d321a
│   ├── 71      ├── 120a132264f1823a4bf4a42298d1586548716b
│      └── 5c93c23f99d7602214513ee965ee0fe0514aff
│   ├── 77      └── 7f68a6ba056e0bdfd674c3f8646ea2f9b04520
│   ├── 78      └── 9492681e6b0d56b1fe142431bae464d630f2d3
│   ├── 7e
│      └── b6f87f0dae69f6a068004caea18eb0b3868e51
│   ├── 80      └── 2992c4220de19a90767f3000a79a31b98d0df7
│   ├── 8c
│      └── d6f22b567ea17f6ac6f69e20e26d5824a47e7f
│   ├── 8d
│      └── d87969d42bdfaf073149647688bf2c5c0db840
│   ├── 8f
│      └── d49c9aea2c510c690e234cbd31938b188f4f81
│   ├── 95      └── e7356f75c2d844d9d1d7ba42fd5b40a5fd5ecf
│   ├── 99      └── da6674f09ff4194a01b3380fb5591537a9384c
│   ├── 9a
│      └── 63d6499f568868f73613412f881a770f33342d
│   ├── b5
│      └── b6c0910ab195204dec707b3370b8e70b47eabc
│   ├── ce
│      └── f4e449c3fcab258db56a2954cc144ae3271584
│   ├── da
│      └── 9d8ba3ad2add71c335a269ada194455a06831e
│   ├── dc
│      └── 2243f197f26719f94687238bb0f9310da223d2
│   ├── de
│      └── 0bdc0759033f9b112a4d874c8aaff95959f198
│   ├── eb
│      └── 02546d739a068296f32e1340f4754bbbd3e922
│   ├── ee
│      └── cd54119ac8e595f09320298ff46bab502d5861
│   ├── ef
│      └── f951c5a4da7490ac1022508fc98ee4533897db
│   ├── fe
│      └── b734c0ff9cfb31293961003c8bcf6fadd86514
│   └── info
├── packed-refs
└── refs
    ├── heads
       ├── dal-A
       ├── dal-B
       ├── main
       ├── yeni-bir-branch
       ├── yeni-branch
       └── yeni-branch-2
    ├── remotes
    └── tags

45 directories, 67 files

Artık .git dizinimizin içinde branch'lerimizi görebiliyoruz. Buna ek olarak objects dizininin altında çok sayıda kayıt görüyoruz. objects dizininin içinde çok sayıda 2 harfli dizin, ve o dizinlerin altında da commit hash'lerimize benzeyen kayıtlar görüyoruz.

Bu aşamada bu hash gibi ismi olan kayıtların hepsinin aslında bizim commit'lerimiz olduğunu düşünmek için bir varsayımda bulunabiliriz. Fakat kayıtların sayısı bizim repository'deki commit'lerimizin sayısından daha fazla gibi gözüküyor. Bu ön yargımız doğru mu? Bu önermemizin doğruluğunu ispatlamaya çalışalım.

Unix Bilgisi

Buradaki ispatımızda wc isimli aracı kullanacağız. wc ismi word count kelimelerinin kısaltmasından gelmektedir. Kendisine beslenen girdi hakkında sayım bilgisi verir. Biz -l bayrağını kullanarak girdimizde kaç satır olduğunu göstereceğiz.

Buna ek olarak find komutunu kullanarak .git/objects klasöründe recursive bir şekilde dosya listeleme yapacağız. ls yerine find aracını tercih etmemizin sebebi ise her satıra yalnızca bir dosya isminin basılmasına ihtiyaç duymamız. Bu ihtiyacı ls ile gerçekleştirmek bu aşama için gereksiz bir karmaşıklık meydana getirecektir. Buna ek olarak dizinleri basmamasını, yani sadece dosyaları basmasını istediğimiz için -type f bayrağını kullanacağız. Bu çıktıyı wc komutuna besleyerek dosya sayısını öğreneceğiz.

$ git log --all --oneline | wc -l
      13
#     ^^ repository'deki toplam commit sayimiz

$ find .git/objects -type f | wc -l
      34
#     ^^ repository'deki toplam obje sayimiz

git log komutunun bize verdiği bilgiye göre repository'mizde sadece 13 adet commit bulunuyorken .git/objects dizininde 34 adet kayıt bulunmaktadır. Bu bulgulardan yola çıkarak repository'mizde mevcutta commit'lerimizden daha fazla objemizin olduğu sonucuna varıyoruz. Peki commit'lerimiz de bir obje sayılıyor mu? Objelerle birlikte mi saklanıyor?

Dikkatli bakarsak .git/objects dizininin altında commit hash'lerimizi bulmamız mümkün. Fakat bunu yapmak için normalde yapacağımızdan farklı bir yöntem izlememiz gerekiyor. Aramak istediğimiz commit hash'imizin ilk iki karakterini alarak saklandığı dizini bulduktan sonra o dizin içindeki dosyalarda ismi commit hash'imizin son 38 karakterine eşit olan dosya bizim commit'imiz olacaktır.

Örnek olarak 789492681e6b0d56b1fe142431bae464d630f2d3 hash'li commitimiz .git/objects/78/9492681e6b0d56b1fe142431bae464d630f2d3 şeklinde saklanmaktadır.

Objelerin alt dizinlere ayrılması

Bir git repository'sinin yapısı gereği çok sayıda objeye sahip olması olağan bir durumdur. Git objeleri objects dizininde saklarken hash'lerinin ilk 2 karakterine göre alt dizinlere yerleştirir. Bu sayede büyük repository'lerde çok büyük sayılarda objeler olmasına rağmen objeler içinde arama yapmayı gerektiren işlemlerin zamansal karmaşıklığında iyileşme sağlanır. Bu işlem veritabanı sistemlerinde yapılan partition işlemiyle benzer bir yaklaşım ortaya koyar.

$ ls .git/objects/78/9492681e6b0d56b1fe142431bae464d630f2d3
.git/objects/78/9492681e6b0d56b1fe142431bae464d630f2d3

Bu sayede commit'lerimizin bir dosya halinde .git/objects dizininin altında saklandığını göstermiş olduk. Bu noktada commit'lerimizin nasıl saklandığını, hangi formatta saklandığını ve içinde neler bulunduğunu da gösterelim.

Obje Dosyalarının Formatı

Commit'imiz bir dosya halinde saklandığı için cat komutu ile terminalimize bastırabiliriz ancak git'in objeleri saklamak için kullandığı dosya biçimi xml, json veya yaml gibi insan tarafından kolaylıkla okunabilecek bir format olmadığı için, yani binary bir formatta olduğu için cat komutunu kullansak bile ekrana belli bir anlam taşıyan karakterler basmaktan farklı bir sonuca ulaşamayız.

Git'in kendi formatıyla sakladığı bir git objesinin içeriğinde ne olduğunu öğrenmek için git'ten yardım alarak git cat-file komutunu kullanmamız gerekmektedir. Bu komut ile birlikte git obje dosyasını açarak okunabilir bir formatta ekrana basacaktır. Bu komut yardımı ile mevcut commit'imizin anatomisini gözden geçirebiliriz.

$ git cat-file commit 789492681e6b0d56b1fe142431bae464d630f2d3
tree eecd54119ac8e595f09320298ff46bab502d5861
parent eb02546d739a068296f32e1340f4754bbbd3e922
author <username> <email> 1746280401 +0200
committer <username> <email> 1746280401 +0200

squash merge dal-B

Buradan da açıkça görüldüğü üzere commit'imizi meydana getiren öğeler şunlardır:

  • tree
  • parent commit
  • author kişi ve zamanı
  • commiter kişi ve zamanı
  • commit mesajı

Author ve Commiter

Bu aşamaya kadar bizim git'i kullanım biçimimiz gereği author ve commiter daima aynı kişiye işaret edecektir. Yazıya karmaşıklık eklemekten kaçınmak amacıyla bu iki öğenin farkını bu aşamada açıklamaktan kaçınmayı tercih ediyorum.

Burada listelenmiş öğelere, tree öğesi haricinde daha önce değinmiştik. Fakat commit'imizin repository'mizde meydana getirdiği değişiklikleri burada bir öğe olarak göremiyoruz. O halde commit'in yaptığı değişiklikler nerede saklanıyor?

Kılavuzumuzda commit'lere değinirken commit'lerimizin repository'mizin o andaki halinin bir kopyasını sakladığından bahsetmiştik. Bu ifade ile commit'lerimizin aslında ayrı ayrı dosyalarda yaptığımız değişiklikleri saklamadığını dolaylı olarak ifade etmiş oluyoruz.

Commit'ler repository'nin bir kopyasını saklıyor ifadesi doğrudan olmasa da dolaylı olarak doğru bir ifadedir. Bir commit aslında repository'mizin herhangi bir andaki dizin ağacına işaret etmektedir. Yukarıda listelediğimiz tree öğesi aslında repository'mizin commit'i attığımız andaki dizin yapısını recursive bir şekilde saklayan bir objeye işaret etmektedir.