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