Git cherry-pick: Tek Bir Commit'i Başka Branch'e Taşımak
Bir hotfix'i main'den release branch'ine git cherry-pick ile taşıyoruz: yeni commit'in neden yeni hash aldığı, author ve committer farkı, -x ile iz bırakmak, commit aralığı almak, conflict'te --continue/--abort/--skip, git cherry ile eşdeğer commit'leri bulmak ve cherry-pick'in ne zaman koku olduğu.
Sürüm notu: Bu yazıdaki komutlar Git 2.54 ile denenmiştir.
Önce Tahmin Et
bookstore repo'sunda iki branch var. main ilerlemeye devam ediyor; release/1.0 ise müşteride çalışan sürümü temsil ediyor ve oraya yeni özellik girmiyor. Geçen hafta main'de sepet toplamındaki bir hatayı düzelttin. Şimdi aynı düzeltmenin release/1.0'a da girmesi lazım, ama arkasından gelen yeni özelliklerin girmemesi lazım.
$ git log --oneline --graph --all
* 706e4da (HEAD -> main) Reject negative prices
* 9986143 Add qty validation
* 2abb9e8 Round total to two decimals
* 287f848 Rename price to unitPrice
* 671b87a Fix total for items without qty
* e40e9bd Add search by title
* a7bb319 (release/1.0) Add .gitignore
* d6db968 Add cart total
* ff579a7 Add book list
* 7ff0565 Add README
Düzeltme 671b87a. Onu release/1.0'a taşıdığında oradaki yeni commit'in hash'i yine 671b87a mı olur? Cevabı vermeden önce bir düşün; yazının geri kalanı bu sorunun etrafında dönüyor.
Cherry-pick Ne Yapar?
git merge bir branch'in bütün geçmişini getirir. Senin istediğin ise o geçmişin içinden tek bir değişikliği almak. git cherry-pick tam olarak bunu yapar: verdiğin commit'in getirdiği değişikliği, yani o commit ile parent'ı arasındaki diff'i alır ve bulunduğun branch'in ucuna yeni bir commit olarak uygular.
Buradaki kilit kelime "yeni". Bir commit'in hash'i içeriğinden, parent'ından, yazarından ve tarihlerinden hesaplanır. Cherry-pick ile oluşan commit'in parent'ı farklıdır, commit tarihi farklıdır; dolayısıyla hash'i de farklıdır. Tahmin sorusunun cevabı: hayır, aynı hash'i görmeyeceksin.
Bir yemek tarifini defterden deftere kopyalamak gibi düşün. Tarif aynı, ama yeni defterde başka bir sayfada, başka sayfaların arasında duruyor. Git'in gözünde bunlar iki ayrı sayfa.
Hotfix'i Release Branch'ine Taşımak
Önce taşıyacağın commit'e bir bak. Ne değiştirdiğini bilmeden commit taşımak, içeriğini bilmediğin bir kutuyu başka bir rafa koymaktır.
$ git show --stat 671b87a
commit 671b87a8bbd577707a2e31d6e5df934d3909c774
Author: Ada Lovelace <ada@example.com>
Date: Sat Nov 7 10:06:00 2026 +0300
Fix total for items without qty
src/cart.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
Tek dosya, tek satır. Hedef branch'e geç ve commit'i al:
$ git switch release/1.0
Switched to branch 'release/1.0'
$ git cherry-pick 671b87a
[release/1.0 5e32f5f] Fix total for items without qty
Date: Sat Nov 7 10:06:00 2026 +0300
1 file changed, 1 insertion(+), 1 deletion(-)
$ git log --oneline -3
5e32f5f (HEAD -> release/1.0) Fix total for items without qty
a7bb319 Add .gitignore
d6db968 Add cart total
Yeni commit 5e32f5f. Çıktıdaki Date: satırı ilk bakışta tuhaf gelebilir: Git sana bu commit'in yazarlık tarihinin bugün olmadığını hatırlatıyor. Bir commit'te iki tarih ve iki kişi bulunur:
$ git show -s --format=fuller HEAD
commit 5e32f5fd546ea330660777826ea169456e9600e5 (HEAD -> release/1.0)
Author: Ada Lovelace <ada@example.com>
AuthorDate: Sat Nov 7 10:06:00 2026 +0300
Commit: Ada Lovelace <ada@example.com>
CommitDate: Sat Nov 7 10:11:00 2026 +0300
Fix total for items without qty
Author, değişikliği ilk yazan kişidir ve cherry-pick onu korur. Committer ise commit'i bu branch'e uygulayan kişidir. Alan'ın yazdığı bir fix'i sen cherry-pick'lersen author Alan kalır, committer sen olursun. Kimin emeği olduğu kaybolmaz.
Aynı Commit'i İki Kez Almaya Çalışırsan
Bir saat sonra fix'i taşıyıp taşımadığını hatırlamadın ve komutu bir daha çalıştırdın:
$ git cherry-pick 671b87a
The previous cherry-pick is now empty, possibly due to conflict resolution.
If you wish to commit it anyway, use:
git commit --allow-empty
Otherwise, please use 'git cherry-pick --skip'
On branch release/1.0
You are currently cherry-picking commit 671b87a.
(all conflicts fixed: run "git cherry-pick --continue")
(use "git cherry-pick --skip" to skip this patch)
(use "git cherry-pick --abort" to cancel the cherry-pick operation)
nothing to commit, working tree clean
$ git cherry-pick --skip
$ git status -s
Git değişikliği uygulamaya çalıştı, branch'te zaten aynısı olduğu için ortaya boş bir commit çıktı ve durdu. --skip bu commit'i atlayıp işlemi bitirir. Burada şanslıydın, çünkü satır o hâliyle duruyordu. Satır sonradan değişmiş olsaydı aynı komut bir conflict'le karşına çıkacaktı. Hangi commit'in nereye taşındığını takip etmenin daha iyi bir yolu var.
-x ile İz Bırakmak
-x seçeneği, yeni commit'in mesajının sonuna orijinal commit'in tam hash'ini ekler. Bu kez main'deki iki doğrulama commit'ini birlikte taşıyalım. Aralık yazımı git log'daki gibidir: A..B, A'dan sonra gelip B'ye kadar olan commit'ler demektir; A dahil değildir.
$ git cherry-pick -x 2abb9e8..706e4da
[release/1.0 46301d7] Add qty validation
Date: Sat Nov 7 10:09:00 2026 +0300
1 file changed, 5 insertions(+)
create mode 100644 src/validate.js
[release/1.0 060fe43] Reject negative prices
Date: Sat Nov 7 10:10:00 2026 +0300
1 file changed, 5 insertions(+), 1 deletion(-)
$ git log -1
commit 060fe43547f42db0175fd6da048418ebbd98fcb8 (HEAD -> release/1.0)
Author: Ada Lovelace <ada@example.com>
Date: Sat Nov 7 10:10:00 2026 +0300
Reject negative prices
(cherry picked from commit 706e4da750ab578951cdc4605e7abfe191bba3c2)
2abb9e8 aralığa girmedi; ilk alınan commit 9986143 oldu. İlk commit'i de dahil etmek istersen 2abb9e8^..706e4da yazarsın; ^ "bunun parent'ı" demektir. Commit'ler sırayla, eskiden yeniye uygulanır.
(cherry picked from commit ...) satırı altı ay sonra hayat kurtarır. Release branch'inde bir satıra git blame ile baktığında, değişikliğin main'deki aslına tek adımda ulaşırsın. Release ve hotfix branch'lerine yaptığın her cherry-pick'te -x kullanmayı alışkanlık hâline getir. Tek istisna, kimsenin göremeyeceği yerel bir branch'ten taşıma yapmak; orada hash'e referans vermek kimsenin işine yaramaz.
Conflict Çıkarsa: --continue, --abort, --skip
Şimdi yuvarlama commit'ini taşımak istiyorsun. Ama o commit, kendisinden önceki Rename price to unitPrice commit'inin üzerine yazılmıştı ve release/1.0'da o yeniden adlandırma yok:
$ git cherry-pick -x 2abb9e8
Auto-merging src/cart.js
CONFLICT (content): Merge conflict in src/cart.js
error: could not apply 2abb9e8... Round total to two decimals
hint: After resolving the conflicts, mark them with
hint: "git add/rm <pathspec>", then run
hint: "git cherry-pick --continue".
hint: You can instead skip this commit with "git cherry-pick --skip".
hint: To abort and get back to the state before "git cherry-pick",
hint: run "git cherry-pick --abort".
hint: Disable this message with "git config set advice.mergeConflict false"
$ cat src/cart.js
function total(items) {
<<<<<<< HEAD
return items.reduce((sum, item) => sum + item.price * (item.qty ?? 1), 0);
=======
const sum = items.reduce((acc, item) => acc + item.unitPrice * (item.qty ?? 1), 0);
return Math.round(sum * 100) / 100;
>>>>>>> 2abb9e8 (Round total to two decimals)
}
module.exports = { total };
Bu tabloyu conflict yazısından tanıyorsun. HEAD tarafı release/1.0'daki satır, diğer taraf taşımaya çalıştığın commit'in hâli. Dikkat et: commit'i birebir kabul edersen release/1.0'a unitPrice sızar ve orada hiçbir kitapta unitPrice olmadığı için toplam NaN olur. Doğru çözüm iki tarafın niyetini birleştirmek: yuvarlamayı al, price adını koru.
$ cat src/cart.js
function total(items) {
const sum = items.reduce((acc, item) => acc + item.price * (item.qty ?? 1), 0);
return Math.round(sum * 100) / 100;
}
module.exports = { total };
$ git add src/cart.js
$ git cherry-pick --continue
[release/1.0 3125d31] Round total to two decimals
Date: Sat Nov 7 10:08:00 2026 +0300
1 file changed, 2 insertions(+), 1 deletion(-)
--continue commit mesajını editörde açar; mesaj ve -x satırı hazır gelir, kaydedip kapatman yeterli. Çıkış yolları üç tane ve hepsi git status çıktısında yazar. --continue çözümü kaydedip devam eder. --skip sıradaki commit'i atlar; aralık taşıyorsan geri kalanlarla devam eder. --abort ise her şeyi cherry-pick başlamadan önceki hâline döndürür:
$ git cherry-pick 287f848
Auto-merging src/cart.js
CONFLICT (content): Merge conflict in src/cart.js
error: could not apply 287f848... Rename price to unitPrice
...
$ git cherry-pick --abort
$ git status -s
$ git log --oneline -1
3125d31 (HEAD -> release/1.0) Round total to two decimals
Conflict'i çözerken kafan karıştıysa --abort her zaman güvenli kapıdır. Sıfırdan, daha sakin bir kafayla başlarsın.
Cherry-pick Kokusu: Aynı Değişiklik İki Kez
Şimdi iki branch'te aynı mesajla ama farklı hash'lerle duran commit'ler var. Git bunların eşdeğer olup olmadığını patch'lerine bakarak söyleyebilir:
$ git cherry -v main
- 5e32f5fd546ea330660777826ea169456e9600e5 Fix total for items without qty
- 46301d78e62685d1d0a57ddc08ec671af0110573 Add qty validation
- 060fe43547f42db0175fd6da048418ebbd98fcb8 Reject negative prices
+ 3125d31e4ff49fbd2c97f5835958a41c4f7d33a5 Round total to two decimals
$ git log --oneline --cherry-mark main...release/1.0
+ 3125d31 (HEAD -> release/1.0) Round total to two decimals
= 060fe43 Reject negative prices
= 46301d7 Add qty validation
= 5e32f5f Fix total for items without qty
= 706e4da (main) Reject negative prices
= 9986143 Add qty validation
+ 2abb9e8 Round total to two decimals
+ 287f848 Rename price to unitPrice
= 671b87a Fix total for items without qty
+ e40e9bd Add search by title
git cherry, release/1.0'daki her commit için main'de aynı patch'e sahip bir commit olup olmadığına bakar: - "var", + "yok" demektir. --cherry-mark ise aynı bilgiyi iki tarafı birlikte göstererek verir: = eşdeğerini, + eşi olmayanı işaretler. Elle çözdüğümüz yuvarlama commit'i + çıktı, çünkü patch'i artık orijinalinden farklı. Git ikisinin aynı niyeti taşıdığını bilemez; bunu ancak -x satırı söyler.
Bu tablo bir uyarı da veriyor. Aynı değişikliğin geçmişte iki kez bulunması tek başına hata değildir; release branch'i yaşadığı sürece normaldir. Ama iki uzun ömürlü feature branch'i arasında sürekli commit taşıyorsan bir şeyler yanlış gidiyor demektir. O değişiklik muhtemelen ortak bir branch'e merge edilmeliydi. Böyle geçmişte her merge'de aynı satırlar yeniden karşılaşır, conflict ihtimali artar ve git log okunmaz hâle gelir. Cherry-pick bir cımbızdır, kepçe değil.
Ekiplerin çoğu bu yüzden "önce upstream" kuralını uygular: fix önce main'e girer, sonra -x ile release branch'lerine taşınır. Böylece fix'in her zaman tek bir asıl kaydı olur. CI tarafında da aynı fix'in testleri her iki branch'te ayrı ayrı koşar; GitHub Actions serisinde bunu pipeline olarak kuracağız.
Sık Düşülen Tuzaklar
Yanlış branch'te cherry-pick yapmak. Komut her zaman bulunduğun branch'in ucuna yazar. Önce git switch ile hedefe geç, sonra git status'un ilk satırına bak.
Aralığın başını unutmak. A..B yazımında A dahil değildir. Tek tek bakmak istersen önce git log --oneline A..B çalıştır ve listeyi gör.
Bağımlı commit'i tek başına almak. Yuvarlama örneğindeki gibi, taşıdığın commit kendisinden önceki bir değişikliğe yaslanıyorsa ya conflict alırsın ya da daha kötüsü, conflict almadan bozuk kod üretirsin. Taşımadan önce git show ile commit'in neye dokunduğunu oku.
Merge commit'ini cherry-pick'lemek. Merge commit'inin iki parent'ı olduğu için Git hangisine göre diff alacağını bilemez ve -m 1 ister. Bunu revert yazısında gördük; cherry-pick'te de aynı kural geçerli, ama çoğu zaman merge'ün içindeki asıl commit'leri taşımak daha temizdir.
Paylaşılmış commit'ler için -x'i atlamak. Satır sonra eksik kalır, hangi commit'in aslı olduğunu bulmak için mesaj karşılaştırmak zorunda kalırsın.
Küçük Bir Deneme
★☆☆ Kendi bookstore repo'nda main'den bir release/1.0 branch'i aç, main'de iki commit daha yap ve ikincisini -x ile release'e taşı. git show -s --format=fuller ile author ve committer tarihlerini karşılaştır.
★★☆ Aynı commit'i bilerek iki kez cherry-pick'le. Bir kez satır değişmeden, bir kez de arada aynı satıra dokunan başka bir commit varken dene. Hangisinde boş commit mesajı, hangisinde conflict aldığını not et ve nedenini açıkla.
★★★ Conflict'li bir cherry-pick'i elle çöz, sonra git cherry -v main ve git log --cherry-mark çıktılarına bak. Çözülmüş commit neden + görünüyor? Ardından release/1.0'ı main'e merge etmeyi dene ve hangi dosyalarda conflict çıktığını tahmin et.
Cherry-pick tek bir commit'i yeni bir tabana taşıyor. Peki bir branch'teki bütün commit'leri, sırasıyla, başka bir commit'in üzerine yeniden yazmak istersen? Bir sonraki yazıda git rebase ile tam olarak bunu yapacağız: merge ile farkı, hash'lerin neden değiştiği, golden rule ve --force-with-lease.