Version Kontrol (Git)

Versiyon kontrol sistemleri (VKS’leri), kaynak koddaki (veya diğer dosya ve klasörlerdeki) değişiklikleri izlemek için kullanılan araçlardır. Adından da anlaşılacağı gibi, bu araçlar değişiklik geçmişinin korunmasına yardımcı olur; ayrıca, işbirliğini de kolaylaştırırlar. VKS’ler bir klasördeki ve içeriğindeki değişiklikleri bir dizi snapshots’da (anlık görüntüde) izler, burada her snapshot (anlık görüntü), üst düzey bir dizindeki tüm dosya/klasör durumunu kapsar. Ayrıca VKS’ler oluşturulan tüm snapshot’larda bu snapshot’ı oluşturanın kim olduğunu ve snapshot ile ilişkilenmiş mesajlar ve benzeri meta verileri de beraberinde saklar.

Versiyon kontrolü neden yararlıdır? Kendi kendinize çalışırken bile, bir projenin eski anlık görüntülerine bakmanıza, belirli değişikliklerin neden yapıldığına dair kayıtlar tutmanıza, paralel gelişimtirme dallarında çalışmanıza ve çok daha fazlasına izin verebilir. Başkalarıyla çalışırken, diğer insanların neleri değiştirdiğini görmek ve eş zamanlı geliştirmedeki conflict’leri (çatışmaları) çözmek için paha biçilmez bir araçtır.

Ayrıca modern VKS’ler aşağıdaki soruları kolayca (ve genellikle otomatik olarak) cevaplamanızı sağlar:

Birçok VKS mevcut olsa da Git, versiyon kontrolü için fiili standarttır. Bu XKCD karikatürü Git’in izlenimini anlatır.

xkcd 1597

Çünkü Git arayüzleri bazen soyut kalıp kafa karışıklığına sebep olabiliyor. Buna başlarken seçtiğiniz arayüz sebep olabilir (komut satırı arayüzü ya da görsel arayüzlü arabirim). Bir avuç komutu ezberleyip onları büyülü sözler gibi düşünmek ve bir şeyler ters gittiğinde yukarıdaki karikatür gibi davranmak mümkündür.

Hiç kuşkusuz ki Git’in çirkin bir arayüzü olsa da altında yatan fikri ve tasarımı çok güzeldir. Çirkin bir arayüzün ezberlenmesi gerekirken, güzel bir tasarım anlaşılabilir. Bu nedenle, Git’in veri modelinden başlayıp daha sonra komut satırı arayüzüyle devam eden tepeden tırnağa bir anlatımını yapacağız. Veri modeli anlaşıldıktan sonra, komutların “temeldeki veri modelini nasıl manipüle ettikleri” daha iyi anlaşılabilir.

Git’in veri modelleri

Versiyon kontrolünde uygulayabileceğiniz birçok geçici yaklaşım vardır. Git, versiyon kontrolünün; versiyon geçmişini yönetebilmek, dallar (branch’lar) ile çalışmayı desteklemek ve iş birliği içinde çalışmayı mümkün kılmak gibi güzel özellikler sağlayan iyi düşünülmüş bir modele sahiptir.

Snapshots (Anlık görüntüler)

Git, dosya ve dizinlerdeki kolleksiyonların geçmişini bazı üst düzey dizinler içinde anlık görüntüler (snapshots) halinde modeller. Git terminolojisinde bir dosyaya “blob” denir ve bu sadece bir bayt’tır. Bir dizin “tree” olarak adlandırılır ve adları blob’larla veya tree’lerle eşleştirilir (böylece dizinler başka dizinler de içerebilir). Snapshot’lar (anlık görüntüler), izlenmekte olan en üst düzey tree’lerdir. Örneğin, aşağıdaki gibi bir ağacımız olabilir:

<root> (tree)
|
+- foo (tree)
|  |
|  + bar.txt (blob, contents = "merhaba dünya")
|
+- baz.txt (blob, contents = "git muhteşemdir")

Üst düzey tree iki eleman içerir. Bunlar; biri tree olan “foo” (bu da adı “bar.txt” olan bir blob element barındırır) ile bir blob olan “baz.txt” dir.

Geçmiş modellemesi: ilişkili anlık görüntüler (snapshot’lar)

Bir versiyon kontrol sistemi anlık görüntüleri nasıl ilişkilendirmelidir? Basit bir model doğrusal bir geçmişe sahip olurdu. Bu geçmiş, snapshot’ların zaman sıralamasına uygun şekilde bir listesi olurdu. Birçok nedenden dolayı Git böyle basit bir model kullanmaz.

Git’te geçmiş, anlık görüntülerin (snapshots’ların) yönlendirilmiş çevrimsel olmayan bir grafiğidir (DAG directed acyclic graph). Bu kulağa havalı bir matematik cümlesi gibi gelebilir ama sizi korkutmamalıdır. Tüm bunlar, Git’deki her anlık görüntünün (snapshot’ın) kendinden önceki bir dizi “ebeveyn’lerle” ilişkisi var demektir. Bu, tek bir ebeveyn yerine bir ebeveyn grubudur, çünkü bir anlık görüntü (snapshot) birden çok ebeveynden gelebilir (doğrusal bir tarihte olduğu gibi). Örneğin, iki paralel geliştirme dalının birleştirilmesi (merge) gibi.

Git, bu anlık görüntüleri (snapshot’ları) “commit” olarak adlandırır. Bir commit geçmişini görselleştirmek bu şekilde görünebilir:

o <-- o <-- o <-- o
            ^  
             \
              --- o <-- o

Yukarıdaki ASCII sanatında, olar tekil commit’lere (snapshot’lara) karşılık gelir. Oklar her bir commit’in ebeveynini işareteder (Bu “önce gelir” ilişkisidir; “sonra gelir” değil). Üçüncü commit’den sonra dallanma geçmişi, iki ayrı dala (branch’a) ayrılıyor. Bu iki ayrı özelliğin birbirinden bağımsız aynı anda geliştirilmesine örnek olabilir. Bu brach’lar gelecekte her iki özelliği de barındıran yeni bir snapshot oluşturmak için birleştirilebilir (merge edilebilir). Ve yeni üretilen bu geçmiş kalın puntolarla gösterilir:

o <-- o <-- o <-- o <---- o
            ^            /
             \          v
              --- o <-- o

Git’teki commit’ler değişmezler. Bu, hataların düzeltilemeyeceği anlamına gelmez. Şu var ki commit geçmişini düzenlemek için aslında tamamen yeni bir commit oluşturuyoruz ve referanslar (aşağıya bakınız) yenilerini gösterecek şekilde güncelleniyor.

Sözde kod olarak veri modeli

Git’in veri modelinin sözde kodla yazıldığını görmek öğretici olabilir:

// bir dosya bir sürü bayttır
type blob = array<byte>

// bir dizinde adlandırılmış dosyalar ve dizinler bulunur
type tree = map<string, tree | blob>

// bir commit'te ebeveynler, meta veriler ve en üst düzey ağaç(top-level tree) vardır
type commit = struct {
    parent: array<commit>
    author: string
    message: string
    snapshot: tree
}

Temiz, basit bir geçmiş modeli.

Nesneler ve içerik adresleme

Bir “nesne” bir blob, tree ya da commit’tir.

type object = blob | tree | commit

Git veri deposunda, tüm nesneler SHA-1 hash’leri tarafından içerik-adreslenir.

objects = map<string, object>

def store(object):
    id = sha1(object)
    objects[id] = object

def load(id):
    return objects[id]

Blob’lar, tree’ler ve commit’ler bu şekilde birleştirilir ve hepsi nesnedir. Bunlar başka bir objeyi referans edeceklerinde aslında onları kendi disk gösterimlerinde direkt içermezler ama onları hash’lerle referans gösterirler.

Örnek olarak; Yukarıdaki örnek dizin yapısı için tree şöyle görünür: (görselleştirilmek için git cat-file -p 698281bc680d1995c5f4caaf3359721a5a58d48d kullanıldı)

100644 blob 4448adbf7ecd394f42ae135bbeed9676e894af85    baz.txt
040000 tree c68d233a33c5c06e0340e4c224f0afca87c8ce87    foo

Tree, içindeki bilgiler için işaretçilere (pointer’lara) sahiptir, baz.txt (blob) ve foo (tree). Eğer baz.txt’ye uyumlu hash tarafından adreslenmiş içeriklere git cat-file -p 4448adbf7ecd394f42ae135bbeed9676e894af85 ile bakarsak aşağıdakini elde ederiz:

git is wonderful

Referanslar

Şu an tüm snapshot’lar kendi SHA-1 hash fonksiyonları ile tespit edilebilirler. Bu elverişli değildir çünkü insanlar 40 karakterli hexadecimal sayıları hatırlamakta iyi değildirler.

Git’in bu soruna çözümü, SHA-1 hashleri yerine “referanslar” adı verilen, insanlar tarafından okunabilir isimlerdir. Referanslar commit’leri işaret ederler. Değişmez olan obje’lerin aksine, referanslar değiştirilebilirdir. (yeni bir commit’i işaret edecek şekilde güncellenebilir). Örneğin; master referansı genellikle ana geliştirme branch’daki (daldaki) son commit’i işaret eder.

references = map<string, string>

def update_reference(name, id):
    references[name] = id

def read_reference(name):
    return references[name]

def load_reference(name_or_id):
    if name_or_id in references:
        return load(references[name_or_id])
    else:
        return load(name_or_id)

Bunla birlikte Git uzun hexadecimal string’ler yerine “master” gibi insan tarafından kolay okunabilen isimlerle geçmişteki bir snapshot’ı temsil edebilir.

Bir detay da genellikle geçmişte “şu an nerdeyiz” kavramını bilmek isteriz. Bu sebeple yeni snapshot aldığımızda neyle ilişkili olduğunu biliriz.(commit’in parents‘ını nasıl belirledik?) Git’te “şu anda bulunduğumuz yer”, “HEAD” adı verilen özel bir referanstır.

Repo’lar

Son olarak Git repo’larını; veri objeleri ve referanslar olarak kabaca tanımlayabiliriz.

Diskte, tüm Git depoları nesneler ve referanslardan oluşmaktadır: Git’in veri modeli bundan ibarettir. Bütün git komutları objeler ekleyip ve referanslar ekleyip/güncelleyerek bazı commit DAG (directed acyclic graph) manipülasyonları ile ilişkilidir.

Herhangi bir komut yazarken, komutun grafik ve veri yapısının altında ne gibi bir değişiklik yaptığını düşünün. Buna karşılık, commit DAG’de belli başlı bir değişiklik yapmaya çalışıyorsanız örnek olarak; “commit edilmemiş değişiklikleri atın ve 5d83f9 commit’ini işlemek için ‘master’ referans noktası olarak belirtin”. Muhtemelen, bunu uygulamak için bir komut vardır. (Bu duruma örnek olarak git checkout master; git reset -- hard 5d83f9e)

Staging area (Hazırlanma alanı)

Bu, veri modeline dikey olan başka bir konsepttir. Fakat commit oluşturmak için gereken arayüzün bir parçasıdır da.

Anlık görüntü uygulamasının yukarıda açıklandığı gibi uygulanacağını hayal etmenin bir yolu da, çalışma dizininin mevcut durumuna göre yeni bir anlık görüntü oluştur “anlık görüntü (snapshot) oluştur” komutuna sahip olmaktır. Bazı versiyon kontrol araçları bu şekilde çalışır, ama Git bu şekilde çalışmaz. Temiz anlık görüntüler isteriz ve mevcut durumdan anlık görüntü oluşturmak her zaman ideal olmayabilir. Örneğin, iki ayrı özellik uyguluyoruz; Birincisinin A özelliğini, diğerinin de B özelliğini tanıttığı iki ayrı commit oluşturmak istiyorsunuz ve bugfix’ler ile beraber kodunuzun her yerine hata ayıklama ekran çıktıları eklemek istiyorsunuz. Tüm bu hata ayıklama ekran çıktılarını göndermeden (discarding) bir yandan da bugfix’i commit’lemek istediğiniz bir senaryo düşünün.

Git “staging area” denen bir mekanizma ile bir dahaki snapshot’da hangi değişikliklerin olması gerektiğini belirlemenizi yarayacak senaryolar sağlar.

Git komut satırı arabirimi

Bilgilerin kopyalanmasını önlemek için aşağıdaki komutları ayrıntılı olarak açıklamayacağız. Daha fazla bilgi için şiddetle tavsiye edilen Pro Git‘e bakın veya ders videosunu izleyin.

Temeller

Dallanma (Branching) ve birleştirme (merging)

Remotes (Uzak repo)

Geri alma

Gelişmiş Git

Çeşitli

Kaynaklar

Alıştırmalar

  1. Git ile ilgili geçmiş bir deneyiminiz yoksa, Pro Git’in ilk birkaç bölümünü okumayı deneyin veya Learn Git Branching gibi bir eğitimden faydalanın. Ve bunlar üzerinde çalışırken Git komutlarını veri modeliyle ilişkilendirmeye çalışın.
  2. Repoyu dersin sitesinden clone’layın.
    1. Versiyon geçmişini grafik olarak görselleştirp keşfedin.
    2. README.md‘de en son değişiklik yapan kişi kim? (İpucu: parametre ekleyerek git log‘u kullan)
    3. _config.yml‘ın collections: satırına yapılan son değişiklik ile alakalı commit mesajı hangisidir? (İpucu: git blame ve git show‘u kullanın)
  3. Git’i öğrenirken yapılan yaygın hatalardan biri de git tarafından yönetilmemesi gereken büyük dosyaları commit’lemek veya hassas bilgileri eklemektir. Bir repoya dosya eklemeyi, bazı commit’ler oluşturmayı ve ardından o dosyayı geçmişten silmeyi deneyin (buna bakmak isteyebilirsiniz).
  4. GitHub’daki bazı depoları klonlayın ve mevcut dosyalarından birini değiştirin. git stash yaptığınızda ne olur? git log --all --oneline‘ı çalıştırdığınızda ne görüyorsunuz? git stash ile yaptıklarınızı geri almak için git stash pop komutunu çalıştırın. Hangi senaryoda bu yararlı olabilir?
  5. Birçok komut satırı aracı gibi Git de ~/.gitconfig adlı bir yapılandırma dosyası (veya dotfile) sağlar. git graph komutunu çalıştırdığınızda git log --all --graph --decorate --oneline çıktısını almanız için ~/.gitconfig içinde bir takma ad (alias) oluşturun.
  6. git config --global core.excludesfile ~/.gitignore_global komutunu çalıştırdıktan sonra ~/.gitignore_global içinde global yok sayma kalıplarını tanımlayabilirsiniz. Bunu yapın ve genel gitignore dosyanızı, .DS_Store gibi işletim sistemine özgü veya metin editörlerine özgü geçici dosyaları yok sayacak şekilde ayarlayın.
  7. Sınıfın web sitesinden repoyu clone’layın ve yapabileceğiniz bir iyileştirme bulun (yazım yanlışı gibi) ve Github’dan bir pull request gönderin.

Bu sayfayı düzenle.

CC BY-NC-SA lisansı ile lisanslanmıştır.