Commit Grafı / Soyağacı İlişkisi
Önceki bölümlerde fast-forward merge'ye değinirken bahsettiğimiz önemli bir durum vardı. Bir branch diğer branch'i kapsıyor mu? Bu sorgunun bizim için önemi fast-forward merge bölümünde öne çıkmıştı. Fakat bu sorgunun ima ettiklerinin ayrı bir bölümde değinilmeyi gerektirdiğini düşünüyorum.
Mental Model
Bu konuda git'i mental olarak modelleme biçimimizi tekrar gözden geçirmemizde fayda olacaktır. Bir branch bir commit'e işaret eden bir işaretçidir. Diğer bir deyişle bir branch aynı anda birden çok commit'e değil, yalnızca tek bir commit'e işaret eder.
Bir commit ise bir commit grafının bir parçasıdır ve kendisinden önceki commit'e veya commit'lere parent ilişkisiyle bağlıdır.
Bir branch'in diğerini kapsıyor olması durumu aslında birkaç olay sonucunda gerçekleşebilir. A ve B isminde iki branch'imiz olsun. B branch'i A branch'ini kapsıyor ( \(\text{branch}_B \supseteq \text{branch}_A\) ) önermesi aşağıdaki durumlarda gerçekleşebilir:
BileAbranch'i aynı commit'e işaret ediyordur.Bbranch'iAbranch'inden oluşturulmuş veBbranch'iAbranch'ine merge edilmeden önceAbranch'inin işaret ettiği commit değişmemiştir. YaniAbranch'i,Bbranch'inin atasıdır.Abranch'iBbranch'ine bir merge commit ile merge edilmiştir.
Fakat burada bu sorguyu branch'ler ile değil de commit'ler ile ele almak daha doğru çıkarımlara ulaşmamıza yardımcı olacaktır. Bizim A veya B branch'i olarak isimlendirdiğimiz yapılar aslında bahsi geçen anda bu branch'lerin baktığı commit'ler olarak düşünülmelidir. Aksi takdirde yanlış varsayımlarla yanlış sonuçlara ulaşabiliriz.
Branch terimini ortadan kaldırıp yerine commit terimini kullanmaya başladığımız zaman, kapsamak olarak isimlendirdiğimiz durum aslında basitçe bir commit'in diğer commit'in atası olmasını ima etmektedir.
X ve Y hash'li iki commit'imiz olsun. X commit'inin parent commit grafını gezerek Y commit'ine ulaşabiliyorsak Y commit'i X commit'inin atasıdır diyebiliriz.
Şimdi üzerinde çalıştığımız repository'mize dönüp bu sorguyu farklı commit ikilileri için deneyelim.
$ git log --graph --all --oneline
* 7894926 (HEAD -> dal-A) squash merge dal-B
* eb02546 Merge branch 'dal-B' into dal-A
|\
* | 8cd6f22 test.txt dal-A icin degistirdim
| | * 5fa6930 (dal-B) degisiklik-2
| | * 8dd8796 degisiklik-1
| |/
| * 2c6d144 test.txt dal-B icin degistirdim
|/
* cef4e44 (main) dal-B icin degisiklik yaptim
* b5b6c09 Merge branch 'yeni-branch'
|\
| * dc2243f (yeni-branch-2, yeni-branch) yeni-branch icin ilk commitimi atiyorum
* | 9a63d64 test-2.txt dosyasini ekledim
|/
* 777f68a Dosyaya Merhaba Dunya ekledim
* 95e7356 Ilk commit
Sorgular
Repository'deki ilk commit, mevcut commit'imizin atası mı?
Repository'mizin mevcuttaki halinde bu sorgunun cevabı her zaman evet olacaktır. Ancak bunu bir komut ile doğrulayalım.
Terminoloji
Bir git repository'sinde hiçbir atası olmayan commit'lere root commit adı verilir. Türkçede kök commit diye isimlendirebiliriz.
Birden çok root commit
Bir git repository'sinde birden çok root commit bulunması mümkündür. Bunu gerçekleştirmenin yollarından birisi orphan bir branch oluşturmaktır. Bunu checkout yaparken --orphan bayrağı ile yeni bir branch oluşturarak gerçekleştirebiliriz.
Daha sonra log'u --all --max-parents=0 bayrakları ile sadece root commit'leri gösterecek şekilde ayarlayarak birden çok root commit'imizin olduğunu doğrulayabiliriz.
$ git checkout --orphan yeni-bir-branch
Switched to a new branch 'yeni-bir-branch'
# eski commit'ten kalan dizin yapisini temizleyelim
$ git rm -rf .
rm 'test-2.txt'
rm 'test.txt'
$ echo "Yeni Bir Baslangic Yapiyorum" > yeni-bir-baslangic.txt
$ git add yeni-bir-baslangic.txt
$ git commit -m "yeni bir baslangic"
[yeni-bir-branch (root-commit) 7eb6f87] yeni bir baslangic
1 file changed, 1 insertion(+)
create mode 100644 yeni-bir-baslangic.txt
$ git log --all --max-parents=0
commit 7eb6f87f0dae69f6a068004caea18eb0b3868e51 (HEAD -> yeni-bir-branch)
Author: <username> <email>
Date: Sun May 4 13:58:48 2025 +0200
yeni bir baslangic
commit 95e7356f75c2d844d9d1d7ba42fd5b40a5fd5ecf
Author: <username> <email>
Date: Fri May 2 21:49:02 2025 +0200
Ilk commit
git checkout dal-A ile önceki branch'imize geri dönelim.
git log için --all bayrağı
Sorgularımız için log'u filtrelerken --all bayrağını kullanmamamız gerekiyor, aksi takdirde mevcut commit grafından ziyade bütün repository'nin commit history'sini yazdıracağı için yanlış sonuçlara varmamıza sebep olacaktır.
$ git log --oneline | grep "95e7356"
95e7356 Ilk commit
Unix Bilgisi
grep Unix sistemlerde kurulu olarak gelen bir programdır. Bir dosyanın veya kendisine beslenen bir girdinin belli bir filtreye uyan satırlarını yazdırmaya yarar.
| (pipe) operatörü ile bir önceki komutun çıktısını bir sonraki komuta girdi olarak besleyebiliyor, diğer bir deyişle pipe'leyebiliyoruz. | grep "95e7356" yaparak git log komutunun çıktısını grep programına girdi olarak besledik ve grep de bize sadece ve sadece "95e7356" kısmını içeren satırları çıkardı.
Yukarıdaki komut ile mevcut history grafımızı yazdırdık ve grep ile bu grafı filtreleyerek repository'deki ilk commit mevcut history'mizde var mı yok mu sorgusuna cevabımızı "evet" olarak bulduk.
yeni-branch isimli branch'in baktığı commit, şu anki commit'imizin atası mı?
$ git log --oneline | grep "dc2243f"
dc2243f (yeni-branch-2, yeni-branch) yeni-branch icin ilk commitimi atiyorum
yeni-branch isimli branch'i bir merge commit ile commit history'mize, diğer bir deyişle soyağacımıza bağlamıştık ve bunun sonucunda artık onun da mevcut commit'imizin bir atası olduğunu görebiliyoruz.
dal-B branch'inin baktığı commit, şu anki commit'imizin atası mı?
Hatırlarsanız dal-B branch'ine 2 yeni commit atıp onu daha sonra squash merge ile dal-A branch'imize merge etmiştik.
$ git log --oneline | grep "5fa6930"
Yukarıdaki komutun hiçbir çıktısı vermemesiyle aşikar olduğu üzere dal-B branch'i şu anda dal-A branch'inin baktığı commit'in bir atası olarak görünmüyor. dal-B branch'ini merge etmiş olmamıza rağmen, yaptığımız merge işlemi squash merge olduğu için aslında dal-B branch'ini dal-A branch'ine bağlayan herhangi bir commit oluşmamış oluyor. Squash merge ile birlikte aslında dal-A branch'ine yalnızca bağımsız yeni bir commit atmış oluyoruz.
Buradan yola çıkarak ne zaman squash merge yapmak istediğimize dikkat etmekte fayda olduğunu söyleyebiliriz. Branch'ler veya commitler arası soyağacı ilişkisini korumak istiyorsak squash merge yapmaktan kaçınmalıyız.
Alternatif komutlar
Bu şekilde log'u kullanarak commit soyağacını sorgulamak yanlış bir yaklaşım olmasa da, git'in bu amaç için kullanabileceğimiz farklı komutları da mevcut.
-
rev-listbelirtilen commit'in ulaşabildiği bütün commit hash'lerini listeler.logkomutu insan tarafından okunabilen bir komut ikenrev-listscript amacıyla kullanılan bir komuttur.Terminoloji
Git komutlarından bahsederken insan tarafından okunabilmesi amaçlanan komutlara porcelain sıfatı kullanılır.
git logbir porselen komut ikenrev-listbir porselen komut değildir.$ git rev-list dal-A | grep "dc2243f197f26719f94687238bb0f9310da223d2" dc2243f197f26719f94687238bb0f9310da223d2 -
merge-basekomutu--is-ancestorbayrağı ile kullanıldığı zaman bir commit diğerinin atasıdir önermesi doğru ise0kodu ile, yanlış ise0'dan farklı bir kod ile çıkış yapar.# `yeni-branch` isimli branch'in isaret ettigi commit # `dal-A` branch'inin isaret ettigi commit'in atasi mi? # $? ile bir onceki komutun cikis kodunu alip bunu da `echo` ile yazdirabiliriz. $ git merge-base --is-ancestor yeni-branch dal-A && echo $? 0# `dal-B` isimli branch'in isaret ettigi commit # `dal-A` branch'inin isaret ettigi commit'in atasi mi? # Burada bir onceki komutun aksine `||` operatorunu kullanmamiz gerekiyor # aksi taktirde `&&` kullansaydik sol taraftaki komut # 0'dan farkli bir kod ile cikis yapacagi icin sagdaki komut hic calismayacakti. $ git merge-base --is-ancestor dal-B dal-A || echo $? 1