Versiyalarni boshqarish va Git

Versiyalarni boshqarish tizimlari (VCS) manba kodidagi (yoki boshqa fayl va kataloglar to’plamidagi) o’zgarishlarni kuzatish uchun ishlatiladigan vositalardir. Nomi aytib turganidek, bu vositalar o’zgarishlar tarixini saqlashga yordam beradi; bundan tashqari, ular hamkorlikda ishlashni osonlashtiradi. Mantiqiy jihatdan, VCS’lar katalog va uning tarkibidagi o’zgarishlarni bir qator snapshot‘lar sifatida kuzatib boradi, bunda har bir snapshot eng yuqori darajadagi katalog ichidagi fayllar/kataloglarning butun holatini qamrab oladi. VCS’lar shuningdek, har bir snapshot’ni kim yaratganligi, har bir snapshot bilan bog’liq xabarlar va shunga o’xshash metama’lumotlarni saqlaydi.

Nima uchun versiyalarni boshqarish foydali? Hattoki o’zingiz yolg’iz ishlayotganingizda ham, u sizga loyihaning eski snapshot’larini ko’rishga, nima uchun ma’lum o’zgarishlar qilinganligi jurnalini yuritishga, parallel tarmoqlarda ishlashga va boshqa ko’plab ishlarni bajarishga imkon beradi. Boshqalar bilan ishlaganda esa, u boshqa odamlar nimalarni o’zgartirganini ko’rish, shuningdek, parallel dasturlashdagi ziddiyatlarni hal qilish uchun bebaho vositadir.

Zamonaviy VCS’lar quyidagi kabi savollarga osonlik bilan (va ko’pincha avtomatik ravishda) javob topishga imkon beradi:

Boshqa VCS’lar ham mavjud bo’lsa-da, Git versiyalarni boshqarish uchun amaldagi standart (de facto) hisoblanadi. Ushbu XKCD komiksi Git’ning obro’sini aks ettiradi:

xkcd 1597

Git’ning interfeysi yetarlicha mukammal bo’lmagan abstraksiya (leaky abstraction) bo’lgani uchun, Git’ni yuqoridan pastga (interfeysidan / buyruqlar satridan boshlab) o’rganish ko’p chalkashliklarga olib kelishi mumkin. Bir nechta buyruqlarni yodlab olish va ularni sehrli afsun sifatida tasavvur qilish, hamda biror narsa noto’g’ri ketganda yuqoridagi komiksdagi yondashuvga amal qilish oson.

Garchi Git interfeysi xunuk bo’lsa-da, uning asosiy dizayni va g’oyalari chiroyli. Xunuk interfeysni yodlash kerak bo’lsa, chiroyli dizaynni tushunish mumkin. Shu sababli, biz Git’ni uning ma’lumotlar modelidan boshlab va keyinroq buyruqlar satri interfeysini qamrab olgan holda pastdan yuqoriga qarab tushuntiramiz. Ma’lumotlar modeli tushunilgandan so’ng, buyruqlarni ular asosiy ma’lumotlar modelini qanday o’zgartirishi orqali yaxshiroq tushunish mumkin.

Git’ning ma’lumotlar modeli

Git’ning ixtirosi uning yaxshi o’ylangan ma’lumotlar modelida bo’lib, u versiyalarni boshqarishning tarixni saqlash, tarmoqlarni qo’llab-quvvatlash va hamkorlik qilishga imkon berish kabi barcha ajoyib xususiyatlarini ta’minlaydi.

Snapshot’lar

Git biror yuqori darajadagi katalog ichidagi fayllar va kataloglar to’plami tarixini snapshot’lar seriyasi sifatida modellashtiradi. Git terminologiyasida fayl “blob” deb ataladi va u shunchaki baytlar to’plamidir. Katalog esa “daraxt” deb ataladi va u nomlarni blob’lar yoki daraxtlarga xaritalaydi (shuning uchun kataloglar ichida boshqa kataloglar bo’lishi mumkin). Snapshot esa kuzatilayotgan eng yuqori darajadagi daraxtdir. Masalan, bizda quyidagicha daraxt bo’lishi mumkin:

<root> (tree)
|
+- foo (tree)
|  |
|  + bar.txt (blob, contents = "hello world")
|
+- baz.txt (blob, contents = "git is wonderful")

Eng yuqori darajadagi daraxt ikkita elementni o’z ichiga oladi: “foo” daraxti (u o’z ichida bitta “bar.txt” blob’ini saqlaydi) va “baz.txt” blob’i.

Tarixni modellashtirish: snapshot’larni bog’lash

Versiyalarni boshqarish tizimi snapshot’larni qanday bog’lashi kerak? Bitta oddiy model tarixni chiziqli qilib yaratish bo’lishi mumkin. Tarix vaqt tartibida joylashgan snapshot’lar ro’yxati bo’ladi. Ko’pgina sabablarga ko’ra, Git bunday oddiy modeldan foydalanmaydi.

Git’da tarix snapshot’larning yo’naltirilgan asiklik grafidir (DAG). Bu qandaydir murakkab matematik atama bo’lib tuyulishi mumkin, ammo qo’rqmang. Bu shunchaki Git’dagi har bir snapshot o’zidan oldingi snapshot’lar to’plamiga, ya’ni “ota-ona” (parents) larga ishora qilishini anglatadi. Bu yagona ota-ona emas (chiziqli tarixda bo’lgani kabi), balki ota-onalar to’plamidir, chunki bitta snapshot bir nechta ota-onadan kelib chiqqan bo’lishi mumkin, masalan, ikkita parallel dasturlash tarmog’ini birlashtirish (merge) natijasida.

Git bu snapshot’larni “commit” deb ataydi. Commit’lar tarixini vizualizatsiya qilish taxminan shunday ko’rinishi mumkin:

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

Yuqoridagi ASCII tasvirlarda o lar alohida commit’larga (snapshot’larga) mos keladi. O’qlar har bir commit’ning ota-onasiga ishora qiladi (bu “keyin keladi” emas, balki “oldin keladi” munosabatidir). Uchinchi commit’dan so’ng tarix ikkita alohida tarmoqqa bo’linadi. Bu, masalan, bir-biridan mustaqil ravishda parallel ishlab chiqilayotgan ikkita alohida xususiyatga mos kelishi mumkin. Kelajakda bu tarmoqlar har ikkala xususiyatni o’z ichiga olgan yangi snapshot yaratish uchun birlashtirilishi mumkin, va buning natijasida quyidagicha yangi tarix hosil bo’ladi, bu yerda yangi yaratilgan birlashtirilgan commit qalin qilib ko’rsatilgan:


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

Git’da commit’lar o’zgarmasdir (immutable). Biroq, bu xatolarni to’g’irlab bo’lmaydi degani emas; shunchaki commit tarixidagi “tahrirlar” aslida mutlaqo yangi commit’larni yaratadi va havolalar (quyiga qarang) yangilariga ishora qilish uchun yangilanadi.

Ma’lumotlar modeli, psevdokod sifatida

Git’ning ma’lumotlar modelini psevdokodda yozib ko’rish foydali bo’lishi mumkin:

// a file is a bunch of bytes
type blob = array<byte>

// a directory contains named files and directories
type tree = map<string, tree | blob>

// a commit has parents, metadata, and the top-level tree
type commit = struct {
    parents: array<commit>
    author: string
    message: string
    snapshot: tree
}

Bu tarixning toza va oddiy modelidir.

Obyektlar va kontentni manzillash

“Obyekt” bu blob, daraxt yoki commit’dir:

type object = blob | tree | commit

Git’ning ma’lumotlar omborida barcha obyektlar SHA-1 xeshi orqali kontent bilan manzillanadi.

objects = map<string, object>

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

def load(id):
    return objects[id]

Blob’lar, daraxtlar va commit’lar shu tarzda birlashtiriladi: ularning barchasi obyektlardir. Ular boshqa obyektlarga havola berganda, ularni haqiqatda diskdagi ko’rinishida o’z ichiga olmaydi, balki ularga xeshlari orqali havola qiladi.

Masalan, yuqoridagi misol katalog tuzilmasi uchun daraxt (git cat-file -p 698281bc680d1995c5f4caaf3359721a5a58d48d yordamida vizualizatsiya qilingan) quyidagicha ko’rinadi:

100644 blob 4448adbf7ecd394f42ae135bbeed9676e894af85    baz.txt
040000 tree c68d233a33c5c06e0340e4c224f0afca87c8ce87    foo

Daraxt o’z ichiga olgan elementlariga, baz.txt (blob) va foo (daraxt) ga ko’rsatkichlarni saqlaydi. Agar biz baz.txt ga mos keluvchi xesh bilan manzillangan kontentga git cat-file -p 4448adbf7ecd394f42ae135bbeed9676e894af85 bilan qarasak, quyidagini ko’ramiz:

git is wonderful

Havolalar

Endi, barcha snapshot’lar o’zlarining SHA-1 xeshlari orqali aniqlanishi mumkin. Ammo bu noqulay, chunki odamlar 40 ta o’n oltilik tizimdagi belgilardan iborat qatorlarni eslab qolishga usta emas.

Git’ning bu muammoga yechimi SHA-1 xeshlari uchun “havolalar” deb ataluvchi inson o’qiy oladigan nomlarni taqdim etishdir. Havolalar commit’larga ko’rsatkichlardir. O’zgarmas (immutable) bo’lgan obyektlardan farqli o’laroq, havolalar o’zgaruvchandir (yangi commit’ga ishora qilish uchun yangilanishi mumkin). Masalan, master havolasi odatda asosiy ish tarmog’idagi oxirgi commit’ga ishora qiladi.

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)

Buning yordamida, Git tarixning qaysidir bir qismiga uzundan-uzoq o’n oltilik satr bilan emas, balki “master” kabi odam o’qiy oladigan nomlardan foydalanib havola qilishi mumkin.

Yana bir detal shundaki, biz yangi snapshot yaratganimizda, u nimaga nisbatan olinganligini (commit’ning parents maydonini qanday o’rnatishimizni) bilishimiz uchun ko’pincha tarixda “hozir qayerdaligimiz” haqida tushunchaga ega bo’lishni xohlaymiz. Git’da o’sha “hozir qayerdamiz” degan tushuncha “HEAD” deb ataluvchi maxsus havoladir.

Repozitoriylar

Nihoyat, biz Git repozitoriysi (taxminan) nima ekanligini ta’riflashimiz mumkin: bu objects (obyektlar) va references (havolalar) ma’lumotlaridir.

Diskda Git butunlay faqat obyektlar va havolalarni saqlaydi: Git’ning ma’lumotlar modeli faqat shulardan iborat. Barcha git buyruqlari obyektlar qo’shish va havolalarni qo’shish/yangilash orqali commit DAG’ini (yo’naltirilgan asiklik grafini) qandaydir tahrirlashga mos keladi.

Har qanday buyruqni kiritganingizda, o’ylab ko’ringki bu buyruq ostidagi graf ma’lumotlar tuzilmasida qanday o’zgartirishlar qilmoqda. Va aksincha, agar siz commit DAG’ida biror o’zgartirish kiritmoqchi bo’lsangiz, masalan, “commit qilinmagan o’zgarishlarni bekor qilish va ‘master’ havolasini 5d83f9e commit’ga o’tkazish”, bu uchun ham, ehtimol, buyruq mavjud (masalan, ushbu holatda, git checkout master; git reset --hard 5d83f9e).

Staging area

Bu ma’lumotlar modeliga bevosita bog’liq bo’lmagan yana bir tushuncha, lekin u commit’larni yaratish uchun mo’ljallangan interfeysning bir qismidir.

Siz yuqorida ta’riflanganidek, snapshot yaratishni ishchi katalogning joriy holatiga asoslangan yangi snapshot yaratadigan “snapshot yaratish” buyrug’i bo’lishi deb o’ylashingiz mumkin. Ba’zi versiyalarni boshqarish vositalari shunday ishlaydi, ammo Git emas. Bizga toza snapshot’lar kerak, va doim ham mavjud holatdan snapshot yaratish eng to’g’ri tanlov bo’lmasligi mumkin. Masalan, ikkita turli xususiyat qo’shganingizni va bittasi faqat birinchi xususiyatni, ikkinchisi esa faqat ikkinchisini qo’shadigan ikkita alohida commit yaratmoqchi ekanligingizni tasavvur qiling. Yoki butun kodingiz bo’ylab xatoliklarni topish (debugging) uchun print ko’rsatmalari va qo’shimcha ravishda xatolik tuzatmasi (bugfix) bor deylik; barcha print’larni chetga surib faqat bugfix’ni commit qilmoqchi bo’lishingiz mumkin.

Git bunday ssenariylarga “staging area” deb ataladigan mexanizm orqali navbatdagi snapshot’ga aynan qaysi o’zgarishlar kiritilishini aniq ko’rsatishga ruxsat berish orqali moslashadi.

Git buyruqlar satri interfeysi

Ma’lumotlarni takrorlamaslik uchun, quyidagi buyruqlarni ushbu ma’ruza matnlarida batafsil tushuntirmaymiz. Ko’proq ma’lumot olish uchun tavsiya etilgan Pro Git kitobini o’qing yoki ma’ruza videosini ko’ring.

Asoslar

Tarmoqlash va birlashtirish

Masofaviy repozitoriylar

Bekor qilish (Undo)

Murakkab Git

Turli xil narsalar

Manbalar

Mashqlar

  1. Agar siz Git bilan oldin ishlash tajribasiga ega bo’lmasangiz, Pro Git ning dastlabki bir necha bobini o’qishga yoki Learn Git Branching kabi qo’llanmadan o’tishga harakat qiling. O’qish jarayonida Git buyruqlarini ma’lumotlar modeliga bog’lab o’rganing.
  2. Kurs veb-sayti repozitoriysini klonlang.
    1. Versiyalar tarixini graf sifatida tasvirlab o’rganing.
    2. README.md faylini oxirgi marta kim o’zgartirgan? (Maslahat: argument bilan git log dan foydalaning).
    3. _config.yml faylidagi collections: qatoriga kiritilgan oxirgi o’zgartirish bilan bog’liq commit xabari nima edi? (Maslahat: git blame va git show dan foydalaning).
  3. Git’ni o’rganayotgandagi eng keng tarqalgan xatolardan biri bu Git tomonidan boshqarilmasligi kerak bo’lgan katta hajmdagi fayllarni yoki maxfiy ma’lumotlarni commit qilishdir. Repozitoriyga bitta fayl qo’shishga urinib ko’ring, bir nechta commit’lar yarating va so’ngra ushbu faylni tarixdan butunlay o’chirib tashlang (nafaqat so’nggi commit’dan). Buni qanday qilishni bilish uchun buni o’qib chiqsangiz bo’ladi.
  4. GitHub’dan biron bir repozitoriyni klonlang va undagi mavjud fayllardan birini o’zgartiring. Agar siz git stash ni bajarsangiz nima sodir bo’ladi? git log --all --oneline ni bajarganda nimani ko’ryapsiz? git stash orqali qilgan amalingizni orqaga qaytarish uchun git stash pop buyrug’ini bajaring. Bu qaysi ssenariyda foydali bo’lishi mumkin?
  5. Ko’pgina buyruqlar satri vositalari singari, Git ham ~/.gitconfig nomli konfiguratsiya fayli (yoki nuqtali fayl) ni taqdim etadi. ~/.gitconfig ichida qisqartma (alias) yarating, shu tufayli qachonki git graph buyrug’ini bersangiz, sizga git log --all --graph --decorate --oneline natijasi chiqadigan bo’lsin. Buni ~/.gitconfig faylini bevosita tahrirlash orqali yoki alias qo’shish uchun git config buyrug’ini ishlatish bilan amalga oshirishingiz mumkin. Git alias’lari bo’yicha ma’lumotlarni bu yerdan topsangiz bo’ladi.
  6. git config --global core.excludesfile ~/.gitignore_global buyrug’ini berib, ~/.gitignore_global faylida global e’tiborsiz qoldirish naqshlari (ignore patterns) ni aniqlashingiz mumkin. Bu Git foydalanadigan global ignore fayli manzilini o’rnatadi, biroq siz baribir o’sha manzilda faylni qo’lda yaratishingiz kerak. O’zingizning operatsion tizimingiz yoki tahrirlovchingizga xos vaqtinchalik fayllarni, masalan, .DS_Store ni e’tiborsiz qoldirish (ignore qilish) uchun global gitignore faylingizni sozlang.
  7. Kurs veb-sayti repozitoriysini fork qiling, imlo xatosini (typo) yoki o’zingiz kiritishingiz mumkin bo’lgan boshqa yaxshilanishni topib, GitHub’da pull request jo’nating (Buning uchun buni ko’rishingiz mumkin). Iltimos, faqatgina foydali bo’lgan PR’larni yuboring (iltimos, bizga spam yubormang!). Agar hech qanday yaxshilanish topa olmasangiz, bu mashqni o’tkazib yuborishingiz mumkin.
  8. Hamkorlikdagi stsenariyni simulyatsiya qilib, ziddiyatlarni (merge conflicts) hal qilishni amalda sinab ko’ring:
    1. git init orqali yangi repozitoriy yarating va ichida bir nechta qator (masalan, retsept) dan iborat recipe.txt degan fayl yarating.
    2. Uni commit qiling, so’ngra ikkita tarmoq (branch) yarating: git branch salty va git branch sweet.
    3. salty tarmog’ida bir qatorni o’zgartiring (masalan, “1 piyola shakar” ni “1 piyola tuz” ga o’zgartiring) va commit qiling.
    4. sweet tarmog’ida ham xuddi shu qatorni boshqacha qilib o’zgartiring (masalan, “1 piyola shakar” ni “2 piyola shakar” ga o’zgartiring) va commit qiling.
    5. Endi master tarmog’iga qayting va avval git merge salty, so’ng git merge sweet ni bajarib ko’ring. Nima sodir bo’ladi? recipe.txt faylining tarkibini ko’ring - undagi <<<<<<<, ======= va >>>>>>> belgilari nimani anglatadi?
    6. O’zingiz qoldirmoqchi bo’lgan tarkibni saqlab faylni tahrirlash orqali ziddiyatni hal qiling, ziddiyat belgilarini olib tashlang va birlashtirishni git add va git commit (yoki git merge --continue) orqali yakunlang. Muqobil variant sifatida, grafik yoki terminal asosidagi merge vositalaridan foydalanib ziddiyatni hal qilish uchun git mergetool ni ishlating.
    7. O’zingiz hozirgina yaratgan birlashish tarixini (merge history) ko’rish uchun git log --graph --oneline dan foydalaning.

Sahifani tahrirlash.

CC BY-NC-SA asosida litsenziyalangan.