Skip to content

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:

  • B ile A branch'i aynı commit'e işaret ediyordur.
  • B branch'i A branch'inden oluşturulmuş ve B branch'i A branch'ine merge edilmeden önce A branch'inin işaret ettiği commit değişmemiştir. Yani A branch'i, B branch'inin atasıdır.
  • A branch'i B branch'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
Şimdi ise 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-list belirtilen commit'in ulaşabildiği bütün commit hash'lerini listeler. log komutu insan tarafından okunabilen bir komut iken rev-list script 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 log bir porselen komut iken rev-list bir porselen komut değildir.

    $ git rev-list dal-A | grep "dc2243f197f26719f94687238bb0f9310da223d2"
    dc2243f197f26719f94687238bb0f9310da223d2
    
  • merge-base komutu --is-ancestor bayrağı ile kullanıldığı zaman bir commit diğerinin atasıdir önermesi doğru ise 0 kodu ile, yanlış ise 0'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