Çoğu Git eğitimi commit atmayı öğretip orada bırakır. Cuma akşamı yanlış şeyi main‘e ittiğinde ve üç ekip arkadaşın tam o sırada çekerken ne yapılacağını anlatan neredeyse hiçbir kaynak yok. Bu Git rehberinde ilk commit’ten paralel branch’lere, merge conflict’lere, force push’a ve kaybolduğunu sandığın işi geri getirmeye kadar tüm yolu anlatıyorum. Örneklerin hepsi gerçekten çalıştırdığım küçük bir ekip projesinden geliyor, yani gördüğün komutlar ve çıktılar Git’in kendi yazdıklarıdır.
Ekiple ve projeyle tanışma
Git, kodun üzerinde birden fazla kişi çalıştığında can yakmaya başlar. Bu yüzden yazıdaki örnekler tek başına çalışan bir geliştirici yerine küçük bir ekibi izliyor.
Proje, kafe-menu adında bir kafenin menü sitesi. Dört kişi aynı anda üzerinde çalışıyor.
- Ayse menü kalemlerinden ve fiyatlardan sorumlu
- Mehmet sipariş mantığından sorumlu
- Can stillerden sorumlu
- Elif vegan seçeneklerden ve dokümantasyondan sorumlu
Zamanın çoğunda farklı dosyalarda, bir kısmında ise aynı satırlarda çalışıyorlar. Conflict’ler, hatalı push’lar ve “main’i kim bozdu” konuşmaları hep o ikinci kısımdan çıkar. Hepsini tek tek çalıştıracağız. İsimlerde Türkçe karakter kullanmadım, çünkü Git çıktılarında ve commit geçmişinde isimler tam bu haliyle görünüyor.
Tüm ekibi kendi bilgisayarında çalıştırmak
Bu yazıyı takip etmek için GitHub’a ya da üç arkadaşa ihtiyaç yok. Bare bir repository uzak sunucunun (GitHub’ın oynayacağı rolün) yerini alıyor, dört klasör de dört ekip arkadaşının bilgisayarı oluyor.
mkdir lab && cd lab git init --bare kafe-menu.git mkdir ayse && cd ayse git init # index.html ve README.md dosyalarını oluştur, sonra git add index.html README.md git commit -m "Add first menu page" git remote add origin ../kafe-menu.git git push -u origin main cd .. git clone kafe-menu.git mehmet git clone kafe-menu.git can git clone kafe-menu.git elif
Her klasörün içinde geçmişte kimin ne yaptığı görünsün diye o ekip arkadaşının kimliği tanımlanır.
git config user.name "Mehmet" git config user.email "mehmet@kafe.dev"
Yazıda “Mehmet’in klasöründe” ifadesi geçtiğinde önce cd mehmet yapıldığı anlaşılır.
Örnekler nasıl okunur
$ile başlayan satırlar yazdığın komutlardır. Altındaki metin Git’in verdiği çıktıdır.- Tüm örnekleri Git 2.43 sürümünde, varsayılan branch
mainolacak şekilde çalıştırdım. - Commit hash’leri senin makinende farklı çıkar. Geri kalan her şey aynı görünmeli.
- Her senaryo bir öncekinin üzerine kurulur, yani geçmiş gerçek bir proje gibi büyür.
Her şeyden önce bilinmesi gereken Git temelleri
Git nedir, ne değildir
Git, dağıtık bir sürüm kontrol sistemidir. Projenin zaman içindeki snapshot’larını kaydeder. Böylece neyin, kim tarafından, ne zaman değiştirildiği görülür ve herhangi bir eski duruma dönülebilir. Dağıtık olması, her klonun yalnızca son dosyaları değil tüm geçmişi taşıması demektir.
Git ile GitHub aynı şey değil. Git, bilgisayarında çalışan araçtır. GitHub, GitLab ve Bitbucket ise repository’nin ortak bir kopyasını barındıran ve üzerine pull request, code review ve issue takibi ekleyen sitelerdir.
Kurulum ve ilk ayarlar
# macOS brew install git # Windows winget install --id Git.Git -e --source winget # Debian ve Ubuntu sudo apt install git git --version
Git’e kim olduğun söylenir. Her commit bu kimliği taşır.
git config --global user.name "Adın Soyadın" git config --global user.email "sen@ornek.com" git config --global init.defaultBranch main git config --global core.editor "code --wait"
Sonradan baş ağrıtmaması için iki ayar daha yapılır. İlki, farklı işletim sistemlerindeki satır sonu farklarını halleder (Windows’ta true, macOS ve Linux’ta input kullanılır). İkincisi, yüz kez yazacağın komutlara kısa takma adlar verir.
git config --global core.autocrlf input git config --global alias.st "status -sb" git config --global alias.lg "log --oneline --graph --all --decorate"
Tüm ayarlar git config --list ile kontrol edilir.
Üç alan ve geri kalan her şeyi açıklayan tek fikir
Projedeki her dosya üç yerden birinde durur.
- Çalışma dizini (working directory). Dosyaların gördüğün ve düzenlediğin hali.
- Staging area (index de denir). Bir sonraki commit’in taslağı. Neyin gireceğine sen karar verirsin.
- Repository.
.gitklasöründe duran, commit’lerden oluşan kalıcı geçmiş.
Commit, projenin bir snapshot’ı artı bir önceki commit’e (parent) işaret eden bir göstericidir. Branch, tek bir commit’i gösteren ve taşınabilen bir etikettir. HEAD ise şu an nerede olduğunu söyleyen etikettir ve genellikle bir branch’i gösterir.
[embed: node/ff1c199d-5c49]
Bu son paragrafı aklda tutmak gerekir. Bu yazıdaki geri alma komutlarının neredeyse hepsi bu etiketlerden birini taşımayı ya da dosyaları üç alan arasında kopyalamayı anlatır. Bu oturduktan sonra reset, revert ve restore rastgele büyüler gibi görünmez.

İlk repository ve ilk commit
Ayse projeyi başlatır. git status, en sık çalıştıracağın komuttur.
$ git init $ git status On branch main No commits yet Untracked files: (use "git add <file>..." to include in what will be committed) README.md index.html nothing added to commit but untracked files present (use "git add" to track)
İki dosya staging area’ya alınır, sonuç kontrol edilir, sonra commit atılır.
$ git add index.html README.md $ git status --short A README.md A index.html $ git commit -m 'Add first menu page' [main (root-commit) 45db0c3] Add first menu page 2 files changed, 6 insertions(+) create mode 100644 README.md create mode 100644 index.html
Kısa status çıktısında ilk sütun staging area’yı, ikinci sütun çalışma dizinini gösterir. A staging area’ya eklendi demektir. M değişti ama stage edilmedi, M hem değişti hem stage edildi anlamına gelir. Bu harfleri yazı boyunca göreceksin.
Okunabilir commit mesajı yazmak
Commit mesajı, gece yarısı senin kodunda hata arayan ekip arkadaşına bırakılan nottur ve o ekip arkadaşı çoğu zaman yine sensin. Üç alışkanlık çok işe yarar.
- Konu satırı emir kipinde ve kısa yazılır, örneğin
Add cortado to menu. - Commit’in ne yaptığı yazılır, ne hissedildiği değil.
fix,updatevechangeskimseye bir şey anlatmaz. - Her commit tek bir fikri taşır. Küçük commit’ler kolay incelenir, kolay geri alınır ve nadiren conflict üretir.
.gitignore içine neler girer
Bazı dosyalar repository’ye hiç girmemelidir. Bağımlılıklar, build çıktıları, log dosyaları ve içinde gizli bilgi bulunan yerel ayarlar bunlara örnektir. Proje kökündeki .gitignore dosyasına yazılırlar.
node_modules/ dist/ *.log config.local
Bir dosya zaten takip ediliyorsa onu .gitignore‘a eklemek hiçbir şey yapmaz. Git, dosya index’ten çıkarılana kadar takibe devam eder. --cached bayrağı dosyayı diskten silmeden Git’ten çıkarır.
git rm --cached config.local echo "config.local" >> .gitignore git add .gitignore git commit -m "Stop tracking config.local"
Bu yalnızca gelecekteki takibi durdurur. Bir gizli bilgi zaten commit edildiyse geçmişte durmaya devam eder. Bunun nasıl halledildiği sonraki bölümlerde anlatılıyor.
Uzak repository ile çalışmak
Remote, repository’nin başka bir yerdeki kopyasıdır ve genellikle GitHub’da ya da şirket sunucusunda durur. Varsayılan remote’un adı origin‘dir. Dört komut işü makine ile remote arasında taşır.
git clone <url>uzak repository’yi tüm geçmişiyle makineye kopyalar.git fetchremote’taki yeni commit’leri indirir ama dosyalara ve branch’e dokunmaz.git pull,fetchve ardından mevcut branch’e merge işlemidir.git pushcommit’leri remote’a yükler.
Bir branch ilk kez push edilirken -u eklenir, böylece Git upstream’i hatırlar. Bundan sonra çıplak git push ve git pull nereye gideceğini bilir.
git push -u origin main
git remote -v remote’ları ve adreslerini listeler. git branch -vv hangi yerel branch’in hangi uzak branch’i takip ettiğini ve ne kadar ileride ya da geride olduğunu gösterir.
İlk reddedilen push
Mehmet sipariş listesini ekler ve push eder. Birkaç dakika sonra Ayse menüye bir cortado ekler ve o da push eder. Henüz pull yapmamıştır, Git onu durdurur.
$ git push To ../kafe-menu.git ! [rejected] main -> main (fetch first) error: failed to push some refs to '../kafe-menu.git' hint: Updates were rejected because the remote contains work that you do not hint: have locally. This is usually caused by another repository pushing to hint: the same ref. If you want to integrate the remote changes, use hint: 'git pull' before pushing again.
Bir şey bozuk değil. Git, Mehmet’in commit’ini onu içermeyen bir geçmişle ezmeyi reddediyor. Güvenli alışkanlık önce fetch yapıp bakmak, sonra birleştirmektir.
$ git fetch From ../kafe-menu 45db0c3..24a4468 main -> origin/main $ git status On branch main Your branch and 'origin/main' have diverged, and have 1 and 1 different commits each, respectively. $ git log --oneline --graph --all * 7506661 Add cortado to menu | * 24a4468 Add order list |/ * 45db0c3 Add first menu page
Graf durumu tam olarak gösteriyor. İkisi de 45db0c3‘ten başladı ve her biri bir commit ekledi. Geçmişler ayrıştı. Ayse, Mehmet’in işini git pull ile alır, bu bir merge commit üretir, sonra push eder.
$ git pull Merge made by the 'ort' strategy. orders.js | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 orders.js $ git log --oneline --graph --all * 40d5bd7 Merge branch 'main' of ../kafe-menu |\ | * 24a4468 Add order list * | 7506661 Add cortado to menu |/ * 45db0c3 Add first menu page $ git push To ../kafe-menu.git 24a4468..40d5bd7 main -> main
Bu merge commit doğrudur ama gürültü üretir, yoğun bir ekipte onlarca tanesi birikir. Pek çok ekip git pull --rebase tercih eder. Bu komut yerel commit’leri az önce çekilenlerin üstüne yeniden oynatır ve geçmiş düz bir çizgi olarak kalır. Varsayılan hale getirmek mümkündür.
git config --global pull.rebase true
İki seçim de geçerli. Asıl önemli olan ekipte herkesin aynısını kullanması. Merge ile rebase arasındaki farkı bir sonraki bölümde tekrar ele alıyoruz.
Branch’ler ve paralel çalışma
Branch, bir commit’i gösteren etikettir. Oluşturmak neredeyse hiç maliyet getirmez, bu yüzden Git her iş için bir branch açılmasını bekler. Dört kişi, işlerini birleştirmeye karar verdikleri ana kadar birbirinin ayağına basmadan aynı anda çalışabilir.
git switch -c feature/prices # yeni branch aç ve ona geç git switch main # geri dön git branch # yerel branch'leri listele git branch -d feature/prices # birleşmiş branch'i sil git push origin --delete feature/prices # remote'taki kopyasını sil
git switch, branch değiştirmenin modern komutudur. Eski eğitimlerde hala git checkout görülür ve aynı işi yapar. Bir isimlendirme düzeni seçilip ona sadık kalınır. feature/, fix/, hotfix/ ve docs/ gibi ön ekler yoğun bir repository’yi okunur tutar. git branch -d birleşmemiş işi olan bir branch’i silmeyi reddeder, -D ise zorla siler. Bu yüzden -D kullanırken dikkatli olmak gerekir.
Dört kişi, dört branch
Pazartesi sabahı ve herkes aynı main‘den başlıyor.
- Ayse fiyatları güncellemek ve bir mocha eklemek için
feature/pricesaçar. - Mehmet sipariş mantığı için iki commit’lik
feature/ordersaçar. - Can ilk stil dosyasıyla
feature/stylesaçar. - Elif yulaf sütü seçeneği için
feature/vegan, proje açıklaması içindocs/readmeaçar.
Herkes commit atar ve git push -u origin <branch> çalıştırır. Sonra Ayse fetch yapıp bütün tabloya bakar.
$ git fetch From ../kafe-menu * [new branch] docs/readme -> origin/docs/readme * [new branch] feature/orders -> origin/feature/orders * [new branch] feature/styles -> origin/feature/styles * [new branch] feature/vegan -> origin/feature/vegan $ git branch -vv * feature/prices 9a38da7 [origin/feature/prices] Update prices and add mocha main 40d5bd7 [origin/main] Merge branch 'main' of ../kafe-menu $ git log --oneline --graph --all * 9a38da7 Update prices and add mocha | * 98c1421 Describe the project |/ | * 8d8621f Add order total | * 8e89a06 Add removeOrder |/ | * f33de8a Add base styles |/ | * 94d375d Add oat milk option and flat white |/ * 40d5bd7 Merge branch 'main' of ../kafe-menu
Aynı commit’ten beş çizgi çıkıyor. Kimse kimseyi beklemiyor. git log --oneline --graph --all, her şeyin nerede durduğunu anlamak için en faydalı komuttur ve başta tanımlanan git lg takma adı olarak durmayı hak eder.
Merge ile branch’leri birleştirmek
Ayse mevcut durumu sürüm olarak etiketler ve ondan bir release branch’i açar. İleride lazım olacak.
$ git tag -a v1.0 -m 'First release' $ git push origin v1.0 $ git branch release/1.0 v1.0 $ git push -u origin release/1.0
Sonra biten branch’leri main‘e birleştirir. İlk sıra README branch’inde. Elif branch açtıktan sonra main‘e kimse dokunmadı, yani Git’in yeni bir commit üretmesine gerek yok. Sadece main etiketini ileri taşır.
$ git merge --ff-only origin/docs/readme Updating 40d5bd7..98c1421 Fast-forward README.md | 2 ++ 1 file changed, 2 insertions(+)
Bu bir fast-forward merge. --ff-only bayrağı, fast-forward mümkün değilse Git’in merge’ü reddetmesini sağlar ve iyi bir güvenlik ağıdır.
Mehmet’in branch’i farklı. main ilerledi, yani iki tarafta da yeni commit’ler var. Git iki parent’lı bir merge commit üretir. --no-ff bayrağı, fast-forward mümkün olsa bile bu commit’i üretir ve özelliğin geçmişte tek bir bütün olarak görünmesini sağlar.
$ git merge --no-ff origin/feature/orders -m 'Merge feature/orders' Merge made by the 'ort' strategy. orders.js | 2 ++ 1 file changed, 2 insertions(+) $ git log --oneline --graph -8 * 694033f Merge feature/orders |\ | * 8d8621f Add order total | * 8e89a06 Add removeOrder * | 98c1421 Describe the project |/ * 40d5bd7 Merge branch 'main' of ../kafe-menu
Üçüncü bir seçenek squash merge. git merge --squash feature/orders branch’in tüm değişikliklerini stage edilmiş tek bir değişiklik olarak toplar ve commit’i sen atarsın. Pek çok ekip bunu pull request’lerde kullanır, çünkü main geçmişinde özellik başına tek commit kalır. Bedeli, branch’in tek tek commit’lerinin main‘de görünmemesidir.
Rebase ile branch’i güncel tutmak
Can’ın feature/styles branch’i main ilerlemeden önce açılmıştı. Branch’i artık eski bir taban üzerinde duruyor. Pull request açmadan önce en son main‘in üzerine inşa etmek istiyor. main‘i branch’ine merge etmek yerine rebase yapar. Rebase, commit’lerini kenara alır, branch’i main‘in ucuna taşır ve commit’leri tek tek üzerine yeniden oynatır.
$ git rebase origin/main Rebasing (1/1)Successfully rebased and updated refs/heads/feature/styles. $ git log --oneline --graph -6 * dff65e7 Add base styles * 694033f Merge feature/orders |\ | * 8d8621f Add order total | * 8e89a06 Add removeOrder * | 98c1421 Describe the project |/ * 40d5bd7 Merge branch 'main' of ../kafe-menu
Commit’inin hash’ine bakılır. Önce f33de8a idi, şimdi dff65e7. Bir commit’in hash’i parent’ına bağlıdır, yani yeni bir parent’a sahip commit yeni bir commit’tir. Sonuç merge commit’siz düz bir çizgi. Bir sonraki push’un neden başarısız olduğu da buradan gelir.
$ git push ! [rejected] feature/styles -> feature/styles (non-fast-forward) $ git push --force-with-lease + f33de8a...dff65e7 feature/styles -> feature/styles (forced update)
Remote hala eski f33de8a‘yı tutuyor, Can’ın branch’inde ise onun yeniden yazılmış hali var, bu yüzden normal push reddedilir. --force-with-lease ile push etmek üzerine yazmanın dikkatli yoludur. Yalnızca remote branch, Can’ın son gördüğü yerdeyse başarılı olur. Çıplak --force bu kontrolü atlar. İleriki bir bölüm --force-with-lease‘in seni tam olarak nerede koruduğunu ve nerede korumadığını gösteriyor.
Branch birleştirmenin iki yolu arasındaki seçim şuna dayanır.
| Merge | Rebase | |
|---|---|---|
| Geçmiş | Gerçekte olanları korur, bir merge commit ekler | Tek düz çizgi |
| Mevcut commit’ler | Dokunulmaz | Yeni hash’lerle yeniden yazılır |
| Ortak branch’lerde güvenli | Evet | Hayır |
| Tipik kullanım | Biten özelliği main‘e almak | Kendi özellik branch’ini güncellemek |
Rebase’in altın kuralı basit. Yalnızca sende olan commit’ler rebase edilir. Başka insanlar bir branch’in üzerine inşa etmeye başladıysa merge kullanılır.
İş yarıda kesilince stash
Ayse feature/prices üzerinde bir Americano eklerken yarıda bir hata bildirimi gelir. Sayfada charset tanımı yok ve özel karakterler bozuk görünüyor. main‘i hemen düzeltmesi gerekiyor.
$ git status --short M index.html $ git switch main error: Your local changes to the following files would be overwritten by checkout: index.html Please commit your changes or stash them before you switch branches. Aborting
Git onun bitmemiş işini koruyor. Çözüm stash. Commit edilmemiş değişiklikleri bir rafa kaldırır ve temiz bir çalışma dizini bırakır.
$ git stash push -m 'wip americano'
Saved working directory and index state On feature/prices: wip americano
$ git stash list
stash@{0}: On feature/prices: wip americano
$ git switch main
$ git switch -c hotfix/charset
$ git commit -am 'Set utf-8 charset'
[hotfix/charset f443f7e] Set utf-8 charset
$ git switch main
$ git merge --no-ff hotfix/charset -m 'Merge hotfix/charset'
$ git push
1388507..80149ff main -> main
Hotfix yayında. Ayse kendi branch’ine döner ve işini raftan alır.
$ git switch feature/prices
$ git stash pop
On branch feature/prices
Changes not staged for commit:
modified: index.html
Dropped refs/stash@{0} (64a79e63427f687a4396e8256ccda0476e34ccf6)
git stash pop stash’i uygular ve listeden siler. git stash apply uygular ama listede tutar, aynı değişikliği iki branch’te denemek istediğinde işe yarar. Yeni, takip edilmeyen dosyaları da rafa kaldırmak gerekirse git stash push komutuna -u eklenir.
Worktree ile iki branch’i aynı anda açık tutmak
Hızlı bir sapma için stash yeterli. Daha uzun bir sapmada ikinci bir çalışma dizini daha rahattır. git worktree, aynı repository’nin başka bir branch’ini geçmişi kopyalamadan ayrı bir klasöre açar.
$ git worktree add ../ayse-hotfix -b hotfix/footer origin/main Preparing worktree (new branch 'hotfix/footer') HEAD is now at 80149ff Merge hotfix/charset $ git worktree list .../lab/ayse 88fdbd1 [feature/prices] .../lab/ayse-hotfix 80149ff [hotfix/footer] $ git worktree remove ../ayse-hotfix
Ayse artık editörünü feature/prices üzerinde açık tutabilir, hotfix’i diğer klasörde yapabilir, geri alması ya da stash’lemesi gereken bir şey kalmaz. Tek bir sınır var. Aynı branch iki worktree’de birden açık olamaz.
Tek bir commit’i başka branch’e taşımak için cherry-pick
Charset düzeltmesi main‘de ama yayındaki sürüm hala release/1.0 üzerinden çalışıyor ve ona da bu düzeltme lazım. git cherry-pick tek bir commit’i mevcut branch’in üzerine kopyalar.
$ git switch release/1.0 $ git cherry-pick f443f7e Auto-merging index.html CONFLICT (content): Merge conflict in index.html error: could not apply f443f7e... Set utf-8 charset
Tek satırlık bir değişiklikte conflict çıktı. Release branch’inde, yeni satırın hemen yanında duran stylesheet satırı yok. Git yeni satırın nereye ait olduğunu bilemiyor. Conflict’leri bir sonraki bölümde parça parça ele alıyoruz. Şimdilik dosya şöyle görünüyor.
<<<<<<< HEAD ======= <meta charset="utf-8"> <link rel="stylesheet" href="style.css"> >>>>>>> f443f7e (Set utf-8 charset) <h1>Kafe Menu</h1>
Release branch’inde stylesheet yok, bu yüzden Ayse yalnızca meta satırını tutar, dosyayı stage eder ve devam eder.
$ git add index.html $ git cherry-pick --continue [release/1.0 673f8f1] Set utf-8 charset 1 file changed, 1 insertion(+)
Aynı düzeltme artık geçmişte iki kez var, main‘de f443f7e, release branch’inde 673f8f1 olarak. İkisi de aynı değişikliği içeriyor ama farklı commit’ler. Cherry-pick’te bu normaldir ve onu seyrek kullanmak için bir sebeptir.
Merge conflict’ler adım adım
Conflict bir hata değildir. İki kişinin aynı satırları değiştirdiğini ve Git’in kimin haklı olduğunu tahmin etmeyeceğini söyleyen bir uyarıdır. Git geri kalan her şeyi kendi başına birleştirebilir, yalnızca bir insan kararının gerektiği noktalarda durur.
Conflict’ler merge, rebase, cherry-pick, stash pop ve revert sırasında çıkabilir. İşaretler her yerde aynı görünür ve çıkış yolu her zaman aynı üç hamledir. Dosya son haline getirilir, git add ile stage edilir, sonra Git’e devam etmesi söylenir.
Merge sırasında conflict
Ayse’nin feature/prices branch’i main‘e merge edildi. Espresso ve latte fiyatlarını değiştirdi, bir mocha ve bir Americano ekledi. Elif’in feature/vegan branch’i aynı latte satırını değiştirdi ve aynı yere bir flat white ekledi. Elif en güncel main‘i kendi branch’ine merge ettiğinde Git durur.
$ git merge origin/main Auto-merging index.html CONFLICT (content): Merge conflict in index.html Automatic merge failed; fix conflicts and then commit the result. $ git status On branch feature/vegan You have unmerged paths. (fix conflicts and run "git commit") (use "git merge --abort" to abort the merge) Unmerged paths: (use "git add <file>..." to mark resolution) both modified: index.html
Dosyanın orta kısmında artık iki sürüm birden duruyor.
<ul> <<<<<<< HEAD <li>Espresso 60</li> <li>Latte 75 (oat milk +10)</li> <li>Cortado 70</li> <li>Flat White 80</li> ======= <li>Espresso 65</li> <li>Latte 80</li> <li>Cortado 70</li> <li>Mocha 85</li> <li>Americano 55</li> >>>>>>> origin/main </ul>
<<<<<<< HEAD ile ======= arası senin tarafın, yani üzerinde olduğun branch. ======= ile >>>>>>> arası gelen taraf. Senin işin son metni üretmek ve üç işaret satırının hepsini silmek.
Panik düğmesi
Hazır değilsen git merge --abort her şeyi merge’den önceki haline aynen geri koyar. Rebase’te git rebase --abort, cherry-pick’te git cherry-pick --abort vardır. Her zaman geri çekilebilirsin.
$ git merge --abort $ git status --short
Boş çıktı, çalışma dizininin temiz olduğu, merge hiç olmamış gibi durduğu anlamına gelir.
Orijinali de görmek
Varsayılan işaretler iki tarafı gösterir, satırın kimse dokunmadan önceki halini saklar. Eksik olan bu parça çoğu zaman hikayenin tamamıdır. Daha iyi bir stil bunu da ekler. Git 2.35 ya da üstü gerekir.
git config merge.conflictStyle zdiff3
Elif tekrar merge eder ve aynı dosya şimdi şöyle görünür.
<ul> <<<<<<< HEAD <li>Espresso 60</li> <li>Latte 75 (oat milk +10)</li> ||||||| 40d5bd7 <li>Espresso 60</li> <li>Latte 75</li> ======= <li>Espresso 65</li> <li>Latte 80</li> >>>>>>> origin/main <li>Cortado 70</li> <<<<<<< HEAD <li>Flat White 80</li> ||||||| 40d5bd7 ======= <li>Mocha 85</li> <li>Americano 55</li> >>>>>>> origin/main </ul>
||||||| sonrasındaki bölüm ortak atadır. Durum artık okunabilir. Ayse espresso ve latte fiyatlarını değiştirdi. Elif espressoya dokunmadı, lattenin yanına yulaf sütü notu ekledi. İkisi de alta yeni satırlar ekledi. Kimse yanlış yapmadı, yani cevap her şeyi tutmak.
Dosya son haline getirilir. İşaretlerden hiçbir şey kalmaz.
<ul> <li>Espresso 65</li> <li>Latte 80 (oat milk +10)</li> <li>Cortado 70</li> <li>Mocha 85</li> <li>Flat White 80</li> <li>Americano 55</li> </ul>
Sonra dosya stage edilir ve merge bitirilir. Bundan önce kalan işaret var mı diye aramak gerekir, çünkü commit’lenmiş bir <<<<<<< klasik bir mahcubiyettir.
$ grep -c '<<<<<<<' index.html 0 $ git add index.html $ git commit [feature/vegan 9b216f7] Merge remote-tracking branch 'origin/main' into feature/vegan
Seçimin insana ait kısmı önemli. Git yalnızca hangi satırların çakıştığını bilir. Birleşen sonucun hâlâ mantıklı olup olmadığını bilmez, bu yüzden push’tan önce kod çalıştırılır ya da sayfa açılır. Araç olarak VS Code her conflict’in üstünde düğmeler gösterir, git mergetool ise yapılandırılmış görsel aracı açar.
Bir tarafın tamamen kazanması gerektiğinde iki kısayol var. git checkout --ours <file> ve git checkout --theirs <file> conflict’li dosyanın tek bir sürümünü alır. Bilinçli kullanılmalı, çünkü o dosyadaki diğer kişinin işini çöpe atarlar.
Rebase sırasında conflict
Can feature/order-log üzerinde addOrder fonksiyonuna loglama ekler. Bu arada Mehmet tam aynı satırı feature/order-limit üzerinde açık siparişleri 20 ile sınırlamak için değiştirir ve önce o merge eder. Can yeni main üzerine rebase ettiğinde Git durur.
$ git rebase origin/main
Rebasing (1/1)Auto-merging orders.js
CONFLICT (content): Merge conflict in orders.js
error: could not apply 4e6229b... Log every order
$ git status
interactive rebase in progress; onto 34b2c65
You are currently rebasing branch 'feature/order-log' on '34b2c65'.
(fix conflicts and then run "git rebase --continue")
(use "git rebase --skip" to skip this patch)
(use "git rebase --abort" to check out the original branch)
$ cat orders.js
const orders = [];
<<<<<<< HEAD
function addOrder(item) { if (orders.length < 20) orders.push(item); }
=======
function addOrder(item) { console.log("order", item); orders.push(item); }
>>>>>>> 4e6229b (Log every order)
İşaretler dikkatle okunmalı, çünkü rebase etiketleri tersine çevirir. HEAD, Mehmet’in sürümüdür, yani Can’ın üzerine rebase ettiği branch. Eşittir işaretlerinin altındaki sürüm Can’ın kendi commit’i, yani yeniden oynatılan. Merge sırasında ours, üzerinde olduğun branch demektir. Rebase sırasında ours, üzerine inşa ettiğin branch, theirs ise kendi işin demektir. Bunu Git’in conflict’li dosya için tuttuğu stage’lerle doğrulamak mümkün.
$ git show :2:orders.js | sed -n 2p
function addOrder(item) { if (orders.length < 20) orders.push(item); }
$ git show :3:orders.js | sed -n 2p
function addOrder(item) { console.log("order", item); orders.push(item); }
Stage 2 ours (Mehmet’in limiti), stage 3 theirs (Can’ın log satırı). Can iki davranışı da istiyor, bu yüzden birleşik satırı yazar, dosyayı stage eder ve devam eder.
function addOrder(item) { console.log("order", item); if (orders.length < 20) orders.push(item); }
$ git add orders.js $ git rebase --continue [detached HEAD b4e9e27] Log every order Successfully rebased and updated refs/heads/feature/order-log. $ git push --force-with-lease + 4e6229b...b4e9e27 feature/order-log -> feature/order-log (forced update)
Rebase commit’leri tek tek yeniden oynatır. Beş commit’lik bir branch beş kereye kadar durabilir, her commit için bir conflict. Düz geçmişin bedeli budur ve uzun branch’lerde sık rebase etmek ya da onun yerine merge etmek için bir sebeptir.
Diğer conflict türleri
Her conflict’te iki işaret bloğu olmaz. git status, hangi türde olduğunu iki harfli bir kodla söyler.
| Kod | Anlamı | Tipik sebep |
|---|---|---|
UU | İkisi de değiştirdi | İki kişi aynı satırları düzenledi |
UD | Karşı taraf sildi | Karşı tarafın sildiği bir dosyayı sen değiştirdin |
DU | Biz sildik | Senin sildiğin bir dosyayı karşı taraf değiştirdi |
AA | İkisi de ekledi | İki kişi aynı yolda dosya oluşturdu |
Bir silme conflict’i örneği. Elif, kullanılmadığını düşündüğü için style.css dosyasını main‘den siler. Can ise feature/dark-theme üzerinde aynı dosyaya yeni koyu tema renkleri eklemiştir.
$ git merge origin/main CONFLICT (modify/delete): style.css deleted in origin/main and modified in HEAD. Version HEAD of style.css left in tree. Automatic merge failed; fix conflicts and then commit the result. $ git status --short M index.html UD style.css
Burada düzenlenecek işaret yok. Git bir soru soruyor. Dosya kalacak mı, gidecek mi? Can soruyu yanıtlar, dosyayı tutar ve stage eder.
$ git add style.css $ git commit [feature/dark-theme 46b20d9] Merge remote-tracking branch 'origin/main' into feature/dark-theme
Silmeyi kabul etmek isteseydi git rm style.css çalıştırdı. Bu conflict aynı zamanda Elif’le konuşmak için bir sinyal, çünkü asıl sorun iki kişinin dosyanın var olup olmadığı konusunda hemfikir olmaması.
Stash pop sırasında conflict
Mehmet’in removeOrder içinde bitmemiş bir guard clause’u var. Onu stash’ler, pull yapar ve Elif’in aynı satırı değiştirip push ettiğini görür. Stash’i pop edince çakışır.
$ git stash pop
Auto-merging orders.js
CONFLICT (content): Merge conflict in orders.js
...
The stash entry is kept in case you need it again.
$ git stash list
stash@{0}: On main: guard removeOrder
Son satır önemli olan. Pop conflict üretince Git stash’i tutar, yani hiçbir şey kaybolmaz. Dosya her zamanki gibi çözülür, stage edilir ve sonuç doğru olduğunda stash git stash drop ile elle silinir.
En başından daha az conflict
Ekip çalışmasından conflict’leri tamamen kaldırmak mümkün değil ama onları nadir ve küçük tutmak mümkün.
- Küçük ve sık commit atılır, günde birkaç kez pull ya da fetch yapılır.
- Branch’ler kısa tutulur, çünkü iki hafta yaşayan bir branch iki haftalık başkasının değişikliğini biriktirir.
- Bir formatter üzerinde anlaşılır, böylece kimse dosyayı yeniden biçimlendirip her satıra dokunmaz.
- Büyük dosyalar bölünür. Farklı dosyalarda çalışan iki kişi çakışmaz.
- Sahibi olan ekip arkadaşına bir şey değiştirmek üzere olduğun söylenir.
git config --global rerere.enabled trueaçılır. Git bir conflict’i nasıl çözdüğünü hatırlar ve aynı conflict’i tekrar gördüğünde aynı çözümü uygular. Tekrarlanan rebase’lerde çok işe yarar.
Bir yazım hatasından push edilmiş commit’e kadar geri almak
Git’te “geri almak” farklı şeyler anlamına gelir ve doğru komut tek bir soruya bağlıdır. Hata şu an nerede duruyor? Çalışma dizininde, staging area’da, yerel bir commit’te, çoktan push ettiğin bir commit’te ya da bütün ekibin çektiği bir merge’de olabilir. Her yerin kendi komutu var ve tehlikeli hatalar yanlış komut kullanınca ortaya çıkar.
[embed: node/ac9447a6-8e29]
Commit’ten önce
En basit durum, istemediğin bir değişiklik. git restore, bir dosyanın commit edilmiş halini çalışma dizinine geri kopyalar.
$ git status --short M index.html $ git restore index.html $ git status --short
İkinci status boş çıkar, yani dosya son commit’lenmiş haline dönmüştür. Buna dikkat edilmeli. Geri yüklediğin commit edilmemiş bir değişiklik gider ve Git’in elinde kopyası yoktur.
Stage’den çıkarmak da --staged ile aynı mantıkla çalışır. Dosya düzenlemelerini korur, yalnızca staging area’dan çıkar.
$ git add README.md $ git status --short M README.md $ git restore --staged README.md $ git status --short M README.md
restore, herhangi bir eski commit’ten ya da tag’den de dosya getirebilir. Ayse sayfanın v1.0‘da nasıl göründüğünü görmek istiyor.
$ git restore --source=v1.0 index.html $ git diff --stat index.html | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) $ git restore index.html
İlk komut eski sürümü çalışma dizinine getirdi, ikincisi güncel sürümü geri koydu. Takip edilmeyen dosyalar farklı bir konu, çünkü Git onları henüz tanımıyor. git clean onları siler. Önce her zaman deneme çalıştırması yapılır.
$ git clean -n Would remove debug.log Would remove notes.tmp $ git clean -f Removing debug.log Removing notes.tmp
Son commit’i amend ile düzeltmek
Ayse commit atar ve mesajda bir yazım hatası fark eder.
$ git commit -am 'Add flter coffee' [main 4abdd9a] Add flter coffee $ git commit --amend -m 'Add filter coffee' [main c364dc1] Add filter coffee
Sonra README’yi güncellemeyi unuttuğunu fark eder. Dosyayı stage eder ve mesajı değiştirmeden tekrar amend eder.
$ git add README.md $ git commit --amend --no-edit [main 21e766c] Add filter coffee 2 files changed, 2 insertions(+)
Hash’e bakılır. 4abdd9a‘dan c364dc1‘e, oradan 21e766c‘ye değişti. Amend bir commit’i düzenlemez, yerine yenisini koyar. Commit yalnızca makinende duruyorken bu zararsızdır. Push ettikten sonra amend etmek ortak geçmişi yeniden yazmak demektir ve el sürülmemelidir.
Reset ve üç modu
git reset mevcut branch’i başka bir commit’e taşır. Dosyalarına ne olduğu moda bağlıdır.
| Mod | Branch etiketi | Staging area | Çalışma dizini |
|---|---|---|---|
--soft | Taşınır | Değişiklikler stage’de kalır | Dokunulmaz |
--mixed (varsayılan) | Taşınır | Sıfırlanır, değişiklikler stage dışı kalır | Dokunulmaz |
--hard | Taşınır | Sıfırlanır | Sıfırlanır, değişiklikler gider |
Ayse bir scratch branch’inde üç küçük commit atar ve son commit üzerinde her modu dener.
$ git log --oneline -4 f53a789 Add note 3 666b1bb Add note 2 9aad7f0 Add note 1 21e766c Add filter coffee $ git reset --soft HEAD~1 $ git status --short M notes.md
Commit branch’ten gitti ama değişikliği staging area’da duruyor ve yeniden commit edilmeye hazır. “Çok erken commit attım” ya da “son üç commit’imi tek yapmak istiyorum” durumları için uygun mod bu.
$ git reset --hard ORIG_HEAD HEAD is now at f53a789 Add note 3 $ git reset --mixed HEAD~1 Unstaged changes after reset: M notes.md $ git status --short M notes.md
Değişiklik hala dosyada ve artık stage’de değil. Şimdi tehlikeli olan.
$ git reset --hard ORIG_HEAD HEAD is now at f53a789 Add note 3 $ git reset --hard HEAD~1 HEAD is now at 666b1bb Add note 2 $ git status --short $ git log --oneline -3 666b1bb Add note 2 9aad7f0 Add note 1 21e766c Add filter coffee
Üçüncü not geçmişten, staging area’dan ve dosyadan gitti. Yine de geri dönüşü var ve bu, denemeler arasında kullandığım satır.
$ git reset --hard ORIG_HEAD HEAD is now at f53a789 Add note 3
Reset, merge ya da rebase gibi riskli bir işlemden önce Git, branch’inin önceki konumunu ORIG_HEAD olarak saklar. Branch’i oraya geri göstermek işlemi geri alır. Bu işe yarar çünkü commit hala duruyor. reset --hard commit’i branch’ten kaldırır, Git’in deposundan değil.
--hard‘ın asıl tehlikesi commit edilmemiş iştir. Bir commit neredeyse her zaman kurtarılabilir, bir sonraki bölüm bunu gösteriyor. Hiç commit etmediğin bir değişiklik ise kurtarılamaz. Reset’ten önce commit atmak ya da stash’lemek gerekir.
Yanlış branch’e commit atmak
Can bir sadakat kartı fikrine yeni bir özellik branch’inde başlamak istiyor. Önce geçiş yapmayı unutur ve doğrudan main‘e commit atar. Henüz push etmemiştir.
$ git commit -am 'Add loyalty card idea' [main 529d877] Add loyalty card idea $ git status -sb ## main...origin/main [ahead 1]
Düzeltme üç komut sürüyor. Mevcut commit’te yeni bir branch açar, bu işi güvene alır. main‘i remote ile eşleşecek şekilde geri taşır ve yeni branch’e geçer.
$ git branch feature/loyalty $ git reset --hard origin/main HEAD is now at b1f1aeb Save after removing an order $ git switch feature/loyalty $ git log --oneline -3 529d877 Add loyalty card idea b1f1aeb Save after removing an order d04a94d Remove unused styles
Sıra önemli. Önce branch açılır, sonra reset yapılır ve commit bir an bile üzerinde etiket olmayan halde kalmaz. Commit zaten main‘e push edildiyse reset yapılmaz, bir sonraki kısımdaki revert yolu kullanılır.
Push edilmiş commit için revert
Başka insanlar bir commit’i çekmiş olabilirse geçmişi yeniden yazmak masadan kalkar. Mehmet bir debug satırını push etti ve üzerine bir dokümantasyon commit’i geldi.
$ git log --oneline -3 9ef4e09 Document order storage 53f0b22 Add debug output 21e766c Add filter coffee
git revert, ters değişikliği uygulayan yeni bir commit üretir. Eski commit geçmişte kalır, sonrasındaki her şey de.
$ git revert --no-edit 53f0b22 [main d88473f] Revert "Add debug output" 1 file changed, 1 deletion(-) $ git push 9ef4e09..d88473f main -> main $ git log --oneline -4 d88473f Revert "Add debug output" 9ef4e09 Document order storage 53f0b22 Add debug output 21e766c Add filter coffee
Hiçbir şey yeniden yazılmadı, bu yüzden her ekip arkadaşı düzeltmeyi normal bir pull ile alır. Birden fazla commit’i tek değişiklik olarak geri almak için bir aralık commit’siz revert edilir, sonra tek commit atılır.
$ git revert --no-commit HEAD~2..HEAD $ git status --short M orders.js $ git commit -m 'Revert debug output' [main 1adee1e] Revert debug output 1 file changed, 2 deletions(-)
Bir revert, sonraki commit’lerin aynı satırlarla çakışırsa her conflict gibi durur. Çözülür, dosya stage edilir ve git revert --continue çalıştırılır.
Bir merge’i geri almak ve ardından gelen tuzak
Elif feature/special‘ı main‘e merge eder ve bir hata çıkar. Tüm özelliğin gitmesini istiyor. Merge commit’in iki parent’ı vardır, bu yüzden Git hangi tarafın tutulacağını bilmek ister. -m 1, ilk parent’ı yani merge ettiğin branch’i tut demektir.
$ git revert -m 1 HEAD --no-edit [main 8216539] Revert "Merge feature/special" 1 file changed, 1 deletion(-) $ grep -c Special index.html 0
Special gitti. Sonra Elif sorunu branch’te düzeltir ve tekrar merge eder. Ne olduğuna bakılır.
$ git merge --no-ff feature/special -m 'Merge feature/special again' Merge made by the 'ort' strategy. README.md | 1 + 1 file changed, 1 insertion(+) $ grep -c Special index.html 0
Merge başarılı oldu ama special geri gelmedi. Git, branch’in ilk commit’inin zaten merge edildiğini hatırlıyor ve revert yalnızca onun etkisini kaldırmıştı. Git’in gözünde eksik olan yalnızca yeni commit. Çıkış yolu revert’i revert etmek.
$ git revert --no-edit 8216539 [main aeaec53] Reapply "Merge feature/special" 1 file changed, 1 insertion(+) $ grep Special index.html <li>Special 99</li>
Şimdi özellik düzeltmesiyle birlikte geri geldi. Bir merge’i revert edip branch’i tekrar merge etmeyi planlıyorsan bunu hatırla. Deneyimli geliştiricileri de yanıltan bir tuzaktır.
Hangi geri alma komutu
| Durum | Komut | Ortak branch’te güvenli mi |
|---|---|---|
| Dosyadaki düzenlemeleri at | git restore <file> | Evet, ama düzenlemeler gider |
| Dosyayı stage’den çıkar | git restore --staged <file> | Evet |
| Son commit’i düzelt | git commit --amend | Yalnızca push’tan önce |
| Yerel commit’leri geri al, işi tut | git reset --soft ya da --mixed | Yalnızca push’tan önce |
| Yerel commit’leri geri al, işi at | git reset --hard | Yalnızca push’tan önce |
| Push edilmiş commit’i geri al | git revert <hash> | Evet |
| Push edilmiş merge’i geri al | git revert -m 1 <merge> | Evet |
| Kötü bir reset ya da rebase’ten geri dön | git reset --hard ORIG_HEAD | Yalnızca yerel |
Kaybolan işi reflog ile geri getirmek
Git hakkında en rahatlatıcı gerçek şu. Commit’leri kaybetmek çok zordur. Bir branch etiketi bir commit’ten uzaklaştığında commit haftalarca repository’de durur ve Git, etiketlerinin nerelerde bulunduğuna dair özel bir günlük tutar. O günlük reflog‘dur.
[embed: node/2bb53749-7276]
Reflog yereldir. Hiçbir zaman push edilmez ve kimse göremez. Varsayılan olarak kayıtlar 90 gün, erişilemez durumdakiler 30 gün yaşar. HEAD’i hareket ettiren her şey için kişisel geri alma geçmişin gibi düşünülür.
Bir reset –hard’ı geri almak
Ayse bir scratch branch’inde üç commit atar ve sonra üç adım geri reset eder. Üç commit de log’dan kaybolur.
$ git log --oneline -4 12c8d5f Idea 3 6315b95 Idea 2 f2d6d44 Idea 1 aeaec53 Reapply "Merge feature/special" $ git reset --hard HEAD~3 HEAD is now at aeaec53 Reapply "Merge feature/special"
Reflog’a bakar.
$ git reflog -5
aeaec53 HEAD@{0}: reset: moving to HEAD~3
12c8d5f HEAD@{1}: commit: Idea 3
6315b95 HEAD@{2}: commit: Idea 2
f2d6d44 HEAD@{3}: commit: Idea 1
aeaec53 HEAD@{4}: checkout: moving from main to scratch/reflog
HEAD’in her konumu en yeniden başlayarak listelenir. HEAD@{1}, HEAD’in bir hareket önce nerede olduğudur ve Ayse’nin istediği commit tam olarak odur.
$ git reset --hard HEAD@{1}
HEAD is now at 12c8d5f Idea 3
Üç fikir de geri geldi. İki şey hatırlanmalı. HEAD@{n} numaraları her yeni hareketle değişir, bu yüzden birini seçmeden önce liste okunur. Reflog yalnızca commit edilmiş durumları kaydeder. Hiç commit edilmemiş bir değişikliğin kaydı yoktur.
Silinen bir branch’i geri getirmek
Bir branch’i silmek yalnızca etiketi kaldırır. Ayse scratch branch’ini, birleşmemiş iş için güvenlik kontrolünü atlayan -D ile siler.
$ git branch -D scratch/reflog Deleted branch scratch/reflog (was 12c8d5f).
Mesaja bakılır. Git hash’i senin için yazdı, yani kurtarma tek satır.
$ git branch scratch/reflog-restored 12c8d5f $ git log --oneline -3 scratch/reflog-restored 12c8d5f Idea 3 6315b95 Idea 2 f2d6d44 Idea 1
Bu mesaj kaçırıldıysa git reflog silinen branch’in son commit’ini listeler, çünkü oradan ayrılırken HEAD orada duruyordu.
Rebase’i geri almak
Ayse bir deneyi main üzerine rebase eder ve pişman olur. Rebase commit’leri yeniden yazar, yani orijinal commit’in artık yeni bir hash’i var.
$ git rebase main
Successfully rebased and updated refs/heads/scratch/rebase.
$ git reflog -5
392636e HEAD@{0}: rebase (finish): returning to refs/heads/scratch/rebase
392636e HEAD@{1}: rebase (pick): Try an experiment
aeaec53 HEAD@{2}: rebase (start): checkout main
f2addd7 HEAD@{3}: commit: Try an experiment
40d5bd7 HEAD@{4}: checkout: moving from main to scratch/rebase
rebase (start) satırından hemen önceki kayıt eski uç, yani f2addd7. HEAD@{3}‘e reset edebilir ama Git bunu ORIG_HEAD olarak da saklamış ve bu hatırlaması daha kolay.
$ git reset --hard ORIG_HEAD HEAD is now at f2addd7 Try an experiment $ git log --oneline -3 f2addd7 Try an experiment 40d5bd7 Merge branch 'main' of ../kafe-menu 7506661 Add cortado to menu
Branch tam olarak rebase öncesi yerinde. ORIG_HEAD, bir sonraki riskli komutla üzerine yazılır, bu yüzden hemen kullanılır. Aksi halde reflog’a başvurulur.
Detached HEAD’de atılan commit’i kurtarmak
Bir commit’i, tag’i ya da uzak branch’i doğrudan checkout ettiğinde detached HEAD’desin. Orada atılan commit’ler hiçbir branch’e ait olmaz. Başka yere geçince onlara işaret eden bir şey kalmaz.
$ git switch --detach HEAD~2 HEAD is now at 8216539 Revert "Merge feature/special" $ git commit -m 'Experiment on detached HEAD' [detached HEAD 52a630d] Experiment on detached HEAD $ git switch main Warning: you are leaving 1 commit behind, not connected to any of your branches: 52a630d Experiment on detached HEAD 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> 52a630d
Git uyarır ve komutu bile yazar. Uyarıyı kaçırırsan hash reflog’da durur.
$ git branch rescue/detached 52a630d $ git log --oneline -2 rescue/detached 52a630d Experiment on detached HEAD 8216539 Revert "Merge feature/special"
Basit bir kural var. Detached HEAD’de yapılan işi tutmak istiyorsan, ayrılmadan önce ona bir branch ismi verilir.
Düşürülen bir stash’i geri getirmek
Stash’ler de commit’tir, yani düşürülen bir stash geri gelebilir. Git, drop ettiğin anda hash’ini yazar.
$ git stash push -m 'precious wip'
Saved working directory and index state On main: precious wip
$ git stash drop
Dropped refs/stash@{0} (8de51d696a07269a557982428985d1fd47e899ed)
Hash not edilmediyse Git’ten hiçbir şeyin işaret etmediği commit’ler istenir.
$ git fsck --no-reflog | grep 'dangling commit' dangling commit 392636e2c4085a6f1ba1b95776f4ea09414bd3e7 dangling commit 64a79e63427f687a4396e8256ccda0476e34ccf6 dangling commit 8de51d696a07269a557982428985d1fd47e899ed ...
Liste uzun olabilir ve stash’ler sıradan commit’lerin arasında görünür. Adaylar git show <hash> ya da git log -1 --format=%s <hash> ile kontrol edilir. Bir stash commit’inin konusu “On branch” ya da “WIP on branch” ile başlar, eşleşme kolaydır.
$ git stash store -m 'recovered wip' 8de51d6
$ git stash list
stash@{0}: recovered wip
git stash store commit’i stash listesine geri koyar ve normal git stash pop yine çalışır.
Force push sonrası kurtarma
Git’te en kötü gün, birinin bir ekip arkadaşının işinin üzerine force push yaptığı gündür. Laboratuvarda Ayse, Mehmet B isimli bir commit push ettikten sonra shared/demo üzerinde çıplak git push --force kullandı. Remote 7a22d4b‘den 1267686‘ya geri gitti ve B artık branch’te değildi.
Remote’un geri alması yok ama Mehmet’in kendi klonu hatırlıyor. Uzak branch takip eden branch’inin de reflog’u var.
$ git reflog show origin/shared/demo
O listedeki eski kayıt 7a22d4b‘yi, yani onun push ettiği commit’i gösteriyordu. Klonunda commit hala durduğu için onu yeni remote durumunun üzerine rebase etti.
$ git rebase origin/shared/demo
Rebase demo.txt içinde bir conflict ile durdu, çünkü ikisi de aynı satırlara dokunmuştu. Dosyayı A, A2 ve B kalacak şekilde çözdü, rebase’e devam etti ve normal push etti.
$ git push 1267686..56188f2 shared/demo -> origin/shared/demo
Ders şu. En son push eden kişinin klonu güvenlik ağıdır. Bir force push ters giderse her şeyden önce commit’lerini kaybeden ekip arkadaşından reflog’una bakması istenir. Kimsede yerel kopya yoksa GitHub gibi servisler push edilmiş commit’leri bir süre hash ile erişilebilir tutar, bu yüzden destek ekibiyle iletişime geçilir ya da repository’nin aktivite görünümüne bakılır.
Reflog’un kurtaramadıkları
| Kaybolan şey | Kurtarılabilir mi | Nerede aranır |
|---|---|---|
| Reset, rebase ya da amend ile kalkan commit | Evet | git reflog, ORIG_HEAD |
| Silinen branch | Evet | git reflog, silme mesajı |
| Düşürülen stash | Evet | git fsck --no-reflog, drop mesajı |
| Detached HEAD’deki commit | Evet | git reflog |
| Force push ile ezilen commit’ler | Evet, onlara sahip klonda | git reflog show origin/<branch> |
| Restore ya da reset ile kaybolan commit edilmemiş düzenlemeler | Hayır | Editör geçmişi, yedekler |
git clean ile silinen takipsiz dosyalar | Hayır | Çöp kutusu, yedekler |
git gc --prune=now sonrası her şey | Hayır | Hiçbir yerde |
Klasik Git hataları ve her birinin çözümü
Her ekip aşağı yukarı aynı bir düzine sorunla karşılaşır. Aşağıdaki tablo, yıllar önce masamın üstüne asmış olmayı dilediğim tablo. Her satır bu rehberde gördüğün bir çözüme işaret eder, yani aynı zamanda bir dizin gibi işler.
| Ne oldu | Ne görülür | Çözüm |
|---|---|---|
| Son commit mesajında yazım hatası | Henüz bir sorun görünmez | Push’tan önce git commit --amend -m 'yeni mesaj' |
| Son commit’e bir dosya eklemeyi unutma | Dosya modified ya da untracked görünür | git add <file>, sonra git commit --amend --no-edit |
| Yanlış branch’e commit atma | main, origin/main‘in ilerisinde | git branch yeni-branch, sonra git reset --hard origin/main, sonra git switch yeni-branch |
| Yanlış dosyayı stage etme | Dosya commit edilecek değişiklikler altında | git restore --staged <file> |
| Kötü bir commit’i push etme | Ekip arkadaşları çoktan çekti | git revert <hash> ve normal push |
| Push “fetch first” ile reddedildi | ! [rejected] main -> main (fetch first) | git pull --rebase, conflict varsa çöz, tekrar push |
| Merge yanlış gitti | Conflict işaretleri ya da beklenmedik sonuç | git merge --abort ya da git reset --hard ORIG_HEAD |
| Rebase yanlış gitti | Birçok commit’te conflict | git rebase --abort, bittiyse git reset --hard ORIG_HEAD |
| Reset sonrası kaybolan commit’ler | git log‘da yoklar | git reflog, sonra git reset --hard HEAD@{n} |
| Branch’i erken silme | Branch bulunamadı | git branch <isim> <silme mesajındaki ya da reflog'daki hash> |
| Detached HEAD’de kalan iş | Başka yere geçerken uyarı | git branch rescue <hash> |
| Yanlışlıkla düşürülen stash | git stash list boş | git fsck --no-reflog, sonra git stash store |
| Ekip arkadaşının üzerine force push | Onların commit’leri remote’tan kaybolur | Ekip arkadaşının git reflog show origin/<branch> ve rebase, push |
| Commit’e gizli anahtar girmesi | Geçmişte bir API anahtarı görünür | Önce anahtarı yenile, sonra git filter-repo |
| Yanlışlıkla büyük dosya commit’lemek | Klonlar yavaşlar, push reddedilir | filter-repo ile geçmişten çıkar, sonra Git LFS kullan |
| Branch değiştirilemiyor | “Your local changes would be overwritten” | Commit at ya da git stash push -u, sonra geç |
| Commit’lerde yanlış yazar | git log yanlış ismi gösterir | user.name ve user.email ayarla, sonuncu için git commit --amend --reset-author |
| Her diff’te satır sonu gürültüsü | Tüm dosyalar değişmiş görünür | * text=auto içeren bir .gitattributes ekle |
Dosya .gitignore‘a geç eklendi | .gitignore‘daki dosya hâlâ takipte | git rm --cached <file>, sonra commit |
Yakından bakılmaya değer üç hata
Zaten push edilmiş gizli bilgi. Dosyayı yeni bir commit’te silmek, onu yalnızca güncel sürümden gizler. Eski commit’ler hâlâ içinde tutar ve git log -S onları bulur. Tek tam çözüm git filter-repo ile geçmişi yeniden yazmaktır ve ilk adım her zaman anahtarı yenilemektir, çünkü remote’a değen bir gizli bilgi sızmış sayılır. Tam anlatım ileri seviye bölümde var.
“Zorla geçsin” refleksi. Reddedilen bir push sonrası çok kişi --force‘a uzanır. Uzanma. Reddedilmek, remote’ta senin sahip olmadığın commit’ler olduğu anlamına gelir. Fetch yapılır, git log HEAD..origin/main ile onlara bakılır, rebase ya da merge ile birleştirilir ve normal push edilir. Force push yalnızca kendi özellik branch’lerinde yapılır ve orada bile --force-with-lease daha güvenli biçimdir, ileri seviye bölüm bunu açıklıyor.
Panik. Bu tablodaki hemen her hata, durulursa kurtarılabilir. Biri işe yarar umuduyla daha fazla komut çalıştırılmaz. git status çalıştırılır, sonra git reflog, Git’in söylediği okunur ve ancak ondan sonra hareket edilir. Git commit edilmiş işi nadiren yok eder. Asıl zarar commit edilmemiş işten ve aceleyle yazılan ikinci komuttan gelir.
Ekip akışları, pull request’ler, tag’ler ve release’ler
Git’in yarısı komutlar, diğer yarısı ekibin o komutları nasıl kullanacağına dair verdiği sözdür. İyi bir akış main‘i sağlıklı tutar, incelemeleri kolaylaştırır ve yayına alacak güvenli bir yer sağlar.
Bir branch modeli seçmek
| Model | Nasıl çalışır | En uygun olduğu yer |
|---|---|---|
| Trunk-based | Kısa ömürlü branch’ler bir iki gün içinde main‘e alınır, çoğu zaman feature flag arkasında | Küçük ekipler, sürekli teslimat |
| GitHub Flow | Değişiklik başına bir branch, pull request, inceleme, main‘e merge, deploy | Çoğu web ürünü |
| Git Flow | main, develop, feature, release ve hotfix branch’leri | Birden fazla sürümü desteklenen sürümlü yazılımlar |
Bu rehberdeki gibi bir ekip için varsayılan tercihim, birkaç kesin kuralı olan GitHub Flow. main her zaman çalışır. Kimse ona doğrudan push etmez. Her değişiklik bir pull request ile gelir ve branch’ler merge’den kısa süre sonra silinir.
Günlük döngü
Ayse’nin her görevde izlediği rutin, komut olarak aşağıda.
$ git switch main $ git pull $ git switch -c feature/prices ... düzenle, test et, küçük adımlarla commit at ... $ git fetch $ git rebase origin/main $ git push -u origin feature/prices ... pull request aç, inceleme yorumlarını yanıtla ... $ git switch main $ git pull $ git branch -d feature/prices
Push’tan önceki rebase branch’i ekiple güncel tutar, böylece pull request sürpriz içermez. Branch zaten push edilmişse ve başkaları onu kullanıyorsa rebase yerine origin/main o branch’e merge edilir.
Hızlı merge edilen pull request’ler
Pull request, bir branch hakkında yapılan konuşmadır. Küçük olanlar dakikalar içinde incelenir, büyükler günlerce bekler. Birkaç alışkanlık yardım eder.
- Her pull request tek bir amacı taşır. Bir düzeltme ile bir refactor iki ayrı PR olur.
- Açıklamada ne değiştiği, neden değiştiği ve nasıl test edileceği yazılır.
- Yön konusunda geri bildirim isteniyorsa erkenden draft açılır.
- İnceleme sırasında düzeltmeler aynı branch’e push edilir. Pull request kendini günceller.
- Merge düğmesi bilinçli seçilir. Merge commit tüm geçmişi korur. Squash merge her pull request için tek düzgün bir commit verir. Rebase merge, her commit’i koruyan doğrusal bir geçmiş sağlar.
Laboratuvarda ekip --no-ff merge kullandı, böylece her özellik grafikte net bir kabarık olarak görünüyor.
$ git log --oneline --graph --all -8 * 694033f Merge feature/orders |\ | * 8d8621f Add order total | * 8e89a06 Add removeOrder * | 98c1421 Describe the project
Bir hosting servisinde pull request merge ettiğinde çıkan sonuç tam olarak budur.
Okunabilir commit’ler yazmak
Commit mesajı, gelecekteki sana ve iki yıl sonra git blame çalıştıran kişiye bırakılan bir nottur. Neredeyse her yerde işe yarayan biçim, kısa bir konu, bir boş satır ve isteye bağlı bir gövdedir.
Add order total to the cart The cart showed items but no sum, which confused customers. Compute the total from item prices and show it below the list.
Birkaç pratik kural.
- Konu 50 karaktere yakın tutulur ve emir kipinde yazılır. “Add” ve “Fix” kullanılır, “Added” ve “Fixes” değil.
- Gövdede neden açıklanır, çünkü ne yapıldığını diff zaten gösterir.
- Commit’ler küçük ve kendi başına yeterli olur, böylece her biri derlenir ve tek başına geri alınabilir.
- Changelog üreten ekipler sıklıkla
feat,fix,docsverefactorgibi ön eklerle Conventional Commits kullanır. Bu bir Git özelliği değil, bir gelenek. Bir araç kullanacaksa benimsenir.
Pull request öncesi yerel geçmişi toparlamak için git rebase -i iyi bir arkadaştır ve ileri seviye bölümde anlatılıyor.
main’i korumak
Barındırma servisinin ayarlarından main için branch protection açılır. Pull request, en az bir onay ve geçen kontroller zorunlu kılınır, force push engellenir. Bu, önceki bölümdeki hataların yarısını imkânsız hale getirir.
Tag’ler ve release’ler
Tag, tek bir commit üzerindeki kalıcı bir etikettir ve sürümler için idealdir. Annotated tag, etiketleyeni, tarihi ve mesajı saklar, bu yüzden release’ler için bu tür kullanılır.
$ git tag -a v1.0 -m 'First release' $ git push origin v1.0 To ../kafe-menu.git * [new tag] v1.0 -> v1.0
Tag’ler varsayılan olarak push edilmez, bu yüzden ikinci komut önemli. Numara kullanıcılara ne bekleyeceklerini söylesin diye semantic versioning kullanılır. 1.0.1 gibi bir patch hata düzeltir, 1.1.0 gibi bir minor uyumlu özellik ekler, 2.0.0 gibi bir major ise birşeyleri kırabilir.
Release branch’i ve hotfix
main ilerlediğinde ve 1.0 sürümünde bir hata çıktığında, bitmemiş özellikleri yayına almadan yayındaki kodu düzeltmek gerekir. Ekip tag’den bir branch açtı.
$ git branch release/1.0 v1.0 $ git push -u origin release/1.0 To ../kafe-menu.git * [new branch] release/1.0 -> release/1.0 branch 'release/1.0' set up to track 'origin/release/1.0'.
Ayse charset hatasını hotfix/charset branch’inde düzeltti ve bir pull request ile main‘e merge etti, commit f443f7e. Release’e aynı düzeltmeyi vermek için cherry-pick yaptı.
$ git switch release/1.0 $ git cherry-pick f443f7e Auto-merging index.html CONFLICT (content): Merge conflict in index.html error: could not apply f443f7e... Set utf-8 charset
Bir conflict, çünkü release branch’inde düzeltmenin hemen yanındaki main satırı yok. Dosyayı çözdü, stage etti ve devam etti.
$ git add index.html $ git cherry-pick --continue [release/1.0 673f8f1] Set utf-8 charset 1 file changed, 1 insertion(+) $ git push 40d5bd7..673f8f1 release/1.0 -> release/1.0
Düzeltme artık iki farklı hash ile iki branch’te duruyor, main‘de f443f7e, release’te 673f8f1. Cherry-pick’te bu normal. Release branch’i v1.0.1 olarak tag’lenir ve yayına alınır.
$ git tag -a v1.0.1 -m 'Charset hotfix' $ git push origin v1.0.1
Bir tag’i taşımak gerekirse yerelde ve remote’ta silinip yeniden oluşturulur. İnsanların kullandığı bir şey için bundan kaçınılır ve yeni bir sürüm numarası verilir.
İleri seviye Git araçları
Bu bölümü bilmeden yıllarca mutlu çalışılabilir. Ama bir kez öğrenilince sanılı görünen bazı işler tek komuta düşer.
Bisect ile bozan commit’i bulmak
Bir test geçen hafta geçiyordu, bugün çöküyor ve aradaki 50 commit’in hangisinin sorumlu olduğu belli değil. git bisect o commit’ler üzerinde ikili arama yapar, yani 50 commit için yaklaşık altı kontrol yeter. Laboratuvarda on commit var, v0 iyi bilinen sürüm ve test.sh her şey yolundayken 0, değilken 1 ile çıkıyor.
$ ./test.sh; echo exit=$? exit=1 $ git bisect start $ git bisect bad $ git bisect good v0 Bisecting: 4 revisions left to test after this (roughly 2 steps) [f5194c58600cc1518359e0ef2b506f6ab9465081] Change 5
Her adım git bisect good ya da git bisect bad ile elle yanıtlanabilir. Daha iyisi tüm işi script’e bırakmak.
$ git bisect run ./test.sh
running './test.sh'
Bisecting: 2 revisions left to test after this (roughly 1 step)
[4628a98a9eea707512630cb320ee03ee77c09319] Change 7
running './test.sh'
Bisecting: 0 revisions left to test after this (roughly 0 steps)
[9c61da9a94cfe89bd618f6ac7aba607d1abbeaa2] Change 6
running './test.sh'
9c61da9a94cfe89bd618f6ac7aba607d1abbeaa2 is the first bad commit
commit 9c61da9a94cfe89bd618f6ac7aba607d1abbeaa2
Author: Ayse <ayse@kafe.dev>
Change 6
changes.txt | 1 +
status.txt | 2 +-
2 files changed, 2 insertions(+), 1 deletion(-)
bisect found first bad commit
$ git bisect reset
Previous HEAD position was 9c61da9 Change 6
Switched to branch 'main'
Üç adımda Git suçlu commit’in adını verdi. İşlem her zaman git bisect reset ile bitirilir, bu seni branch’ine geri koyar. Küçük ve odaklı commit’ler bisect’i çok daha faydalı yapar, yani küçük commit’ler için bir sebep daha.
Bu satırı kim değiştirdi, bir kelime ne zaman çıktı
git blame her satıra en son kimin dokunduğunu gösterir, git log -S ise bir metni ekleyen ya da silen commit’leri bulur.
$ git blame -L 1,4 orders.js
24a4468d (Mehmet 2026-10-21 18:44:26 +0000 1) const orders = [];
d98a07b4 (Mehmet 2026-10-21 18:44:28 +0000 2) function addOrder(item) { if (orders.length < 20) orders.push(item); }
b1f1aeb6 (Elif 2026-10-21 18:44:28 +0000 3) function removeOrder(i) { orders.splice(i, 1); save(); }
8d8621ff (Mehmet 2026-10-21 18:44:26 +0000 4) function total() { return orders.reduce((s, o) => s + o.price, 0); }
$ git log -S'debug' --oneline -- orders.js
d88473f Revert "Add debug output"
53f0b22 Add debug output
İkinci komut hem debug satırını ekleyen commit’i hem de onu kaldıran revert’i buldu. Blame suçlamak için değil. Kime sorulacağını ve hangi commit mesajının “neden”i açıkladığını söyler.
Interactive rebase ile geçmişi toparlamak
Pull request açmadan önce yerel commit’lerin şeklini değiştirmek mümkün. git rebase -i origin/main, commit’lerini en eskiden başlayarak listeleyen bir editör açar.
pick 1a2b3c4 Add prices page pick 5d6e7f8 Fix typo pick 9a0b1c2 Add price tests
Satırın başındaki kelimeyi değiştirmek commit’e ne olacağını belirler.
| Komut | Etkisi |
|---|---|
pick | Commit’i olduğu gibi tutar |
reword | Commit’i tutar, mesajını düzenlemeni sağlar |
squash | Önceki commit ile birleşir ve mesajları birleştirir |
fixup | Önceki commit ile birleşir, bu mesajı atar |
drop | Commit’i siler |
edit | Commit’i değiştirmen ya da bölmen için burada durur |
Yazım düzeltmesi ilk commit’in altına alınıp fixup işaretlenirse iki commit tek temiz commit olur. Kısayol, çalışırken git commit --fixup <hash> ile fixup commit’leri üretmek, sonra git rebase -i --autosquash origin/main çalıştırmaktır. Bir şey ters giderse git rebase --abort seni başa döndürür, bitirip pişman olursan reflog orada. Daha önce görülen altın kural geçerli. Yalnızca kimsede olmayan commit’ler yeniden yazılır.
Daha güvenli force push ve bir uyarı
Kendi push edilmiş branch’ini rebase ettikten sonra normal push reddedilir ve force push gerekir. --force-with-lease kullanılır. Bu, remote branch senin son gördüğü yerde değilse üzerine yazmayı reddeder. Laboratuvarda bir ekip arkadaşının commit’ini silecek bir force push’u durdurdu. Çıplak --force durdurmadı.
Bir tuzak var. Lease yalnızca uzak takip branch’iyle karşılaştırır. Önce git fetch çalıştırırsan, ki editörün ya da arka plandaki bir iş bunu senin yerine yapabilir, Git yeni remote durumunu görülmüş sayar. O zaman lease geçer ve ekip arkadaşının commit’i ezilir. Laboratuvar çalışması şöyle. Ayse ve Mehmet shared/demo2 branch’ini paylaşıyor, Mehmet c37a7fa‘yı push etti ve Ayse onu birleştirmeden fetch yaptı.
$ git fetch 2df0e81..c37a7fa shared/demo2 -> origin/shared/demo2 $ git push --force-with-lease --force-if-includes ! [rejected] shared/demo2 -> shared/demo2 (remote ref updated since checkout) $ git push --force-with-lease=shared/demo2:2df0e81 ! [rejected] shared/demo2 -> shared/demo2 (stale info) $ git push --force-with-lease + c37a7fa...995acfa shared/demo2 -> shared/demo2 (forced update)
Son komut, yani çıplak lease, Mehmet’in commit’inin üzerine yazdı. İlk ikisi, yani --force-if-includes ekleyen ya da beklenen hash’i ismen veren, reddetti. Önerim, --force-with-lease --force-if-includes ikilisini alışkanlık yapmak ve güvenli olmayan sürümü yazmamak için bir alias tanımlamak.
Geçmişten bir gizli bilgiyi temizlemek
Klasik hatalar tablosundaki en kötü durum biraz daha yakından bakılmayı hak ediyor. Bir geliştirici içinde API anahtarı olan config.local dosyasını commit eder, fark eder ve bir sonraki commit’te dosyayı siler.
$ git rm --cached config.local $ git commit -m 'Stop tracking config.local' [main 97ed927] Stop tracking config.local $ git log --oneline -S sk_test_FAKE123456 97ed927 Stop tracking config.local 9a906c3 Add config $ git show 9a906c3:config.local API_KEY=sk_test_FAKE123456
Anahtar, repository’ye erişen herkes için hâlâ tek komut uzaklığında. Temizliğin sırası önemli.
- Anahtarı yenile. Servis sağlayıcıda iptal edilir ve yenisi oluşturulur. Bu atlanırsa geri kalanın bir önemi yok.
- Temiz bir klonda çalış.
git filter-repo, koruma olarak, temiz klon olmayan bir repository’de çalışmayı reddeder. - Geçmişi yeniden yaz.
- Tüm branch’leri ve tag’leri force push et ve herkesten yeniden klonlamasını iste, çünkü eski geçmişlerinde gizli bilgi hâlâ duruyor.
$ git clone --no-local secret-lab secret-clean
$ git filter-repo --invert-paths --path config.local
NOTICE: Removing 'origin' remote; see 'Why is my origin removed?'
in the manual if you want to push back there.
Parsed 4 commits
New history written in 0.05 seconds; now repacking/cleaning...
HEAD is now at bc29b10 Stop tracking config.local
Completely finished after 0.10 seconds.
$ git log --oneline
bc29b10 Stop tracking config.local
4be86ee Update readme
bcdb80e Start project
$ git log --oneline -S sk_test_FAKE123456
Son arama boş dönüyor, yani anahtar her commit’ten gitti. Hash’lerin değiştiğine dikkat edilir, çünkü silinen dosyadan sonraki her commit yeni bir parent aldı. Ayrıca filter-repo origin remote’unu bilerek kaldırır, bu yüzden push etmeden önce onu bilinçli olarak geri eklersin. Barındırma servisleri eski commit’leri bir süre önbellekte tutabilir ve bu, anahtarın çoktan yenilenmiş olması gerektiğinin bir sebebi daha. Bir sonraki sızıntıyı önlemek için ilk commit’ten önce .gitignore kaydı ve bir hook ya da CI kontrolü olarak gizli bilgi tarayan bir araç kullanılır.
git filter-repo ayrı bir araçtır, Git’in parçası değildir. pip install git-filter-repo ile kurulur.
Hook’lar
Hook’lar, Git’in belirli anlarda çalıştırdığı script’lerdir. .git/hooks içinde dururlar ve bu klasör commit’lerle paylaşılmaz, bu yüzden ekipler onları bir araçla kurar ya da core.hooksPath‘i repository içindeki bir klasöre ayarlar.
| Hook | Ne zaman çalışır | Tipik kullanım |
|---|---|---|
pre-commit | Commit oluşturulmadan önce | Lint, biçimlendirme, gizli bilgi taraması |
commit-msg | Mesajı yazdıktan sonra | Mesaj biçimini zorunlu kılmak |
pre-push | Push’tan önce | Hızlı testleri çalıştırmak |
Sıfır olmayan bir çıkış koduyla biten hook işlemi durdurur. Hook’lar --no-verify ile bir kereliğine atlanabilir. Bu acil durumda sorun değil, başka zaman kötü bir alışkanlık.
Submodule’ler ve büyük dosyalar
Submodule, başka bir repository’yi sabit bir commit’te gömer. Ortak kütüphaneler için faydalı ama sürtünme ekler.
$ git submodule add https://example.com/shared-ui.git libs/shared-ui $ git clone --recurse-submodules <url> $ git submodule update --init --recursive
Ana proje, submodule’ün tam commit’ini kaydeder, yani onu güncellemek ana projede bir commit demektir. Pek çok ekip bunun yerine bir paket yöneticisi kullanır. Submodule, yalnızca başka bir repository’nin kaynak koduna ihtiyacın varsa tercih edilir.
Git büyük ikili dosyalarda zayıftır, çünkü her sürüm tam olarak saklanır. Git LFS bu tür dosyaları küçük işaretçilerle değiştirir ve gerçek içeriği başka bir yerde tutar.
$ git lfs install $ git lfs track "*.psd" $ git add .gitattributes
Karar ilk commit’ten önce verilir, çünkü büyük dosyaları sonradan geçmişten taşımak yine filter-repo gerektirir.
Commit’leri imzalamak
user.email‘i herkes senin adresine ayarlayabilir, yani bir commit üzerindeki isim hiçbir şeyi kanıtlamaz. İmza kanıtlar. Modern Git, zaten sahip olduğun bir SSH anahtarıyla imzalayabilir.
$ git config --global gpg.format ssh $ git config --global user.signingkey ~/.ssh/id_ed25519.pub $ git config --global commit.gpgsign true
Aynı genel anahtar barındırma servisine imza anahtarı olarak eklenir ve commit’lerin yanında verified rozeti görünür.
.git klasörünün içinde ne var
Git, dört tür nesnesi olan içerik adresli bir veritabanıdır. Blob dosya içeriğini tutar. Tree, blob’lara ve diğer tree’lere işaret eden bir dizin listesidir. Commit, bir tree’ye ve parent commit’lerine işaret eder, yazarını ve mesajını taşır. Tag ise bir commit’e mesajla işaret eder. Her nesne, içeriğinin hash’iyle adlandırılır. Geçmişte bir şeyi değiştirmenin kendinden sonraki her hash’i değiştirmesinin sebebi budur.
Hepsine kendi gözlerinle bakabilirsin.
$ git cat-file -t HEAD commit $ git cat-file -p HEAD
Branch’ler ve tag’ler, içinde bir hash bulunan küçük dosyalardır. HEAD, mevcut branch’in adını tutan bir dosyadır. Git’in tüm gizemi, değiştirilemez commit’lerden oluşan bir graf ve taşınabilen bir avuç etikettir. Bu rehberdeki hemen her hatanın bir etiketi geri taşıyarak düzeltilebilmesinin sebebi de budur.
Git cheat sheet
Bu bölüm bir sekmede açık tutulabilir. Komutlar yaptığın işe göre gruplanmıştır.
Kurulum ve temel komutlar
| Komut | Ne yapar |
|---|---|
git config --global user.name "Ad" | Commit’ler için adını ayarlar |
git config --global user.email "sen@ornek.com" | Commit’ler için e-postanı ayarlar |
git config --global init.defaultBranch main | Yeni repository’lerin varsayılan branch’ini main yapar |
git config --global pull.rebase true | git pull sırasında merge yerine rebase yapar |
git config --global merge.conflictStyle zdiff3 | Conflict içinde orijinal metni de gösterir |
git config --global rerere.enabled true | Conflict çözümlerini hatırlar |
git init | Bulunulan klasörde repository oluşturur |
git clone <url> | Uzak repository’yi kopyalar |
git status -sb | Branch bilgisiyle kısa durum |
git add -p | Değişiklikleri parça parça stage eder |
git commit -m "mesaj" | Stage edilenleri commit eder |
git diff ve git diff --staged | Stage edilmemiş ve stage edilmiş değişiklikleri gösterir |
git log --oneline --graph --all | Branch grafini gösterir |
Branch’ler ve merge
| Komut | Ne yapar |
|---|---|
git switch <branch> | Bir branch’e geçer |
git switch -c <branch> | Branch açar ve ona geçer |
git branch -vv | Branch’leri upstream ve durum bilgisiyle listeler |
git branch -d <branch> | Birleşmiş branch’i siler |
git merge <branch> | Bir branch’i mevcut branch’e merge eder |
git merge --no-ff <branch> | Her zaman merge commit üretir |
git merge --squash <branch> | Merge commit’siz değişiklikleri toplar |
git merge --abort | Conflict’li merge’i iptal eder |
git rebase <branch> | Commit’lerini başka bir branch’in üzerine yeniden oynatır |
git rebase --abort | Rebase’i iptal eder |
git cherry-pick <hash> | Tek bir commit’i mevcut branch’e kopyalar |
git worktree add ../dir <branch> | Başka bir branch’i ikinci bir klasöre açar |
Uzak repository
| Komut | Ne yapar |
|---|---|
git remote -v | Remote’ları listeler |
git fetch | Çalışmana dokunmadan uzak değişiklikleri indirir |
git pull | Fetch edip birleştirir |
git push -u origin <branch> | Branch’i yayınlar ve upstream’ini ayarlar |
git push --force-with-lease --force-if-includes | Daha güvenli force push |
git push origin <tag> | Tag’i yayınlar |
git push origin --delete <branch> | Uzak branch’i siler |
İşi sonraya saklamak
| Komut | Ne yapar |
|---|---|
git stash push -m "not" | Takip edilen değişiklikleri rafa kaldırır |
git stash push -u | Takip edilmeyen dosyaları da rafa kaldırır |
git stash list | Stash’lerini listeler |
git stash pop | En yeni stash’i uygular ve siler |
git stash apply | Stash’i uygular ve saklar |
Geri alma ve kurtarma
| Komut | Ne yapar |
|---|---|
git restore <file> | Dosyadaki değişiklikleri atar |
git restore --staged <file> | Dosyayı stage’den çıkarır |
git restore --source=<ref> <file> | Dosyayı başka bir commit’ten geri getirir |
git clean -n sonra git clean -f | Önce önizler, sonra takip edilmeyen dosyaları siler |
git commit --amend | Son commit’i düzeltir |
git reset --soft HEAD~1 | Commit’i geri alır, değişiklikler stage’de kalır |
git reset --mixed HEAD~1 | Commit’i geri alır, değişiklikler stage dışı kalır |
git reset --hard HEAD~1 | Commit’i geri alır ve değişiklikleri atar |
git reset --hard ORIG_HEAD | Reset, merge ya da rebase’i geri alır |
git revert <hash> | Push edilmiş commit’i yeni bir commit ile geri alır |
git revert -m 1 <merge> | Bir merge commit’i geri alır |
git reflog | HEAD’in bulunduğu her yeri gösterir |
git branch <isim> <hash> | Kaybolmuş commit’e branch verir |
git fsck --no-reflog | Erişilemez commit’leri bulur |
Araştırma ve ileri seviye
| Komut | Ne yapar |
|---|---|
git blame <file> | Her satırı kimin değiştirdiğini gösterir |
git log -S'metin' | Metni ekleyen ya da silen commit’leri bulur |
git bisect start, bad, good, run <script> | Bozan commit için ikili arama |
git rebase -i <base> | Commit’leri yeniden adlandırır, birleştirir, siler ve sıralar |
git tag -a v1.0 -m "mesaj" | Annotated tag oluşturur |
git filter-repo --invert-paths --path <file> | Bir dosyayı tüm geçmişten kaldırır |
git cat-file -p <hash> | Ham bir Git nesnesine bakar |
Sık sorulan sorular
Git ile GitHub arasındaki fark nedir?
Git, makinende çalışan sürüm kontrol aracıdır. GitHub, GitLab ve Bitbucket ise Git repository’lerini barındıran ve üzerine pull request, issue ve erişim kontrolü ekleyen servislerdir. Git, bunların hiçbiri olmadan da kullanılabilir.
git fetch ile git pull arasındaki fark nedir?
git fetch yeni commit’leri indirir ve uzak takip branch’lerini günceller, kendi branch’lerine ve dosyalarına dokunmaz. git pull, fetch’in ardından mevcut branch’e merge ya da rebase yapar. Şüphede kalınca önce fetch yapılır ve gelenlere bakılır.
Merge mi rebase mi kullanılmalı?
Biten bir branch’i main‘e almak için merge kullanılır, çünkü gerçekte olanları kaydeder. Rebase, kendi yayınlanmamış commit’lerini toparlamak ya da özel bir özellik branch’ini güncellemek için kullanılır. Başkalarının zaten sahip olduğu commit’ler asla rebase edilmez.
Son commit nasıl geri alınır?
Push edilmediyse değişiklikleri tutmak için git reset --soft HEAD~1, düzeltmek için git commit --amend kullanılır. Push edildiyse git revert <hash> kullanılır.
Silinen branch ya da kaybolan commit’ler nasıl geri getirilir?
git reflog çalıştırılır, istenen commit’in hash’i bulunur ve git branch <isim> <hash> ile oraya bir branch açılır. Bu, reset, rebase, amend ve branch silme ile kalkan commit’ler için işe yarar.
Detached HEAD ne demek?
HEAD bir branch yerine doğrudan bir commit’i gösteriyor demektir. Bir tag’i, bir commit hash’ini ya da bir uzak branch’i checkout edince olur. Etrafa bakmak için zararsızdır. Orada commit atılırsa ayrılmadan önce bir branch açılır.
Push’um neden reddedildi?
Remote branch’te sende olmayan commit’ler var. git pull --rebase ya da git pull çalıştırılır, varsa conflict’ler çözülür ve tekrar push edilir. Hatadan kurtulmak için force push yapılmaz.
Merge conflict nasıl çözülür?
Conflict’li olarak işaretlenen dosyalar açılır, işaretlerin arasından doğru içerik seçilir, işaretler silinir, git add <file> çalıştırılır ve sonra git commit ya da ilgili --continue komutu verilir. Kaybolunursa git merge --abort seni başa döndürür.
reset ile revert arasındaki fark nedir?
reset bir branch’i eski bir commit’e taşır ve geçmişi yeniden yazar, bu yüzden yerel iş içindir. revert, eski bir commit’i iptal eden yeni bir commit ekler, bu yüzden ortak branch’lerde güvenlidir.
Yanlışlıkla commit ettiğim bir dosyayı nasıl kaldırırım?
Yalnızca bundan sonrası için istenmiyorsa git rm --cached <file> ve commit yeterlidir. İçinde gizli bilgi varsa ya da çok büyükse geçmişten git filter-repo ile kaldırılmalı ve gizli bilgi yenilenmelidir.
.gitignore’a neler yazılmalı?
Build çıktıları, bağımlılık klasörleri, yerel ayarlar, log’lar, editör dosyaları ve gizli bilgi içeren her şey. .gitignore‘un yalnızca takip edilmeyen dosyaları etkilediği unutulmaz, yani zaten takip edilen bir dosya için önce git rm --cached gerekir.
git push –force kullanmak güvenli mi?
Yalnızca sadece senin kullandığın bir branch’te. Ortak branch’lerde başkalarının işini silebilir. --force-with-lease --force-if-includes tercih edilir ve main için branch protection açılır.
Ne sıklıkla commit atılmalı?
Çalışan tek bir küçük mantıksal adım bittiğinde. Sık ve küçük commit’ler inceleme, geri alma ve bisect işlerini kolaylaştırır. Paylaşmadan önce interactive rebase ile her zaman toparlanabilir.
Son söz
Git koca bir araç gibi görünür ama küçük bir fikre dayanır. Commit’ler değiştirilemez snapshot’lardır. Branch’ler, tag’ler ve HEAD ise taşıyabileceğin etiketlerdir. Bu resim kafada oturunca merge, rebase, reset ve reflog sihir olmaktan çıkar. Hepsi ya etiket taşımayı ya da yeni commit eklemeyi anlatır.
Önemli olan alışkanlıklar basit. Küçük ve sık commit atılır. Push’tan önce fetch yapılır. Başkalarının sahip olduğu geçmiş yeniden yazılmaz. Ortak branch’lerde revert, özel branch’lerde reset kullanılır. Bir şey ters gittiğinde de durulur, git status ve git reflog çalıştırılır ve yazmadan önce okunur.

Tüm projelerim ve üzerinde çalıştığım konuları incelemek istersen https://hub.barisgunduz.com/ adresini ziyaret edebilirsin.