版本控制 (Git)

版本控制系統 (VCSs) 是用來追蹤程式原始碼(或者檔案與檔案夾)內在改動的工具。 如同名字一樣,這些工具將會幫助我們管理程式碼的修改歷史記錄;更進一步,它也促進了合作。 VCS透過一系列快照將檔案夾與其內容儲存起來,每個快照都包含了檔案夾或者檔案的完整訊息。 同時它還維護類似於快照建立者以及每個快照的相關資訊等元數據。

為什麼版本控制系統十分有用?即使我們一個人進行工作,它也可以提供讓我們閱讀專案舊快照的能力, 記錄每次編輯的目的,以及基於多個分支並行開發。 與別人協同開發時,它展現了檢視別人對程式碼修改的能力,同時可以解決憂鬱並行開發引起的衝突。

現代的版本控制系統可以幫助我們輕鬆(經常是全自動)地回答以下問題:

版本控制系統有很多,但是 Git 是所有版本控制系統的標準。 這篇 XKCD 漫畫 展現了Git的聲望:

xkcd 1597

因為 Git 的抽象洩漏(leaky abstraction)問題,從總體到細節的方式(從介面開始)的學習方式會令人感到非常困惑。 很多時候我們只能記住一些指令,然後如同詠唱魔法一般地使用他們。一旦出現問題,就只能如同上篇的漫畫一樣處理了。

Git 的介面非常醜陋,但他有優雅的底層設計與思考方式。 醜陋的介面只能被背誦,而優雅的底層會非常容易被理解。 所以,我們將從底層到介面的方式介紹Git。 從資料模型開始,最後再學習介面。 一旦我們理解了Git的資料模型,學習介面並理解介面是如何操作底層資料將會非常容易。

Git 的資料模型

. 有許多方式可以做到版本控制。 Git 使用良好設計的模型,來支援版本控制所需要的所有功能,例如維護歷史記錄,記錄分支與合作支援。

快照

Git 將頂級目錄中檔案與檔案夾的集合的歷史記錄建立為一系列快照。 在 Git 內,檔案被稱為「blob」,只是一堆字元。目錄被稱爲「樹」,並且它將名稱對映到Blob或樹(因此目錄可以包含其他目錄)。快照是被追蹤的頂級樹。例如,我們可能有如下的一棵樹:

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

頂級樹包含兩個元素,一棵樹「foo」(本身包含一個元素,即blob 「bar.txt」),和一個blob 「baz.txt」。

建模歷史:相關快照

版本控制系統應該如何關聯快照?一種簡單的模型是建立線性歷史記錄。歷史記錄將是按照時間順序排列的快照列表。 出於許多原因,Git沒有使用這樣的簡單模型。

在 Git 中,歷史記錄是快照的有向無環圖 (DAG)。這聽起來像個花哨的數學詞,不要被嚇到。 這意味著 Git 中的每一個快照都參考一組「母物件」,即之前的快照。 他是一組母節點集合而非單個母節點,因為他可能從多個母節點中繼承,例如合併後的兩條分支。

Git 將這些快照稱作 「提交」(commit)。將其視覺化後大概像這樣:

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

在上圖中,其中的 o 代表一次提交。 箭頭指出的是當前的母節點。(注意是「在自己之前」的節點,而非「之後」)。 在第三次提交之後,歷史記錄將分為兩個單獨的分支。 例如,這可能對應於兩個單獨的功能,他們彼此獨立,並行開發。 將來,這些分支可能會合併以建立一個融合了這兩個功能的新快照,從而生成看起來像下圖這樣的新歷史記錄,新建立的合併提交以粗體顯示:

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

Git中的提交是不可更改的。 但這並不意味著不能糾正錯誤。 只是對提交歷史記錄的“編輯”實際上是建立全新的提交,並且參考(參見下文)也已更新為指向新的提交。

以假碼表示的資料模型

假碼記錄下來的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 {
    parent: array<commit>
    author: string
    message: string
    snapshot: tree
}

這是一個簡單整潔的歷史模型。

物件與內容定位

一個「物件」是一個blob,樹或者提交:

type object = blob | tree | commit

Git在儲存資料時,所有物件都被記錄SHA-1 hash以供定位。

objects = map<string, object>

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

def load(id):
    return objects[id]

Blob,樹和提交以這種方式統一:它們都是物件。 當他們參考其他物件時,它們實際上並沒有真正被 寫入 硬碟,而是通過雜湊值對其進行引用。

例如,示例目錄above中的樹(使用 git cat-file -p 698281bc680d1995c5f4caaf3359721a5a58d48d 來顯示)看起來像這樣:

100644 blob 4448adbf7ecd394f42ae135bbeed9676e894af85    baz.txt
040000 tree c68d233a33c5c06e0340e4c224f0afca87c8ce87    foo

樹本身會包含指向其他內容的指標,例如 baz.txt (一個 blob )與 foo (一棵樹)。 如果我們使用 git cat-file -p 4448adbf7ecd394f42ae135bbeed9676e894af85 來透過hash檢視 baz.txt 的內容, 我們會得到這種結果:

git is wonderful

參考

現在,所有的快照都通過其SHA-1值識別,這對於人類來說太不方便了,我們不善於記憶長達40個16進位的字串。

Git解決此問題的方法是為SHA-1值提供易於理解的名稱,稱為”參考”。 參考是指向提交的指標。 與不可變的物件不同,參考是可變的(可以更新以指向新的提交)。 例如,master 通常指向主分支的最新提交。

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” 來參考歷史記錄中的特定快照,而非使用16進位字串。

我們需要注意一個細節,就是我們常常會需要知道在全部歷史記錄中 “我們目前的位置”, 以便在建立新快照時,我們知道快照的相對於誰(提交中的 parents, 即母節點)。 “我們目前的位置” 是一個特別的參考,稱作 “HEAD”。

倉儲

終於,我們可以大致定義Git 倉儲 是什麼了:是資料物件 objects 與其參考 references

在硬碟上,所有 Git 儲存都是物件與參考。 所有的 git 指令都對應某種建立物件,增添或刪除參考的操作。

當我們輸入指令時,想一想這個指令對基礎的圖資料結構進行了什麼操作。 如果要對提交DAG進行特定型別的編輯,例如 “扔掉未提交的編輯與更改 ‘master’ 參考至 5d83f9e“時, 有什麼指令可以執行此編輯。 (此例中,我們可以使用 git checkout master; git reset --hard 5d83f9e

暫存區域 (”staging area”)

這是一個與資料模型無關的概念,但他是建立提交的介面的一部分。

在我們之前介紹的快照機制中,也許你想實現一個 “建立快照” 的指令,此指令可以基於工作目錄的 目前狀態 建立新的快照。 有些版本控制系統的工作方式與此類似,但是 Git 不是。 我們希望快照簡潔乾淨,從當前狀態建立快照可能不是個好選擇。 例如,考慮這個場景: 我們實現了兩個單獨的功能,並且想要建立兩個獨立的提交,體一個提交僅含有第一個功能,第二個提交了另一個功能。 或者,設想另一種場景: 我們向程式碼中添加了許多列印語句以及錯誤修正,而我們希望放棄所有列印語句的同時提交錯誤修正。

Git透過 “暫存區域” 來允許我們指定下次快照中要包含哪些改動。

Git 的命令列介面

為了避免資訊重複,我們不會詳細解釋。 十分建議閱讀 Pro Git 或者觀看課程回放來學習。

基礎

分支與合併

遠端

回滾

高階技巧

雜項

資源

Exercises

  1. If you don’t have any past experience with Git, either try reading the first couple chapters of Pro Git or go through a tutorial like Learn Git Branching. As you’re working through it, relate Git commands to the data model.
  2. Clone the repository for the class website.
    1. Explore the version history by visualizing it as a graph.
    2. Who was the last person to modify README.md? (Hint: use git log with an argument)
    3. What was the commit message associated with the last modification to the collections: line of _config.yml? (Hint: use git blame and git show)
  3. One common mistake when learning Git is to commit large files that should not be managed by Git or adding sensitive information. Try adding a file to a repository, making some commits and then deleting that file from history (you may want to look at this).
  4. Clone some repository from GitHub, and modify one of its existing files. What happens when you do git stash? What do you see when running git log --all --oneline? Run git stash pop to undo what you did with git stash. In what scenario might this be useful?
  5. Like many command line tools, Git provides a configuration file (or dotfile) called ~/.gitconfig. Create an alias in ~/.gitconfig so that when you run git graph, you get the output of git log --all --graph --decorate --oneline.
  6. You can define global ignore patterns in ~/.gitignore_global after running git config --global core.excludesfile ~/.gitignore_global. Do this, and set up your global gitignore file to ignore OS-specific or editor-specific temporary files, like .DS_Store.
  7. Clone the repository for the class website, find a typo or some other improvement you can make, and submit a pull request on GitHub.

Edit this page.

Licensed under CC BY-NC-SA.