Git
Chapters ▾ 2nd Edition

7.8 Git Tools - İleri Seviye Birleştirme

İleri Seviye Birleştirme

Git’te birleştirme genellikle çok kolaydır. Git, başka bir dalı bir çok defa birleştirmeyi kolaylaştırdığı için; ufak çakışmaları sık sık çözerek, çok uzun ömürlü dallarınızı bile sürekli güncel tutabilir ve böylece en sonda devasa tek bir çakışmayla karşılaşmazsınız.

Ancak, bazen zorlu çatışmalar ortaya çıkabilir. Diğer bazı sürüm kontrol sistemlerinin aksine, Git birleştirme çakışması çözümünde "aşırı akıllı" olmaya çalışmaz. Git’in felsefesi, birleştirme çözümünün net olduğunu belirlemede akıllı olmaktır. Ancak bir çakışma varsa, onu otomatik olarak çözmeye çalışmak konusunda akıllı davranmaz. Bu nedenle, hızla ayrılan iki dalı birleştirmek için çok uzun süre beklerseniz, bazı sorunlarla karşılaşabilirsiniz.

Bu bölümde, bu sorunların neler olabileceğini ve Git’in size bu daha zorlu durumlarla başa çıkmada yardımcı olmak için hangi araçları sunduğunu ele alacağız. Ayrıca, yapabileceğiniz farklı, standart olmayan birleştirmelerin bazılarını ele alacak ve yaptığınız birleştirmelerden nasıl geri adım atılacağını göreceğiz.

Birleştirme Çakışmaları

Çok karmaşık çatışmalar için bazı temel adımları Birleştirme Çakışması (merge conflict) bölümünde ele almış olsak da, Git daha karmaşık çatışmaları çözmenize ve bunlarla daha iyi başa çıkmanıza yardımcı olmak için birkaç araç sağlar.

Öncelikle, mümkünse, çatışma olabilecek birleştirmeyi yapmadan önce çalışma dizininizin temiz olduğundan emin olmaya çalışın. Geliştirmeye devam ettiğiniz bir çalışmanız varsa, bunu geçici bir dalda katkılayın veya stash ile saklayın. Bu burada denediğiniz herhangi bir şeyi geri almanızı sağlar. Birleştirmeyi yaparken çalışma dizininizde kaydedilmemiş değişiklikleriniz varsa, verdiğimiz ipuçlarından bazıları bu çalışmayı korumanıza yardımcı olabilir.

Şimdi çok basit bir örnek üzerinden ilerleyelim. hello world yazısını yazdıran çok basit bir Ruby dosyamız var.

#! /usr/bin/env ruby

def hello
  puts 'hello world'
end

hello()

Repomuzda, whitespace adında yeni bir dal oluşturuyoruz ve tüm Unix satır sonlarını DOS satır sonlarına çevirerek yolumuza devam ediyoruz. Esasında dosyanın her satırını sadece boşluklarla değiştirmiş olduk. Ardından da ``hello world`` satırını ``hello mundo`` olarak değiştiriyoruz.

$ git checkout -b whitespace
Switched to a new branch 'whitespace'

$ unix2dos hello.rb
unix2dos: converting file hello.rb to DOS format ...
$ git commit -am 'converted hello.rb to DOS'
[whitespace 3270f76] converted hello.rb to DOS
 1 file changed, 7 insertions(+), 7 deletions(-)

$ vim hello.rb
$ git diff -b
diff --git a/hello.rb b/hello.rb
index ac51efd..e85207e 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,7 +1,7 @@
 #! /usr/bin/env ruby

 def hello
-  puts 'hello world'
+  puts 'hello mundo'^M
 end

 hello()

$ git commit -am 'hello mundo change'
[whitespace 6d338d2] hello mundo change
 1 file changed, 1 insertion(+), 1 deletion(-)

Şimdi master dalımıza geçiyoruz ve fonksiyon için bazı dokümentasyon belgeleri ekliyoruz.

$ git checkout master
Switched to branch 'master'

$ vim hello.rb
$ git diff
diff --git a/hello.rb b/hello.rb
index ac51efd..36c06c8 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,5 +1,6 @@
 #! /usr/bin/env ruby

+# prints out a greeting
 def hello
   puts 'hello world'
 end

$ git commit -am 'document the function'
[master bec6336] document the function
 1 file changed, 1 insertion(+)

Şimdi whitespace dalını birleştirmeyi deneriz ve boşluk değişiklikleri nedeniyle çatışma alırız.

$ git merge whitespace
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Automatic merge failed; fix conflicts and then commit the result.

Birleştirmeyi İptal

Şimdi birkaç seçeneğimiz var. Öncelikle, bu durumdan nasıl çıkacağımızı ele alalım. Belki de çatışmaları beklemiyordunuz ve şu anda durumla uğraşmak istemiyorsunuz; bu durumda git merge --abort komutu ile birleştirmeyi hemen iptal edebilirsiniz.

$ git status -sb
## master
UU hello.rb

$ git merge --abort

$ git status -sb
## master

git merge --abort seçeneği, birleştirmeyi çalıştırmadan önceki durumunuza geri çevirmeye çalışır. Bu işlemi mükemmel bir şekilde yapamayabileceğiz tek durum, çalışma dizinizde birleştirmeyi çalıştırdığınızda kaydedilmemiş veya katkılanmamış değişiklikleriniz olmasıdır. Aksi takdirde sorunsuz çalışması beklenir.

Eğer bir sebeple sıfırdan başlamak istiyorsanız, git reset --hard HEAD komutunu da çalıştırabilir ve reponun en son katkı işlenmiş durumuna geri dönebilirsiniz. Unutmayın ki, katkılanmamış çalışmalarınız kaybolur, bu yüzden değişikliklerinizden hiçbirini birini istemediğinizden emin olun.

Boşlukları (Whitespace) Yoksayma

Bu özel senaryoda, çakışmalar boşluklarla ilgilidir. Örnek basit olduğundan bunu biliyoruz, ancak gerçek durumlarda da çakışmayı incelediğinizde bu oldukça kolay anlaşılır. Çünkü her satır bir taraftan kaldırılır ve diğer tarafta yeniden eklenir. Varsayılan olarak, Git tüm bu satırları değişmiş olarak görür, bu yüzden dosyaları birleştiremez.

Ancak varsayılan birleştirme stratejisi argümanlar alabilir ve bunlardan birkaçı boşluk değişikliklerini doğru şekilde yok saymayla ilgilidir. Birleştirmede çok sayıda boşluk sorunu olduğunu görürseniz, doğrudan birleştirmeyi iptal edip -Xignore-all-space veya -Xignore-space-change ile tekrarlayabilirsiniz. İlk seçenek, satırları karşılaştırırken boşlukları tamamen yoksayar; ikincisi ise bir veya daha fazla boşluk karakteri dizilerini eşit kabul eder.

$ git merge -Xignore-space-change whitespace
Auto-merging hello.rb
Merge made by the 'recursive' strategy.
 hello.rb | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

Bu örnekte, gerçek dosya değişiklikleri çatışmıyordu. Dolayısıyla boşluk değişikliklerini yoksaydığımızda, her şey sorunsuz bir şekilde birleşir.

Ekip üyelerinizden birinin ara sıra her şeyi (boşluklardan sekme karakterlerine veya tersi yönde) yeniden biçimlendirmek gibi bir karakteri varsa, bu özellik gerçekten hayat kurtarıcıdır.

Manuel Olarak Yeniden Birleştirme

Git boşluk ön işlemesini oldukça iyi bir şekilde ele alsa da, Git’in otomatik olarak halledemediği ancak betikle düzeltilebilecek diğer türde değişiklikler olabilir. Örnek olması açısından, Git’in boşluk değişikliğini halledemediğini ve bunu kendimizin düzeltmesi gerektiğini varsayalım.

Aslında yapmamız gereken şey, birleştirmeye çalıştığımız dosyayı, birleştirmeden önce bir dos2unix programından geçirmektir. Peki, bunu nasıl yapabiliriz?

Öncelikle, çakışma durumuna geliyoruz. Sonra, dosyanın kendi sürümümüzün, onların (birleştirmek istediğimiz dalın) sürümünün ve ortak sürümün (her iki tarafın da dallandığı yerden) kopyalarını almak istiyoruz. Daha sonra, onların tarafını veya kendi tarafımızı düzeltip sadece bu tek dosya için birleştirmeyi tekrar denemek istiyoruz.

Üç dosya sürümünü almak aslında oldukça kolaydır. Git, bu sürümlerin hepsini izlemde stages altında, her birini kendiyle ilişkilendirilmiş numaralara sahip olmak üzede saklar. 1: ortak öncel, 2: sizin sürümünüz, ve 3: birleştirmeyi yaptığınız sürüm olan MERGE_HEAD 'den gelir.

Çakışan dosyanın her bir sürümünü git show komutunu ve özel bir sözdizimini kullanarak çıkarabilirsiniz.

$ git show :1:hello.rb > hello.common.rb
$ git show :2:hello.rb > hello.ours.rb
$ git show :3:hello.rb > hello.theirs.rb

Daha detaylı bir şekilde ilerlemek isterseniz, ls-files -u tesisat komutunu kullanarak bu dosyaların her biri için Git bloblarının gerçek SHA-1’lerini alabilirsiniz.

$ git ls-files -u
100755 ac51efdc3df4f4fd328d1a02ad05331d8e2c9111 1	hello.rb
100755 36c06c8752c78d2aff89571132f3bf7841a7b5c3 2	hello.rb
100755 e85207e04dfdd5eb0a1e9febbc67fd837c44a1cd 3	hello.rb

:1:hello.rb, bu blobun SHA-1’ini aramak için kullanılan bir kısaltmadır.

Artık çalışma dizinimizde her üç izlem’in de içeriğine sahip olduğumuza göre, boşluk sorununu çözmek için onların tarafını elle düzeltebilir ve az bilinen git merge-file komutunu kullanarak dosyayı yeniden birleştirebiliriz.

$ dos2unix hello.theirs.rb
dos2unix: converting file hello.theirs.rb to Unix format ...

$ git merge-file -p \
    hello.ours.rb hello.common.rb hello.theirs.rb > hello.rb

$ git diff -b
diff --cc hello.rb
index 36c06c8,e85207e..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,8 -1,7 +1,8 @@@
  #! /usr/bin/env ruby

 +# prints out a greeting
  def hello
-   puts 'hello world'
+   puts 'hello mundo'
  end

  hello()

Bu noktada dosyayı güzel bir şekilde birleştirdik. Aslında bu, boşluk değişikliklerini doğrudan yoksaymak yerine, gerçekte onları birleştirme öncesinde düzelttiği için ignore-space-change seçeneğinden daha iyi çalışır. ignore-space-change birleştirmesinde, DOS satır sonlarına sahip olan satırlar biraz kafa karıştırıcıdır.

Bu katkıyı işlemeden önce, son bir kontrol amacıyla, gerçekte hangi tarafın hangi değişiklikleri yaptığını görmek isterseniz; katkılamak üzere çalışma dizinizde olup birleştirme sonucu olarak katkılayacağınız çalışmayı bu izlemlerdekilerle kıyaslamak için git diff komutunu kullanabilirsiniz. Şimdi hepsini deneyelim.

Sonucunuzu birleştirmeden önce dalınızdaki son durumuyla karşılaştırmak, diğer bir deyişle birleştirmenin ne tür değişiklikler getirdiğini görmek için git diff --ours komutunu çalıştırabilirsiniz.

$ git diff --ours
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index 36c06c8..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -2,7 +2,7 @@

 # prints out a greeting
 def hello
-  puts 'hello world'
+  puts 'hello mundo'
 end

 hello()

Bu şekilde, dalımızda olup biteni, bu dosyaya bu birleştirmeyle tanıttığımız şeyin, o tek satırı değiştirmek olduğunu kolaylıkla görebiliriz.

Birleştirmenin sonucunun, onların tarafındakinden nasıl farklı olduğunu görmek istiyorsak, git diff --theirs komutunu çalıştırabiliriz. Bu ve aşağıdaki örnekte, boşlukları çıkarmak için ‘-b’ kullanmamız gerekiyor çünkü onu temizlenmiş hello.theirs.rb dosyamızla değil, Git’tekiyle karşılaştırıyoruz.

$ git diff --theirs -b
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index e85207e..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,5 +1,6 @@
 #! /usr/bin/env ruby

+# prints out a greeting
 def hello
   puts 'hello mundo'
 end

Son olarak, dosyanın her iki taraftan da nasıl değiştiğini git diff --base ile görebilirsiniz.

$ git diff --base -b
* Unmerged path hello.rb
diff --git a/hello.rb b/hello.rb
index ac51efd..44d0a25 100755
--- a/hello.rb
+++ b/hello.rb
@@ -1,7 +1,8 @@
 #! /usr/bin/env ruby

+# prints out a greeting
 def hello
-  puts 'hello world'
+  puts 'hello mundo'
 end

 hello()

Bu noktada, manuel birleştirmeyi yapmak için oluşturduğumuz, ancak artık ihtiyaç duymadığımız ek dosyaları temizlemek için git clean komutunu kullanabiliriz.

$ git clean -f
Removing hello.common.rb
Removing hello.ours.rb
Removing hello.theirs.rb

Çakışmaları Kontrol Etme

Belki bu aşamada bir nedenle çözümden memnun değilizdir. Ya da belki bir veya her iki tarafı manuel olarak düzenlemek çok da iyi çalışmamıştır ve daha fazla bağlam gerekmektedir.

Örneği biraz değiştirelim. Bu örnekte, her birinde birkaç katkı olan iki uzun ömürlü dalımız var, ancak birleştirildiğinde meşru bir içerik çakışması oluşturuyorlar.

$ git log --graph --oneline --decorate --all
* f1270f7 (HEAD, master) update README
* 9af9d3b add a README
* 694971d update phrase to hola world
| * e3eb223 (mundo) add more tests
| * 7cff591 add testing script
| * c3ffff1 changed text to hello mundo
|/
* b7dcc89 initial hello world code

Şimdi, yalnızca master dalında bulunan üç benzersiz katkımız var ve diğer üçü de mundo dalında bulunuyor. mundo dalını birleştirmeye çalışırsak, bir çakışma alırız.

$ git merge mundo
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Automatic merge failed; fix conflicts and then commit the result.

Çakışmanın ne olduğunu görmek istiyoruz. Dosyayı açarsak, şuna benzer bir şey göreceğiz:

#! /usr/bin/env ruby

def hello
<<<<<<< HEAD
  puts 'hola world'
=======
  puts 'hello mundo'
>>>>>>> mundo
end

hello()

Her iki tarafın da bu dosyaya içerik eklemiş, ancak bazı katkılar bu çakışmaya sebep olan dosyayı değiştirmiştir.

Şimdi, bu çatışmayı nasıl düzelteceğiniz tam olarak açık değilse, kullanabileceğiniz birkaç aracı keşfedelim. Belki de bu çatışmayı nasıl çözeceğiniz tam olarak belli değil ve daha fazla bağlama ihtiyacınız var.

Yararlı bir araç, git checkout komutuna eklenen --conflict seçeneğidir. Bu komut dosyayı yeniden kontrol eder ve çakışma işaretlerini değiştirir. İşaretleri sıfırlamak ve yeniden çözmeyi denemek istiyorsanız, bu faydalı olabilir.

--conflict seçeneğine diff3 veya merge (varsayılan) değerlerinden herhangi birini ekleyebilirsiniz. Eğer diff3 'ü eklerseniz, Git biraz farklı bir çatışma işareti versiyonu kullanır; size sadece ours (bizimki) ve theirs (onlarınki) versiyonlarını değil, aynı zamanda daha fazla bağlam için base versiyonunu da içerir.

$ git checkout --conflict=diff3 hello.rb

Bunu çalıştırdıktan sonra, dosya şuna benzer olacaktır:

#! /usr/bin/env ruby

def hello
<<<<<<< ours
  puts 'hola world'
||||||| base
  puts 'hello world'
=======
  puts 'hello mundo'
>>>>>>> theirs
end

hello()

Eğer bu formatı begendiyseniz, gelecekteki birleştirme çakışmalarında varsayılan olarak ayarlamak için merge.conflictstyle ayarını diff3 olarak değiştirebilirsiniz.

$ git config --global merge.conflictstyle diff3

git checkout komutu ayrıca --ours ve --theirs seçeneklerini alabilir, bu da bir tarafı seçmek veya diğerini tamamen birleştirmeden önce seçmenin çok hızlı bir yoludur.

Bu, özellikle sadece bir tarafı seçmek istediğiniz ikili (binary) dosya çatışmaları veya başka bir dalda belirli dosyaları birleştirmek istediğiniz durumlar için kullanışlı olabilir; birleştirmeyi yapabilir ve ardından katkıyı işlemeden önce belirli dosyaları bir taraftan veya diğerinden seçebilirsiniz.

Merge Günlüğü

Git birleştirme çatışmalarını çözerken yararlı bir başka araç da git log komutudur. Bu komut çatışmalara katkıda bulunan faktörleri anlamanıza yardımcı olabilir. Bazen iki geliştirme hattının aynı kod bölgesine dokunmasının nedenlerini hatırlamak için, biraz geçmişi gözden geçirmek gerçekten yardımcı olabilir.

Bu birleştirmeye dahil olan her iki dalın da içerdiği benzersiz tüm katkıların tam bir listesini almak için, Üçlü Nokta (…​) sözdizimini kullanabiliriz.

$ git log --oneline --left-right HEAD...MERGE_HEAD
< f1270f7 update README
< 9af9d3b add a README
< 694971d update phrase to hola world
> e3eb223 add more tests
> 7cff591 add testing script
> c3ffff1 changed text to hello mundo

Bu, her katkının hangi geliştirme hattında olduğuyla birlikte, sürece dahil olan toplam altı katkının güzel bir listesidir.

Ancak, bunu çok daha spesifik bir bağlama sahip olması için biraz daha basitleştirebiliriz. git log komutuna --merge seçeneğini eklersek, çakışmada olan bir dosyayı değiştiren katkıları gösterecektir.

$ git log --oneline --left-right --merge
< 694971d update phrase to hola world
> c3ffff1 changed text to hello mundo

Eğer -p seçeneği ile çalıştırırsanız, çakışmaya neden olan dosyalara ilişkin farklılıkları alırsınız. Bu, bir şeyin neden çakıştığını anlamanıza ve daha akıllıca nasıl çözeceğinize dair ihtiyacınız olan bağlamı hızlı bir şekilde sağlamak için gerçekten faydalı olabilir.

Birleşik Fark Formatı

Git, başarılı olan her birleştirmeyi izleme alır; bu nedenle çakışma durumunda git diff komutunu çalıştırdığınızda sadece hala çakışma durumunda olanları alırsınız. Bu, çözmeniz gerekenleri görmek için faydalı olabilir.

Birleştirme çakışmasının hemen ardından git diff komutunu çalıştırdığınızda, size benzersiz bir formatta bir fark çıktısı verecektir.

$ git diff
diff --cc hello.rb
index 0399cd5,59727f0..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,11 @@@
  #! /usr/bin/env ruby

  def hello
++<<<<<<< HEAD
 +  puts 'hola world'
++=======
+   puts 'hello mundo'
++>>>>>>> mundo
  end

  hello()

Bu format ``Birleşik Fark`` olarak adlandırılır ve her satırın yanında iki sütun veriye sahiptir. İlk sütun, bu satırın ``ours`` (bizim) dalı ve çalışma dizininizdeki dosya arasında farklı olup olmadığını (eklenmiş veya kaldırılmış olarak) gösterir ve ikinci sütun aynı şeyi ``theirs`` (onların) dalı ve çalışma dizininizdeki kopya arasında yapar.

Bu örnekte <<<<<<< ve >>>>>>> satırlarının çalışma kopyasında olduğunu ancak birleştirmenin her iki tarafında da olmadığını görebilirsiniz. Birleştirme aracı bunları nelerin eklenip çıkarıldığını anlamamız için yerleştirdiğinden mantıklıdır, ancak bunları kaldırmamız beklenir.

Çatışmayı çözer ve git diff komutunu yeniden çalıştırırsak, aynı şeyi göreceğiz, ancak bu sefer biraz daha kullanışlıdır.

$ vim hello.rb
$ git diff
diff --cc hello.rb
index 0399cd5,59727f0..0000000
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,7 @@@
  #! /usr/bin/env ruby

  def hello
-   puts 'hola world'
 -  puts 'hello mundo'
++  puts 'hola mundo'
  end

  hello()

Bu, ``hola world`` 'un tarafımızda olduğunu ancak çalışma kopyasında olmadığını, ``hello mundo`` 'nun onların arafında olduğunu ancak çalışma kopyasında olmadığını ve son olarak ``hola mundo`` 'nun her iki tarafta da olmadığını ancak şimdi çalışma kopyasında olduğunu gösterir. Bu, çözümü katkı olarak işlemeden önce bir gözden geçirmek açısından faydalı olabilir.

Ayrıca, birleştirmenin nasıl çözüldüğünü sonradan görmek için herhangi bir birleştirmenin git log çıktısından da yararlanabilirsiniz. Git, bir birleştirme katkısı üzerine git show komutunu çalıştırdığınızda veya bir git log -p komutuna (varsayılan olarak yalnızca birleştirme katkısı olmayan yamaları gösterir) --cc seçeneği eklediğinizde karşınıza bu formatı çıkarır.

$ git log --cc -p -1
commit 14f41939956d80b9e17bb8721354c33f8d5b5a79
Merge: f1270f7 e3eb223
Author: Scott Chacon <schacon@gmail.com>
Date:   Fri Sep 19 18:14:49 2014 +0200

    Merge branch 'mundo'

    Conflicts:
        hello.rb

diff --cc hello.rb
index 0399cd5,59727f0..e1d0799
--- a/hello.rb
+++ b/hello.rb
@@@ -1,7 -1,7 +1,7 @@@
  #! /usr/bin/env ruby

  def hello
-   puts 'hola world'
 -  puts 'hello mundo'
++  puts 'hola mundo'
  end

  hello()

Birleştirmeyi Geri Almak

Artık bir birleştirme katkısı oluşturmayı nasıl yapacağınızı bildiğinize göre, muhtemelen bazen kazara kullanacaksınız. Git ile çalışmanın harika yanlarından biri, hata yapmanın sorun olmadığıdır, çünkü onları düzeltmek mümkündür (ve birçok durumda kolaydır).

Birleştirme komutları da istisna değildir. Diyelim ki bir konu dalında çalışmaya başladınız, bunu yanlışlıkla master dalına birleştirdiniz ve şimdi katkı geçmişiniz şöyle görünüyor:

Kazara birleştirme katkısı.
Görsel 137. Kazara birleştirme katkısı

Bu sorunu çözmek için istediğiniz sonuca bağlı olarak iki farklı yaklaşım bulunmaktadır.

Referansları Düzeltmek

Eğer istenmeyen birleştirme katkısı yalnızca yerel repoda varsa; en kolay ve en iyi çözüm, dalları istediğiniz yere işaret edecek şekilde taşımaktır. Çoğu durumda, yanlış git merge işleminden sonra git reset --hard HEAD~ komutunu takip ederseniz, bu, dal işaretçilerini aşağıdaki gibi sıfırlayacaktır:

`git reset --hard HEAD~` sonrası geçmiş.
Görsel 138. git reset --hard HEAD~ sonrası geçmiş

Bir önceki konumuzda reset komutunu ele aldık (<<_git_reset>>), bu yüzden burada neler olup bittiğini anlamak çok zor olmamalı. Hemen hatırlatalım: reset --hard genellikle üç adımdan oluşur:

  1. Dalın HEAD’inin işaret ettiği yeri taşı. Bu durumda, master 'ı birleştirme katkısından önceki konuma (C6) taşımak istiyoruz.

  2. İndeksi HEAD’e benzet.

  3. Çalışma dizinini indekse benzet.

Bu yaklaşımın dezavantajı, geçmişi yeniden yazmasıdır ve bu, paylaşılan bir repoda sorun olabilir. Neler olabileceğine dair daha fazla bilgi için Yeniden Temellemenin Riskleri bölümüne göz atabilirsiniz; kısaca, yeniden yazdığınız katkıları kullanan başkaları da varsa, reset 'ten kaçınmanız gerekebilir. Bu yaklaşım, birleştirmenin ardından başka bir katkı oluşturulduğu durumlarda da çalışmaz; işaretçileri taşımak bu değişiklikleri kaybetmeye yol açar.

Katkıyı Geri Alma

Eğer dal işaretçilerini taşımak sizin işinizi görmeyecekse, Git size mevcut bir katkıdan kaynaklanan tüm değişiklikleri geri alacak yeni bir katkı işleme seçeneği sunar. Git, bu işlemi revert (geri alma) olarak adlandırır, ve bu senaryoda, onu şu şekilde çağırırsınız:

$ git revert -m 1 HEAD
[master b1d8379] Revert "Merge branch 'topic'"

-m 1 bayrağı, hangi öncelin mainline (ana hat) olduğunu ve korunması gerektiğini belirtir. HEAD 'e birleştirme çağrısında bulunduğunuzda (git merge topic), yeni katkı iki öncele sahiptir: birinci öncel HEAD (C6) ve ikinci öncel birleştirilen dalın ucudur (C4). Burada, ikinci öncelin (C4) birleştirmesiyle gelen tüm değişiklikleri geri almak ama birinci öncelin (C6) tüm içeriğini de korumak istiyoruz.

Geri alma katkısı sonrasında geçmişimiz şöyle görünür:

`git revert -m 1` sonrası geçmiş.
Görsel 139. git revert -m 1 sonrası geçmiş

Yeni katkı olan ^M, tam olarak C6 ile aynı içeriğe sahiptir. Bu yüzden buradan itibaren, birleştirilmemiş katkıların halâ HEAD 'in geçmişinde bulunması dışında, sanki birleştirme hiç olmamış gibidir. topic 'i tekrar master 'a birleştirmeye çalışırsanız Git’in kafası karışır:

$ git merge topic
Already up-to-date.

topic 'te master 'dan ulaşılamayan bir şey yoktur. Daha fenası, topic 'e iş ekler ve tekrar birleştirirseniz, Git geri alınan birleştirmeden sonraki değişiklikleri getirecektir:

Kötü birleştirilmiş geçmiş.
Görsel 140. Kötü birleştirilmiş geçmiş

Bu durumda en iyi çözüm, özgün birleştirmeyi geri alıp (çünkü şimdi geri alınan değişiklikleri getirmek istiyorsunuz), ardından yeni bir birleştirme katkısı oluşturmaktır:

$ git revert ^M
[master 09f0126] Revert "Revert "Merge branch 'topic'""
$ git merge topic
Geri alınan bir birleştirmeyi yeniden birleştirdikten sonra geçmiş.
Görsel 141. Geri alınan bir birleştirmeyi yeniden birleştirdikten sonra geçmiş:

Bu örnekte, M ve ^M birbirini iptal eder. ^^M katkısı, C3 ve C4 'ten gelen değişiklikleri etkin bir şekilde birleştirirken, C8 de, C7`den gelen değişiklikleri birleştirir; böylece `topic tamamen birleştirilmiş olur.

Diğer Birleştirme Tipleri

Şimdiye kadar iki dalın normal birleştirilmesini ele aldık, bunlar genellikle birleştirmenin "özyinelemeli" (recursive) stratejisiyle ele alınır. Ancak, dalları birleştirmenin başka yolları da vardır. Hızlı bir şekilde birkaçını ele alalım.

Bizim (ours) veya onların (theirs) tercihi

Öncelikle, normal "özyinelemeli" birleştirme moduyla yapabileceğimiz başka bir yararlı işlev daha var. Zaten bir -X ile iletilen ignore-all-space ve ignore-space-change seçeneklerini gördük, ancak Git’e bir çakışma gördüğünde belli bir tarafı tercih etmesini söyleyebiliriz.

Varsayılan olarak, Git birleştirilmekte olan iki dal arasında bir çakışma gördüğünde; kodunuza birleştirme çakışma işaretçilerini ekler, dosyayı çakışmış olarak işaretler ve sizin çakışmayı çözmenize izin verir. Eğer manuel olarak çözmenize gerek kalmadan, Git’in belli bir tarafı seçerek çakışmayı otomatik olarak çözmesini tercih ederseniz; merge komutuna -Xours veya -Xtheirs bayraklarını ekleyebilirsiniz.

Eğer Git bunu görürse, çakışma işaretçilerini eklemeyecektir. Birleştirilebilir tüm farkları ise birleştirir. İkili (binary) dosyalar da dahil, çakışan tüm farklarda ise, tamamen belirttiğiniz tarafları seçer.

Eğer önceki "hello world" örneğimize geri dönersek, kendi dalımıza birleştirmenin çatışmalara neden olduğunu görebiliriz.

$ git merge mundo
Auto-merging hello.rb
CONFLICT (content): Merge conflict in hello.rb
Resolved 'hello.rb' using previous resolution.
Automatic merge failed; fix conflicts and then commit the result.

Ancak -Xours veya -Xtheirs ile çalıştırırsak, bu işaretçileri eklemeyecektir.

$ git merge -Xours mundo
Auto-merging hello.rb
Merge made by the 'recursive' strategy.
 hello.rb | 2 +-
 test.sh  | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)
 create mode 100644 test.sh

Bu durumda, dosyada bir tarafta ``hello mundo`` ve diğer tarafta ``hola world`` olan çatışma işaretçilerini almak yerine, sadece ``hola world`` 'ü seçecektir. Ancak, bu dalda diğer çakışmayan tüm değişiklikler başarıyla birleştirilir.

Bu seçenek ayrıca önceki gördüğümüz git merge-file komutuna da geçirilebilir, örneğin, bireysel dosya birleştirmeleri için git merge-file --ours şeklinde.

Eğer bunun gibi bir şey yapmak istiyorsanız ve Git’in diğer taraftaki değişiklikleri bile birleştirmeye çalışmasını istemiyorsanız, daha katı bir seçenek olan ours birleştirme stratejisi vardır. Bu, ours özyinelemeli birleştirme seçeneğinden farklıdır.

Bununla aslında sahte bir birleştirme yapılacaktır. Birleştirmeye çalıştığınız dala bile bakmadan, her iki dalı da öncel kabul ederek yeni bir birleştirme katkısı kaydedecektir. Sadece birleştirmenin sonucunu, mevcut dalınızdaki kodun tamamı olarak kaydeder.

$ git merge -s ours mundo
Merge made by the 'ours' strategy.
$ git diff HEAD HEAD~
$

Görebileceğiniz gibi, bulunduğumuz dal ile birleştirmenin sonucu arasında herhangi bir fark yoktur.

Birleştirme yaparken bunu, Git’i bir dalın zaten birleştirilmiş olduğu şeklinde kandırmak için kullanabilirsiniz. Örneğin, bir release dalından yeniden dallandınız, bu dalda bazı çalışmalar yaptınız ve bu işleri bir noktada master dalınıza geri birleştirmek istiyorsunuz. Ama bu arada, master üzerindeki bazı hata düzeltmelerinin release dalına alınması gerekiyor. Hata düzeltme (bugfix) dalını release dalına birleştirebilir ve aynı dalı master dalınıza da merge -s ours ile (bugfix dalı zaten orada olmasına rağmen) birleştirebilirsiniz. Böylece daha sonra release dalını tekrar birleştirdiğinizde bugfix yüzünden çakışmalar olmaz.

Subtree Merging

The idea of the subtree merge is that you have two projects, and one of the projects maps to a subdirectory of the other one. When you specify a subtree merge, Git is often smart enough to figure out that one is a subtree of the other and merge appropriately.

We’ll go through an example of adding a separate project into an existing project and then merging the code of the second into a subdirectory of the first.

First, we’ll add the Rack application to our project. We’ll add the Rack project as a remote reference in our own project and then check it out into its own branch:

$ git remote add rack_remote https://github.com/rack/rack
$ git fetch rack_remote --no-tags
warning: no common commits
remote: Counting objects: 3184, done.
remote: Compressing objects: 100% (1465/1465), done.
remote: Total 3184 (delta 1952), reused 2770 (delta 1675)
Receiving objects: 100% (3184/3184), 677.42 KiB | 4 KiB/s, done.
Resolving deltas: 100% (1952/1952), done.
From https://github.com/rack/rack
 * [new branch]      build      -> rack_remote/build
 * [new branch]      master     -> rack_remote/master
 * [new branch]      rack-0.4   -> rack_remote/rack-0.4
 * [new branch]      rack-0.9   -> rack_remote/rack-0.9
$ git checkout -b rack_branch rack_remote/master
Branch rack_branch set up to track remote branch refs/remotes/rack_remote/master.
Switched to a new branch "rack_branch"

Now we have the root of the Rack project in our rack_branch branch and our own project in the master branch. If you check out one and then the other, you can see that they have different project roots:

$ ls
AUTHORS         KNOWN-ISSUES   Rakefile      contrib         lib
COPYING         README         bin           example         test
$ git checkout master
Switched to branch "master"
$ ls
README

This is sort of a strange concept. Not all the branches in your repository actually have to be branches of the same project. It’s not common, because it’s rarely helpful, but it’s fairly easy to have branches contain completely different histories.

In this case, we want to pull the Rack project into our master project as a subdirectory. We can do that in Git with git read-tree. You’ll learn more about read-tree and its friends in Git Internals, but for now know that it reads the root tree of one branch into your current staging area and working directory. We just switched back to your master branch, and we pull the rack_branch branch into the rack subdirectory of our master branch of our main project:

$ git read-tree --prefix=rack/ -u rack_branch

When we commit, it looks like we have all the Rack files under that subdirectory – as though we copied them in from a tarball. What gets interesting is that we can fairly easily merge changes from one of the branches to the other. So, if the Rack project updates, we can pull in upstream changes by switching to that branch and pulling:

$ git checkout rack_branch
$ git pull

Then, we can merge those changes back into our master branch. To pull in the changes and prepopulate the commit message, use the --squash option, as well as the recursive merge strategy’s -Xsubtree option. (The recursive strategy is the default here, but we include it for clarity.)

$ git checkout master
$ git merge --squash -s recursive -Xsubtree=rack rack_branch
Squash commit -- not updating HEAD
Automatic merge went well; stopped before committing as requested

All the changes from the Rack project are merged in and ready to be committed locally. You can also do the opposite – make changes in the rack subdirectory of your master branch and then merge them into your rack_branch branch later to submit them to the maintainers or push them upstream.

This gives us a way to have a workflow somewhat similar to the submodule workflow without using submodules (which we will cover in Submodules). We can keep branches with other related projects in our repository and subtree merge them into our project occasionally. It is nice in some ways, for example all the code is committed to a single place. However, it has other drawbacks in that it’s a bit more complex and easier to make mistakes in reintegrating changes or accidentally pushing a branch into an unrelated repository.

Another slightly weird thing is that to get a diff between what you have in your rack subdirectory and the code in your rack_branch branch – to see if you need to merge them – you can’t use the normal diff command. Instead, you must run git diff-tree with the branch you want to compare to:

$ git diff-tree -p rack_branch

Or, to compare what is in your rack subdirectory with what the master branch on the server was the last time you fetched, you can run

$ git diff-tree -p rack_remote/master
scroll-to-top