YAZILIM

Git Nedir? Sıfırdan İleri Seviyeye Git Rehberi

Git Nedir? Sıfırdan İleri Seviyeye Git Rehberi yazısının kapak görseli

Ç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 main olacak ş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.

Git'in nasıl çalıştığını gösteren animasyon: dosya çalışma dizininden staging alanına, local repository'ye ve uzak sunucuya ilerler

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. .git klasö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, update ve changes kimseye 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 fetch remote’taki yeni commit’leri indirir ama dosyalara ve branch’e dokunmaz.
  • git pull, fetch ve ardından mevcut branch’e merge işlemidir.
  • git push commit’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/prices açar.
  • Mehmet sipariş mantığı için iki commit’lik feature/orders açar.
  • Can ilk stil dosyasıyla feature/styles açar.
  • Elif yulaf sütü seçeneği için feature/vegan, proje açıklaması için docs/readme aç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.

MergeRebase
GeçmişGerçekte olanları korur, bir merge commit eklerTek düz çizgi
Mevcut commit’lerDokunulmazYeni hash’lerle yeniden yazılır
Ortak branch’lerde güvenliEvetHayır
Tipik kullanımBiten özelliği main‘e almakKendi ö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 conflict çözümünü gösteren animasyon: conflict işaretleri, doğru satırın seçilmesi, stage ve merge adımları

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.

KodAnlamıTipik sebep
UUİkisi de değiştirdiİki kişi aynı satırları düzenledi
UDKarşı taraf sildiKarşı tarafın sildiği bir dosyayı sen değiştirdin
DUBiz sildikSenin 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 true açı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.

ModBranch etiketiStaging areaÇalışma dizini
--softTaşınırDeğişiklikler stage’de kalırDokunulmaz
--mixed (varsayılan)TaşınırSıfırlanır, değişiklikler stage dışı kalırDokunulmaz
--hardTaşınırSıfırlanırSı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

DurumKomutOrtak branch’te güvenli mi
Dosyadaki düzenlemeleri atgit restore <file>Evet, ama düzenlemeler gider
Dosyayı stage’den çıkargit restore --staged <file>Evet
Son commit’i düzeltgit commit --amendYalnızca push’tan önce
Yerel commit’leri geri al, işi tutgit reset --soft ya da --mixedYalnızca push’tan önce
Yerel commit’leri geri al, işi atgit reset --hardYalnızca push’tan önce
Push edilmiş commit’i geri algit revert <hash>Evet
Push edilmiş merge’i geri algit revert -m 1 <merge>Evet
Kötü bir reset ya da rebase’ten geri döngit reset --hard ORIG_HEADYalnı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 şeyKurtarılabilir miNerede aranır
Reset, rebase ya da amend ile kalkan commitEvetgit reflog, ORIG_HEAD
Silinen branchEvetgit reflog, silme mesajı
Düşürülen stashEvetgit fsck --no-reflog, drop mesajı
Detached HEAD’deki commitEvetgit reflog
Force push ile ezilen commit’lerEvet, onlara sahip klondagit reflog show origin/<branch>
Restore ya da reset ile kaybolan commit edilmemiş düzenlemelerHayırEditör geçmişi, yedekler
git clean ile silinen takipsiz dosyalarHayırÇöp kutusu, yedekler
git gc --prune=now sonrası her şeyHayırHiç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 olduNe görülürÇözüm
Son commit mesajında yazım hatasıHenüz bir sorun görünmezPush’tan önce git commit --amend -m 'yeni mesaj'
Son commit’e bir dosya eklemeyi unutmaDosya modified ya da untracked görünürgit add <file>, sonra git commit --amend --no-edit
Yanlış branch’e commit atmamain, origin/main‘in ilerisindegit branch yeni-branch, sonra git reset --hard origin/main, sonra git switch yeni-branch
Yanlış dosyayı stage etmeDosya commit edilecek değişiklikler altındagit restore --staged <file>
Kötü bir commit’i push etmeEkip arkadaşları çoktan çektigit 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ış gittiConflict işaretleri ya da beklenmedik sonuçgit merge --abort ya da git reset --hard ORIG_HEAD
Rebase yanlış gittiBirçok commit’te conflictgit rebase --abort, bittiyse git reset --hard ORIG_HEAD
Reset sonrası kaybolan commit’lergit log‘da yoklargit reflog, sonra git reset --hard HEAD@{n}
Branch’i erken silmeBranch 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 stashgit stash list boşgit fsck --no-reflog, sonra git stash store
Ekip arkadaşının üzerine force pushOnların commit’leri remote’tan kaybolurEkip arkadaşının git reflog show origin/<branch> ve rebase, push
Commit’e gizli anahtar girmesiGeçmişte bir API anahtarı görünürÖnce anahtarı yenile, sonra git filter-repo
Yanlışlıkla büyük dosya commit’lemekKlonlar yavaşlar, push reddedilirfilter-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ış yazargit log yanlış ismi gösteriruser.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â takiptegit 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

ModelNasıl çalışırEn uygun olduğu yer
Trunk-basedKısa ömürlü branch’ler bir iki gün içinde main‘e alınır, çoğu zaman feature flag arkasındaKüçük ekipler, sürekli teslimat
GitHub FlowDeğişiklik başına bir branch, pull request, inceleme, main‘e merge, deployÇoğu web ürünü
Git Flowmain, develop, feature, release ve hotfix branch’leriBirden 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, docs ve refactor gibi ö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.

KomutEtkisi
pickCommit’i olduğu gibi tutar
rewordCommit’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
dropCommit’i siler
editCommit’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.

  1. Anahtarı yenile. Servis sağlayıcıda iptal edilir ve yenisi oluşturulur. Bu atlanırsa geri kalanın bir önemi yok.
  2. Temiz bir klonda çalış. git filter-repo, koruma olarak, temiz klon olmayan bir repository’de çalışmayı reddeder.
  3. Geçmişi yeniden yaz.
  4. 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.

HookNe zaman çalışırTipik kullanım
pre-commitCommit oluşturulmadan önceLint, biçimlendirme, gizli bilgi taraması
commit-msgMesajı yazdıktan sonraMesaj biçimini zorunlu kılmak
pre-pushPush’tan önceHı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

KomutNe 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 mainYeni repository’lerin varsayılan branch’ini main yapar
git config --global pull.rebase truegit pull sırasında merge yerine rebase yapar
git config --global merge.conflictStyle zdiff3Conflict içinde orijinal metni de gösterir
git config --global rerere.enabled trueConflict çözümlerini hatırlar
git initBulunulan klasörde repository oluşturur
git clone <url>Uzak repository’yi kopyalar
git status -sbBranch bilgisiyle kısa durum
git add -pDeğişiklikleri parça parça stage eder
git commit -m "mesaj"Stage edilenleri commit eder
git diff ve git diff --stagedStage edilmemiş ve stage edilmiş değişiklikleri gösterir
git log --oneline --graph --allBranch grafini gösterir

Branch’ler ve merge

KomutNe yapar
git switch <branch>Bir branch’e geçer
git switch -c <branch>Branch açar ve ona geçer
git branch -vvBranch’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 --abortConflict’li merge’i iptal eder
git rebase <branch>Commit’lerini başka bir branch’in üzerine yeniden oynatır
git rebase --abortRebase’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

KomutNe yapar
git remote -vRemote’ları listeler
git fetchÇalışmana dokunmadan uzak değişiklikleri indirir
git pullFetch edip birleştirir
git push -u origin <branch>Branch’i yayınlar ve upstream’ini ayarlar
git push --force-with-lease --force-if-includesDaha güvenli force push
git push origin <tag>Tag’i yayınlar
git push origin --delete <branch>Uzak branch’i siler

İşi sonraya saklamak

KomutNe yapar
git stash push -m "not"Takip edilen değişiklikleri rafa kaldırır
git stash push -uTakip edilmeyen dosyaları da rafa kaldırır
git stash listStash’lerini listeler
git stash popEn yeni stash’i uygular ve siler
git stash applyStash’i uygular ve saklar

Geri alma ve kurtarma

KomutNe 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 --amendSon commit’i düzeltir
git reset --soft HEAD~1Commit’i geri alır, değişiklikler stage’de kalır
git reset --mixed HEAD~1Commit’i geri alır, değişiklikler stage dışı kalır
git reset --hard HEAD~1Commit’i geri alır ve değişiklikleri atar
git reset --hard ORIG_HEADReset, 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 reflogHEAD’in bulunduğu her yeri gösterir
git branch <isim> <hash>Kaybolmuş commit’e branch verir
git fsck --no-reflogErişilemez commit’leri bulur

Araştırma ve ileri seviye

KomutNe 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.

Web ve mobil teknolojilere ilgi duyuyorum. Yaklaşık 14 yıldır web geliştirme ve internet reklamcılığı üzerinde çalışıyorum. Web yazılım teknolojileri ve yönetimi konusunda uzmanım. Birçok şirkete teknoloji ve reklamcılık danışmanlığı verdim. Hâlâ bazılarına destek vermeye devam ediyorum. Kendi projelerimle ilgili bazı çalışmalarım var. Çoğu içerik üreticiliği ile ilgili. Projelerimin hepsi Gündüz Medya markası altında toplanmıştır.

Bir yanıt yazın

E-posta adresiniz yayınlanmayacak. Gerekli alanlar * ile işaretlenmişlerdir