Fast implementation of Git in pure Go
1package furgit
2
3import (
4 "bytes"
5 "errors"
6 "fmt"
7)
8
9// Commit represents a Git commit object.
10type Commit struct {
11 // Tree represents the tree hash referenced by the commit.
12 Tree Hash
13 // Parents represents the parent commit hashes.
14 // Commits that have 0 parents are root commits.
15 // Commits that have >= 2 parents are merge commits.
16 Parents []Hash
17 // Author represents the author of the commit.
18 Author Ident
19 // Committer represents the committer of the commit.
20 Committer Ident
21 // Message represents the commit message.
22 Message []byte
23 // ChangeID represents the change-id header used by
24 // Gerrit and Jujutsu.
25 ChangeID string
26 // ExtraHeaders holds any extra headers present in the commit.
27 ExtraHeaders []ExtraHeader
28}
29
30// StoredCommit represents a commit stored in the object database.
31type StoredCommit struct {
32 Commit
33 hash Hash
34}
35
36// Hash returns the hash of the stored commit.
37func (sCommit *StoredCommit) Hash() Hash {
38 return sCommit.hash
39}
40
41// ObjectType returns the object type of the commit.
42//
43// It always returns ObjectTypeCommit.
44func (commit *Commit) ObjectType() ObjectType {
45 _ = commit
46 return ObjectTypeCommit
47}
48
49func parseCommit(id Hash, body []byte, repo *Repository) (*StoredCommit, error) {
50 c := new(StoredCommit)
51 c.hash = id
52 i := 0
53 for i < len(body) {
54 rel := bytes.IndexByte(body[i:], '\n')
55 if rel < 0 {
56 return nil, errors.New("furgit: commit: missing newline")
57 }
58 line := body[i : i+rel]
59 i += rel + 1
60 if len(line) == 0 {
61 break
62 }
63
64 switch {
65 case bytes.HasPrefix(line, []byte("tree ")):
66 treeID, err := repo.ParseHash(string(line[5:]))
67 if err != nil {
68 return nil, fmt.Errorf("furgit: commit: tree: %w", err)
69 }
70 c.Tree = treeID
71 case bytes.HasPrefix(line, []byte("parent ")):
72 parent, err := repo.ParseHash(string(line[7:]))
73 if err != nil {
74 return nil, fmt.Errorf("furgit: commit: parent: %w", err)
75 }
76 c.Parents = append(c.Parents, parent)
77 case bytes.HasPrefix(line, []byte("change-id ")):
78 c.ChangeID = string(line)
79 case bytes.HasPrefix(line, []byte("author ")):
80 idt, err := parseIdent(line[7:])
81 if err != nil {
82 return nil, fmt.Errorf("furgit: commit: author: %w", err)
83 }
84 c.Author = *idt
85 case bytes.HasPrefix(line, []byte("committer ")):
86 idt, err := parseIdent(line[10:])
87 if err != nil {
88 return nil, fmt.Errorf("furgit: commit: committer: %w", err)
89 }
90 c.Committer = *idt
91 case bytes.HasPrefix(line, []byte("gpgsig ")), bytes.HasPrefix(line, []byte("gpgsig-sha256 ")):
92 // TODO: handle this
93 for i < len(body) {
94 nextRel := bytes.IndexByte(body[i:], '\n')
95 if nextRel < 0 {
96 return nil, errors.New("furgit: commit: unterminated gpgsig")
97 }
98 if body[i] != ' ' {
99 break
100 }
101 i += nextRel + 1
102 }
103 default:
104 key, value, found := bytes.Cut(line, []byte{' '})
105 if !found {
106 return nil, errors.New("furgit: commit: malformed header")
107 }
108 c.ExtraHeaders = append(c.ExtraHeaders, ExtraHeader{Key: string(key), Value: value})
109 }
110 }
111
112 if i > len(body) {
113 return nil, ErrInvalidObject
114 }
115
116 c.Message = append([]byte(nil), body[i:]...)
117 return c, nil
118}
119
120func (commit *Commit) serialize() ([]byte, error) {
121 var buf bytes.Buffer
122 fmt.Fprintf(&buf, "tree %s\n", commit.Tree.String())
123 for _, p := range commit.Parents {
124 fmt.Fprintf(&buf, "parent %s\n", p.String())
125 }
126 buf.WriteString("author ")
127 ab, err := commit.Author.Serialize()
128 if err != nil {
129 return nil, err
130 }
131 buf.Write(ab)
132 buf.WriteByte('\n')
133 buf.WriteString("committer ")
134 cb, err := commit.Committer.Serialize()
135 if err != nil {
136 return nil, err
137 }
138 buf.Write(cb)
139 buf.WriteByte('\n')
140 buf.WriteByte('\n')
141 buf.Write(commit.Message)
142
143 return buf.Bytes(), nil
144}
145
146// Serialize renders the commit into its raw byte representation,
147// including the header (i.e., "type size\0").
148func (commit *Commit) Serialize() ([]byte, error) {
149 body, err := commit.serialize()
150 if err != nil {
151 return nil, err
152 }
153 header, err := headerForType(ObjectTypeCommit, body)
154 if err != nil {
155 return nil, err
156 }
157 raw := make([]byte, len(header)+len(body))
158 copy(raw, header)
159 copy(raw[len(header):], body)
160 return raw, nil
161}