Git Remote ve Clone: origin, fetch, push ve Upstream
Remote'un ne olduğunu, GitHub yerine yerel bir bare repo ile git remote add, push -u ve upstream ayarını, origin/main gibi remote-tracking branch'leri, git clone'un yaptığı işleri, fetch ile merge'ün ayrımını, yeni bir branch'i paylaşmayı, git remote show/rename'i ve reddedilen push'u gerçek çıktılarla görüyoruz.
Sürüm notu: Bu yazıdaki komutlar Git 2.54 ile denenmiştir; remote olarak GitHub yerine yerel bir bare repo kullanılmıştır.
Önce Tahmin Et
Bookstore repo'su şimdiye kadar yalnızca Ada'nın bilgisayarında yaşadı. Alan'ın commit'ini bile aynı repo'da, onun adıyla attık. Ada bir gün git push yazarsa ne olur? Git commit'leri nereye gönderir?
$ git remote -v
$ git push
fatal: No configured push destination.
Either specify the URL from the command-line or configure a remote repository using
git remote add <name> <url>
and then push using the remote name
git push <name>
Hiçbir yere. git remote -v boş döndü, yani bu repo başka hiçbir repo'nun varlığından haberdar değil. Git'te "sunucu" diye özel bir kavram yok; GitHub'daki repo da senin bilgisayarındaki gibi bir repo. Onu tanıtmadıkça Git'in gidecek bir adresi olmaz.
Remote Nedir?
Remote, başka bir repo'ya verdiğin kısa bir addır: bir URL ve bir takma ad. Telefon rehberindeki bir kayıt gibi düşün: numarayı her seferinde ezbere yazmak yerine "Alan" dersin. Remote'un kendisi bir şey indirmez ya da göndermez; sadece "o repo şurada" der.
Gerçek hayatta bu adres GitHub'daki repo'nun URL'idir: HTTPS için https://github.com/ada/bookstore.git, SSH için git@github.com:ada/bookstore.git biçiminde. GitHub'da repo oluşturmayı ve kimlik doğrulamayı ayrı bir yazıda anlatacağız. Bu yazıdaki komutları çalıştırılabilir tutmak için GitHub'daki repo'nun yerine yerel bir bare repo kullanıyoruz. Bare repo, working tree'si olmayan, yalnızca .git içeriğinden oluşan bir repo'dur; GitHub'ın sunucularında tuttuğu da tam olarak budur:
$ git init --bare ../bookstore.git
Initialized empty Git repository in .../bookstore.git/
$ ls ../bookstore.git
HEAD
config
description
hooks
info
objects
refs
Çıktıdaki uzun yolu ... ile kısalttık. İçeride src/ ya da README yok, sadece object'ler ve ref'ler var. Kimse bu repo'da doğrudan çalışmayacak; herkes kendi clone'unda çalışıp buraya push edecek.
Remote Eklemek ve İlk Push
$ git remote add origin ../bookstore.git
$ git remote -v
origin ../bookstore.git (fetch)
origin ../bookstore.git (push)
$ git push -u origin main
To ../bookstore.git
* [new branch] main -> main
branch 'main' set up to track 'origin/main'.
origin bir gelenek, özel bir kelime değil. git clone ilk remote'a bu adı verdiği için herkes onu kullanır. git push -u origin main ise "yerel main'i origin'e gönder ve bundan sonra ikisini eşleştir" demek. Terminalde bunlara ek olarak Enumerating objects gibi ilerleme satırları da görürsün; Git bunları yalnızca etkileşimli bir terminalde basar.
-u (uzun adı --set-upstream) kalıcı bir ayar bırakır. .git/config'e bakalım:
$ cat .git/config
[core]
repositoryformatversion = 0
filemode = true
bare = false
logallrefupdates = true
ignorecase = true
precomposeunicode = true
[remote "origin"]
url = ../bookstore.git
fetch = +refs/heads/*:refs/remotes/origin/*
[branch "main"]
remote = origin
merge = refs/heads/main
[remote "origin"] bölümü rehberdeki kayıt. fetch satırı, remote'taki her branch'in yerelde refs/remotes/origin/ altına kopyalanacağını söylüyor. [branch "main"] bölümü ise main'in upstream'ini tanımlıyor: bu branch, origin'deki main ile eşleşiyor. Bundan sonra main'deyken düz git push ve git pull nereye gideceğini bilir.
Remote-tracking Branch: origin/main
$ git branch -vv
* main 85ae714 [origin/main] Merge branch 'spring-sale'
$ git branch -a
* main
remotes/origin/main
Branch yazısında boş kalan git branch -a artık bir satır daha gösteriyor: remotes/origin/main. Bu bir remote-tracking branch, yani "son konuştuğumda origin'deki main buradaydı" diyen yerel bir pointer. Üzerinde çalışamazsın, commit atamazsın; Git onu yalnızca fetch, pull ve push sırasında günceller.
Bunu bir hava durumu raporu gibi düşün. Rapor sabah alındı; öğleden sonra hava değişmiş olabilir ama rapor yenilenene kadar eski bilgiyi gösterir. origin/main canlı bir bağlantı değil, son fetch anındaki fotoğraftır. git branch -vv'deki [origin/main] da main'in upstream'ini gösterir; yerel branch remote'un önünde ya da gerisindeyse burada ahead 1, behind 2 gibi bilgiler de çıkar.
Clone: Alan'ın Kopyası
Alan ekibe katılıyor. Kendi klasöründe repo'yu clone'luyor:
# Alan'ın bilgisayarında
$ git clone ../bookstore.git
Cloning into 'bookstore'...
done.
$ cd bookstore
$ git remote -v
origin .../bookstore.git (fetch)
origin .../bookstore.git (push)
$ git branch -a
* main
remotes/origin/HEAD -> origin/main
remotes/origin/main
$ git status
On branch main
Your branch is up to date with 'origin/main'.
nothing to commit, working tree clean
git clone dört işi tek seferde yapar: yeni bir klasör açar, bütün geçmişi indirir, origin remote'unu ekler ve remote'un varsayılan branch'ini yerelde açıp upstream'ini ayarlar. Alan'ın elinde artık geçmişin tamamı var; Ada'nın bilgisayarı kapansa da bütün commit'ler Alan'da duruyor. Dağıtık sürüm kontrolü tam olarak bu demek. remotes/origin/HEAD ise remote'un varsayılan branch'inin main olduğunu not eder.
Fetch: Haberleri Almak
Alan kitap listesine Foundation'ı ekleyip push ediyor:
# Alan'ın bilgisayarında
$ git commit -am "Add Foundation"
[main 3510c22] Add Foundation
1 file changed, 1 insertion(+)
$ git status -sb
## main...origin/main [ahead 1]
$ git push
To .../bookstore.git
85ae714..3510c22 main -> main
Ada'nın tarafında hiçbir şey kendiliğinden değişmez. Git sunucuyu kendi kendine yoklamaz; origin/main hâlâ eski commit'i gösterir. Haberleri almak için git fetch gerekir:
$ git log --oneline -1 origin/main
85ae714 Merge branch 'spring-sale'
$ git fetch
From ../bookstore
85ae714..3510c22 main -> origin/main
$ git status
On branch main
Your branch is behind 'origin/main' by 1 commit, and can be fast-forwarded.
(use "git pull" to update your local branch)
nothing to commit, working tree clean
$ git log --oneline main..origin/main
3510c22 Add Foundation
fetch yeni commit'leri indirdi ve origin/main'i ileri aldı, ama Ada'nın main'ine ve working tree'sine dokunmadı. Bu yüzden fetch her zaman güvenlidir; istediğin kadar çalıştırabilirsin. Gelenleri inceledikten sonra kendi branch'ine almak ayrı bir adımdır:
$ git merge origin/main
Updating 85ae714..3510c22
Fast-forward
src/books.js | 1 +
1 file changed, 1 insertion(+)
Bu iki adımın, yani fetch ve ardından merge'ün kısayolu git pull'dur. Onu bir sonraki yazıda ayrıntısıyla açacağız.
Yeni Branch'i Paylaşmak
Ada arama özelliğini bir feature branch'te geliştirip ilk kez push ediyor:
$ git switch -c feature/search
Switched to a new branch 'feature/search'
$ git add src/search.js
$ git commit -m "Add title search"
[feature/search dee3ed6] Add title search
1 file changed, 7 insertions(+)
create mode 100644 src/search.js
$ git push -u origin feature/search
To ../bookstore.git
* [new branch] feature/search -> feature/search
branch 'feature/search' set up to track 'origin/feature/search'.
Alan fetch yaptığında yeni branch'i remote-tracking branch olarak görür. Üzerinde çalışmak için yerel bir branch'e ihtiyacı var, ama bunu elle kurmasına gerek yok:
# Alan'ın bilgisayarında
$ git fetch
From .../bookstore
* [new branch] feature/search -> origin/feature/search
$ git switch feature/search
Switched to a new branch 'feature/search'
branch 'feature/search' set up to track 'origin/feature/search'.
Yerelde feature/search adında bir branch yoktu, ama origin'de bu adla tek bir branch vardı. git switch bunu tahmin edip yerel branch'i oluşturdu ve upstream'ini de ayarladı. Bu davranış Git 2.23'ten beri varsayılan; istemezsen --no-guess ile ya da Git 2.30'dan beri checkout.guess ayarıyla kapatabilirsin.
Remote'ları Yönetmek
$ git remote show origin
* remote origin
Fetch URL: ../bookstore.git
Push URL: ../bookstore.git
HEAD branch: main
Remote branches:
feature/search tracked
main tracked
Local branches configured for 'git pull':
feature/search merges with remote feature/search
main merges with remote main
Local refs configured for 'git push':
feature/search pushes to feature/search (up to date)
main pushes to main (up to date)
$ git remote rename origin hub
$ git branch -vv
* feature/search dee3ed6 [hub/feature/search] Add title search
main 3510c22 [hub/main] Add Foundation
$ git remote rename hub origin
git remote show bir remote hakkında bildiğin her şeyi tek ekranda toplar ve bunun için remote'a gerçekten bağlanır. git remote rename, remote'un adıyla birlikte remote-tracking branch'leri ve upstream ayarlarını da yeniden adlandırır. Açık kaynakta fork'la çalışırken genelde iki remote olur: kendi fork'un origin, asıl proje upstream. Bu düzene pull request yazısında döneceğiz. Bir remote'u silmek için git remote remove, adresini değiştirmek için git remote set-url kullanılır.
Sık Düşülen Tuzaklar
Birincisi, origin/main'i canlı sanmak. git log origin/main sana sunucunun bugünkü hâlini değil, son fetch anını gösterir. "Remote'ta ne var?" diye bakmadan önce git fetch çalıştır.
İkincisi, başkası senden önce push ettiğinde gelen ret. Alan README'ye bir satır ekleyip push etti; main'e dönen Ada ise bundan habersiz kendi commit'ini push etmeye çalışıyor:
$ git commit -am "Raise Snow Crash price"
[main d76d45c] Raise Snow Crash price
1 file changed, 1 insertion(+), 1 deletion(-)
$ git push
To ../bookstore.git
! [rejected] main -> main (fetch first)
error: failed to push some refs to '../bookstore.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.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
Git, remote'taki main'i fast-forward olmayacak şekilde ilerletmeni reddediyor; aksi hâlde Alan'ın commit'i ezilirdi. Çözüm önce remote'taki değişikliği almak. Panikleyip --force'a uzanma; o, Alan'ın işini siler.
Üçüncüsü, yeni bir branch'in ilk push'unda -u'yu unutmak. Upstream ayarlanmazsa sonraki düz git push ve git pull nereye gideceğini bilmez ve hata verir. Bir kez -u ile push etmek yeter.
Dördüncüsü, URL'ye parola ya da token gömmek. https://ada:token@github.com/... gibi bir adres .git/config'e düz metin olarak yazılır. Doğru yol credential helper ya da SSH; GitHub'a bağlanmayı anlatacağımız yazıda ikisini de kuracağız. CI/CD tarafında da token'lar URL'ye değil, pipeline'ın secret mekanizmasına konur.
Küçük Bir Deneme
★☆☆ Kendi bilgisayarında bir bare repo oluştur, bookstore'u ona push et ve başka bir klasöre clone'la. İki tarafta da git branch -a ve git remote -v çıktılarını karşılaştır.
★★☆ Clone'da bir commit atıp push et. Orijinal repo'da fetch'ten önce ve sonra git log --oneline -1 origin/main çalıştır. Hangi adımda değiştiğini ve main'in neden değişmediğini açıkla.
★★★ İki taraftan aynı anda main'e commit atıp ikisinden de push etmeyi dene. İkinci push neden reddediliyor? Reddedilen tarafta git fetch sonrası git status ne diyor?
Ada'nın push'u reddedildi ve git status birazdan iki branch'in ayrıştığını söyleyecek. Bir sonraki yazıda tam bu noktadan devam ediyoruz: git pull aslında ne yapar, "divergent branches" hatası neden çıkar ve pull.rebase ile pull.ff arasında nasıl seçim yapılır.