1package object 2 3import ( 4 "bufio" 5 "bytes" 6 "context" 7 "errors" 8 "fmt" 9 "io" 10 "strings" 11 12 "github.com/ProtonMail/go-crypto/openpgp" 13 14 "github.com/go-git/go-git/v5/plumbing" 15 "github.com/go-git/go-git/v5/plumbing/storer" 16 "github.com/go-git/go-git/v5/utils/ioutil" 17) 18 19const ( 20 beginpgp string = "-----BEGIN PGP SIGNATURE-----" 21 endpgp string = "-----END PGP SIGNATURE-----" 22 headerpgp string = "gpgsig" 23) 24 25// Hash represents the hash of an object 26type Hash plumbing.Hash 27 28// Commit points to a single tree, marking it as what the project looked like 29// at a certain point in time. It contains meta-information about that point 30// in time, such as a timestamp, the author of the changes since the last 31// commit, a pointer to the previous commit(s), etc. 32// http://shafiulazam.com/gitbook/1_the_git_object_model.html 33type Commit struct { 34 // Hash of the commit object. 35 Hash plumbing.Hash 36 // Author is the original author of the commit. 37 Author Signature 38 // Committer is the one performing the commit, might be different from 39 // Author. 40 Committer Signature 41 // PGPSignature is the PGP signature of the commit. 42 PGPSignature string 43 // Message is the commit message, contains arbitrary text. 44 Message string 45 // TreeHash is the hash of the root tree of the commit. 46 TreeHash plumbing.Hash 47 // ParentHashes are the hashes of the parent commits of the commit. 48 ParentHashes []plumbing.Hash 49 50 s storer.EncodedObjectStorer 51} 52 53// GetCommit gets a commit from an object storer and decodes it. 54func GetCommit(s storer.EncodedObjectStorer, h plumbing.Hash) (*Commit, error) { 55 o, err := s.EncodedObject(plumbing.CommitObject, h) 56 if err != nil { 57 return nil, err 58 } 59 60 return DecodeCommit(s, o) 61} 62 63// DecodeCommit decodes an encoded object into a *Commit and associates it to 64// the given object storer. 65func DecodeCommit(s storer.EncodedObjectStorer, o plumbing.EncodedObject) (*Commit, error) { 66 c := &Commit{s: s} 67 if err := c.Decode(o); err != nil { 68 return nil, err 69 } 70 71 return c, nil 72} 73 74// Tree returns the Tree from the commit. 75func (c *Commit) Tree() (*Tree, error) { 76 return GetTree(c.s, c.TreeHash) 77} 78 79// PatchContext returns the Patch between the actual commit and the provided one. 80// Error will be return if context expires. Provided context must be non-nil. 81// 82// NOTE: Since version 5.1.0 the renames are correctly handled, the settings 83// used are the recommended options DefaultDiffTreeOptions. 84func (c *Commit) PatchContext(ctx context.Context, to *Commit) (*Patch, error) { 85 fromTree, err := c.Tree() 86 if err != nil { 87 return nil, err 88 } 89 90 var toTree *Tree 91 if to != nil { 92 toTree, err = to.Tree() 93 if err != nil { 94 return nil, err 95 } 96 } 97 98 return fromTree.PatchContext(ctx, toTree) 99} 100 101// Patch returns the Patch between the actual commit and the provided one. 102// 103// NOTE: Since version 5.1.0 the renames are correctly handled, the settings 104// used are the recommended options DefaultDiffTreeOptions. 105func (c *Commit) Patch(to *Commit) (*Patch, error) { 106 return c.PatchContext(context.Background(), to) 107} 108 109// Parents return a CommitIter to the parent Commits. 110func (c *Commit) Parents() CommitIter { 111 return NewCommitIter(c.s, 112 storer.NewEncodedObjectLookupIter(c.s, plumbing.CommitObject, c.ParentHashes), 113 ) 114} 115 116// NumParents returns the number of parents in a commit. 117func (c *Commit) NumParents() int { 118 return len(c.ParentHashes) 119} 120 121var ErrParentNotFound = errors.New("commit parent not found") 122 123// Parent returns the ith parent of a commit. 124func (c *Commit) Parent(i int) (*Commit, error) { 125 if len(c.ParentHashes) == 0 || i > len(c.ParentHashes)-1 { 126 return nil, ErrParentNotFound 127 } 128 129 return GetCommit(c.s, c.ParentHashes[i]) 130} 131 132// File returns the file with the specified "path" in the commit and a 133// nil error if the file exists. If the file does not exist, it returns 134// a nil file and the ErrFileNotFound error. 135func (c *Commit) File(path string) (*File, error) { 136 tree, err := c.Tree() 137 if err != nil { 138 return nil, err 139 } 140 141 return tree.File(path) 142} 143 144// Files returns a FileIter allowing to iterate over the Tree 145func (c *Commit) Files() (*FileIter, error) { 146 tree, err := c.Tree() 147 if err != nil { 148 return nil, err 149 } 150 151 return tree.Files(), nil 152} 153 154// ID returns the object ID of the commit. The returned value will always match 155// the current value of Commit.Hash. 156// 157// ID is present to fulfill the Object interface. 158func (c *Commit) ID() plumbing.Hash { 159 return c.Hash 160} 161 162// Type returns the type of object. It always returns plumbing.CommitObject. 163// 164// Type is present to fulfill the Object interface. 165func (c *Commit) Type() plumbing.ObjectType { 166 return plumbing.CommitObject 167} 168 169// Decode transforms a plumbing.EncodedObject into a Commit struct. 170func (c *Commit) Decode(o plumbing.EncodedObject) (err error) { 171 if o.Type() != plumbing.CommitObject { 172 return ErrUnsupportedObject 173 } 174 175 c.Hash = o.Hash() 176 177 reader, err := o.Reader() 178 if err != nil { 179 return err 180 } 181 defer ioutil.CheckClose(reader, &err) 182 183 r := bufPool.Get().(*bufio.Reader) 184 defer bufPool.Put(r) 185 r.Reset(reader) 186 187 var message bool 188 var pgpsig bool 189 var msgbuf bytes.Buffer 190 for { 191 line, err := r.ReadBytes('\n') 192 if err != nil && err != io.EOF { 193 return err 194 } 195 196 if pgpsig { 197 if len(line) > 0 && line[0] == ' ' { 198 line = bytes.TrimLeft(line, " ") 199 c.PGPSignature += string(line) 200 continue 201 } else { 202 pgpsig = false 203 } 204 } 205 206 if !message { 207 line = bytes.TrimSpace(line) 208 if len(line) == 0 { 209 message = true 210 continue 211 } 212 213 split := bytes.SplitN(line, []byte{' '}, 2) 214 215 var data []byte 216 if len(split) == 2 { 217 data = split[1] 218 } 219 220 switch string(split[0]) { 221 case "tree": 222 c.TreeHash = plumbing.NewHash(string(data)) 223 case "parent": 224 c.ParentHashes = append(c.ParentHashes, plumbing.NewHash(string(data))) 225 case "author": 226 c.Author.Decode(data) 227 case "committer": 228 c.Committer.Decode(data) 229 case headerpgp: 230 c.PGPSignature += string(data) + "\n" 231 pgpsig = true 232 } 233 } else { 234 msgbuf.Write(line) 235 } 236 237 if err == io.EOF { 238 break 239 } 240 } 241 c.Message = msgbuf.String() 242 return nil 243} 244 245// Encode transforms a Commit into a plumbing.EncodedObject. 246func (c *Commit) Encode(o plumbing.EncodedObject) error { 247 return c.encode(o, true) 248} 249 250// EncodeWithoutSignature export a Commit into a plumbing.EncodedObject without the signature (correspond to the payload of the PGP signature). 251func (c *Commit) EncodeWithoutSignature(o plumbing.EncodedObject) error { 252 return c.encode(o, false) 253} 254 255func (c *Commit) encode(o plumbing.EncodedObject, includeSig bool) (err error) { 256 o.SetType(plumbing.CommitObject) 257 w, err := o.Writer() 258 if err != nil { 259 return err 260 } 261 262 defer ioutil.CheckClose(w, &err) 263 264 if _, err = fmt.Fprintf(w, "tree %s\n", c.TreeHash.String()); err != nil { 265 return err 266 } 267 268 for _, parent := range c.ParentHashes { 269 if _, err = fmt.Fprintf(w, "parent %s\n", parent.String()); err != nil { 270 return err 271 } 272 } 273 274 if _, err = fmt.Fprint(w, "author "); err != nil { 275 return err 276 } 277 278 if err = c.Author.Encode(w); err != nil { 279 return err 280 } 281 282 if _, err = fmt.Fprint(w, "\ncommitter "); err != nil { 283 return err 284 } 285 286 if err = c.Committer.Encode(w); err != nil { 287 return err 288 } 289 290 if c.PGPSignature != "" && includeSig { 291 if _, err = fmt.Fprint(w, "\n"+headerpgp+" "); err != nil { 292 return err 293 } 294 295 // Split all the signature lines and re-write with a left padding and 296 // newline. Use join for this so it's clear that a newline should not be 297 // added after this section, as it will be added when the message is 298 // printed. 299 signature := strings.TrimSuffix(c.PGPSignature, "\n") 300 lines := strings.Split(signature, "\n") 301 if _, err = fmt.Fprint(w, strings.Join(lines, "\n ")); err != nil { 302 return err 303 } 304 } 305 306 if _, err = fmt.Fprintf(w, "\n\n%s", c.Message); err != nil { 307 return err 308 } 309 310 return err 311} 312 313// Stats returns the stats of a commit. 314func (c *Commit) Stats() (FileStats, error) { 315 return c.StatsContext(context.Background()) 316} 317 318// StatsContext returns the stats of a commit. Error will be return if context 319// expires. Provided context must be non-nil. 320func (c *Commit) StatsContext(ctx context.Context) (FileStats, error) { 321 fromTree, err := c.Tree() 322 if err != nil { 323 return nil, err 324 } 325 326 toTree := &Tree{} 327 if c.NumParents() != 0 { 328 firstParent, err := c.Parents().Next() 329 if err != nil { 330 return nil, err 331 } 332 333 toTree, err = firstParent.Tree() 334 if err != nil { 335 return nil, err 336 } 337 } 338 339 patch, err := toTree.PatchContext(ctx, fromTree) 340 if err != nil { 341 return nil, err 342 } 343 344 return getFileStatsFromFilePatches(patch.FilePatches()), nil 345} 346 347func (c *Commit) String() string { 348 return fmt.Sprintf( 349 "%s %s\nAuthor: %s\nDate: %s\n\n%s\n", 350 plumbing.CommitObject, c.Hash, c.Author.String(), 351 c.Author.When.Format(DateFormat), indent(c.Message), 352 ) 353} 354 355// Verify performs PGP verification of the commit with a provided armored 356// keyring and returns openpgp.Entity associated with verifying key on success. 357func (c *Commit) Verify(armoredKeyRing string) (*openpgp.Entity, error) { 358 keyRingReader := strings.NewReader(armoredKeyRing) 359 keyring, err := openpgp.ReadArmoredKeyRing(keyRingReader) 360 if err != nil { 361 return nil, err 362 } 363 364 // Extract signature. 365 signature := strings.NewReader(c.PGPSignature) 366 367 encoded := &plumbing.MemoryObject{} 368 // Encode commit components, excluding signature and get a reader object. 369 if err := c.EncodeWithoutSignature(encoded); err != nil { 370 return nil, err 371 } 372 er, err := encoded.Reader() 373 if err != nil { 374 return nil, err 375 } 376 377 return openpgp.CheckArmoredDetachedSignature(keyring, er, signature, nil) 378} 379 380func indent(t string) string { 381 var output []string 382 for _, line := range strings.Split(t, "\n") { 383 if len(line) != 0 { 384 line = " " + line 385 } 386 387 output = append(output, line) 388 } 389 390 return strings.Join(output, "\n") 391} 392 393// CommitIter is a generic closable interface for iterating over commits. 394type CommitIter interface { 395 Next() (*Commit, error) 396 ForEach(func(*Commit) error) error 397 Close() 398} 399 400// storerCommitIter provides an iterator from commits in an EncodedObjectStorer. 401type storerCommitIter struct { 402 storer.EncodedObjectIter 403 s storer.EncodedObjectStorer 404} 405 406// NewCommitIter takes a storer.EncodedObjectStorer and a 407// storer.EncodedObjectIter and returns a CommitIter that iterates over all 408// commits contained in the storer.EncodedObjectIter. 409// 410// Any non-commit object returned by the storer.EncodedObjectIter is skipped. 411func NewCommitIter(s storer.EncodedObjectStorer, iter storer.EncodedObjectIter) CommitIter { 412 return &storerCommitIter{iter, s} 413} 414 415// Next moves the iterator to the next commit and returns a pointer to it. If 416// there are no more commits, it returns io.EOF. 417func (iter *storerCommitIter) Next() (*Commit, error) { 418 obj, err := iter.EncodedObjectIter.Next() 419 if err != nil { 420 return nil, err 421 } 422 423 return DecodeCommit(iter.s, obj) 424} 425 426// ForEach call the cb function for each commit contained on this iter until 427// an error appends or the end of the iter is reached. If ErrStop is sent 428// the iteration is stopped but no error is returned. The iterator is closed. 429func (iter *storerCommitIter) ForEach(cb func(*Commit) error) error { 430 return iter.EncodedObjectIter.ForEach(func(obj plumbing.EncodedObject) error { 431 c, err := DecodeCommit(iter.s, obj) 432 if err != nil { 433 return err 434 } 435 436 return cb(c) 437 }) 438} 439 440func (iter *storerCommitIter) Close() { 441 iter.EncodedObjectIter.Close() 442} 443