150行Go代碼實現git checkout功能

因爲歷史起因,git一直是被黑成比較難用的版本控制器。其實近年來git的用戶界面已經被簡化的很是簡單了,配上github、bitbucket等hosting,已接近完美。
git其實挺簡單的,本文用了約150行golang代碼實現了git checkout功能,閱讀代碼以前,您應該讀過《Git Pro》中的git內部原理一節。php

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指向一要樹形的根節點,沿此根結點能夠檢出全部的文件。
對照下面這副圖就比較容易理解:
請輸入圖片描述git

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/路徑下的文件github

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》一書中沒有):golang

<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的格式以下:ajax

<OCTAL_MODE> <SP> <NAME> <NUL> <BINARY_OBJ_ID>

OCTAL_MODE的前三個字節定義了object類型,"100"爲Blob,"400"爲Tree,若是是Tree對像,則須要遞歸調用。segmentfault

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則會生成文件。app

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$ ls
ajaxupload     formupload   resumbleupload uptoken

在運行gogit以前,刪除了本地文件,而運行gogit後,全部文件又恢復了,所以實現了git checkout功能。工具

注意:本文的git checkout不能處理壓縮過的git庫

相關文章
相關標籤/搜索