Neden Git?
Ilgaz Bank'ın risk ekibinde üç analist aynı rapor betiğini geliştiriyor. "rapor_v2_son_FINAL.py" dosyaları e-postayla dolaşıyor, kimin hangi satırı değiştirdiği belli değil, dün çalışan sürüm kayboldu. Git, sürüm kontrol sistemi (version control system) olarak bu kaosu bitirir: her değişiklik kim, ne zaman, neden bilgisiyle kaydedilir; herkes aynı anda ayrı dallarda çalışır; istenirse herhangi bir güne geri dönülür. Git öğrenmek Python'dan bağımsız bir beceri ama Python geliştiricisi için şart: işe alım testlerinde ilk sorulan şey GitHub profilindir.
Temel akış: init, add, commit
Git üç bölgeyle çalışır: çalışma dizini (dosyaların), hazırlık alanı (staging, "sonraki kayda girecekler") ve depo (repository, kalıcı geçmiş). Günlük döngü şudur:
git init— klasörü depo yapar (bir kez).- Dosyaları düzenle;
git statusile neyin değiştiğini gör. git add src/rapor.py— değişikliği hazırlık alanına koy (git add .hepsini).git commit -m "Şehir raporuna KDV sütunu eklendi"— anlamlı bir mesajla kaydet.git log --oneline— geçmişi gör.
İyi commit mesajı emir kipinde, kısa ve "ne, neden" sorusuna yanıt verir: "KDV hesabını Decimal'e taşı (float yuvarlama hatası)". "düzeltmeler", "wip", "asdf" gibi mesajlar üç ay sonra hiçbir şey anlatmaz.
Dal, birleştirme ve pull request
Dal (branch), ana koda dokunmadan deneme yapmanı sağlar. git switch -c sehir-raporu yeni dal açar; orada commit'lersin; iş bitince git switch main ve git merge sehir-raporu ile birleştirirsin. Ekip çalışmasında birleştirme doğrudan değil, pull request (PR; GitLab'da merge request) üzerinden yapılır: dalını sunucuya git push ile gönderirsin, PR açarsın, ekip arkadaşın inceler, onaylar, sonra main'e birleşir. Aynı satırı iki kişi farklı değiştirdiyse çakışma (conflict) çıkar; Git dosyaya <<<<<<< ve >>>>>>> işaretleri koyar, sen doğru halini seçip yeniden commit'lersin.
| Komut | Ne yapar |
|---|---|
| git clone URL | Uzak depoyu indirir |
| git switch -c dal-adi | Yeni dal açıp ona geçer |
| git pull | Uzaktaki değişiklikleri alır |
| git push -u origin dal-adi | Dalı uzak depoya gönderir |
| git merge dal-adi | Dalı bulunduğun dala birleştirir |
| git diff | Kaydedilmemiş değişiklikleri gösterir |
| git restore dosya | Dosyadaki kaydedilmemiş değişikliği geri alır |
| git stash | Yarım işi geçici rafa kaldırır |
.gitignore: neyi depoya koymazsın?
Sanal ortam (virtual environment), önbellek, gizli anahtar ve büyük veri dosyaları depoya girmez. .gitignore dosyası satır satır desen (pattern) içerir; Git o desenlere uyan dosyaları görmezden gelir. Desenler kabuk joker karakterleriyle yazılır: * herhangi karakter dizisi, ? tek karakter, sonda / klasör, başta ! istisna. Python tarafında aynı jokerleri fnmatch modülü çözer; CI betiklerinde "bu dosya ignore'da mı" kontrolü böyle yazılır.
from fnmatch import fnmatch
gitignore = """
# Python
__pycache__/
*.pyc
.venv/
# Gizli bilgi
.env
# Veri ve çıktı
data/*.csv
*.log
!onemli.log
"""
def kurallar(metin: str) -> list[str]:
return [s.strip() for s in metin.splitlines() if s.strip() and not s.strip().startswith("#")]
def yok_sayilir_mi(yol: str, kurallar: list[str]) -> bool:
sonuc = False
for k in kurallar:
istisna = k.startswith("!")
desen = k[1:] if istisna else k
if desen.endswith("/"):
uydu = yol.startswith(desen) or ("/" + desen) in ("/" + yol)
else:
uydu = fnmatch(yol, desen) or fnmatch(yol.rsplit("/", 1)[-1], desen)
if uydu:
sonuc = not istisna
return sonuc
k = kurallar(gitignore)
for yol in ["src/rapor.py", "src/__pycache__/rapor.cpython-312.pyc", ".env", "data/satis.csv", "data/README.md", "hata.log", "onemli.log", ".venv/bin/python"]:
print(f"{yol:40} {'YOK SAY' if yok_sayilir_mi(yol, k) else 'izle'}")
Kurallar sırayla uygulanır ve son eşleşen kazanır; bu yüzden !onemli.log satırı *.log'dan sonra gelince istisna olur. Gerçek Git'in eşleştirme kuralları biraz daha zengindir (**, kök bağıl yollar) ama bu basit model günlük ihtiyacın yüzde doksanını karşılar.
Proje yapısı: src yerleşimi
Küçük bir betik tek dosya olabilir; ama test, bağımlılık ve ekip devreye girince standart bir ağaç gerekir. Python topluluğunun önerdiği src yerleşimi (src layout), paketi src/ altına koyar; böylece testler yanlışlıkla kurulmamış yerel kodu değil, kurulmuş paketi test eder.
import tempfile
from pathlib import Path
AGAC = {
"README.md": "# Anadolu Rapor\n\nŞehir bazlı satış raporu aracı.\n\n## Kurulum\n\n pip install -e .\n",
".gitignore": ".venv/\n__pycache__/\n.env\n",
"pyproject.toml": '[project]\nname = "anadolu-rapor"\nversion = "0.1.0"\n',
".env.example": "DB_URL=sqlite:///market.db\nAPI_KEY=buraya-anahtar\n",
"src/anadolu_rapor/__init__.py": "",
"src/anadolu_rapor/rapor.py": "def sehir_ozeti(satirlar):\n return {}\n",
"tests/test_rapor.py": "import unittest\n",
}
with tempfile.TemporaryDirectory() as d:
kok = Path(d) / "anadolu-rapor"
for yol, icerik in AGAC.items():
hedef = kok / yol
hedef.parent.mkdir(parents=True, exist_ok=True)
hedef.write_text(icerik, encoding="utf-8")
for p in sorted(kok.rglob("*")):
derinlik = len(p.relative_to(kok).parts) - 1
print(" " * derinlik + ("+ " if p.is_dir() else "- ") + p.name)
print("README ilk satır:", (kok / "README.md").read_text(encoding="utf-8").splitlines()[0])
README (benioku) dosyası projenin kapısıdır: ne yapar, nasıl kurulur, nasıl çalıştırılır, nasıl test edilir. Dört başlık yeterlidir. .env.example ise gerçek değerler olmadan hangi ortam değişkenlerinin gerektiğini gösterir; depoya girer, .env girmez.
Config ve .env: gizli bilgi koddan ayrı
Veritabanı parolası, API anahtarı, e-posta şifresi kodda yazmaz. Bunlar ortam değişkeni (environment variable) olarak işletim sisteminden okunur: os.environ bir sözlük gibi davranır. Geliştirme makinesinde değişkenleri .env dosyasından yüklersin (python-dotenv paketi ya da birkaç satırlık kendi okuyucun); sunucuda ise dağıtım sistemi onları doğrudan tanımlar. Kod iki ortamda da aynıdır.
import os
ENV_ORNEK = """
# Anadolu Market yerel ayarlar
DB_URL=sqlite:///market.db
API_KEY="abc-123"
DEBUG=true
"""
def env_yukle(metin: str) -> dict[str, str]:
degerler = {}
for satir in metin.splitlines():
satir = satir.strip()
if not satir or satir.startswith("#") or "=" not in satir:
continue
ad, deger = satir.split("=", 1)
degerler[ad.strip()] = deger.strip().strip('"').strip("'")
return degerler
for ad, deger in env_yukle(ENV_ORNEK).items():
os.environ.setdefault(ad, deger) # gerçek ortamda zaten varsa ezme
def ayar(ad: str, varsayilan: str | None = None) -> str:
deger = os.environ.get(ad, varsayilan)
if deger is None:
raise RuntimeError(f"Eksik ortam değişkeni: {ad} (.env dosyasını kontrol et)")
return deger
print("DB_URL :", ayar("DB_URL"))
print("API_KEY:", ayar("API_KEY")[:3] + "***")
print("DEBUG :", ayar("DEBUG").lower() == "true")
print("PORT :", ayar("PORT", "8000"))
try:
ayar("SMTP_SIFRE")
except RuntimeError as e:
print("hata:", e)
setdefault önemli: sunucuda tanımlı gerçek değer varsa .env'deki değeri yazmaz. Eksik değişkende programın ilk saniyede anlaşılır bir mesajla durması, iki saat sonra "None" hatasıyla çökmesinden iyidir.
print(os.environ) yazmak tüm gizli bilgileri log dosyasına döker. Değeri göstermen gerekiyorsa ilk üç karakter artı yıldız yeter.Kod inceleme kültürü
Pull request'in asıl değeri kodun ikinci bir göz tarafından okunmasıdır. İnceleyen (reviewer) şunlara bakar: iş kuralı doğru mu, test var mı, isimler anlaşılır mı, gizli bilgi sızmış mı, aynı şey daha basit yazılabilir mi? Yorum yazarken kişiyi değil kodu konuş: "Bu döngü O(n²), sözlükle tek geçişte olur" iyi; "Bunu nasıl böyle yazarsın" kötü. Yazar olarak ise PR'ı küçük tut (bir PR bir iş), açıklamaya "neden"i yaz ve yorumları savunma değil öğrenme fırsatı olarak gör. İncelemeyi bekletmemek de nezakettir: küçük PR aynı gün incelenir.
Derinleş: Git'i gerçekten kullanmak
Temel akışı (add, commit, dal, PR) ve proje ağacını gördün. İşte ilk haftanda ise şu sorular gelir: "Git'i nasıl kurarım, kimliğimi nasıl tanıtırım?", "Yanlış commit'i nasıl geri alırım?", "Birleştirmede çakışma çıktı, bu <<<<<<< işaretleri ne?", "Windows'ta çalışan arkadaşımın dosyalarında neden her satır değişmiş görünüyor?", "Hangi commit hatayı getirdi?". Bu bölüm bunları yanıtlar ve Git çıktılarını Python'la işleyen küçük araçlar yazdırır. Otomatik kalite kontrolleri (pre-commit, GitHub Actions) kalite-ci, paket sürümleme ve yayımlama paketleme-dagitim dersinde.
Kurulum ve ilk ayarlar
# Windows: winget install --id Git.Git -e (ya da git-scm.com; Git Bash da gelir) # macOS: xcode-select --install ya da brew install git # Ubuntu: sudo apt install git | Fedora: sudo dnf install git git --version git config --global user.name "Ayşe Yılmaz" git config --global user.email "ayse@ilgazbank.example" git config --global init.defaultBranch main git config --global pull.rebase true # pull sırasında gereksiz merge commit'i olmasın git config --global core.autocrlf true # yalnız Windows: depoda LF, diskte CRLF git config --global core.autocrlf input # macOS/Linux git config --list --show-origin # hangi ayar nereden geliyor # GitHub/GitLab için SSH anahtarı (parola yerine) ssh-keygen -t ed25519 -C "ayse@ilgazbank.example" cat ~/.ssh/id_ed25519.pub # bu satırı GitHub > Settings > SSH keys'e yapıştır ssh -T git@github.com # "Hi ayse! You've successfully authenticated"
Satır sonu farkı (Windows CRLF \r\n, Linux/macOS LF \n) karışık ekiplerde "her satır değişti" görünen diff'lerin bir numaralı nedenidir. Ekip genelinde kalıcı çözüm depo köküne bir .gitattributes koymaktır: * text=auto eol=lf ve *.bat text eol=crlf.
dosyalar = {
"src/rapor.py": b"import csv\nprint('ok')\n",
"src/kargo.py": b"UCRET = 45\r\nKDV = 0.2\r\n",
"scripts/kur.bat": b"@echo off\r\npy -m venv .venv\r\n",
"README.md": b"# Rapor\r\nKurulum\n",
}
for yol, icerik in dosyalar.items():
crlf = icerik.count(b"\r\n")
lf = icerik.count(b"\n") - crlf
tur = "KARIŞIK" if crlf and lf else "CRLF" if crlf else "LF"
uyari = "" if tur == "LF" or yol.endswith(".bat") else " <- .gitattributes ile LF'e çevrilmeli"
print(f"{yol:18} {tur}{uyari}")
Geri alma: hangi komut ne zaman?
| Durum | Komut | Paylaşılmış geçmişte güvenli mi? |
|---|---|---|
| Dosyadaki kaydedilmemiş değişikliği at | git restore dosya | Evet (yerel) |
Yanlışlıkla add ettin | git restore --staged dosya | Evet (yerel) |
| Son commit mesajı yanlış / dosya unutuldu | git commit --amend | Yalnız push etmediysen |
| Son commit'i geri al, değişiklikler kalsın | git reset --soft HEAD~1 | Yalnız push etmediysen |
| Push edilmiş commit'i geri al | git revert <hash> | Evet: ters commit üretir |
| "Kaybettim" sandığın commit'i bul | git reflog | Evet (yerel kayıt) |
| Yarım işi rafa kaldır / geri al | git stash / git stash pop | Evet |
git reset --hard kaydedilmemiş işi geri dönüşsüz siler. Paylaşılan dalda geçmişi yeniden yazıp push --force yapmak ekip arkadaşlarının işini bozar; zorunluysan --force-with-lease kullan ve önce haber ver. Ana dalda geri almanın yolu her zaman revert'tür.Birleştirme çakışması (merge conflict)
İki dal aynı satırı farklı değiştirdiyse Git karar veremez ve dosyaya işaretler koyar. <<<<<<< HEAD ile ======= arası senin dalın, ======= ile >>>>>>> arası gelen dal. Doğru sonucu elle yazar, işaretleri silersin, sonra git add dosya ve git commit (rebase sırasındaysan git rebase --continue). Vazgeçmek için git merge --abort. İşaretlerin yanlışlıkla commit'lenmemesi için CI'da basit bir kontrol çok işe yarar:
dosya = """VERGI_ORANI = 0.20
<<<<<<< HEAD
KARGO_UCRETI = 45
=======
KARGO_UCRETI = 49.90
>>>>>>> kargo-zammi
PARA_BIRIMI = "TL"
"""
def cakismalar(metin):
bloklar, durum, bizim, onlarin = [], None, [], []
for satir in metin.splitlines():
if satir.startswith("<<<<<<<"):
durum, bizim, onlarin = "bizim", [], []
elif satir.startswith("=======") and durum == "bizim":
durum = "onlarin"
elif satir.startswith(">>>>>>>") and durum == "onlarin":
bloklar.append({"bizim": bizim, "onlarin": onlarin, "dal": satir[8:].strip()})
durum = None
elif durum == "bizim":
bizim.append(satir)
elif durum == "onlarin":
onlarin.append(satir)
return bloklar
for b in cakismalar(dosya):
print(f"main: {b['bizim']} | {b['dal']}: {b['onlarin']}")
Rebase, etiket ve geçmişte arama
git rebase main dalındaki commit'leri main'in en güncel hâlinin üstüne taşır; geçmiş düz kalır. Kural: yalnızca kendi, paylaşılmamış dalını rebase et. Sürüm yayımlarken git tag -a v1.4.0 -m "Kasım sürümü" ve git push origin v1.4.0. Hatanın hangi commit'le geldiğini bulmak için git bisect ikili arama yapar: iyi ve kötü commit'i söylersin, Git ortadakini açar, sen test edersin; birkaç adımda suçlu bulunur. git blame dosya her satırı son değiştireni, git log -S "KARGO_UCRETI" --oneline o metni ekleyen/silen commit'leri gösterir.
git log --oneline --graph --all -20 # dalları görsel izle git log --since="2 weeks" --author="Ayşe" -- src/ git show v1.4.0 --stat git bisect start; git bisect bad; git bisect good v1.3.0 git bisect run python -m pytest tests/test_kargo.py # otomatik ikili arama git bisect reset
Git'in "makine dostu" çıktılarını betiklerle işlemek CI ve sürüm otomasyonunun temelidir. git status --porcelain her satırda iki harfli durum kodu verir (M değişti, A eklendi, ?? izlenmiyor); Conventional Commits (feat:, fix:, feat!:) mesajlarından ise sürüm numarası ve değişiklik günlüğü (CHANGELOG) otomatik üretilir:
import re
log = """a1b2c3d feat(rapor): şehir kırılımı eklendi
9f8e7d6 fix: KDV yuvarlaması Decimal'e taşındı
5c4b3a2 docs: README kurulum adımları
1a2b3c4 feat!: CSV ayırıcısı ; oldu"""
gruplar = {"Yeni özellikler": [], "Hata düzeltmeleri": [], "Kırıcı değişiklikler": []}
for satir in log.splitlines():
h, mesaj = satir.split(" ", 1)
m = re.match(r"(\w+)(\([\w-]+\))?(!)?: (.+)", mesaj)
if not m:
continue
tur, _, kirici, ozet = m.groups()
if kirici:
gruplar["Kırıcı değişiklikler"].append(f"{ozet} ({h})")
elif tur == "feat":
gruplar["Yeni özellikler"].append(f"{ozet} ({h})")
elif tur == "fix":
gruplar["Hata düzeltmeleri"].append(f"{ozet} ({h})")
for baslik, maddeler in gruplar.items():
if maddeler:
print("##", baslik)
print("\n".join("- " + x for x in maddeler))
README.md, kullanım hakkını belirten LICENSE (şirket içi kodda genellikle "tescilli"), sürüm farklarını anlatan CHANGELOG.md, örnek ayar .env.example ve satır sonu kuralı için .gitattributes.git revert ile hızla geri dönülür, sonra kök neden ayrı bir dalda düzeltilir. Sürüm etiketleri ve CHANGELOG, Conventional Commits mesajlarından otomatik üretilir; bu yüzden commit mesajı kuralı PR kontrolünde denetlenir.Ortak çalışma: issue, inceleme, birleştirme kararı
Git komutları işin yarısıdır; diğer yarısı insanlarla anlaşmaktır. Depo paylaşılınca değişiklik bir sohbetle başlar, bir kararla biter.
| Aşama | İyi uygulama |
|---|---|
| Issue (kayıt) | Ne bekleniyor, ne oluyor, nasıl tekrar edilir, hangi sürüm. Karar ve gerekçe de buraya yazılır. |
| Dal ve commit | Tek amaçlı dal, anlaşılır mesaj: Kupon tutarını Decimal'e taşı. Issue numarasını mesaja ekle. |
| Pull request | Neyi, neden değiştirdiğin ve nasıl denediğin. Küçük tutulan değişiklik daha iyi incelenir. |
| Kod inceleme (code review) | Kişiye değil koda, somut ve gerekçeli yorum: "Boş liste gelirse burada sıfıra bölünür, bir test ekleyelim mi?" |
| Yoruma cevap | Kabul ettiysen düzelt, etmediysen nedenini yaz. Sessizce kapatma; karar kayıtta kalsın. |
| CI (sürekli entegrasyon) | Her değişiklikte aynı testler aynı ortamda çalışır. "Bende çalışıyor" tartışmasını bitirir. |
| Birleştirme kararı | Testler geçti, inceleme onaylandı, açık yorum kalmadı. Biri eksikse birleştirme. |
Çakışma (merge conflict) en çok iki kişi aynı satırı değiştirince çıkar. Sık git pull, küçük değişiklik ve işi önceden konuşmak çakışmayı azaltır. Çıktığında iki tarafın niyetini oku, gerekiyorsa arkadaşına sor, sonra testleri yeniden çalıştır.
Bir işe alımcı portföy deponu ortalama bir-iki dakika inceler ve çoğu zaman yalnız README'yi okur. Bu bölüm, bir analiz ya da yazılım projesini GitHub'da "okunur" bir portföy parçasına çevirmeyi anlatıyor: README şablonu, bulgu özeti, ekran görüntüleri ve depoyu yayına alma adımları.
Portföy deposunun yapısı
anadolu-market-analizi/ ├── README.md ← vitrin: 1 dakikada okunur ├── notebooks/ ← keşif (çıktıları temizlenmiş, nbstripout) ├── src/analiz/ ← tekrar kullanılan kod ├── raporlar/rapor.html ← nbconvert/Quarto çıktısı ├── gorseller/ ← README'de kullanılan PNG'ler (küçük, sıkıştırılmış) ├── data/README.md ← veri nereden, nasıl indirilir (ham veri depoda değil) ├── tests/ ├── pyproject.toml / requirements.txt └── LICENSE
README şablonu
İyi bir portföy README'si bir hikâye anlatır: sorun neydi, ne yaptın, ne buldun, nasıl tekrar üretilir. Aşağıdaki başlıklar hem analiz hem yazılım projelerinde işe yarar:
# Anadolu Market: Şehir bazlı satış ve iade analizi
> 910 siparişlik e-ticaret verisinde ciro ve iadeyi şehir, kanal ve zamana göre inceledim.
> En önemli bulgu: ... (tek cümle, rakamla)

## Sorun
Yönetim hangi şehirlerde büyümeye yatırım yapılacağını soruyor.
## Veri
- Kaynak: Kesit Python "anadolu_market" kurgusal veri seti (10 tablo, 6.236 satır)
- Dönem: 2022-06 – 2024-12
## Yöntem
SQL (sqlite3) ile birleştirme → pandas ile temizlik → kohort ve RFM → görselleştirme
## Bulgular
| Şehir | Teslim edilen sipariş | Ciro (TL) |
|---|---|---|
| İstanbul | 162 | 199.387,25 |
| ...
1. ...
2. ...
## Öneriler ve sınırlamalar
## Nasıl çalıştırılır
git clone https://github.com/kullanici/anadolu-market-analizi
cd anadolu-market-analizi && pip install -r requirements.txt
python -m analiz.rapor # raporlar/rapor.html üretir
## Teknolojiler
Python 3.12 · pandas · SQLite · matplotlib · pytest
Bulgu tablosunu elle yazmak yerine koddan üret; README sayıları ile rapor sayıları hiç ayrışmaz:
import veri
conn = veri.baglan("anadolu_market", tablolar=["orders", "order_items", "customers"])
satirlar = conn.execute("""
SELECT c.city, COUNT(DISTINCT o.order_id),
ROUND(SUM(oi.quantity * oi.unit_price * (1 - oi.discount)), 2) AS ciro
FROM orders o JOIN customers c USING (customer_id) JOIN order_items oi USING (order_id)
WHERE o.status = 'delivered' AND c.city IS NOT NULL
GROUP BY c.city ORDER BY ciro DESC LIMIT 3""").fetchall()
tl = lambda x: f"{x:,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
print("| Şehir | Teslim edilen sipariş | Ciro (TL) |")
print("|---|---|---|")
for sehir, n, ciro in satirlar:
print(f"| {sehir} | {n} | {tl(ciro)} |")
Ekran görüntüleri ve görseller
README'nin en üstündeki tek bir grafik ya da uygulama ekran görüntüsü, okuyucunun kalmasını sağlar. Görselleri gorseller/ klasörüne koy, göreli yolla bağla (), alternatif metni anlamlı yaz, boyutu küçük tut (genişlik 1200 px civarı, birkaç yüz KB). Etkileşimli bir uygulama ise kısa bir GIF ya da canlı demo bağlantısı (Streamlit Community Cloud, Render, GitHub Pages) çok etkilidir.
GitHub'da yayın
- Gizli bilgi taraması:
.env, parola, kişisel veri depoda olmamalı (git log -p | grep -i parola; GitHub secret scanning da uyarır). - Büyük dosya: GitHub 100 MB üstü dosyayı reddeder, 50 MB'ta uyarır. Veriyi depoya koyma; nasıl indirileceğini
data/README.md'de anlat ya da Git LFS kullan. - Notebook çıktılarını temizle (
nbstripout) ama rapor HTML'iniraporlar/'a koy. gh repo create anadolu-market-analizi --public --source=. --pushya da web arayüzünden depo oluşturupgit remote add origin ...+git push -u origin main.- Depo sayfasında "About" kısmına tek cümlelik açıklama ve konular (topics:
python,pandas,data-analysis) ekle; profilinde en iyi 4-6 depoyu sabitle (pin). - Rapor HTML'ini GitHub Pages ile yayınla (Settings → Pages →
main/docs); README'den bağlantı ver.
Dal stratejisi ve otomasyon
Ekipler büyüdükçe dal düzeni kural haline gelir. En yaygın model "GitHub flow": main her zaman çalışır durumdadır; her iş kısa ömürlü bir dalda yapılır (feature/sehir-raporu, fix/kdv-yuvarlama); PR ile birleşir; birleşince dal silinir. Daha katı kurumlarda (bankacılık gibi) develop ve release dalları da olur. Hangi modeli kullanırsan kullan, kural aynı: main'e doğrudan push yok, her değişiklik incelenir.
Otomasyon bunu destekler. Pre-commit kancaları (hook) commit'ten önce biçimlendiriciyi (ruff, black) ve basit kontrolleri çalıştırır; böylece inceleme, girinti ve boşluk tartışmasına harcanmaz. CI (GitHub Actions, GitLab CI) her PR'da testleri koşar ve kırmızıysa birleştirmeyi engeller. Commit mesajlarında "Conventional Commits" düzeni (feat:, fix:, docs:) değişiklik günlüğünü otomatik üretmeyi sağlar. İleri seviyedeki "Kalite ve CI" dersinde bu zinciri uçtan uca kuracaksın; bugün için hedefin küçük: anlamlı commit, temiz .gitignore, gizli bilgi .env'de.