這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
由於曆史原由,git一直是被黑成比較難用的版本控制器。其實近年來git的使用者介面已經被簡化的非常簡單了,配上github、bitbucket等hosting,已接近完美。
git其實挺簡單的,本文用了約150行golang代碼實現了git checkout功能,閱讀代碼之前,您應該讀過《Git Pro》中的git內部原理一節。
1. 資料定義:
type blob struct { sha1 string filename string}type tree struct { b []*blob name string child []*tree}type commit struct { sha1 string tree *tree parent *commit}
其中blob定義一個檔案 ,sha1是檔案的sha1值,filename是不包括路徑的檔案名稱。
tree定義相當於目錄,b是目錄下的檔案,name是目前的目錄名,不包括父路徑,child是目錄下的目錄。
commit是一次提交,sha1是提交的sha1值,tree指向一要樹形的根節點,沿此根結點可以檢出所有的檔案。
對照下面這副圖就比較容易理解:
2. 工具函數
func readSha1FileReader(sha1 string) (reader io.Reader, err error) { f, err := os.Open(getSha1FilePath(sha1)) if err != nil{ return } return zlib.NewReader(f)}func readSha1FileContent(sha1 string) (content []byte, err error) { if reader, err := readSha1FileReader(sha1);err == nil{ buf := new(bytes.Buffer) buf.ReadFrom(reader) content = buf.Bytes() } return}func getSha1FileContentBody(content []byte) []byte { i := bytes.IndexByte(content, 0) return content[i+1:]}func getSha1FilePath(sha1 string) string { return ".git/objects/" + sha1[0:2] + "/" + sha1[2:]}
- getSha1FilePath 根據sha1值取得對應的object路徑。
- readSha1FileReader 根據sha1值讀取object內容,注意原始內容是經過壓縮的,調用zlib是為了對其解壓。
- readSha1FileContent 對readSha1FileReader的一層封裝,返回的是byte數組
- getSha1FileContentBody 返回object的內容的body部分,header的內容我們直接忽略了
上面提到的object是位於路徑.git/objects/路徑下的檔案
3. 構建樹
func BuildTree(sha1 string) *tree { all, err := readSha1FileContent(sha1) if err != nil { log.Fatal("BuildTree error:", err) return nil } content := getSha1FileContentBody(all) start := 0 tree := tree{} for i := 0; i < len(content); { if content[i] == 0 { line := content[start : i+21] _type := line[:6] id := line[i-start+1:] obj_sha1 := fmt.Sprintf("%x", id) switch string(_type[0:3]) { //BLOB case "100": name := string(line[7 : i-start]) b := blob{sha1: obj_sha1, filename: name} tree.b = append(tree.b, &b) break //TREE case "400": name := string(line[6 : i-start]) child := BuildTree(obj_sha1) child.name = name tree.child = append(tree.child, child) break } i += 21 start = i } else { i++ } } return &tree}
以上便是檢出git的庫的核心函數,其入參是一次Commit的Sha1值。要理解這個函數,需要知道tree檔案的格式定義(《Git Pro》一書中沒有):
<TREE> : _deflate_( <OBJECT_HEADER> <TREE_CONTENTS> ) | <COMPACT_OBJECT_HEADER> _deflate_( <TREE_CONTENTS> ) ;<TREE_CONTENTS> : <TREE_ENTRIES> ;<TREE_ENTRIES> # Tree entries are sorted by the byte sequence that comprises # the entry name. However, for the purposes of the sort # comparison, entries for tree objects are compared as if the # entry name byte sequence has a trailing ASCII '/' (0x2f). : ( <TREE_ENTRY> )* ;<TREE_ENTRY> # The type of the object referenced MUST be appropriate for # the mode. Regular files and symbolic links reference a BLOB # and directories reference a TREE. : <OCTAL_MODE> <SP> <NAME> <NUL> <BINARY_OBJ_ID> ;
通過getSha1FileContentBody函數即可取得TREE_CONTENTS,TREE_CONTENTS包括一個或多個TREE_ENTRY,TREE_ENTRY的格式如下:
<OCTAL_MODE> <SP> <NAME> <NUL> <BINARY_OBJ_ID>
OCTAL_MODE的前三個位元組定義了object類型,"100"為Blob,"400"為Tree,如果是Tree對像,則需要遞迴調用。
4. 檢出檔案
BuildTree根據指定的Commit構建出所有檔案形成的樹型結構,有了它,就很容易檢出檔案。
func (b *blob) checkout(prefix string) { if content, err := readSha1FileContent(b.sha1);err!=nil{ log.Fatal("blob checkout error:", err) }else{ body := getSha1FileContentBody(content) filename := prefix + "/" + b.filename log.Println("WriteFile:",filename) if err = ioutil.WriteFile(filename, body, 0644);err!=nil{ log.Fatal("blob checkout error:", err) } }}func (t *tree) checkout(path string) { if _, err := os.Stat(path); os.IsNotExist(err) { log.Println("Mkdir:",path) if err := os.Mkdir(path, 0777); err != nil { log.Fatal("mkdir error:", err) return } } for _, v := range t.b { v.checkout(path) //BLOB checkout } for _, v := range t.child { v.checkout(path + "/" + v.name) //TREE checkout }}func (c *commit) CheckOut() { if pwd, err := os.Getwd();err==nil{ c.tree.checkout(pwd) }else{ log.Fatal("commit checkout error:", err) }}
以上三個函數的調用順序為commit.CheckOUt->tree.checkout->blob.checkout.
如果有目錄,tree.checkout會組建目錄。blob.checkout則會組建檔案。
5. 樣本
完整的代碼見這裡
編譯
~/tmp$ git clone git@github.com:icattlecoder/gogit.git~/tmp$ cd gogit~/tmp$ go build gogit.go
檢出樣本庫的代碼
~/tmp$ git clone git@github.com:icattlecoder/jsfiddle.git~/tmp$ cd jsfiddle~/tmp$ rm -Rf ajaxupload/ formupload/ resumbleupload/ uptoken/~/tmp$ mv ../gogit/gogit .~/tmp$ ./gogit~/tmp$ lsajaxupload formupload resumbleupload uptoken
在運行gogit之前,刪除了本地檔案,而運行gogit後,所有檔案又恢複了,因此實現了git checkout功能。
注意:本文的git checkout不能處理壓縮過的git庫