150行Go代碼實現git checkout功能

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

由於曆史原由,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庫

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.