Git Branch: Hareketli Bir Pointer, HEAD ve git switch
Branch'in içinde tek bir commit hash'i taşıyan 41 byte'lık bir dosya olduğunu, HEAD'in nasıl çalıştığını, git branch ile listeleme, yeniden adlandırma ve silmeyi, git switch ve switch -c ile gezinmeyi, detached HEAD'e girip çıkmayı ve git checkout'un eski kullanımını gerçek çıktılarla görüyoruz.
Sürüm notu: Bu yazıdaki komutlar Git 2.54 ile denenmiştir; git switch Git 2.23'ten beri vardır.
Önce Tahmin Et
Bookstore repo'sunda sekiz commit var. Şimdi wishlist adında yeni bir branch açacaksın. Sence bu işlem diskte ne kadar yer kaplar? Git bütün dosyaları bir yere kopyalar mı, yoksa sadece geçmişi mi çoğaltır? Biraz düşün, sonra şu iki komuta bak:
$ cat .git/refs/heads/main
5bcf046dbd24e20d7e3374f432be72472e31ac56
$ wc -c .git/refs/heads/main
41 .git/refs/heads/main
Cevap 41 byte: 40 karakterlik bir hash ve bir satır sonu. Branch, içinde tek bir commit hash'i yazan küçük bir dosyadır. Ne dosyalarını kopyalar ne de geçmişi çoğaltır. Bu yüzden Git'te branch açmak neredeyse bedavadır ve ekipler gün içinde onlarca branch açıp kapatmaktan çekinmez.
Branch Bir Pointer'dan İbaret
Yukarıdaki hash, main'in gösterdiği commit'in tam hâli. Aynı bilgiyi Git'e sorarak da alabilirsin:
$ git rev-parse main
5bcf046dbd24e20d7e3374f432be72472e31ac56
$ git branch -v
discount 86303f4 Add discount to cart total
* main 5bcf046 Add Snow Crash
git branch tek başına branch'lerin adlarını listeler; -v her birinin gösterdiği commit'i ve o commit'in mesajını ekler. Başındaki yıldız şu an hangi branch'te olduğunu gösterir.
Branch'i kitabın arasındaki bir ayraç gibi düşün. Ayraç sayfanın kendisi değil, sadece "buradayım" diyen bir işaret. Yeni bir commit attığında Git, üzerinde bulunduğun branch'in dosyasına yeni commit'in hash'ini yazar; ayraç bir sayfa ileri kayar. Commit'ler yerinde kalır, hareket eden yalnızca pointer'dır. Bu "hareketli pointer" fikri, merge'den rebase'e kadar ilerideki her konunun temelidir.
Bir not: ref'leri reftable formatında tutan repo'larda .git/refs/heads/ altında bu tek tek dosyaları görmezsin. Git 3.0'da reftable'ın varsayılan olması planlanıyor, ama mantık değişmiyor; branch yine bir ad ile bir hash'in eşleşmesi.
HEAD: Şu An Neredesin?
Peki Git hangi branch'te olduğunu nereden biliyor? Cevap .git/HEAD dosyasında:
$ cat .git/HEAD
ref: refs/heads/main
HEAD normalde bir commit'i değil, bir branch'i gösterir. "Şu an main'deyim" der. Commit attığında Git önce HEAD'e bakar, hangi branch'i gösterdiğini bulur ve o branch'i yeni commit'e taşır. Yani pointer'ı gösteren bir pointer. Sokak tabelası benzetmesiyle: branch'ler sokak adları, HEAD de haritadaki "buradasınız" işareti. İşaret bir sokağın üzerinde durur ve sen yürüdükçe o sokakla birlikte ilerler.
Branch Açmak ve Branch'ler Arasında Gezmek
İstek listesi özelliği için bir branch açalım:
$ git branch wishlist
$ git branch -v
discount 86303f4 Add discount to cart total
* main 5bcf046 Add Snow Crash
wishlist 5bcf046 Add Snow Crash
git branch wishlist sadece pointer'ı oluşturur, seni o branch'e götürmez. Yıldız hâlâ main'in yanında ve iki branch aynı commit'i gösteriyor. Geçmek için git switch kullanırsın:
$ git switch wishlist
Switched to branch 'wishlist'
$ cat .git/HEAD
ref: refs/heads/wishlist
$ git add src/wishlist.js
$ git commit -m "Add wishlist"
[wishlist a170771] Add wishlist
1 file changed, 9 insertions(+)
create mode 100644 src/wishlist.js
$ git branch -v
discount 86303f4 Add discount to cart total
main 5bcf046 Add Snow Crash
* wishlist a170771 Add wishlist
HEAD artık wishlist'i gösteriyor. Yeni commit'le birlikte wishlist bir adım ilerledi, main yerinde kaldı. Commit satırındaki [wishlist a170771] da commit'in hangi branch'e yazıldığını söylüyor. Grafikte durum şöyle:
$ git log --oneline --graph --all
* a170771 Add wishlist
* 5bcf046 Add Snow Crash
| * 86303f4 Add discount to cart total
|/
* 6651492 Reword README
* a5a7a57 Update book prices
* 6dde20b Add cart total
* f9b9660 Add .gitignore
* 02ea975 Add book list
* 7ff0565 Add README
main'e döndüğünde working tree da o branch'in snapshot'ına döner. src/wishlist.js diskten kaybolur, çünkü main'in commit'inde yok. Silinmedi, wishlist'e geçtiğin an geri gelir:
$ git switch main
Switched to branch 'main'
$ ls src
books.js
cart.js
$ git switch -
Switched to branch 'wishlist'
$ git switch -
Switched to branch 'main'
git switch -, terminaldeki cd - gibi bir önceki branch'e döner; iki branch arasında gidip gelirken çok işe yarar. Branch açıp aynı anda ona geçmek için ise iki komut yerine git switch -c yazarsın:
$ git switch -c search
Switched to a new branch 'search'
$ git add src/search.js
$ git commit -m "Add title search"
[search bd6dc8a] Add title search
1 file changed, 7 insertions(+)
create mode 100644 src/search.js
Yeniden Adlandırmak ve Silmek
Branch'in adını beğenmedin mi? -m ile değiştirirsin. Pointer aynı commit'i göstermeye devam eder, sadece dosyanın adı değişir:
$ git branch -m search title-search
$ git branch
discount
main
* title-search
wishlist
Silmek için -d kullanılır. Ama Git, başka hiçbir branch'ten ulaşılamayan commit'leri olan bir branch'i sessizce silmez:
$ git switch main
Switched to branch 'main'
$ git branch -d title-search
error: the branch 'title-search' is not fully merged
hint: If you are sure you want to delete it, run 'git branch -D title-search'
hint: Disable this message with "git config set advice.forceDeleteBranch false"
$ git branch -D title-search
Deleted branch title-search (was bd6dc8a).
-d güvenli silmedir: branch'in commit'leri başka bir yere merge edilmişse siler, edilmemişse durur. -D ise "biliyorum, yine de sil" demektir. Silinen şey yine yalnızca pointer. bd6dc8a commit'i bir süre daha repo'da durur ve çıktıdaki hash'i not ettiysen git branch title-search bd6dc8a ile geri getirebilirsin. Not etmediysen reflog var; ona kurtarma yazısında geleceğiz.
git branch -a ise remote-tracking branch'leri de listeler. Henüz bir remote eklemediğimiz için şu an git branch ile aynı çıktıyı verir; remote yazısında remotes/origin/main gibi satırlarla dolacak.
Detached HEAD

Eski bir commit'e bakmak istediğinde ne olur? git switch'e bir hash verirsen önce itiraz eder:
$ git switch a5a7a57
fatal: a branch is expected, got commit 'a5a7a57'
hint: If you want to detach HEAD at the commit, try again with the --detach option.
$ git switch --detach a5a7a57
HEAD is now at a5a7a57 Update book prices
$ cat .git/HEAD
a5a7a57e162d35efad9ce807a3711ebc054faa00
$ git status
HEAD detached at a5a7a57
nothing to commit, working tree clean
.git/HEAD'in içeriğine dikkat et. Artık bir branch adı değil, doğrudan bir hash var. Buna detached HEAD denir: "buradasınız" işareti bir sokağın üzerinden kalkmış, haritada boş bir noktaya bırakılmış. Bu durum hata değil. Eski bir sürümü derleyip denemek, bir hatanın hangi commit'te başladığına bakmak için gayet normal bir yer.
Sorun, burada commit atınca başlar. Commit atabilirsin, ama yeni commit'i hiçbir branch göstermez:
$ git commit -am "Try a lower price"
[detached HEAD 593811b] Try a lower price
1 file changed, 1 insertion(+), 1 deletion(-)
$ git switch main
Warning: you are leaving 1 commit behind, not connected to
any of your branches:
593811b Try a lower price
If you want to keep it by creating a new branch, this may be a good time
to do so with:
git branch <new-branch-name> 593811b
Switched to branch 'main'
Git seni uyarıyor: main'e geçtiğin an 593811b'ye işaret eden hiçbir şey kalmadı. Tabelası olmayan bir sokak gibi; ev orada ama kimse yolu bilmiyor.
Detached HEAD'den Çıkmak
İki çıkış yolu var. Detached HEAD'deyken yaptığın denemeyi saklamak istiyorsan, hâlâ oradayken git switch -c price-test ile bulunduğun yerde yeni bir branch açarsın. Commit'i attıktan sonra başka bir branch'e geçtiysen, uyarının önerdiği gibi hash'ten branch oluşturursun:
$ git branch price-test 593811b
$ git branch -v
discount 86303f4 Add discount to cart total
* main 5bcf046 Add Snow Crash
price-test 593811b Try a lower price
wishlist a170771 Add wishlist
Denemeyi saklamak istemiyorsan hiçbir şey yapmana gerek yok: git switch main ya da git switch - ile bir branch'e dönersin ve sahipsiz commit zamanla temizlenir.
checkout'un Eski Kullanımı
Eski yazılarda ve StackOverflow cevaplarında branch değiştirmek için hep git checkout göreceksin. git checkout main, git switch main ile aynı işi yapar; git checkout -b search ise git switch -c search ile. Hash verirsen sorgusuz detached HEAD'e geçer ve uzun bir açıklama basar:
$ git checkout 6dde20b
Note: switching to '6dde20b'.
You are in 'detached HEAD' state. You can look around, make experimental
changes and commit them, and you can discard any commits you make in this
state without impacting any branches by switching back to a branch.
If you want to create a new branch to retain commits you create, you may
do so (now or later) by using -c with the switch command. Example:
git switch -c <new-branch-name>
Or undo this operation with:
git switch -
Turn off this advice by setting config variable advice.detachedHead to false
HEAD is now at 6dde20b Add cart total
Açıklamanın bile git switch önerdiğine dikkat et. Sorun şu ki checkout iki ayrı işi tek komutta yapıyordu: branch değiştirmek ve dosyayı geri yüklemek. git checkout -- src/books.js dosyadaki değişikliğini sessizce siler. Aynı komutun bir kullanımı zararsız, öbürü yıkıcıydı. Git 2.23 bu iki işi git switch ve git restore olarak ayırdı. İkisi uzun süre experimental olarak işaretlendi, Git 2.51 ile bu işaret kalktı. checkout kaldırılmıyor; Git 3.0 planında da yerinde duruyor. Bu seride ise branch işleri için switch, dosya işleri için restore kullanacağız.
Sık Düşülen Tuzaklar
Birincisi, commit'lenmemiş değişiklikle branch değiştirmeye çalışmak. Değişikliğin hedef branch'te farklı olan bir dosyadaysa Git seni durdurur:
$ git switch discount
error: Your local changes to the following files would be overwritten by checkout:
src/books.js
Please commit your changes or stash them before you switch branches.
Aborting
Bu bir koruma. Ya commit'lersin, ya stash'e alırsın (stash'e ayrı bir yazıda geleceğiz), ya da git restore ile değişiklikten vazgeçersin. Değişiklik iki branch'te de aynı olan bir dosyadaysa Git onu yanında taşır; bu da "değişikliğim neden öbür branch'te de duruyor?" şaşkınlığının kaynağıdır. Commit'lenmemiş değişiklik hiçbir branch'e ait değildir, working tree'ye aittir.
İkincisi, git branch new-name yazıp yeni branch'e geçtiğini sanmak. Bu komut sadece pointer oluşturur; sonraki commit hâlâ eski branch'e gider. Yeni bir iş için alışkanlığın git switch -c olsun.
Üçüncüsü, detached HEAD'de saatlerce çalışıp sonra branch değiştirmek. Uyarıyı okumadan geçersen commit'lerin sahipsiz kalır. git status'un ilk satırında "HEAD detached" görüyorsan commit atmadan önce bir branch aç.
Dördüncüsü, branch adlarında boşluk ya da tutarsız biçim. Git boşluğu kabul etmez; ekipler genellikle feature/wishlist, fix/cart-total gibi önekli adlar kullanır. Bu önekler CI/CD (Continuous Integration / Continuous Delivery) tarafında da işe yarar, ör. sadece release/ ile başlayan branch'lerde deploy adımını çalıştırmak gibi.
Küçük Bir Deneme
★☆☆ git switch -c experiment ile yeni bir branch aç, .git/HEAD ve .git/refs/heads/experiment dosyalarının içeriğine bak. Bir commit at ve iki dosyadan hangisinin değiştiğini kontrol et.
★★☆ git switch --detach HEAD~2 ile iki commit geriye git, bir commit at ve bu sefer branch değiştirmeden önce git switch -c rescue ile commit'i kurtar. git log --oneline --graph --all ile sonucu çiz.
★★★ src/books.js'te bir değişiklik yap ve commit'lemeden önce wishlist'e, sonra discount'a geçmeyi dene. Biri izin verip öbürü neden reddediyor? Cevabı iki branch'in src/books.js içeriğiyle açıkla.
Elimizde artık birbirinden ayrılmış branch'ler var: wishlist, main'in hemen önünde; discount ise main'le yollarını ayırmış. Bir sonraki yazıda bu iki durumun merge'de neden farklı davrandığını, fast-forward ile three-way merge arasındaki farkı ve merge commit'in ne olduğunu göreceğiz.