Mert Özen Her Satırda Daha İleri
DevOps Git & GitHub — #05

Git ile Dosya Takibi: .gitignore, git rm --cached ve git mv

Mert Özen 6 Kas 2026 9 dk 1 görüntülenme
Git ile Dosya Takibi: .gitignore, git rm --cached ve git mv

.gitignore pattern'lerini, ! ile istisnayı ve git check-ignore -v ile hangi kuralın devrede olduğunu görüyoruz. Zaten commit'lenmiş bir .env'in neden .gitignore'a rağmen takip edildiğini ve git rm --cached ile nasıl çıkarıldığını, global excludes'u, git mv'yi ve .gitattributes ile satır sonlarını ele alıyoruz.

Sürüm notu: Bu yazıdaki komutlar Git 2.54 ile denenmiştir.

Önce Tahmin Et

Geçen hafta, repo'da henüz .gitignore yokken, içinde bir API anahtarı olan .env dosyasını yanlışlıkla commit'ledin. Bugün fark edip .gitignore'a .env satırını yazdın ve dosyadaki anahtarı değiştirdin. git status ne der: .env artık görünmez mi, yoksa "modified" olarak listede mi durur?

Bu yazıda .gitignore'un ne yaptığını ve daha önemlisi ne yapmadığını göreceğiz. Yanına dosya takibinin diğer araçlarını da ekleyeceğiz: git check-ignore, git rm --cached, git mv ve .gitattributes.

Sorun: Repo'ya Girmemesi Gerekenler

Önce açılıştaki hatayı gerçekten yapalım. bookstore repo'suna bir lisans dosyası ve yerel ayarları tutan bir .env eklemiştik:

# Geçen hafta, .gitignore yokken
$ git add LICENSE.txt .env
$ git commit -m "Add license and local settings"
[main 5517793] Add license and local settings
 2 files changed, 4 insertions(+)
 create mode 100644 .env
 create mode 100644 LICENSE.txt
$ git log --oneline -3
5517793 (HEAD -> main) Add license and local settings
e3ef418 Add cart total
fae4454 Add shopping cart

Sonra proje büyüdü: bağımlılıklar kuruldu, bir build alındı, uygulama çalışıp log yazdı. macOS da her klasöre yaptığı gibi bir .DS_Store bıraktı. Ekip için örnek bir ayar dosyası olarak .env.example da oluşturduk:

# npm install, build ve uygulamayı çalıştırdıktan sonra
$ git status -s
?? .DS_Store
?? .env.example
?? debug.log
?? dist/
?? node_modules/

Bu listede repo'ya girmesi gereken tek dosya .env.example. node_modules/ klasörü binlerce dosya içerir ve bağımlılıkları tanımlayan dosyadan (Node projelerinde package.json) her zaman yeniden kurulabilir. dist/ build'in çıktısı; kaynaktan her zaman yeniden üretilir. debug.log senin makinene özgü. .DS_Store ise projeyle hiç ilgisi olmayan bir işletim sistemi dosyası. Bu gürültü git status'u okunmaz hâle getirir ve bir git add . ile hepsi repo'ya girer.

İlk .gitignore

Repo'nun köküne .gitignore adında bir dosya açıp şunu yazıyoruz:

# Bağımlılıklar
node_modules/

# Build çıktısı
dist/

# Log dosyaları
*.log

# Yerel ayarlar
.env
.env.*
!.env.example
$ git status -s
?? .DS_Store
?? .env.example
?? .gitignore
$ git status -s --ignored
?? .DS_Store
?? .env.example
?? .gitignore
!! debug.log
!! dist/
!! node_modules/

Gürültü kayboldu. Geriye kalan üç dosyadan .gitignore ve .env.example commit'lenmeyi bekliyor; .DS_Store'u birazdan başka bir yolla halledeceğiz. --ignored seçeneği yok sayılan dosyaları da !! ile gösterir; neyin gerçekten yok sayıldığını görmek için kullanışlı.

.gitignore sıradan bir dosyadır ve repo'ya commit'lenir. Böylece kurallar bütün ekip için geçerli olur. Bir kütüphanenin giriş kapısındaki "çanta ve yiyecek alınmaz" levhası gibi düşün: kural bir kez asılır, herkes aynı kuralla karşılaşır.

Pattern'ler ve ! ile İstisna

Yazdığımız birkaç satır, pattern dilinin neredeyse tamamını kullanıyor. # ile başlayan satırlar yorumdur, boş satırlar yok sayılır. * eğik çizgi hariç her şeyle eşleşir: *.log her klasördeki log dosyalarını yakalar. Sonda eğik çizgi olan node_modules/ ve dist/ yalnızca klasörlerle eşleşir. Başta eğik çizgi varsa pattern .gitignore'un bulunduğu klasöre bağlanır: /build yalnızca kökteki build'i yakalar, src/build'e dokunmaz. ** ise klasör sınırlarını aşar: **/logs her derinlikteki logs'u, logs/** o klasörün altındaki her şeyi yakalar.

! ile başlayan satır bir istisnadır ve kendisinden önceki bir kuralın yakaladığı dosyayı geri içeri alır. .env.*, .env.local ya da .env.production gibi bütün varyantları yok sayıyor. Ama bu kural .env.example'ı da yakalardı, onu da !.env.example kurtarıyor. Sıra önemli: Git satırları yukarıdan aşağı okur ve son eşleşen kural kazanır.

Bir kısıt var: yok sayılan bir klasörün içindeki dosyayı ! ile geri alamazsın. dist/ yazdıktan sonra !dist/keep.txt işe yaramaz, çünkü Git yok sayılan klasörün içine hiç bakmaz. Klasörün kendisini değil içeriğini yok saymak gerekir: dist/* ve ardından !dist/keep.txt.

Sıfırdan yazmak zorunda da değilsin. GitHub'ın github/gitignore repo'su Node, Python, .NET, Java gibi onlarca dil ve framework için hazır şablonlar topluyor. GitHub'da yeni bir repo oluştururken "Add .gitignore" listesinden seçtiğin şablon da bu koleksiyondan gelir. Şablonu başlangıç noktası olarak al, sonra projene göre kısalt ya da genişlet.

Kurallar çoğaldıkça bir dosya beklemediğin şekilde yok sayılıyorsa ya da sayılmıyorsa tahmin yürütme, Git'e sor:

$ git check-ignore -v debug.log dist/bundle.js node_modules/left-pad/index.js
.gitignore:8:*.log	debug.log
.gitignore:5:dist/	dist/bundle.js
.gitignore:2:node_modules/	node_modules/left-pad/index.js
$ git check-ignore -v .env.example
.gitignore:13:!.env.example	.env.example

-v her dosya için kuralın hangi dosyada, kaçıncı satırda olduğunu ve pattern'in kendisini yazar. .env.example için çıkan satır ! ile başlıyor; yani dosya yok sayılmıyor, son eşleşen kural onu içeri alıyor. Büyük bir projede, iç içe .gitignore dosyaları ve bir global dosya varken bu komut saatler kazandırır.

Zaten Takip Edilen Dosya: git rm --cached

Şimdi açılıştaki soru. .env geçen hafta commit'lenmişti ve .gitignore'da da var:

# Takip edilen dosya için çıktı yok
$ git check-ignore -v .env
$ git check-ignore -v --no-index .env
.gitignore:11:.env	.env
# .env'deki anahtarı değiştirdikten sonra
$ git status -s
 M .env
?? .DS_Store
?? .env.example
?? .gitignore

.env, .gitignore'da yazmasına rağmen " M" ile listede. Açılıştaki sorunun cevabı bu: .gitignore yalnızca untracked dosyalar için geçerlidir. Bir dosya bir kez takibe girdiyse Git onu izlemeye devam eder ve .gitignore'daki satır hiçbir şeyi değiştirmez. git check-ignore bu yüzden takip edilen dosya için hiçbir şey yazmıyor; --no-index ile sorunca kuralın aslında eşleştiğini görüyoruz.

Çözüm, dosyayı diskte bırakıp yalnızca takipten çıkarmak:

$ git rm --cached .env
rm '.env'
$ git status -s
D  .env
?? .DS_Store
?? .env.example
?? .gitignore
$ ls -1a
.
..
.DS_Store
.env
.env.example
.git
.gitignore
LICENSE.txt
README.md
debug.log
dist
node_modules
src

--cached, "yalnızca staging area'dan sil, working tree'ye dokunma" demek. D sol sütunda: bir sonraki commit'te .env repo'dan çıkacak. ls -1a dosyanın diskte durduğunu gösteriyor. --cached'i unutup git rm .env deseydin dosya diskten de silinirdi. Bu commit'ten sonra .env untracked olur ve .gitignore nihayet işini yapar.

Ama bir şey değişmez: anahtar geçen haftaki commit'in içinde duruyor. Bir önceki yazıda gördüğümüz gibi commit'ler snapshot'tır ve o snapshot'ta .env var. Repo push edildiyse anahtarı yanmış say ve hemen yenisini üret. Geçmişi yeniden yazan araçlar var, ama paylaşılmış bir repo'da her clone'da eski kopya kalabileceği için asıl güvenlik önlemi anahtarı değiştirmektir.

Global Excludes: Sana Ait Gürültü

.DS_Store projeyle değil senin işletim sisteminle ilgili. Windows kullanan ekip arkadaşının böyle bir dosyası yok; onun da kendi editörünün geride bıraktığı dosyalar var. Her projenin .gitignore'una herkesin kişisel gürültüsünü yazmak yerine Git'e kendi makinen için global bir yok sayma dosyası verirsin:

$ git config set --global core.excludesFile '~/.gitignore_global'
$ git config get core.excludesFile
~/.gitignore_global
# ~/.gitignore_global dosyasına .DS_Store satırını yazdıktan sonra
$ git status -s
D  .env
?? .env.example
?? .gitignore

~'yi tırnak içinde yazdık; Git onu kendisi home klasörüne çevirir. Ayar vermesen de Git, varsayılan olarak ~/.config/git/ignore dosyasına bakar; dosyayı oraya koymak da aynı işi görür. Bir de .git/info/exclude var: yalnızca o repo'da ve yalnızca senin için geçerli, commit'lenmeyen bir yok sayma dosyası. Kendi deneme script'lerin için ideal.

Artık commit'leyebiliriz. .env'in silinmesi ve iki yeni dosya aynı commit'e giriyor:

$ git add .gitignore .env.example
$ git status -s
D  .env
A  .env.example
A  .gitignore
$ git commit -m "Ignore build output, logs and local settings"
[main 6d89b81] Ignore build output, logs and local settings
 3 files changed, 14 insertions(+), 1 deletion(-)
 delete mode 100644 .env
 create mode 100644 .env.example
 create mode 100644 .gitignore

git mv: Taşımak ve Yeniden Adlandırmak

Lisans dosyasının adını yaygın kullanıma uygun hâle getirelim:

$ git mv LICENSE.txt LICENSE
$ git status -s
R  LICENSE.txt -> LICENSE
$ git commit -m "Rename LICENSE.txt to LICENSE"
[main 1634232] Rename LICENSE.txt to LICENSE
 1 file changed, 0 insertions(+), 0 deletions(-)
 rename LICENSE.txt => LICENSE (100%)

R, rename. Commit çıktısındaki 100% içeriğin hiç değişmediğini söylüyor. İşin ilginç tarafı, Git rename diye bir şey saklamıyor. Commit'in tree'sinde eski isim yok, yeni isim var; o kadar. Git, aynı ya da çok benzer içeriğin bir isimden kaybolup başka bir isimde ortaya çıktığını görünce bunu rename olarak gösterir. Bu yüzden dosyayı Finder'da ya da mv ile taşıyıp ardından git add -A desen de sonuç aynı R olur. git mv sadece iki adımı bire indirir.

.gitattributes ile Satır Sonları

Kurulum yazısında core.autocrlf'in her geliştiricinin kendi makinesindeki bir ayar olduğunu söylemiştik. Ekibin yarısı Windows'taysa ve birinin ayarı farklıysa, bir dosyanın bütün satırları değişmiş gibi görünen commit'ler gelmeye başlar. .gitattributes bu kuralı repo'nun içine koyar:

* text=auto
*.sh text eol=lf
*.png binary

* text=auto, "metin olduğunu tespit ettiğin her dosyanın satır sonlarını repo'da LF olarak sakla" demek. *.sh text eol=lf shell script'lerinin her makinede LF ile çıkmasını zorunlu kılar; CRLF'li bir script Linux'ta çalışmaz. *.png binary ise Git'e bu dosyaları metin gibi işlememesini, diff'ini göstermeye çalışmamasını söyler.

$ git add .gitattributes
$ git ls-files --eol
i/lf    w/lf    attr/text=auto        	.env.example
i/lf    w/lf    attr/text=auto        	.gitattributes
i/lf    w/lf    attr/text=auto        	.gitignore
i/lf    w/lf    attr/text=auto        	LICENSE
i/lf    w/lf    attr/text=auto        	README.md
i/lf    w/lf    attr/text=auto        	src/books.js
i/lf    w/lf    attr/text=auto        	src/cart.js
$ git commit -m "Normalize line endings"
[main e351c69] Normalize line endings
 1 file changed, 3 insertions(+)
 create mode 100644 .gitattributes

git ls-files --eol her dosya için üç bilgi verir: i/ staging area'daki, w/ working tree'deki satır sonu, attr/ da uygulanan kural. Burada her şey LF. Bu dosyayı içinde CRLF'li dosyalar olan eski bir repo'ya eklersen git add --renormalize . mevcut dosyaları yeni kurala göre staging area'ya tekrar ekler; sonra tek bir commit'le geçişi tamamlarsın.

Sık Düşülen Tuzaklar

İlk tuzak, açılıştaki soru: takip edilen bir dosyayı .gitignore'a yazıp işin bittiğini sanmak. Önce git rm --cached, sonra commit. Bütün bir klasörü takipten çıkarmak için git rm -r --cached logs/ gerekir.

İkincisi, sırrı takipten çıkarınca sorunun çözüldüğünü sanmak. Geçmişteki commit'lerde hâlâ duruyor. Push edilmiş bir anahtar her zaman yenilenmelidir.

Üçüncüsü, yok sayılan bir klasörün içindeki dosyayı ! ile geri almaya çalışmak. dir/ yerine dir/* yazmadıkça istisna çalışmaz. Şüphe duyduğunda git check-ignore -v ile kontrol et.

Dördüncüsü, kişisel dosyaları projenin .gitignore'una doldurmak. .DS_Store, Thumbs.db ya da editör klasörleri senin makinenle ilgili; global excludes dosyasına aittir. Proje .gitignore'u projenin ürettiği dosyalar için kalmalı.

Küçük Bir Deneme

★☆☆ bookstore'a coverage/ adında bir klasör ve içine bir dosya ekle. .gitignore'a bir satır yazarak onu yok say ve git check-ignore -v ile hangi satırın yakaladığını doğrula.

★★☆ dist/ klasörünün geri kalanını yok saymaya devam ederken dist/README.md dosyasının repo'ya girmesini sağla. Önce .gitignore'un sonuna !dist/README.md eklemeyi dene. İlk denemende neden çalışmadığını git status -s --ignored ile gör, sonra pattern'i düzelt.

★★★ Yeni bir config.local.json oluşturup commit'le, sonra .gitignore'a ekle ve dosyayı değiştir. git status ile açılıştaki durumu kendi gözünle gör, git rm --cached ile takipten çıkar ve commit'le. Son olarak git log --stat ile dosyanın eski commit'te hâlâ durduğunu kontrol et.

Repo'nun geçmişi uzamaya başladı. Bir sonraki yazıda o geçmişte gezinmeyi öğreniyoruz: git log'un --oneline, --graph, -p, --stat, --author, --since, --grep ve -S seçenekleri, git show, satır satır kimin neyi yazdığını gösteren git blame ve sık kullandığın komutlar için alias'lar.