Хувилбар хяналтын систем (Git)

Хувилбарын хяналтын системүүд (Version Control Systems буюу VCS) нь эх кодын (эсвэл бусад файл, хавтасны цуглуулгын) өөрчлөлтийг хянахад ашиглагддаг хэрэгслүүд юм. Нэрнээс нь харахад эдгээр хэрэгсэл нь өөрчлөлтийн түүхийг хадгалахад туслахаас гадна хамтын ажиллагааг хөнгөвчилж өгдөг. Логик хувьд VCS нь хавтас болон түүний агуулгын өөрчлөлтийг цуврал снапшотууд (snapshots) хэлбэрээр хянах бөгөөд снапшот бүр нь хамгийн дээд түвшний директор доторх файл/хавтасны тухайн үеийн төлөвийг бүрэн агуулдаг. Мөн VCS нь снапшот бүрийг хэн үүсгэсэн, ямар тайлбар мэдээлэл дагалдсан зэрэг мета-өгөгдлийг (metadata) хадгалдаг.

Хувилбарын хяналт яагаад хэрэгтэй вэ? Ганцаараа ажиллаж байхдаа ч энэ нь төслийн хуучин снапшотуудыг харах, тодорхой өөрчлөлтүүд яагаад хийгдсэн тухай лог хөтлөх, хөгжүүлэлтийн зэрэгцээ салаалан (branches) ажиллах зэрэг олон боломжийг олгодог. Бусадтай хамтран ажиллах үед бусад хүмүүс юу өөрчилснийг харах, зэрэгцээ хөгжүүлэлтийн зөрчлүүдийг (conflicts) шийдвэрлэхэд маш чухал хэрэгсэл болдог.

Орчин үеийн VCS-үүд нь дараах асуултуудад хялбархан (мөн ихэвчлэн автоматаар) хариулахад тусалдаг:

Бусад VCS байдаг ч Git нь хувилбарын хяналтын де-факто стандарт хэрэгсэл юм. Энэхүү XKCD комик Git-ийн нэр хүндийг маш сайн илэрхийлдэг:

xkcd 1597

Git-ийн интерфейс нь кодын дотоод хэрэгжилтийг ил гаргадаг leaky abstraction тул түүхийг дээрээс нь доош нь (интерфейс буюу командын мөрнөөс нь эхэлж) сурах нь маш их төөрөгдөл үүсгэдэг. Хэдхэн тушаалыг цээжлээд шидэт шившлэг шиг ашиглаж, ямар нэгэн зүйл буруу болох үед дээрх комик шиг бүгдийг устгаад дахин татдаг хүмүүс олон байдаг.

Git нь ашиглахад тийм ч таатай биш интерфейстэй байж болох ч түүний суурь дизайн, санаанууд нь үнэхээр гайхалтай юм. Муухай интерфейсийг цээжлэх шаардлагатай байдаг бол үзэсгэлэнтэй дизайныг ойлгон ухамсарлаж болдог. Энэ шалтгаанаар бид Git-ийг доороос нь дээш буюу өгөгдлийн загвараас (data model) нь эхлэн тайлбарлаж, дараа нь командын мөрийн интерфейсийг нь авч үзэх болно. Өгөгдлийн загварыг ойлгосны дараа командуудыг тэдгээр нь суурь өгөгдлийг хэрхэн өөрчилж байгаагоор нь илүү сайн ойлгож чадна.

Git-ийн өгөгдлийн загвар

Git-ийн давуу тал нь түүний түүхийг хадгалах, салаалах хөгжүүлэлтийг дэмжих, мөн хамтын ажиллагааг хангах зэрэг хувилбарын хяналтын бүх шилдэг боломжуудыг олгодог өгөгдлийн загварт оршдог.

Снапшотууд (Snapshots)

Git нь хамгийн дээд түвшний хавтас доторх файл болон хавтаснуудын түүхийг цуврал снапшот хэлбэрээр загварчилдаг. Git-ийн нэршлээр бол файлыг “blob” гэх бөгөөд энэ нь ердөө л багц байтууд юм. Хавтасыг “tree” (мод) гэх бөгөөд энэ нь нэрсийг blob эсвэл өөр tree-тэй холбодог (ингэснээр хавтас дотор өөр хавтас байж болно). Снапшот нь хянагдаж буй хамгийн дээд түвшний tree (мод) юм. Жишээлбэл, бид дараах бүтэцтэй tree-тэй байж болно:

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

Энэхүү хамгийн дээд түвшний tree нь “foo” нэртэй tree (дотроо “bar.txt” нэртэй blob агуулсан) болон “baz.txt” нэртэй blob гэсэн хоёр элементийг агуулж байна.

Түүхийг загварчлах нь: снапшотуудын хамаарал

Хувилбар хяналтын систем нь снапшотуудыг хэрхэн холбох ёстой вэ? Хамгийн энгийн загвар бол шугаман түүх байх болно. Түүх нь цаг хугацааны дараалал бүхий снапшотуудын жагсаалт байна. Гэвч Git нь олон шалтгааны улмаас ийм энгийн загвар ашигладаггүй.

Git-д түүх нь снапшотуудын directed acyclic graph (DAG) буюу чиглэлт ацикл график юм. Энэ нь математикийн нарийн үг мэт сонсогдож болох ч бүү сандраарай. Энэ нь ердөө л Git дэх снапшот бүр өөрийн өмнөх снапшотууд болох “эцэг (parents)” снапшотууд руу чиглэсэн холбоосыг заадаг гэсэн үг юм. Энэ нь шугаман түүх шиг ганц эцэг биш, харин багц эцэг байна. Учир нь зэрэгцээ салаалсан (branches) хөгжүүлэлтүүдийг нэгтгэх (merging) үед нэг снапшот олон эцгээс үүсэж болно.

Git эдгээр снапшотуудыг “commit” (коммит) гэж нэрлэдэг. Коммитуудын түүхийг дараах байдлаар дүрсэлж болно:

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

Дээрх зураглалд o тэмдэгтүүд нь тусдаа коммитуудыг (снапшот) илэрхийлнэ. Сумнууд нь коммит бүрийн эцэг рүү чиглэж байна (энэ нь “дараагийнх нь өмнөх рүүгээ чиглэх” хамаарал юм). Гурав дахь коммитын дараа түүх хоёр тусдаа салаа (branches) болон хуваагдаж байна. Энэ нь, жишээлбэл, хоёр өөр боломжийг бие даан, зэрэгцээ хөгжүүлж байгааг илэрхийлж болно. Ирээдүйд эдгээр салааг нэгтгэж, хоёр боломжийг хоёуланг нь агуулсан шинэ снапшот үүсгэж болох бөгөөд үр дүнд нь түүх дараах байдалтай харагдах ба шинээр үүссэн нэгтгэлийн (merge) коммитыг тодоор харуулав:


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

Git-ийн коммитууд нь өөрчлөгдөшгүй (immutable) байдаг. Энэ нь алдааг засаж болохгүй гэсэн үг биш; харин түүхийг “засварлах” үед үнэндээ цоо шинэ коммитууд үүсдэг бөгөөд reference-үүд (доороос үзнэ үү) шинэ коммитууд руу чиглэхээр шинэчлэгддэг.

Өгөгдлийн загвар, псевдо кодоор

Git-ийн өгөгдлийн загварыг псевдо кодоор бичсэнийг үзэх нь илүү ойлгомжтой байж болох юм:

// файл нь багц байтууд байна
type blob = array<byte>

// хавтас нь нэр бүхий файл, хавтаснуудыг агуулна
type tree = map<string, tree | blob>

// коммит нь эцэг, мета-өгөгдөл болон дээд түвшний tree-ийг агуулна
type commit = struct {
    parents: array<commit>
    author: string
    message: string
    snapshot: tree
}

Энэ бол түүхийг хадгалах маш цэвэрхэн, энгийн загвар юм.

Объектууд болон агуулгаар хаяглах нь

“Объект” гэдэг нь blob, tree, эсвэл commit-ийн аль нэг нь юм:

type object = blob | tree | commit

Git-ийн өгөгдлийн санд бүх объектууд өөрсдийн SHA-1 хэшээр агуулгаар хаяглагддаг (content-addressed).

objects = map<string, object>

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

def load(id):
    return objects[id]

Blob, tree, коммит зэрэг нь бүгд объектууд бөгөөд ингэж нэгддэг. Тэд бусад объектуудыг заахдаа диск дээр өөртөө шууд агуулдаггүй, харин тэдгээрийн хэшээр холбоосыг хадгалдаг.

Жишээлбэл, дээрх хавтасны бүтцийг харуулсан tree (git cat-file -p 698281bc680d1995c5f4caaf3359721a5a58d48d тушаалаар харвал) дараах байдалтай байна:

100644 blob 4448adbf7ecd394f42ae135bbeed9676e894af85    baz.txt
040000 tree c68d233a33c5c06e0340e4c224f0afca87c8ce87    foo

Энэхүү tree нь өөрийн агуулга болох baz.txt (blob) болон foo (tree) рүү чиглэсэн заагчийг агуулж байна. Хэрэв бид baz.txt-ийн хэшээр хаяглагдсан агуулгыг git cat-file -p 4448adbf7ecd394f42ae135bbeed9676e894af85 ажиллуулж харвал дараах үр дүнг авна:

git is wonderful

Reference-үүд (References)

Одоо бүх снапшотуудыг өөрсдийн SHA-1 хэшээр нь тодорхойлж болно. Гэвч энэ нь тохиромжгүй юм, учир нь хүмүүс 40 тэмдэгтээс бүрдсэн арван зургаатын тоог цээжлэхдээ муу байдаг.

Git-ийн шийдэл бол SHA-1 хэшүүдэд хүнд уншихад хялбар нэр өгөх явдал бөгөөд үүнийг “reference” гэнэ. Reference-үүд нь коммитууд руу чиглэсэн заагч юм. Өөрчлөгдөшгүй объектуудаас ялгаатай нь reference-үүд нь өөрчлөгдөх боломжтой (өөр коммит руу чиглүүлэн шинэчилж болно). Жишээлбэл, master reference нь хөгжүүлэлтийн үндсэн салааны (main branch) хамгийн сүүлийн коммит руу чиглэж байдаг.

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)

Үүний тусламжтайгаар Git урт хэш тооны оронд “master” гэх мэт хүнд ойлгомжтой нэрийг ашиглан түүхэн дэх тодорхой снапшотыг зааж чаддаг.

Нэг зүйлийг анхаарахад бидэнд түүхэн дэх “одоо хаана байна вэ” гэсэн ойлголт хэрэгтэй байдаг бөгөөд ингэшээр шинэ снапшот үүсгэхэд түүнийг ямар коммитоос эхлэхийг (коммитын parents талбарыг хэрхэн оноохыг) мэддэг. Git-д энэхүү одоогийн байршлыг “HEAD” гэж нэрлэдэг тусгай reference заадаг.

Репозиториуд (Repositories)

Эцэст нь бид Git-ийн repository (репозитори) гэж юу болохыг тодорхойлж болно: энэ нь ердөө л objects болон references гэсэн өгөгдөл юм.

Диск дээр Git-ийн хадгалдаг зүйл бол ердөө л объектууд болон reference-үүд юм: Git-ийн өгөгдлийн загварт өөр зүйл байхгүй. Бүх git тушаалууд нь өгөгдлийн граф бүтцэд шинэ объект нэмэх, reference-үүдийг нэмэх/шинэчлэх замаар коммитын DAG-ийг өөрчлөх үйлдэл рүү хөрвөдөг.

Ямар нэгэн тушаал ажиллуулах болгондоо уг тушаал цаанаа өгөгдлийн граф бүтцэд ямар өөрчлөлт оруулж байгааг бодож байгаарай. Жишээлбэл, хэрэв та граф бүтцэд “хийгдээгүй байгаа өөрчлөлтүүдийг хаяад ‘master’ reference-ийг 5d83f9e коммит руу чиглүүлэх” өөрчлөлт хийхийг хүсвэл үүнийг хийх тушаал байгаа (энэ тохиолдолд git checkout master; git reset --hard 5d83f9e байна).

Staging area

Энэ бол өгөгдлийн загвараас тусдаа боловч коммит үүсгэх интерфейсийн нэг хэсэг болох чухал ойлголт юм.

Снапшот үүсгэхийг ажлын хавтасны одоогийн төлөв дээр суурилан шууд үүсгэдэг гэж төсөөлж болох юм. Зарим хувилбар хяналтын хэрэгслүүд ингэж ажилладаг боловч Git тэгдэггүй. Бид цэвэрхэн снапшотуудыг үүсгэхийг хүсдэг бөгөөд одоогийн байгаа бүх өөрчлөлтийг шууд снапшот болгох нь үргэлж тохиромжтой байдаггүй. Жишээлбэл, та хоёр өөр боломжийг хөгжүүлсэн бөгөөд тэдгээрийг хоёр тусдаа коммит болгохыг хүсэж байна гэж бодъё. Эсвэл кодонд дебаг хийх явцдаа олон газар print тушаалуудыг бичсэн бөгөөд тэдгээрийг оруулахгүйгээр зөвхөн алдаа зассан хэсгээ коммит болгохыг хүсэж болно.

Git нь дараагийн снапшотдоо аль өөрчлөлтүүдийг оруулахыг зааж өгөх боломжийг staging area (бэлтгэл талбар) гэх механизмаар дамжуулан олгодог.

Git-ийн командын мөрийн интерфейс

Мэдээллийг давтахгүй байх үүднээс бид лекцийн тэмдэглэлд командуудыг нэг бүрчлэн тайлбарлахгүй. Дэлгэрэнгүй мэдээллийг Pro Git номоос үзэх эсвэл лекцийн бичлэгийг үзнэ үү.

Үндсэн тушаалууд (Basics)

Салаалалт ба Нэгтгэлт (Branching and merging)

Алсын серверүүд (Remotes)

Буцаах (Undo)

Гүнзгийрүүлсэн Git

Бусад сэдвүүд

Эх сурвалжууд

Дасгал ажил

  1. Хэрэв та Git-ийн талаар туршлагагүй бол Pro Git номын эхний хэдэн бүлгийг унших эсвэл Learn Git Branching тоглоомыг тоглож үзээрэй. Ажиллах явцдаа Git-ийн командуудыг өгөгдлийн загвартай хэрхэн холбогдож байгааг бодож үзээрэй.
  2. Энэхүү хичээлийн вэбсайтын репозитор-ыг локал руугаа clone хийнэ үү.
    1. Түүхийг граф хэлбэрээр харж судлаарай.
    2. README.md файлыг хамгийн сүүлд хэн өөрчилсөн бэ? (Сэжүүр: git log командыг аргументтай ашиглана).
    3. _config.yml файлын collections: мөрний хамгийн сүүлийн өөрчлөлтийн коммитын тайлбар мэдээлэл юу байсан бэ? (Сэжүүр: git blame болон git show ашиглана).
  3. Git-ийг сурч байх явцад гаргадаг нийтлэг алдаа бол Git-ээр удирдах шаардлагагүй том файлуудыг эсвэл нууц мэдээллийг коммит болгож нэмэх явдал юм. Репозиторт файл нэмээд, хэдэн коммит хийсний дараа тэр файлыг түүхээс (зөвхөн сүүлийн коммитоос биш) бүрэн устгаж туршина уу. Та үүнийг үзэж болно.
  4. GitHub-аас аль нэг репозиторыг хуулж аваад, түүний бэлэн байгаа файлыг өөрчилнө үү. Таныг git stash ажиллуулахад юу болох вэ? git log --all --oneline ажиллуулахад юу харагдаж байна вэ? git stash pop ажиллуулан stashed хийсэн өөрчлөлтөө буцаан авна уу. Энэ нь ямар нөхцөлд хэрэгтэй байж болох вэ?
  5. Бусад командын мөрийн хэрэгслүүдтэй адил Git нь ~/.gitconfig нэртэй тохиргооны файлтай байдаг. ~/.gitconfig дотор алиас үүсгэснээр таныг git graph ажиллуулахад git log --all --graph --decorate --oneline командын гаралт гардаг болгоорой. Үүний тулд ~/.gitconfig файлыг шууд засварлаж эсвэл git config тушаалыг ашиглаж болно. Git алиасны талаарх мэдээллийг эндээс үзнэ үү.
  6. Та git config --global core.excludesfile ~/.gitignore_global ажиллуулсны дараа ~/.gitignore_global файлд глобал үл тоомсорлох загваруудыг тодорхойлж болно. Энэ нь глобал үл тоомсорлох файлын байршлыг зааж өгдөг боловч та тухайн зам дээр файлыг гараар үүсгэх хэрэгтэй. Өөрийн глобал gitignore файлд үйлдлийн систем эсвэл код засварлагчийн түр зуурын файлуудыг (жишээ нь, .DS_Store) үл тоомсорлохоор тохируулна уу.
  7. Энэхүү хичээлийн вэбсайтын репозитор-ыг fork хийж аваад, үсгийн алдаа эсвэл бусад сайжруулах зүйлийг олж засаад GitHub дээр татах хүсэлт илгээнэ үү (та үүнийг үзэж болно). Зөвхөн хэрэгтэй сайжруулалтыг (спам хийхгүй байхыг хүсье!) илгээгээрэй. Хэрэв засах зүйл олдохгүй бол энэ дасгалыг алгасаж болно.
  8. Хамтын ажиллагааны нөхцөлийг дуурайлган нэгтгэлийн зөрчлийг (merge conflicts) шийдвэрлэх дасгал хийгээрэй:
    1. git init ажиллуулж шинэ репозитор үүсгээд, дотор нь хэдэн мөр бүхий recipe.txt нэртэй файл үүсгэнэ үү.
    2. Үүнийг коммит болгоод, хоёр салаа үүсгэнэ үү: git branch salty болон git branch sweet.
    3. salty салаанд нэг мөрийг өөрчлөөд (жишээ нь, “1 cup sugar”-ыг “1 cup salt” болгож) коммит хийнэ үү.
    4. sweet салаанд мөн ижил мөрийг өөрөөр өөрчлөөд (жишээ нь, “1 cup sugar”-ыг “2 cups sugar” болгож) коммит хийнэ үү.
    5. Одоо master салаа руу шилжиж, эхлээд git merge salty хийж, дараа нь git merge sweet хийхийг оролдоно уу. Юу болох вэ? recipe.txt файлын агуулгыг шалгана уу - энд байгаа <<<<<<<, =======, болон >>>>>>> тэмдэглэгээнүүд ямар утгатай вэ?
    6. Файлыг засварлан хүссэн агуулгаа үлдээж, зөрчлийн тэмдэглэгээнүүдийг устгаад, git add болон git commit (эсвэл git merge --continue) ажиллуулан зөрчлийг шийдвэрлэнэ үү. Эсвэл git mergetool ажиллуулж терминал эсвэл график интерфейс бүхий хэрэгслээр зөрчлийг шийдвэрлэхийг оролдоно уу.
    7. Шинээр үүсгэсэн нэгтгэлийн түүхийг харахын тулд git log --graph --oneline ажиллуулна уу.

Энэ хуудсыг засварлах.

Лиценз: CC BY-NC-SA.