package ipldgit import ( "bytes" "encoding/hex" "encoding/json" "errors" "fmt" "strconv" "sync" cid "github.com/ipfs/go-cid" node "github.com/ipfs/go-ipld-format" ) type Commit struct { DataSize string `json:"-"` GitTree cid.Cid `json:"tree"` Parents []cid.Cid `json:"parents"` Message string `json:"message"` Author *PersonInfo `json:"author"` Committer *PersonInfo `json:"committer"` Encoding string `json:"encoding,omitempty"` Sig *GpgSig `json:"signature,omitempty"` MergeTag []*MergeTag `json:"mergetag,omitempty"` // Other contains all the non-standard headers, such as 'HG:extra' Other []string `json:"other,omitempty"` cid cid.Cid rawData []byte rawDataOnce sync.Once } type PersonInfo struct { Name string Email string Date string Timezone string } func (pi *PersonInfo) MarshalJSON() ([]byte, error) { return json.Marshal(map[string]string{ "name": pi.Name, "email": pi.Email, "date": pi.Date + " " + pi.Timezone, }) } func (pi *PersonInfo) String() string { f := "%s <%s>" arg := []interface{}{pi.Name, pi.Email} if pi.Date != "" { f = f + " %s" arg = append(arg, pi.Date) } if pi.Timezone != "" { f = f + " %s" arg = append(arg, pi.Timezone) } return fmt.Sprintf(f, arg...) } func (pi *PersonInfo) tree(name string, depth int) []string { if depth == 1 { return []string{name} } return []string{name + "/name", name + "/email", name + "/date"} } func (pi *PersonInfo) resolve(p []string) (interface{}, []string, error) { switch p[0] { case "name": return pi.Name, p[1:], nil case "email": return pi.Email, p[1:], nil case "date": return pi.Date + " " + pi.Timezone, p[1:], nil default: return nil, nil, errors.New("no such link") } } type MergeTag struct { Object cid.Cid `json:"object"` Type string `json:"type"` Tag string `json:"tag"` Tagger *PersonInfo `json:"tagger"` Text string `json:"text"` } type GpgSig struct { Text string } func (c *Commit) Cid() cid.Cid { return c.cid } func (c *Commit) Copy() node.Node { nc := *c return &nc } func (c *Commit) Links() []*node.Link { out := []*node.Link{ {Cid: c.GitTree}, } for _, p := range c.Parents { out = append(out, &node.Link{Cid: p}) } return out } func (c *Commit) Loggable() map[string]interface{} { return map[string]interface{}{ "type": "git_commit", } } func (c *Commit) RawData() []byte { c.rawDataOnce.Do(func() { buf := new(bytes.Buffer) fmt.Fprintf(buf, "commit %s\x00", c.DataSize) fmt.Fprintf(buf, "tree %s\n", hex.EncodeToString(cidToSha(c.GitTree))) for _, p := range c.Parents { fmt.Fprintf(buf, "parent %s\n", hex.EncodeToString(cidToSha(p))) } fmt.Fprintf(buf, "author %s\n", c.Author.String()) fmt.Fprintf(buf, "committer %s\n", c.Committer.String()) if len(c.Encoding) > 0 { fmt.Fprintf(buf, "encoding %s\n", c.Encoding) } for _, mtag := range c.MergeTag { fmt.Fprintf(buf, "mergetag object %s\n", hex.EncodeToString(cidToSha(mtag.Object))) fmt.Fprintf(buf, " type %s\n", mtag.Type) fmt.Fprintf(buf, " tag %s\n", mtag.Tag) fmt.Fprintf(buf, " tagger %s\n \n", mtag.Tagger.String()) fmt.Fprintf(buf, "%s", mtag.Text) } if c.Sig != nil { fmt.Fprintln(buf, "gpgsig -----BEGIN PGP SIGNATURE-----") fmt.Fprint(buf, c.Sig.Text) fmt.Fprintln(buf, " -----END PGP SIGNATURE-----") } for _, line := range c.Other { fmt.Fprintln(buf, line) } fmt.Fprintf(buf, "\n%s", c.Message) c.rawData = buf.Bytes() }) return c.rawData } func (c *Commit) Resolve(path []string) (interface{}, []string, error) { if len(path) == 0 { return nil, nil, fmt.Errorf("zero length path") } switch path[0] { case "parents": if len(path) == 1 { return c.Parents, nil, nil } i, err := strconv.Atoi(path[1]) if err != nil { return nil, nil, err } if i < 0 || i >= len(c.Parents) { return nil, nil, fmt.Errorf("index out of range") } return &node.Link{Cid: c.Parents[i]}, path[2:], nil case "author": if len(path) == 1 { return c.Author, nil, nil } return c.Author.resolve(path[1:]) case "committer": if len(path) == 1 { return c.Committer, nil, nil } return c.Committer.resolve(path[1:]) case "signature": return c.Sig.Text, path[1:], nil case "message": return c.Message, path[1:], nil case "tree": return &node.Link{Cid: c.GitTree}, path[1:], nil case "mergetag": if len(path) == 1 { return c.MergeTag, nil, nil } i, err := strconv.Atoi(path[1]) if err != nil { return nil, nil, err } if i < 0 || i >= len(c.MergeTag) { return nil, nil, fmt.Errorf("index out of range") } if len(path) == 2 { return c.MergeTag[i], nil, nil } return c.MergeTag[i].resolve(path[2:]) default: return nil, nil, errors.New("no such link") } } func (c *Commit) ResolveLink(path []string) (*node.Link, []string, error) { out, rest, err := c.Resolve(path) if err != nil { return nil, nil, err } lnk, ok := out.(*node.Link) if !ok { return nil, nil, errors.New("not a link") } return lnk, rest, nil } func (c *Commit) Size() (uint64, error) { return uint64(len(c.RawData())), nil } func (c *Commit) Stat() (*node.NodeStat, error) { return &node.NodeStat{}, nil } func (c *Commit) String() string { return "[git commit object]" } func (c *Commit) Tree(p string, depth int) []string { if depth != -1 { panic("proper tree not yet implemented") } tree := []string{"tree", "parents", "message", "gpgsig"} tree = append(tree, c.Author.tree("author", depth)...) tree = append(tree, c.Committer.tree("committer", depth)...) for i := range c.Parents { tree = append(tree, fmt.Sprintf("parents/%d", i)) } return tree } func (c *Commit) GitSha() []byte { return cidToSha(c.Cid()) } func (t *MergeTag) resolve(path []string) (interface{}, []string, error) { if len(path) == 0 { return nil, nil, fmt.Errorf("zero length path") } switch path[0] { case "object": return &node.Link{Cid: t.Object}, path[1:], nil case "tag": return t.Tag, path[1:], nil case "tagger": if len(path) == 1 { return t.Tagger, nil, nil } return t.Tagger.resolve(path[1:]) case "text": return t.Text, path[1:], nil case "type": return t.Type, path[1:], nil default: return nil, nil, errors.New("no such link") } } var _ node.Node = (*Commit)(nil)