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

git config Derinlemesine: includeIf ile İş/Kişisel Kimlik, İki GitHub Hesabı, url.insteadOf ve Ekip için Önerilen Ayarlar

Mert Özen 15 Kas 2026 10 dk 7 görüntülenme
git config Derinlemesine: includeIf ile İş/Kişisel Kimlik, İki GitHub Hesabı, url.insteadOf ve Ekip için Önerilen Ayarlar

includeIf "gitdir:~/work/" ile iş kimliği yalnız o klasördeki repo'lara uygulanır; sondaki / unutulursa koşul sessizce tutmaz. hasconfig ile remote URL'ine göre kimlik, SSH host alias'ı ve url.insteadOf ile iki GitHub hesabı, credential helper'da useHttpPath ve ekip için önerilen ayarları Git 2.56 ile deniyoruz.

Sürüm notu: Bu yazıdaki komutlar Git 2.56.1 ile denenmiştir. hasconfig:remote.*.url Git 2.36'dan, push.autoSetupRemote Git 2.37'den, includeIf "worktree:..." Git 2.56'dan beri vardır; git config get/set için Git 2.46 ve üstü gerekir.

Önce Tahmin Et

Ada, sitedeki serilerin ortak örnek uygulaması Bookstore'u (bir online kitapçı) geliştiren ekipte çalışıyor ve şirket laptop'unda akşamları açık kaynak projelere de katkı veriyor. Şirket commit'lerinde iş e-postası, kişisel projelerde kendi e-postası görünsün istiyor. Global config'i şöyle:

[user]
	name = Ada Lovelace
	email = ada@example.org
[init]
	defaultBranch = main
[includeIf "gitdir:~/work"]
	path = ~/.gitconfig-work

~/.gitconfig-work dosyasında email = ada@example.com var. Ada ~/work altında yeni bir repo açıyor. Sence hangi e-posta gelir?

$ git init -q work/bookstore-api
$ cd work/bookstore-api
$ git config get user.email
ada@example.org

Kişisel olan. Koşul hiç tutmadı ve Git bunu söylemedi. Bu yazıda config'i klasöre, remote URL'ine ve hesaba göre ayırmayı, sonra da ekip için önerilen ayarları gerekçeleriyle konuşuyoruz.

includeIf gitdir: Klasöre Göre Kimlik

gitdir: koşulu bir glob pattern'idir ve .git klasörünün yoluyla karşılaştırılır. Bu repo'nun .git yolu ~/work/bookstore-api/.git; ~/work bununla eşleşmez. Belgedeki kural şu: pattern / ile bitiyorsa Git sonuna ** ekler, yani "bu klasör ve altındaki her şey" olur. Eski bölümü dosyadan silip doğrusunu yazalım:

$ git config set --global includeIf.gitdir:~/work/.path '~/.gitconfig-work'
$ cat ~/.gitconfig
[user]
	name = Ada Lovelace
	email = ada@example.org
[init]
	defaultBranch = main
[includeIf "gitdir:~/work/"]
	path = ~/.gitconfig-work
$ git config get user.email
ada@example.com
$ git config list --show-origin --show-scope | grep -E 'user|gpg|commit'
global	file:/home/user/.gitconfig	user.name=Ada Lovelace
global	file:/home/user/.gitconfig	user.email=ada@example.org
global	file:/home/user/.gitconfig-work	user.email=ada@example.com
global	file:/home/user/.gitconfig-work	user.signingkey=~/.ssh/id_ed25519_work.pub
global	file:/home/user/.gitconfig-work	gpg.format=ssh
global	file:/home/user/.gitconfig-work	commit.gpgsign=true

--show-origin hangi değerin hangi dosyadan geldiğini gösteriyor. İki user.email satırı var ve sonra okunan kazanıyor; includeIf dosyanın sonunda olduğu için iş e-postası geçerli. Bu sıralama önemli: includeIf bölümünü [user]'dan önce yazarsan kişisel e-posta onu ezer. İş dosyası e-postayla kalmıyor; hook ve commit imzalama yazısında gördüğümüz SSH (Secure Shell; şifreli uzak bağlantı protokolü) imzalama ayarlarını da yalnız iş repo'larına uyguluyor. Kişisel repo'larda hiçbir şey değişmiyor:

$ cd ~/oss && git init -q bookstore-docs-fork && cd bookstore-docs-fork
$ git config get user.email
ada@example.org

Windows'ta Git for Windows ~'yu kullanıcı klasörüne çevirir, gitdir:~/work/ orada da çalışır. Yolu tam yazacaksan sürücü harfinin büyük-küçük farkına takılmamak için gitdir/i:C:/Users/ada/work/ biçimini kullan; /i eşleşmeyi büyük-küçük harfe duyarsız yapar. Git 2.56 ile gelen worktree: koşulu da var: .git yerine çalışma klasörünün yoluna bakar, linked worktree'lerde (worktree yazısındaki gibi ayrı klasörde açılan ek çalışma alanları) daha doğal sonuç verir.

hasconfig: Repo Yanlış Klasördeyse

gitdir: klasöre güvenir. Ada bir gün aceleyle şirket repo'sunu ~/Downloads'a clone'larsa koşul yine tutmaz. İkinci bir emniyet olarak remote URL'ine bakan koşulu ekleyelim:

[includeIf "hasconfig:remote.*.url:https://github.com/bookstore-example/**"]
	path = ~/.gitconfig-work
[includeIf "hasconfig:remote.*.url:git@github-work:bookstore-example/**"]
	path = ~/.gitconfig-work

Önce ve sonra:

$ cd ~/Downloads && git init -q bookstore-api && cd bookstore-api
$ git remote add origin https://github.com/bookstore-example/bookstore-api.git
$ git config get user.email
ada@example.org
$ git config get user.email
ada@example.com
$ git config list --show-origin | grep user.email
file:/home/user/.gitconfig	user.email=ada@example.org
file:/home/user/.gitconfig-work	user.email=ada@example.com

hasconfig:remote.*.url:, repo'nun herhangi bir remote'unun URL'i pattern'e uyuyorsa dosyayı dahil eder. Remote URL'i genelde repo'nun kendi config'inde durduğu için Git bu koşulu çözmek için config dosyalarını önceden tarar. Tavuk-yumurta sorununu önlemek için bu yolla dahil edilen dosyalar remote URL'i tanımlayamaz. İki koşulu birlikte kullanmak en sağlamı: klasör yanlış olsa bile şirket organizasyonundan clone'lanan her repo iş kimliğini alır.

İki GitHub Hesabı: SSH Host Alias'ı

Kimliği ayırmak işin yarısı. Ada'nın iki GitHub hesabı var ve GitHub bir SSH anahtarını yalnızca tek bir hesaba bağlamana izin verir. SSH tarafında çözüm, aynı sunucuya iki farklı takma adla bağlanmak. ~/.ssh/config:

Host github.com
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_ed25519_personal
    IdentitiesOnly yes

Host github-work
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_ed25519_work
    IdentitiesOnly yes

ssh -G bir host için geçerli olacak ayarları bağlanmadan yazdırır; doğru anahtarın seçildiğini görmek için iyi bir kontrol:

$ ssh -T -G -F ~/.ssh/config github-work | grep -E '^(hostname|user|identityfile|identitiesonly) '
user git
hostname github.com
identitiesonly yes
identityfile ~/.ssh/id_ed25519_work

IdentitiesOnly yes önemli: olmazsa ssh-agent'taki bütün anahtarlar sırayla denenir ve GitHub ilk kabul ettiği anahtarın hesabıyla oturum açar; bu da çoğu zaman yanlış hesap demek. Artık şirket repo'ları git@github-work:bookstore-example/bookstore-api.git adresinden çalışır. Ama ekip arkadaşların, dokümanlar ve .gitmodules dosyaları hep https://github.com/bookstore-example/... yazar. Her URL'i elle değiştirmek yerine Git'e çevirmeyi söyle:

$ git config set --global url.git@github-work:bookstore-example/.insteadOf https://github.com/bookstore-example/
$ git remote get-url origin
git@github-work:bookstore-example/bookstore-api.git
$ git remote -v
origin	git@github-work:bookstore-example/bookstore-api.git (fetch)
origin	git@github-work:bookstore-example/bookstore-api.git (push)

url.<base>.insteadOf <prefix>, <prefix> ile başlayan her URL'i kullanırken başına <base> koyar. Repo'nun config'indeki URL değişmez; çeviri her komutta yeniden yapılır. Prefix'i organizasyon adıyla sınırladığımız için Ada'nın kişisel repo'ları github.com host'unu, yani kişisel anahtarı kullanmaya devam eder.

url.insteadOf'un Başka Kullanımları

Aynı mekanizma yalnız SSH için değil. Bookstore ekibi iç ağda bir Git yansısı (mirror) kullanıyor olsun; aşağıdaki örnekte bu yansının yerine 127.0.0.1 üzerindeki yerel Git HTTP sunucusu geçiyor:

$ git config set --global url.http://127.0.0.1:52030/git/.insteadOf https://git.bookstore.example/
$ git clone https://git.bookstore.example/bookstore-api.git api-mirror
Cloning into 'api-mirror'...
$ git -C api-mirror config get remote.origin.url
https://git.bookstore.example/bookstore-api.git
$ git -C api-mirror remote get-url origin
http://127.0.0.1:52030/git/bookstore-api.git

Config'te yazan URL ile Git'in gerçekte bağlandığı URL farklı: config get ham değeri, remote get-url çevrilmiş hâlini gösteriyor. Bir clone "yanlış yere" gidiyorsa ilk bakacağın yer git config list --show-origin | grep insteadof. pushInsteadOf ise yalnız push'u çevirir: fetch HTTPS'ten, push SSH'tan yapılabilir. Submodule yazısındaki tavsiyenin karşılığı da bu: .gitmodules'a HTTPS URL'i yaz, SSH isteyen kendi makinesinde insteadOf ile çevirsin.

HTTPS'te İki Hesap: Credential Helper ve useHttpPath

HTTPS kullanıyorsan ayrım credential helper'da yapılır. GitHub'a bağlanırken parolanın yerine personal access token gider ve bunu sana Git'in değil credential helper'ın getirdiğini GitHub'a bağlanmak yazısında gördük. Helper'a ne gönderildiğini görmek için bu laboratuvarda gelen isteği yazdıran ve açıkça sahte değerler dönen bir test helper'ı kullandık:

$ git config set --local credential.helper /home/user/src/fake-helper.sh
$ printf 'url=https://github.com/bookstore-example/bookstore-api.git\n\n' | git credential fill
--- helper received:
protocol=https
host=github.com
--- end
protocol=https
host=github.com
username=ada-personal
password=fake-token-for-ada-personal

Varsayılan olarak helper yalnızca protokolü ve host'u görür; repo yolu gönderilmez. Yani helper'ın gözünde github.com'daki bütün repo'lar aynıdır ve tek bir token döner. İki çözüm var. Birincisi yolu da göndermek:

$ git config set --global credential.https://github.com.useHttpPath true
$ printf 'url=https://github.com/bookstore-example/bookstore-api.git\n\n' | git credential fill
--- helper received:
protocol=https
host=github.com
path=bookstore-example/bookstore-api.git
--- end
protocol=https
host=github.com
path=bookstore-example/bookstore-api.git
username=ada-personal
password=fake-token-for-ada-personal

Artık helper her repo için ayrı kayıt tutabilir; bedeli her yeni repo'da bir kez daha kimlik sormasıdır. İkincisi kullanıcı adını URL'e yazmak:

$ printf 'url=https://ada-work@github.com/bookstore-example/bookstore-api.git\n\n' | git credential fill
--- helper received:
protocol=https
host=github.com
path=bookstore-example/bookstore-api.git
username=ada-work
--- end
protocol=https
host=github.com
path=bookstore-example/bookstore-api.git
username=ada-work
password=fake-token-for-ada-work

Windows'ta Git for Windows ile gelen Git Credential Manager (GCM) birden fazla GitHub hesabını destekliyor ve belgesi tam bu iki yolu öneriyor: varsayılan hesap için git config --global credential.https://github.com.username <hesap>, belirli repo'lar için URL'de https://ada-work@github.com/.... Kayıtlı hesapları git credential-manager github list ile görebilirsin. Bunu insteadOf ile birleştirmek de mümkün: url.https://ada-work@github.com/bookstore-example/.insteadOf https://github.com/bookstore-example/.

Ekip İçin Önerilen Ayarlar

Bookstore ekibi herkesin global config'ine dahil ettiği küçük bir dosya kullanıyor:

[init]
	defaultBranch = main
[push]
	autoSetupRemote = true
[pull]
	rebase = true
[rebase]
	autoStash = true
	updateRefs = true
[fetch]
	prune = true
	pruneTags = true
[rerere]
	enabled = true
[merge]
	conflictStyle = zdiff3
[diff]
	algorithm = histogram
	colorMoved = default
[help]
	autocorrect = prompt

Gerekçeleri tek tek: push.autoSetupRemote, yeni bir branch'in ilk git push'unda upstream'i kendisi kurar; --set-upstream hatası tarihe karışır. pull.rebase true, pull yazısında gördüğümüz "divergent branches" sorusunu rebase yönünde kapatır ve gereksiz merge commit'lerini önler. rebase.autoStash, rebase öncesi yarım işi stash'leyip sonra geri koyar; rebase.updateRefs, üst üste duran branch'leri birlikte taşır. fetch.prune ve fetch.pruneTags, remote'ta silinen branch ve tag'leri yerelden de temizler. rerere.enabled, bir kez çözdüğün conflict'i hatırlayıp tekrar karşına çıktığında aynı şekilde çözer; uzun ömürlü branch'leri sık rebase'leyen ekiplerde vakit kazandırır. merge.conflictStyle zdiff3 conflict marker'larına ortak atayı da ekler. diff.algorithm histogram, taşınmış ve tekrar eden satırlarda daha okunur diff üretir; diff.colorMoved taşınan satırları ayrı renkle gösterir. Bunların her biri bir tercih; pull.rebase ekipte tartışılmadan dayatılacak bir ayar değil.

$ git config set --global include.path /home/user/src/team.gitconfig
$ git config list --show-scope --show-origin | grep team.gitconfig | head -4
global	file:/home/user/.gitconfig	include.path=/home/user/src/team.gitconfig
global	file:/home/user/src/team.gitconfig	init.defaultbranch=main
global	file:/home/user/src/team.gitconfig	push.autosetupremote=true
global	file:/home/user/src/team.gitconfig	pull.rebase=true
$ git -c help.autocorrect=never stauts
git: 'stauts' is not a git command. See 'git --help'.
$ git -c help.autocorrect=immediate stauts -sb
WARNING: You called a Git command named 'stauts', which does not exist.
Continuing under the assumption that you meant 'status'.
## No commits yet on main

help.autocorrect için immediate tehlikeli olabilir: yanlış yazılmış bir komut, Git'in tahmin ettiği başka bir komut olarak hemen çalışır. prompt önce sorar; dosyaya bu yüzden onu yazdık. Git 2.49'dan önce 1 değeri "0,1 saniye bekle" demekti, artık "hemen çalıştır" anlamına geliyor; eski bir dotfile'ın davranışı değişmiş olabilir.

Git 3.0 ile ilişkisine gelince: Git'in BreakingChanges belgesine göre bu dosyadaki ayarlardan 3.0'da varsayılanı değişecek tek olan init.defaultBranch (main olacak). Diğerlerinin hiçbiri 3.0'da varsayılan olmuyor; yani bu dosya 3.0'dan sonra da gerekli. Git 3.0'a hazırlık yazısındaki kontrol listesine bunu ekleyebilirsin.

Sık Düşülen Tuzaklar

İlki gitdir: yolunun sonundaki /'ı unutmak. Açılıştaki örnek bunun ta kendisi ve Git hiçbir uyarı vermiyor. Kurduktan sonra her iki tür klasörde git config list --show-origin | grep user.email ile kontrol et.

İkincisi yanlış e-postayla atılmış commit'leri geç fark etmek. Ada iş repo'sunda, includeIf'i kurmadan önce üç commit atmış:

$ git log --format='%h %ae %s'
4fdbdc0 ada@example.org Document how to run
b55211d ada@example.org Add notes
6215b60 ada@example.org Add README
$ git config get user.email
ada@example.com
$ git rebase -q --root --exec 'git commit -q --amend --reset-author --no-edit'
$ git log --format='%h %ae %s'
cf8d2c0 ada@example.com Document how to run
255e08e ada@example.com Add notes
5d91916 ada@example.com Add README

--exec her commit'ten sonra verilen komutu çalıştırır; --reset-author author'u o anki config'ten yeniden yazar. Bu repo'da yalnız Ada'nın commit'leri olduğu için --root ile hepsini düzelttik. Paylaşılmış bir branch'te --root yerine origin/main gibi bir taban ver ki başkalarının commit'lerine dokunma. --reset-author author tarihini de günceller. Bunlar geçmişi yeniden yazar: henüz push'lamadıysan sorun yok, push'ladıysan rebase yazısındaki --force-with-lease kuralları geçerli. Geçmişi değiştirmek istemiyorsan, yanlış adresleri .mailmap dosyasıyla doğru adrese eşleyebilirsin; log ve shortlog bunu okur.

Üçüncüsü IdentitiesOnly olmadan SSH alias'ı kurmak. ssh-agent'ta iki anahtar yüklüyse ve alias'ta bu satır yoksa, ssh önce agent'taki anahtarı dener ve yanlış hesapla bağlanırsın. Belirti genelde "Repository not found": repo var, ama yanlış hesabın erişimi yok.

Dördüncüsü insteadOf'u unutup sorunu başka yerde aramak. Global config'teki eski bir insteadOf satırı yıllar sonra bir clone'u bambaşka bir sunucuya yönlendirebilir. Yeni bir makineye dotfile taşırken bu satırları özellikle gözden geçir.

Küçük Bir Deneme

★☆☆ Kendi makinende git config list --show-origin --show-scope çıktısını incele. Her ayarın hangi dosyadan geldiğini ve aynı anahtarın birden fazla yerde tanımlı olup olmadığını bul.

★★☆ ~/work/ için bir includeIf kur, önce sondaki / olmadan, sonra onunla dene. İki durumda ~/work/x ve ~/work/a/b/y repo'larında git config get user.email çıktısını karşılaştır.

★★★ Bir test helper'ı yaz (gelen satırları stderr'e yazsın, sahte bir kullanıcı adı ve token dönsün), useHttpPath kapalı ve açıkken git credential fill ile ne aldığını gör. Sonra bir insteadOf kuralı ekleyip helper'ın gördüğü host'un nasıl değiştiğini incele.

Bu yazıda Git'in nasıl davrandığını ayarladık. Bir sonraki yazıda geçmişin içinde arama yapacağız: git grep, git log -S, -G ve -L ile Bookstore'daki indirim hesabının ne zaman, kim tarafından ve neden değiştiğini bulacağız.