1package types
2
3import (
4 "bytes"
5 "encoding/json"
6 "fmt"
7 "maps"
8 "regexp"
9 "strings"
10
11 "github.com/go-git/go-git/v5/plumbing"
12 "github.com/go-git/go-git/v5/plumbing/object"
13)
14
15type Commit struct {
16 // hash of the commit object.
17 Hash plumbing.Hash `json:"hash,omitempty"`
18
19 // author is the original author of the commit.
20 Author object.Signature `json:"author"`
21
22 // committer is the one performing the commit, might be different from author.
23 Committer object.Signature `json:"committer"`
24
25 // message is the commit message, contains arbitrary text.
26 Message string `json:"message"`
27
28 // treehash is the hash of the root tree of the commit.
29 Tree string `json:"tree"`
30
31 // parents are the hashes of the parent commits of the commit.
32 ParentHashes []plumbing.Hash `json:"parent_hashes,omitempty"`
33
34 // pgpsignature is the pgp signature of the commit.
35 PGPSignature string `json:"pgp_signature,omitempty"`
36
37 // mergetag is the embedded tag object when a merge commit is created by
38 // merging a signed tag.
39 MergeTag string `json:"merge_tag,omitempty"`
40
41 // changeid is a unique identifier for the change (e.g., gerrit change-id).
42 ChangeId string `json:"change_id,omitempty"`
43
44 // extraheaders contains additional headers not captured by other fields.
45 ExtraHeaders map[string][]byte `json:"extra_headers,omitempty"`
46
47 // deprecated: kept for backwards compatibility with old json format.
48 This string `json:"this,omitempty"`
49
50 // deprecated: kept for backwards compatibility with old json format.
51 Parent string `json:"parent,omitempty"`
52}
53
54// types.Commit is an unify two commit structs:
55// - git.object.Commit from
56// - types.NiceDiff.commit
57//
58// to do this in backwards compatible fashion, we define the base struct
59// to use the same fields as NiceDiff.Commit, and then we also unmarshal
60// the struct fields from go-git structs, this custom unmarshal makes sense
61// of both representations and unifies them to have maximal data in either
62// form.
63func (c *Commit) UnmarshalJSON(data []byte) error {
64 type Alias Commit
65
66 aux := &struct {
67 *object.Commit
68 *Alias
69 }{
70 Alias: (*Alias)(c),
71 }
72
73 if err := json.Unmarshal(data, aux); err != nil {
74 return err
75 }
76
77 c.FromGoGitCommit(aux.Commit)
78
79 return nil
80}
81
82// fill in as much of Commit as possible from the given go-git commit
83func (c *Commit) FromGoGitCommit(gc *object.Commit) {
84 if gc == nil {
85 return
86 }
87
88 if c.Hash.IsZero() {
89 c.Hash = gc.Hash
90 }
91 if c.This == "" {
92 c.This = gc.Hash.String()
93 }
94 if isEmptySignature(c.Author) {
95 c.Author = gc.Author
96 }
97 if isEmptySignature(c.Committer) {
98 c.Committer = gc.Committer
99 }
100 if c.Message == "" {
101 c.Message = gc.Message
102 }
103 if c.Tree == "" {
104 c.Tree = gc.TreeHash.String()
105 }
106 if c.PGPSignature == "" {
107 c.PGPSignature = gc.PGPSignature
108 }
109 if c.MergeTag == "" {
110 c.MergeTag = gc.MergeTag
111 }
112
113 if len(c.ParentHashes) == 0 {
114 c.ParentHashes = gc.ParentHashes
115 }
116 if c.Parent == "" && len(gc.ParentHashes) > 0 {
117 c.Parent = gc.ParentHashes[0].String()
118 }
119
120 if len(c.ExtraHeaders) == 0 {
121 c.ExtraHeaders = make(map[string][]byte)
122 maps.Copy(c.ExtraHeaders, gc.ExtraHeaders)
123 }
124
125 if c.ChangeId == "" {
126 if v, ok := gc.ExtraHeaders["change-id"]; ok {
127 c.ChangeId = string(v)
128 }
129 }
130}
131
132func isEmptySignature(s object.Signature) bool {
133 return s.Email == "" && s.Name == "" && s.When.IsZero()
134}
135
136// produce a verifiable payload from this commit's metadata
137func (c *Commit) Payload() string {
138 author := bytes.NewBuffer([]byte{})
139 c.Author.Encode(author)
140
141 committer := bytes.NewBuffer([]byte{})
142 c.Committer.Encode(committer)
143
144 payload := strings.Builder{}
145
146 fmt.Fprintf(&payload, "tree %s\n", c.Tree)
147
148 if len(c.ParentHashes) > 0 {
149 for _, p := range c.ParentHashes {
150 fmt.Fprintf(&payload, "parent %s\n", p.String())
151 }
152 } else {
153 // present for backwards compatibility
154 fmt.Fprintf(&payload, "parent %s\n", c.Parent)
155 }
156
157 fmt.Fprintf(&payload, "author %s\n", author.String())
158 fmt.Fprintf(&payload, "committer %s\n", committer.String())
159
160 if c.ChangeId != "" {
161 fmt.Fprintf(&payload, "change-id %s\n", c.ChangeId)
162 } else if v, ok := c.ExtraHeaders["change-id"]; ok {
163 fmt.Fprintf(&payload, "change-id %s\n", string(v))
164 }
165
166 fmt.Fprintf(&payload, "\n%s", c.Message)
167
168 return payload.String()
169}
170
171var (
172 coAuthorRegex = regexp.MustCompile(`(?im)^Co-authored-by:\s*(.+?)\s*<([^>]+)>`)
173)
174
175func (commit Commit) CoAuthors() []object.Signature {
176 var coAuthors []object.Signature
177 seen := make(map[string]bool)
178 matches := coAuthorRegex.FindAllStringSubmatch(commit.Message, -1)
179
180 for _, match := range matches {
181 if len(match) >= 3 {
182 name := strings.TrimSpace(match[1])
183 email := strings.TrimSpace(match[2])
184
185 if seen[email] {
186 continue
187 }
188 seen[email] = true
189
190 coAuthors = append(coAuthors, object.Signature{
191 Name: name,
192 Email: email,
193 When: commit.Committer.When,
194 })
195 }
196 }
197
198 return coAuthors
199}