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

Version control system (VCS) гэдэг нь эх код, эсвэл ер нь файл/folder-ийн өөрчлөлтийн түүхийг хадгалдаг tool юм. Өөрөөр хэлбэл project чинь цаг хугацааны явцад яаж өөрчлөгдсөнийг харах, буцаах, бусадтай хамтарч ажиллах боломж өгдөг. Дотоод логикоороо VCS нь folder-ийн төлөвийг цуврал snapshot хэлбэрээр хадгална: snapshot бүр тухайн мөчид project-ийн дээд directory дотор байсан файл, folder-уудын бүрэн төлөвийг агуулна. Мөн snapshot-ийг хэн, хэзээ, ямар тайлбартай үүсгэсэн гэх metadata-г хадгална.

Version control ганцаараа ажиллаж байсан ч хэрэгтэй. Өмнөх төлөвөө харах, “энэ өөрчлөлтийг яагаад хийсэн билээ?” гэдгийг log-оос олох, өөр өөр branch дээр зэрэг ажиллах боломжтой. Харин багаар ажиллах үед бусад хүн юу өөрчилснийг харах, conflict шийдэх, нэг codebase дээр мөргөлдөхгүй ажиллах суурь нь болдог.

Орчин үеийн VCS ийм асуултуудад амархан, ихэнхдээ автоматаар хариулж өгнө:

Өөр VCS-үүд бий, гэхдээ өнөөдөр Git бараг де-факто стандарт болсон. Git-ийн нэр хүндийг энэ XKCD комик их сайн илэрхийлдэг:

xkcd 1597

Git-ийн интерфейс дотоод model-оо нэлээд ил гаргадаг, өөрөөр хэлбэл leaky abstraction. Тиймээс Git-ийг шууд CLI command-уудаас нь, дээрээс нь доошоо сурвал амархан төөрнө. Хүмүүс хэдэн command цээжлээд шившлэг шиг хэрэглэдэг, нэг юм эвдэрмэгц дээрх комик шиг бүгдийг устгаад шинээр clone хийдэг нь тийм ч ховор биш.

Git-ийн CLI заримдаа эвгүй, гэхдээ доорх data model нь үнэхээр цэвэрхэн. Эвгүй интерфейсийг цээжилж болно, харин сайн model-ийг ойлгочихвол command-ууд нь цаанаа юу хийж байгааг тааж чаддаг болно. Тиймээс бид Git-ийг data model-оос нь эхэлж тайлбарлаад, дараа нь CLI рүү орно.

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

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

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

Git нь хамгийн дээд түвшний folder доторх файл болон folder-уудын түүхийг цуврал снапшот хэлбэрээр загварчилдаг. Git-ийн нэршлээр бол файлыг “blob” гэх бөгөөд энэ нь ердөө л багц байтууд юм. Хавтасыг “tree” (мод) гэх бөгөөд энэ нь нэрсийг blob эсвэл өөр tree-тэй холбодог (ингэснээр folder дотор өөр folder байж болно). Снапшот нь хянагдаж буй хамгийн дээд түвшний 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-ийн өгөгдлийн загварыг псевдо кодоор бичсэнийг үзэх нь илүү ойлгомжтой байж болох юм:

// 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
}

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

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

“Объект” гэдэг нь 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, коммит зэрэг нь бүгд объектууд бөгөөд ингэж нэгддэг. Тэд бусад объектуудыг заахдаа диск дээр өөртөө шууд агуулдаггүй, харин тэдгээрийн хэшээр холбоосыг хадгалдаг.

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

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-ийн хадгалдаг зүйл нь ердөө object-ууд болон reference-үүд. Git-ийн data model-д өөр далд юм байхгүй. Бүх git command цаанаа өгөгдлийн графт шинэ object нэмэх, reference нэмэх эсвэл шинэчлэх замаар commit DAG-ийг өөрчилдөг.

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

Staging area

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

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

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

Git-ийн CLI интерфейс

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

Үндсэн command-ууд (Basics)

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

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

Буцаах (Undo)

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

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

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

Дасгал ажил

  1. Хэрэв та Git-ийн талаар туршлагагүй бол Pro Git номын эхний хэдэн бүлгийг унших эсвэл Learn Git Branching тоглоомыг тоглож үзээрэй. Ажиллах явцдаа Git-ийн command-уудыг өгөгдлийн загвартай хэрхэн холбогдож байгааг бодож үзээрэй.
  2. Энэ хичээлийн вэбсайтын репозитор-ыг локал руугаа clone хийнэ үү.
    1. Түүхийг граф хэлбэрээр харж судлаарай.
    2. README.md файлыг хамгийн сүүлд хэн өөрчилсөн бэ? (Сэжүүр: git log command-ыг аргументтай ашиглана).
    3. _config.yml файлын collections: мөрний хамгийн сүүлийн өөрчлөлтийн коммитын тайлбар мэдээлэл юу байсан бэ? (Сэжүүр: git blame болон git show ашиглана).
  3. Git-ийг сурч байх явцад гаргадаг нийтлэг алдаа бол Git-ээр удирдах шаардлагагүй том файлуудыг эсвэл нууц мэдээллийг коммит болгож нэмэх юм. Репозиторт файл нэмээд, хэдэн коммит хийсний дараа тэр файлыг түүхээс (зөвхөн сүүлийн коммитоос биш) бүрэн устгаж туршина уу. Та үүнийг үзэж болно.
  4. GitHub-аас аль нэг репозиторыг хуулж аваад, түүний бэлэн байгаа файлыг өөрчилнө үү. Таныг git stash ажиллуулахад юу болох вэ? git log --all --oneline ажиллуулахад юу харагдаж байна вэ? git stash pop ажиллуулан stashed хийсэн өөрчлөлтөө буцаан авна уу. Энэ нь ямар нөхцөлд хэрэгтэй байж болох вэ?
  5. Бусад CLI хэрэгслүүдтэй адил Git нь ~/.gitconfig нэртэй тохиргооны файлтай байдаг. ~/.gitconfig дотор алиас үүсгэснээр таныг git graph ажиллуулахад git log --all --graph --decorate --oneline command-ын гаралт гардаг болгоорой. Үүний тулд ~/.gitconfig файлыг шууд засварлаж эсвэл git config command-ыг ашиглаж болно. Git алиасны талаарх мэдээллийг эндээс үзнэ үү.
  6. Та git config --global core.excludesfile ~/.gitignore_global ажиллуулсны дараа ~/.gitignore_global файлд глобал үл тоомсорлох загваруудыг тодорхойлж болно. Энэ нь глобал үл тоомсорлох файлын байршлыг зааж өгдөг боловч та тухайн зам дээр файлыг гараар үүсгэх хэрэгтэй. Өөрийн глобал gitignore файлд үйлдлийн систем эсвэл код засварлагчийн түр зуурын файлуудыг (жишээ нь, .DS_Store) үл тоомсорлохоор тохируулна уу.
  7. Энэ хичээлийн вэбсайтын репозитор-ыг fork хийж аваад, үсгийн алдаа эсвэл бусад сайжруулах зүйлийг олж засаад GitHub дээр pull request илгээнэ үү (та үүнийг үзэж болно). Зөвхөн хэрэгтэй сайжруулалтыг (спам хийхгүй байхыг хүсье!) илгээгээрэй. Хэрэв засах зүйл олдохгүй бол энэ дасгалыг алгасаж болно.
  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.