at master 199 lines 4.9 kB view raw
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}