├── .gitignore ├── type_test.go ├── pack.go ├── type.go ├── cmd └── ggit │ ├── main.go │ ├── cat_file.go │ └── hash_object.go ├── example_test.go ├── README.md ├── LICENSE.bsd ├── git.go ├── reader.go ├── writer.go ├── store.go └── git_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | \#*\# 3 | .\#* 4 | -------------------------------------------------------------------------------- /type_test.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import "testing" 4 | 5 | var header []byte 6 | 7 | func BenchmarkTypeHeader(b *testing.B) { 8 | for n := 0; n < b.N; n++ { 9 | header = Blob.Header(20048) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /pack.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import "io" 4 | 5 | // PackStore implements Store for packfiles in git repositories. 6 | func PackStore() Store { return &packStore{} } 7 | 8 | type packStore struct{} 9 | 10 | func (st *packStore) Object(hash string) (io.Reader, error) { 11 | panic("Not implemented") 12 | } 13 | 14 | func (st *packStore) Reader(hash string, options ...func(*Reader)) (*Reader, error) { 15 | panic("Not implemented") 16 | } 17 | 18 | func (st *packStore) Writer() Writer { 19 | panic("Not implemented") 20 | } 21 | 22 | type packCloser struct{} 23 | 24 | func (g *packCloser) Close() error { 25 | panic("Not implemented") 26 | } 27 | -------------------------------------------------------------------------------- /type.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | ) 7 | 8 | // Type represents a git object type. 9 | type Type int 10 | 11 | func (t Type) String() string { 12 | switch t { 13 | case Blob: 14 | return "blob" 15 | case Tree: 16 | return "tree" 17 | case Commit: 18 | return "commit" 19 | } 20 | panic(fmt.Sprintf("missing type: %#v", t)) 21 | } 22 | 23 | // Header returns nul terminated header string for git object. 24 | func (t Type) Header(length int) []byte { 25 | return []byte(fmt.Sprintf("%s %v\x00", t, length)) 26 | } 27 | 28 | // ParseType parses object type from bytes. 29 | func ParseType(q []byte) Type { 30 | if bytes.Equal(q, []byte("blob")) { 31 | return Blob 32 | } 33 | if bytes.Equal(q, []byte("tree")) { 34 | return Tree 35 | } 36 | if bytes.Equal(q, []byte("commit")) { 37 | return Commit 38 | } 39 | panic(fmt.Sprintf("missing type: %q", q)) 40 | } 41 | 42 | // Git Object Types 43 | const ( 44 | Blob Type = iota 45 | Tree 46 | Commit 47 | ) 48 | -------------------------------------------------------------------------------- /cmd/ggit/main.go: -------------------------------------------------------------------------------- 1 | // ggit provides a cli for dasa.cc/git functionality. 2 | // This is not a suitable replacement for official git; useful for discovery. 3 | package main 4 | 5 | import ( 6 | "log" 7 | "os" 8 | 9 | "dasa.cc/git" 10 | ) 11 | 12 | var store git.Store 13 | 14 | type Runner interface { 15 | Run() 16 | } 17 | 18 | var commands = map[string]func([]string) Runner{ 19 | "cat-file": NewCatFile, 20 | "hash-object": NewHashObject, 21 | } 22 | 23 | func main() { 24 | log.SetPrefix("ggit: ") 25 | log.SetFlags(0) 26 | 27 | defer func() { 28 | if r := recover(); r != nil { 29 | log.Fatal(r) 30 | } 31 | }() 32 | 33 | wd, err := os.Getwd() 34 | if err != nil { 35 | log.Fatalf("Get working directory: %s", err) 36 | } 37 | store = git.DiskStore(git.Dir(wd)) 38 | 39 | if len(os.Args) == 1 { 40 | log.Fatal("no arguments") 41 | } 42 | 43 | cmd, ok := commands[os.Args[1]] 44 | if !ok { 45 | log.Fatalf("command %q not found", os.Args[1]) 46 | } 47 | cmd(os.Args[2:]).Run() 48 | } 49 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package git_test 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "os" 8 | "strings" 9 | 10 | "dasa.cc/git" 11 | ) 12 | 13 | func Example() { 14 | store := git.TempStore() 15 | defer os.RemoveAll(string(store)) 16 | 17 | buf := new(bytes.Buffer) 18 | 19 | // blob 20 | bdata := []byte("hello, world") 21 | 22 | bw := store.Writer() 23 | bw.WriteHeader(git.Blob, len(bdata)) 24 | bw.Write(bdata) 25 | bw.Close() 26 | 27 | br, _ := store.Reader(bw.Hash()) 28 | io.Copy(buf, br) 29 | br.Close() 30 | 31 | buf.WriteRune('\n') 32 | 33 | // tree 34 | tdata := []byte(fmt.Sprintf("100644 blob %s\t%s\n", bw.Hash(), "hello.txt")) 35 | 36 | tw := store.Writer() 37 | tw.WriteHeader(git.Tree, -1) 38 | tw.Write(tdata) 39 | tw.Close() 40 | 41 | tr, _ := store.Reader(tw.Hash(), git.PrettyReader) 42 | io.Copy(buf, tr) 43 | tr.Close() 44 | 45 | fmt.Println(strings.Replace(buf.String(), "\t", " ", -1)) 46 | // Output: 47 | // hello, world 48 | // 100644 blob 8c01d89ae06311834ee4b1fab2f0414d35f01102 hello.txt 49 | } 50 | -------------------------------------------------------------------------------- /cmd/ggit/cat_file.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "log" 8 | "os" 9 | 10 | "dasa.cc/git" 11 | ) 12 | 13 | type CatFile struct { 14 | fset *flag.FlagSet 15 | 16 | flagType *bool 17 | flagSize *bool 18 | flagPrint *bool 19 | } 20 | 21 | func NewCatFile(args []string) Runner { 22 | r := &CatFile{} 23 | r.fset = flag.NewFlagSet("cat-file", flag.ContinueOnError) 24 | r.flagType = r.fset.Bool("t", false, "display object type") 25 | r.flagSize = r.fset.Bool("s", false, "display object size") 26 | r.flagPrint = r.fset.Bool("p", false, "display object content") 27 | r.fset.Parse(args) 28 | return r 29 | } 30 | 31 | func (cmd *CatFile) Run() { 32 | log.SetPrefix("ggit cat-file: ") 33 | hash := cmd.fset.Arg(0) 34 | if hash == "" { 35 | log.Fatal("no hash given") 36 | } 37 | r, err := store.Reader(hash, git.PrettyReader) 38 | if err != nil { 39 | log.Fatalf("Reader(%s): %s", hash, err) 40 | } 41 | if *cmd.flagType { 42 | fmt.Println(r.Type()) 43 | } 44 | if *cmd.flagSize { 45 | fmt.Println(r.Len()) 46 | } 47 | if *cmd.flagPrint { 48 | if _, err := io.Copy(os.Stdout, r); err != nil { 49 | log.Fatalf("Write stdout: %s", err) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # git [![GoDoc](https://godoc.org/dasa.cc/git?status.svg)](https://godoc.org/dasa.cc/git) 2 | 3 | Package git provides an incomplete pure Go implementation of Git core methods. 4 | 5 | ## Example 6 | 7 | ### Code: 8 | 9 | ```go 10 | store := git.TempStore() 11 | defer os.RemoveAll(string(store)) 12 | 13 | buf := new(bytes.Buffer) 14 | 15 | // blob 16 | bdata := []byte("hello, world") 17 | 18 | bw := store.Writer() 19 | bw.WriteHeader(git.Blob, len(bdata)) 20 | bw.Write(bdata) 21 | bw.Close() 22 | 23 | br, _ := store.Reader(bw.Hash()) 24 | io.Copy(buf, br) 25 | br.Close() 26 | 27 | buf.WriteRune('\n') 28 | 29 | // tree 30 | tdata := []byte(fmt.Sprintf("100644 blob %s\t%s\n", bw.Hash(), "hello.txt")) 31 | 32 | tw := store.Writer() 33 | tw.WriteHeader(git.Tree, -1) 34 | tw.Write(tdata) 35 | tw.Close() 36 | 37 | tr, _ := store.Reader(tw.Hash(), git.PrettyReader) 38 | io.Copy(buf, tr) 39 | tr.Close() 40 | 41 | fmt.Println(strings.Replace(buf.String(), "\t", " ", -1)) 42 | ``` 43 | 44 | ### Output: 45 | 46 | ``` 47 | hello, world 48 | 100644 blob 8c01d89ae06311834ee4b1fab2f0414d35f01102 hello.txt 49 | ``` 50 | 51 | ## Caveats 52 | 53 | * Currently limited to loose objects 54 | * Reader and Writer for tree objects will likely fail on short reads and large content. Straight-forward to fix. 55 | -------------------------------------------------------------------------------- /LICENSE.bsd: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Daniel Skinner 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /cmd/ggit/hash_object.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "log" 9 | "os" 10 | 11 | "dasa.cc/git" 12 | ) 13 | 14 | type HashObject struct { 15 | fset *flag.FlagSet 16 | 17 | flagStdin *bool 18 | flagWrite *bool 19 | flagType *string 20 | } 21 | 22 | func NewHashObject(args []string) Runner { 23 | r := &HashObject{} 24 | r.fset = flag.NewFlagSet("hash-object", flag.ContinueOnError) 25 | r.flagStdin = r.fset.Bool("stdin", false, "read from stdin") 26 | r.flagWrite = r.fset.Bool("w", false, "write object") 27 | r.flagType = r.fset.String("t", "blob", "object type") 28 | r.fset.Parse(args) 29 | return r 30 | } 31 | 32 | func (cmd *HashObject) Run() { 33 | log.SetPrefix("ggit hash-object: ") 34 | 35 | name := cmd.fset.Arg(0) 36 | if name == "" && !*cmd.flagStdin { 37 | log.Fatal("nothing to hash") 38 | } 39 | 40 | check := func(err error) { 41 | if err != nil { 42 | log.Fatal(err) 43 | } 44 | } 45 | 46 | var ( 47 | w git.Writer 48 | n int 49 | r io.Reader 50 | ) 51 | 52 | if *cmd.flagWrite { 53 | w = store.Writer() 54 | } else { 55 | tmp, err := ioutil.TempFile("", "ggithashobject") 56 | defer os.Remove(tmp.Name()) 57 | check(err) 58 | w = git.NewWriter(tmp) 59 | } 60 | 61 | if *cmd.flagStdin { 62 | tmp, err := ioutil.TempFile("", "ggithashobjectstdin") 63 | check(err) 64 | defer os.Remove(tmp.Name()) 65 | _, err = io.Copy(tmp, os.Stdin) 66 | check(err) 67 | name = tmp.Name() 68 | tmp.Close() 69 | } 70 | 71 | if name != "" { 72 | f, err := os.Open(name) 73 | check(err) 74 | fi, err := f.Stat() 75 | check(err) 76 | n = int(fi.Size()) 77 | r = f 78 | } 79 | 80 | _, err := w.WriteHeader(git.ParseType([]byte(*cmd.flagType)), n) 81 | check(err) 82 | _, err = io.Copy(w, r) 83 | check(err) 84 | check(w.Close()) 85 | fmt.Println(w.Hash()) 86 | } 87 | -------------------------------------------------------------------------------- /git.go: -------------------------------------------------------------------------------- 1 | // Package git provides an incomplete pure Go implementation of Git core methods. 2 | // 3 | // Caveats 4 | // 5 | // Only handles loose objects. 6 | // Will fail on short reads and writes or large content. 7 | package git // import "dasa.cc/git" 8 | 9 | import ( 10 | "fmt" 11 | "io" 12 | "io/ioutil" 13 | "os" 14 | "path/filepath" 15 | ) 16 | 17 | // Init initializes a new git repository at the given path. Not recommended for use. 18 | // Panics on error. Panics if path for git directory is not empty. 19 | func Init(path string, bare bool) { 20 | check := func(err error) { 21 | if err != nil { 22 | panic(err) 23 | } 24 | } 25 | 26 | isEmpty := func(x string) bool { 27 | f, err := os.Open(x) 28 | check(err) 29 | defer f.Close() 30 | _, err = f.Readdirnames(1) 31 | return err == io.EOF 32 | } 33 | 34 | if !bare { 35 | path = filepath.Join(path, ".git") 36 | check(os.MkdirAll(path, 0755)) 37 | } 38 | 39 | if !isEmpty(path) { 40 | panic(fmt.Sprintf("directory not empty: %s", path)) 41 | } 42 | 43 | check(os.MkdirAll(filepath.Join(path, "branches"), 0755)) 44 | 45 | check(os.MkdirAll(filepath.Join(path, "hooks"), 0755)) 46 | 47 | check(os.MkdirAll(filepath.Join(path, "info"), 0755)) 48 | check(ioutil.WriteFile(filepath.Join(path, "info", "exclue"), []byte{}, 0644)) 49 | 50 | check(os.MkdirAll(filepath.Join(path, "objects", "info"), 0755)) 51 | check(os.MkdirAll(filepath.Join(path, "objects", "pack"), 0755)) 52 | 53 | check(os.MkdirAll(filepath.Join(path, "refs", "heads"), 0755)) 54 | check(os.MkdirAll(filepath.Join(path, "refs", "tags"), 0755)) 55 | 56 | check(ioutil.WriteFile(filepath.Join(path, "HEAD"), []byte("ref: refs/heads/master"), 0644)) 57 | 58 | config := []byte("[config]\n\trepositoryformatversion = 0\n\tfilemode = true") 59 | if bare { 60 | config = append(config, []byte("\n\tbare = true")...) 61 | } 62 | check(ioutil.WriteFile(filepath.Join(path, "config"), config, 0644)) 63 | 64 | desc := []byte("Unnamed repository; edit this file 'description' to name the repository.") 65 | check(ioutil.WriteFile(filepath.Join(path, "description"), desc, 0644)) 66 | } 67 | -------------------------------------------------------------------------------- /reader.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "compress/flate" 7 | "compress/zlib" 8 | "encoding/hex" 9 | "io" 10 | "strconv" 11 | ) 12 | 13 | // PrettyReader decodes sha1 sum of references in tree objects 14 | // and parses reference types. This has no effect on other types. 15 | // 16 | // NewReader(r, PrettyReader) 17 | func PrettyReader(g *Reader) { g.pretty = true } 18 | 19 | // Reader reads git object format for blobs, trees, and commits. 20 | // 21 | // TODO short reads on tree objects are likely to fail. 22 | type Reader struct { 23 | io.Reader 24 | 25 | pretty bool 26 | 27 | zr io.ReadCloser 28 | t Type 29 | n int 30 | err error 31 | } 32 | 33 | // NewReader returns Reader for r. Most users will want to call store.Reader(r). 34 | func NewReader(r io.Reader, options ...func(*Reader)) (*Reader, error) { 35 | g := new(Reader) 36 | for _, opt := range options { 37 | opt(g) 38 | } 39 | if err := g.Reset(r); err != nil { 40 | return nil, err 41 | } 42 | return g, nil 43 | } 44 | 45 | // Type returns type of object to be read. 46 | func (g *Reader) Type() Type { return g.t } 47 | 48 | // Len returns the length of object's content to be read. 49 | func (g *Reader) Len() int { return g.n } 50 | 51 | // Close does not close the original reader passed in. 52 | func (g *Reader) Close() error { return g.zr.Close() } 53 | 54 | // Reset clears the state of the Reader g such that it is equivalent to its 55 | // initial state from NewReader, but instead reading from r. Any options 56 | // previously set are retained. 57 | func (g *Reader) Reset(r io.Reader) error { 58 | var err error 59 | if g.zr == nil { 60 | if g.zr, err = zlib.NewReader(r); err != nil { 61 | return err 62 | } 63 | } else if err = g.zr.(flate.Resetter).Reset(r, nil); err != nil { 64 | return err 65 | } 66 | 67 | // 28 byte limit means reader can't support content larger than 18,500 petabytes. 68 | b := bufio.NewReader(io.LimitReader(g.zr, 28)) 69 | g.Reader = io.MultiReader(b, g.zr) 70 | 71 | // read header 72 | t, err := b.ReadBytes(' ') 73 | if err != nil { 74 | return err 75 | } 76 | g.t = ParseType(t[:len(t)-1]) 77 | 78 | n, err := b.ReadBytes('\x00') 79 | if err != nil { 80 | return err 81 | } 82 | g.n, err = strconv.Atoi(string(n[:len(n)-1])) 83 | 84 | // trees are different 85 | if g.pretty && g.t == Tree { 86 | g.Reader = &treeReader{ 87 | Reader: bufio.NewReaderSize(g.Reader, 20), 88 | hash: make([]byte, 40), 89 | } 90 | } 91 | 92 | return err 93 | } 94 | 95 | type treeReader struct { 96 | // init with min size 20 to peek sha1 97 | *bufio.Reader 98 | 99 | buf bytes.Buffer 100 | 101 | // init with length 40 102 | hash []byte 103 | } 104 | 105 | func (g *treeReader) Read(p []byte) (n int, err error) { 106 | var mode, name, sum []byte 107 | 108 | // each iteration reads a single line as follows: 109 | // [mode] [name]\x00[[20]byte] 110 | // 111 | // output is as follows (where type is determined by mode[0]): 112 | // [mode] [type] [hexenc]\t[name] 113 | for g.buf.Len() <= len(p) { 114 | mode, err = g.Reader.ReadBytes(' ') 115 | if err != nil { 116 | break 117 | } 118 | 119 | switch mode[0] { 120 | case '1': 121 | g.buf.Write(mode) 122 | g.buf.Write([]byte("blob ")) 123 | case '4': 124 | g.buf.WriteRune('0') 125 | g.buf.Write(mode) 126 | g.buf.Write([]byte("tree ")) 127 | default: 128 | panic("unrecognized mode") 129 | } 130 | 131 | // TODO custom error about malformed tree for below 132 | 133 | name, err = g.Reader.ReadBytes('\x00') 134 | if err != nil { 135 | break 136 | } 137 | name = name[:len(name)-1] 138 | 139 | sum, err = g.Reader.Peek(20) 140 | if err != nil { 141 | break 142 | } 143 | g.Reader.Discard(20) 144 | 145 | hex.Encode(g.hash, sum) 146 | g.buf.Write(g.hash) 147 | g.buf.WriteRune('\t') 148 | g.buf.Write(name) 149 | g.buf.WriteRune('\n') 150 | } 151 | 152 | n = copy(p, g.buf.Next(len(p))) 153 | 154 | return n, err 155 | } 156 | -------------------------------------------------------------------------------- /writer.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "bufio" 5 | "bytes" 6 | "compress/zlib" 7 | "crypto/sha1" 8 | "encoding/hex" 9 | "errors" 10 | "fmt" 11 | "hash" 12 | "io" 13 | "io/ioutil" 14 | "os" 15 | ) 16 | 17 | // Writer writes git object format for blobs, trees, and commits. 18 | // 19 | // TODO short writes on tree objects are likely to fail. 20 | type Writer interface { 21 | // Write writes p to the underlying Writer. Write returns an error 22 | // if caller has not first called WriteHeader. 23 | // 24 | // Close flushes written data. This does not close the original writer. 25 | io.WriteCloser 26 | 27 | // WriteHeader must be called before writing any data. If you don't know 28 | // the size of data to be written, pass a negative integer; size is always 29 | // ignored for tree types. In such cases, an intermediary file is used 30 | // to determine size. 31 | WriteHeader(t Type, size int) (int, error) 32 | 33 | // Hash returns sha1 sum of data written. 34 | Hash() string 35 | } 36 | 37 | type writer struct { 38 | io.Writer 39 | zw *zlib.Writer 40 | hh hash.Hash 41 | 42 | // used in case size is unknown 43 | tmp *os.File 44 | tw *treeWriter 45 | t Type 46 | 47 | wroteHeader bool 48 | err error 49 | finalize func() error 50 | } 51 | 52 | // NewWriter returns a new Writer that writes to staging. 53 | func NewWriter(staging io.Writer) Writer { 54 | // TODO need to insert treeWriter here, before zlib.NewWriter, also not sure about hh ??? 55 | // actually, maybe after WriteHeader is done. 56 | g := &writer{ 57 | zw: zlib.NewWriter(staging), 58 | hh: sha1.New(), 59 | } 60 | g.Writer = io.MultiWriter(g.zw, g.hh) 61 | return g 62 | } 63 | 64 | func (g *writer) WriteHeader(t Type, s int) (n int, err error) { 65 | if g.wroteHeader { 66 | return 0, errors.New("Header already written.") 67 | } 68 | g.t = t 69 | g.wroteHeader = true 70 | if t == Tree || s < 0 { 71 | g.tmp, err = ioutil.TempFile("", "gitwriter") 72 | g.tw = &treeWriter{ 73 | Writer: g.tmp, 74 | rbuf: new(bytes.Buffer), 75 | wbuf: new(bytes.Buffer), 76 | sum: make([]byte, 20), 77 | } 78 | } else { 79 | n, err = g.Write(t.Header(s)) 80 | } 81 | return 82 | } 83 | 84 | func (g *writer) Write(p []byte) (int, error) { 85 | if !g.wroteHeader { 86 | return 0, errors.New("Must call WriteHeader before calling Write.") 87 | } 88 | if g.tw != nil { 89 | return g.tw.Write(p) 90 | } 91 | return g.Writer.Write(p) 92 | } 93 | 94 | func (g *writer) Close() error { 95 | if g.tw != nil { 96 | defer g.tmp.Close() 97 | defer os.Remove(g.tmp.Name()) 98 | 99 | fi, err := g.tmp.Stat() 100 | if err != nil { 101 | return err 102 | } 103 | size := int(fi.Size()) 104 | _, err = g.tmp.Seek(0, 0) 105 | if err != nil { 106 | return err 107 | } 108 | _, err = g.Writer.Write(g.t.Header(size)) 109 | if err != nil { 110 | return err 111 | } 112 | _, err = io.Copy(g.Writer, g.tmp) 113 | if err != nil { 114 | return err 115 | } 116 | } 117 | 118 | if err := g.zw.Close(); err != nil { 119 | return err 120 | } 121 | if g.finalize != nil { 122 | return g.finalize() 123 | } 124 | return nil 125 | } 126 | 127 | func (g *writer) Hash() string { 128 | return fmt.Sprintf("%x", g.hh.Sum(nil)) 129 | } 130 | 131 | // treeWriter handles PrettyReader formatted tree stream. 132 | // TODO this could use some work. 133 | type treeWriter struct { 134 | io.Writer 135 | 136 | rbuf *bytes.Buffer 137 | wbuf *bytes.Buffer 138 | 139 | // len 20 140 | sum []byte 141 | } 142 | 143 | // TODO this is going to bomb on a short read 144 | func (g *treeWriter) Write(p []byte) (n int, err error) { 145 | var mode, h, name []byte 146 | 147 | g.rbuf.Write(p) 148 | r := bufio.NewReader(g.rbuf) 149 | 150 | for { 151 | mode, err = r.ReadBytes(' ') 152 | if err != nil { 153 | break 154 | } 155 | if mode[0] == '0' { 156 | mode = mode[1:] 157 | } 158 | 159 | // discard type 160 | _, err = r.ReadBytes(' ') 161 | if err != nil { 162 | break 163 | } 164 | 165 | h, err = r.ReadBytes('\t') 166 | if err != nil { 167 | break 168 | } 169 | h = h[:len(h)-1] 170 | _, err = hex.Decode(g.sum, h) 171 | if err != nil { 172 | break 173 | } 174 | 175 | name, err = r.ReadBytes('\n') 176 | if err != nil { 177 | break 178 | } 179 | name[len(name)-1] = '\x00' 180 | 181 | g.wbuf.Write(mode) 182 | g.wbuf.Write(name) 183 | g.wbuf.Write(g.sum) 184 | } 185 | 186 | n, _ = g.Writer.Write(g.wbuf.Next(len(p))) 187 | 188 | return n, err 189 | } 190 | -------------------------------------------------------------------------------- /store.go: -------------------------------------------------------------------------------- 1 | package git 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | "path/filepath" 11 | "strings" 12 | ) 13 | 14 | // Store represents a collection of git objects that can be managed. This may 15 | // be loose objects, a packfile, or even a superset of Stores. 16 | type Store interface { 17 | // Object resolves hash to reader of underlying data. Implemenations must 18 | // resolve abbreviated hashes. 19 | Object(hash string) (io.Reader, error) 20 | 21 | // Reader initializes a new Reader by the given hash. Implementations need 22 | // to resolve abbreviated hashes and provide a proper io.Reader to Reader. 23 | Reader(hash string, options ...func(*Reader)) (*Reader, error) 24 | 25 | // Writer initializes a new Writer. Implementations must wrap Writer 26 | // so that Writer.Close() flushes content to storage. 27 | Writer() Writer 28 | } 29 | 30 | // DiskStore implements Store for git repositories on disk. 31 | // 32 | // dir, _ := os.Getwd() 33 | // store := git.DiskStore(dir) 34 | type DiskStore string 35 | 36 | // Dir traverses tree backwards to locate git directory. 37 | // Typically used to create DiskStore. 38 | func Dir(x string) string { 39 | exists := func(args ...string) bool { 40 | for _, arg := range args { 41 | if _, err := os.Stat(arg); os.IsNotExist(err) { 42 | return false 43 | } 44 | } 45 | return true 46 | } 47 | 48 | d := x 49 | for { 50 | if exists(filepath.Join(d, ".git")) { 51 | return filepath.Join(d, ".git") 52 | } 53 | // TODO probably not a very good check 54 | if exists(filepath.Join(d, "config"), filepath.Join(d, "HEAD"), filepath.Join(d, "objects")) { 55 | return d // bare 56 | } 57 | x = filepath.Dir(d) 58 | if x == d || x == "/" || x == "." { 59 | panic("not a git repository") 60 | } 61 | d = x 62 | } 63 | } 64 | 65 | // Object resolves hash to reader of underlying data. 66 | func (st DiskStore) Object(hash string) (io.Reader, error) { 67 | d := filepath.Join(string(st), "objects", hash[:2]) 68 | s := filepath.Join(d, hash[2:]) 69 | if f, err := os.Open(s); !os.IsNotExist(err) { 70 | return f, err 71 | } 72 | dir, err := os.Open(d) 73 | if err != nil { 74 | return nil, err 75 | } 76 | ns, err := dir.Readdirnames(-1) 77 | if err != nil { 78 | return nil, err 79 | } 80 | var match string 81 | for _, e := range ns { 82 | if strings.HasPrefix(e, hash[2:]) { 83 | if match != "" { 84 | return nil, errors.New("ambigious hash " + hash) 85 | } 86 | match = e 87 | } 88 | } 89 | if match == "" { 90 | return nil, fmt.Errorf("object hash %s does not exist", hash) 91 | } 92 | return os.Open(filepath.Join(d, match)) 93 | } 94 | 95 | // Reader returns a new Reader for the given object hash or error otherwise. 96 | // The object's type and length are immediately available. 97 | // Callers must call Reader.Close() when done. 98 | func (st DiskStore) Reader(hash string, options ...func(*Reader)) (*Reader, error) { 99 | r, err := st.Object(hash) 100 | if err != nil { 101 | return nil, err 102 | } 103 | return NewReader(r, options...) 104 | } 105 | 106 | // Writer provides a new Writer that buffers data to a temporary file. 107 | // Callers must call Writer.Close() to flush data to storage. 108 | func (st DiskStore) Writer() Writer { 109 | tmp, err := ioutil.TempFile("", "gitdiskstore") 110 | if err != nil { 111 | panic(err) 112 | } 113 | return &diskCloser{NewWriter(tmp), st, tmp} 114 | } 115 | 116 | // TempStore provides a DiskStore in a temporary directory. Callers are responsible 117 | // for removing the directory when done. 118 | // 119 | // TempStore may not provide a valid git repository. See Init source for layout. 120 | // 121 | // store := git.TempStore() 122 | // defer os.RemoveAll(string(store)) 123 | func TempStore() DiskStore { 124 | name, err := ioutil.TempDir("", "gittempstore") 125 | if err != nil { 126 | panic(err) 127 | } 128 | Init(name, false) 129 | // since not bare, call Dir 130 | return DiskStore(Dir(name)) 131 | } 132 | 133 | // diskCloser wraps a Writer delivered by DiskStore to finalize writing 134 | // object to disk once Writer.Close() is called. 135 | type diskCloser struct { 136 | Writer 137 | st DiskStore 138 | f *os.File 139 | } 140 | 141 | func (g *diskCloser) Close() error { 142 | if err := g.Writer.Close(); err != nil { 143 | return err 144 | } 145 | hash := g.Writer.Hash() 146 | p := filepath.Join(string(g.st), "objects", hash[:2], hash[2:]) 147 | if err := os.MkdirAll(filepath.Dir(p), 0755); err != nil { 148 | return err 149 | } 150 | 151 | // TODO "invalid cross-device link" with this in random cases 152 | // even when not on a different partition 153 | // os.Rename(g.f.Name(), p) 154 | 155 | f, err := os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0444) 156 | if err != nil { 157 | return err 158 | } 159 | defer f.Close() 160 | 161 | g.f.Seek(0, 0) 162 | if _, err := io.Copy(f, g.f); err != nil { 163 | return err 164 | } 165 | 166 | g.f.Close() 167 | os.Remove(g.f.Name()) 168 | 169 | return nil 170 | } 171 | 172 | // MemStore implements Store in-memory. No guarantees are ensured with thread safety 173 | // as the implementation relies on the uniqueness of SHA1 hash, in so far as a hash 174 | // collision could break concurrent access. 175 | func MemStore() Store { 176 | m := make(map[string][]byte) 177 | return memStore(m) 178 | } 179 | 180 | type memStore map[string][]byte 181 | 182 | func (st memStore) Object(hash string) (io.Reader, error) { 183 | if b, ok := st[hash]; ok { 184 | return bytes.NewReader(b), nil 185 | } 186 | var match string 187 | for k := range st { 188 | if strings.HasPrefix(k, hash) { 189 | if match != "" { 190 | return nil, errors.New("ambigious hash " + hash) 191 | } 192 | match = k 193 | } 194 | } 195 | if match == "" { 196 | return nil, fmt.Errorf("object hash %s does not exist", hash) 197 | } 198 | return bytes.NewReader(st[match]), nil 199 | } 200 | 201 | func (st memStore) Reader(hash string, options ...func(*Reader)) (*Reader, error) { 202 | r, err := st.Object(hash) 203 | if err != nil { 204 | return nil, err 205 | } 206 | return NewReader(r, options...) 207 | } 208 | 209 | func (st memStore) Writer() Writer { 210 | g := &memCloser{st: st} 211 | g.Writer = NewWriter(&g.buf) 212 | return g 213 | } 214 | 215 | type memCloser struct { 216 | Writer 217 | st memStore 218 | buf bytes.Buffer 219 | } 220 | 221 | func (g *memCloser) Close() error { 222 | if err := g.Writer.Close(); err != nil { 223 | return err 224 | } 225 | hash := g.Writer.Hash() 226 | if _, ok := g.st[hash]; ok { 227 | return fmt.Errorf("object with hash %s exists", hash) 228 | } 229 | g.st[hash] = g.buf.Bytes() 230 | return nil 231 | } 232 | -------------------------------------------------------------------------------- /git_test.go: -------------------------------------------------------------------------------- 1 | // Package git provides a limited set of git core methods. 2 | package git 3 | 4 | import ( 5 | "bytes" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "log" 10 | "os" 11 | "os/exec" 12 | "path/filepath" 13 | "strings" 14 | "testing" 15 | ) 16 | 17 | var ( 18 | command func(name string, arg ...string) *exec.Cmd 19 | store DiskStore 20 | ) 21 | 22 | func run(cmd *exec.Cmd) (string, error) { 23 | data, err := cmd.CombinedOutput() 24 | if err != nil { 25 | return "", fmt.Errorf("%s", data) 26 | } 27 | return string(data), err 28 | } 29 | 30 | func assertRun(t *testing.T, cmd *exec.Cmd) string { 31 | x, err := run(cmd) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | return x 36 | } 37 | 38 | func assertWrite(t *testing.T, cmd *exec.Cmd, r io.Reader) string { 39 | wc, err := cmd.StdinPipe() 40 | if err != nil { 41 | t.Fatal(err) 42 | } 43 | io.Copy(wc, r) 44 | wc.Close() 45 | 46 | x, err := run(cmd) 47 | if err != nil { 48 | t.Fatal(err) 49 | } 50 | return x 51 | } 52 | 53 | func TestMain(m *testing.M) { 54 | var ( 55 | exitFuncs []func() 56 | 57 | exitFunc = func(fn func()) { 58 | exitFuncs = append(exitFuncs, fn) 59 | } 60 | 61 | exit = func(code int) { 62 | for _, fn := range exitFuncs { 63 | fn() 64 | } 65 | os.Exit(code) 66 | } 67 | 68 | fatal = func(v ...interface{}) { 69 | log.Println(v...) 70 | exit(1) 71 | } 72 | ) 73 | 74 | defer func() { 75 | if r := recover(); r != nil { 76 | fatal(r) 77 | } 78 | }() 79 | 80 | log.SetPrefix("testing: ") 81 | log.SetFlags(0) 82 | 83 | // 84 | cmdDir, err := ioutil.TempDir("", "testing") 85 | if err != nil { 86 | fatal(err) 87 | } 88 | exitFunc(func() { 89 | if err := os.RemoveAll(cmdDir); err != nil { 90 | log.Println(err) 91 | } 92 | }) 93 | 94 | command = func(name string, arg ...string) *exec.Cmd { 95 | cmd := exec.Command(name, arg...) 96 | cmd.Dir = cmdDir 97 | return cmd 98 | } 99 | 100 | if _, err := run(command("git", "init")); err != nil { 101 | fatal(err) 102 | } 103 | 104 | store = DiskStore(Dir(cmdDir)) 105 | 106 | exit(m.Run()) 107 | } 108 | 109 | func TestWriter(t *testing.T) { 110 | data := []byte("hello,\nworld") 111 | 112 | w := store.Writer() 113 | w.WriteHeader(Blob, len(data)) 114 | w.Write(data) 115 | w.Close() 116 | 117 | hash := w.Hash() 118 | 119 | out := assertRun(t, command("git", "cat-file", "-t", hash)) 120 | out = strings.TrimSpace(out) 121 | if out != Blob.String() { 122 | t.Fatalf("git cat-file -t %s => %q, want %q", hash[:8], out, Blob) 123 | } 124 | 125 | out = assertRun(t, command("git", "cat-file", "-p", hash)) 126 | out = strings.TrimSpace(out) 127 | if out != string(data) { 128 | t.Fatalf("git cat-file -p %s => %q, want %q", hash[:8], out, data) 129 | } 130 | } 131 | 132 | func TestReader(t *testing.T) { 133 | data := []byte("hello, world\n") 134 | 135 | cmd := command("git", "hash-object", "-t", "blob", "-w", "--stdin") 136 | hash := assertWrite(t, cmd, bytes.NewReader(data)) 137 | hash = strings.TrimSpace(hash) 138 | 139 | r, err := store.Reader(hash) 140 | if err != nil { 141 | t.Fatal(err) 142 | } 143 | 144 | if r.Type() != Blob { 145 | t.Fatalf("Reader.Type() => %#v, want Blob", r.Type()) 146 | } 147 | if r.Len() != len(data) { 148 | t.Fatalf("Reader.Len() => %v, want %v", r.Len(), len(data)) 149 | } 150 | 151 | b := new(bytes.Buffer) 152 | io.Copy(b, r) 153 | r.Close() 154 | 155 | if !bytes.Equal(b.Bytes(), data) { 156 | t.Fatalf("Buffer.Bytes() => %q, want %q", string(b.Bytes()), string(data)) 157 | } 158 | } 159 | 160 | func TestTree(t *testing.T) { 161 | // init 162 | dir := filepath.Join(string(store), "..") 163 | ioutil.WriteFile(filepath.Join(dir, "foo.txt"), []byte("foo"), 0644) 164 | os.MkdirAll(filepath.Join(dir, "foo", "bar"), 0755) 165 | ioutil.WriteFile(filepath.Join(dir, "foo", "bar", "bar.txt"), []byte("bar"), 0644) 166 | assertRun(t, command("git", "add", "-A", ".")) 167 | assertRun(t, command("git", "commit", "-m", "foobar")) 168 | 169 | hash := strings.TrimSpace(assertRun(t, command("git", "rev-parse", "HEAD"))) 170 | commit := strings.TrimSpace(assertRun(t, command("git", "cat-file", "-p", hash))) 171 | tree := strings.Split(strings.Split(commit, "\n")[0], " ")[1] 172 | 173 | want := assertRun(t, command("git", "cat-file", "-p", tree)) 174 | 175 | // test reader 176 | r, err := store.Reader(tree, PrettyReader) 177 | if err != nil { 178 | t.Fatal(err) 179 | } 180 | b := new(bytes.Buffer) 181 | io.Copy(b, r) 182 | r.Close() 183 | 184 | if b.String() != want { 185 | t.Fatalf("Buffer.Bytes() => %q, want %q", b.String(), want) 186 | } 187 | 188 | // test writer 189 | tmp, err := ioutil.TempFile("", "gittest") 190 | if err != nil { 191 | t.Fatal(err) 192 | } 193 | defer os.Remove(tmp.Name()) 194 | 195 | w := NewWriter(tmp) 196 | if _, err := w.WriteHeader(Tree, -1); err != nil { 197 | t.Fatal(err) 198 | } 199 | if _, err := w.Write(b.Bytes()); err != nil && err != io.EOF { 200 | t.Fatal(err) 201 | } 202 | w.Close() 203 | 204 | orig, err := command("git", "cat-file", "tree", tree).CombinedOutput() 205 | if err != nil { 206 | t.Fatal(err) 207 | } 208 | tmp.Seek(0, 0) 209 | raw, err := NewReader(tmp) 210 | if err != nil { 211 | t.Fatal(err) 212 | } 213 | if raw.Type() != Tree { 214 | t.Fatal("expect tree") 215 | } 216 | if raw.Len() != 65 { 217 | t.Fatal("expect 65", raw.Len()) 218 | } 219 | dat := new(bytes.Buffer) 220 | io.Copy(dat, raw) 221 | raw.Close() 222 | tmp.Close() 223 | 224 | if !bytes.Equal(dat.Bytes(), orig) { 225 | t.Fatalf("bytes.Equal have %v want %v", dat.Bytes(), orig) 226 | } 227 | 228 | if w.Hash() != tree { 229 | t.Fatalf("Writer.Hash() => %q, want %q", w.Hash(), tree) 230 | } 231 | } 232 | 233 | func TestMemStore(t *testing.T) { 234 | st := MemStore() 235 | w := st.Writer() 236 | 237 | data := []byte("hello, world") 238 | 239 | if _, err := w.WriteHeader(Blob, len(data)); err != nil { 240 | t.Fatalf("WriteHeader(%s, %v) failed: %s", Blob, len(data), err) 241 | } 242 | if _, err := w.Write(data); err != nil { 243 | t.Fatalf("Write(%#v) failed: %s", data, err) 244 | } 245 | if err := w.Close(); err != nil { 246 | t.Fatalf("Writer.Close() failed: %s", err) 247 | } 248 | 249 | r, err := st.Reader(w.Hash()) 250 | if err != nil { 251 | t.Fatalf("Reader(%s) failed: %s", w.Hash(), err) 252 | } 253 | buf := new(bytes.Buffer) 254 | if _, err := io.Copy(buf, r); err != nil { 255 | t.Fatalf("Copy failed: %s", err) 256 | } 257 | if err := r.Close(); err != nil && err != io.EOF { 258 | t.Fatalf("Reader.Close() failed: %s", err) 259 | } 260 | if !bytes.Equal(buf.Bytes(), data) { 261 | t.Fatalf("have %q, want %q", buf.Bytes(), data) 262 | } 263 | } 264 | 265 | func BenchmarkMemReader(b *testing.B) { 266 | st := MemStore() 267 | data := []byte("hello, world") 268 | w := st.Writer() 269 | if _, err := w.WriteHeader(Blob, len(data)); err != nil { 270 | b.Fatal(err) 271 | } 272 | if _, err := w.Write(data); err != nil { 273 | b.Fatal(err) 274 | } 275 | if err := w.Close(); err != nil { 276 | b.Fatal(err) 277 | } 278 | hash := w.Hash() 279 | 280 | b.ResetTimer() 281 | for n := 0; n < b.N; n++ { 282 | buf := new(bytes.Buffer) 283 | r, err := st.Reader(hash) 284 | if err != nil { 285 | b.Fatal(err) 286 | } 287 | if _, err := io.Copy(buf, r); err != nil { 288 | b.Fatal(err) 289 | } 290 | r.Close() 291 | } 292 | } 293 | 294 | func BenchmarkMemWriter(b *testing.B) { 295 | st := MemStore() 296 | data := []byte("hello, world") 297 | r := bytes.NewReader(data) 298 | b.ResetTimer() 299 | for n := 0; n < b.N; n++ { 300 | r.Seek(0, 0) 301 | data = append(data, byte('!')) 302 | 303 | w := st.Writer() 304 | if _, err := w.WriteHeader(Blob, len(data)); err != nil { 305 | b.Fatal(err) 306 | } 307 | if _, err := w.Write(data); err != nil { 308 | b.Fatal(err) 309 | } 310 | if err := w.Close(); err != nil { 311 | b.Fatal(n, err) 312 | } 313 | } 314 | } 315 | 316 | func BenchmarkDiskReader(b *testing.B) { 317 | st := TempStore() 318 | defer os.RemoveAll(string(st)) 319 | 320 | data := []byte("hello, world") 321 | 322 | w := st.Writer() 323 | if _, err := w.WriteHeader(Blob, len(data)); err != nil { 324 | b.Fatal(err) 325 | } 326 | if _, err := w.Write(data); err != nil { 327 | b.Fatal(err) 328 | } 329 | if err := w.Close(); err != nil && err != io.EOF { 330 | b.Fatal(err) 331 | } 332 | hash := w.Hash() 333 | 334 | b.ResetTimer() 335 | for n := 0; n < b.N; n++ { 336 | buf := new(bytes.Buffer) 337 | r, err := st.Reader(hash) 338 | if err != nil { 339 | b.Fatal(err) 340 | } 341 | if _, err := io.Copy(buf, r); err != nil { 342 | b.Fatal(err) 343 | } 344 | r.Close() 345 | } 346 | } 347 | 348 | func BenchmarkDiskWriter(b *testing.B) { 349 | st := TempStore() 350 | defer os.RemoveAll(string(st)) 351 | 352 | data := []byte("hello, world") 353 | r := bytes.NewReader(data) 354 | b.ResetTimer() 355 | for n := 0; n < b.N; n++ { 356 | r.Seek(0, 0) 357 | data = append(data, byte('!')) 358 | 359 | w := st.Writer() 360 | if _, err := w.WriteHeader(Blob, len(data)); err != nil { 361 | b.Fatal(err) 362 | } 363 | if _, err := w.Write(data); err != nil { 364 | b.Fatal(err) 365 | } 366 | if err := w.Close(); err != nil { 367 | b.Fatal(n, err) 368 | } 369 | } 370 | } 371 | 372 | // func BenchmarkTree(b *testing.B) { 373 | // for n := 0; n < b.N; n++ { 374 | 375 | // } 376 | // } 377 | --------------------------------------------------------------------------------