fork of go-git with some jj specific features
at main 17 kB view raw
1package git 2 3import ( 4 "bytes" 5 "errors" 6 "io" 7 "os" 8 "path" 9 "path/filepath" 10 "strings" 11 12 "github.com/go-git/go-billy/v5/util" 13 "github.com/go-git/go-git/v5/plumbing" 14 "github.com/go-git/go-git/v5/plumbing/filemode" 15 "github.com/go-git/go-git/v5/plumbing/format/gitignore" 16 "github.com/go-git/go-git/v5/plumbing/format/index" 17 "github.com/go-git/go-git/v5/plumbing/object" 18 "github.com/go-git/go-git/v5/utils/ioutil" 19 "github.com/go-git/go-git/v5/utils/merkletrie" 20 "github.com/go-git/go-git/v5/utils/merkletrie/filesystem" 21 mindex "github.com/go-git/go-git/v5/utils/merkletrie/index" 22 "github.com/go-git/go-git/v5/utils/merkletrie/noder" 23) 24 25var ( 26 // ErrDestinationExists in an Move operation means that the target exists on 27 // the worktree. 28 ErrDestinationExists = errors.New("destination exists") 29 // ErrGlobNoMatches in an AddGlob if the glob pattern does not match any 30 // files in the worktree. 31 ErrGlobNoMatches = errors.New("glob pattern did not match any files") 32 // ErrUnsupportedStatusStrategy occurs when an invalid StatusStrategy is used 33 // when processing the Worktree status. 34 ErrUnsupportedStatusStrategy = errors.New("unsupported status strategy") 35) 36 37// Status returns the working tree status. 38func (w *Worktree) Status() (Status, error) { 39 return w.StatusWithOptions(StatusOptions{Strategy: defaultStatusStrategy}) 40} 41 42// StatusOptions defines the options for Worktree.StatusWithOptions(). 43type StatusOptions struct { 44 Strategy StatusStrategy 45} 46 47// StatusWithOptions returns the working tree status. 48func (w *Worktree) StatusWithOptions(o StatusOptions) (Status, error) { 49 var hash plumbing.Hash 50 51 ref, err := w.r.Head() 52 if err != nil && err != plumbing.ErrReferenceNotFound { 53 return nil, err 54 } 55 56 if err == nil { 57 hash = ref.Hash() 58 } 59 60 return w.status(o.Strategy, hash) 61} 62 63func (w *Worktree) status(ss StatusStrategy, commit plumbing.Hash) (Status, error) { 64 s, err := ss.new(w) 65 if err != nil { 66 return nil, err 67 } 68 69 left, err := w.diffCommitWithStaging(commit, false) 70 if err != nil { 71 return nil, err 72 } 73 74 for _, ch := range left { 75 a, err := ch.Action() 76 if err != nil { 77 return nil, err 78 } 79 80 fs := s.File(nameFromAction(&ch)) 81 fs.Worktree = Unmodified 82 83 switch a { 84 case merkletrie.Delete: 85 s.File(ch.From.String()).Staging = Deleted 86 case merkletrie.Insert: 87 s.File(ch.To.String()).Staging = Added 88 case merkletrie.Modify: 89 s.File(ch.To.String()).Staging = Modified 90 } 91 } 92 93 right, err := w.diffStagingWithWorktree(false, true) 94 if err != nil { 95 return nil, err 96 } 97 98 for _, ch := range right { 99 a, err := ch.Action() 100 if err != nil { 101 return nil, err 102 } 103 104 fs := s.File(nameFromAction(&ch)) 105 if fs.Staging == Untracked { 106 fs.Staging = Unmodified 107 } 108 109 switch a { 110 case merkletrie.Delete: 111 fs.Worktree = Deleted 112 case merkletrie.Insert: 113 fs.Worktree = Untracked 114 fs.Staging = Untracked 115 case merkletrie.Modify: 116 fs.Worktree = Modified 117 } 118 } 119 120 return s, nil 121} 122 123func nameFromAction(ch *merkletrie.Change) string { 124 name := ch.To.String() 125 if name == "" { 126 return ch.From.String() 127 } 128 129 return name 130} 131 132func (w *Worktree) diffStagingWithWorktree(reverse, excludeIgnoredChanges bool) (merkletrie.Changes, error) { 133 idx, err := w.r.Storer.Index() 134 if err != nil { 135 return nil, err 136 } 137 138 from := mindex.NewRootNode(idx) 139 submodules, err := w.getSubmodulesStatus() 140 if err != nil { 141 return nil, err 142 } 143 144 to := filesystem.NewRootNode(w.Filesystem, submodules) 145 146 var c merkletrie.Changes 147 if reverse { 148 c, err = merkletrie.DiffTree(to, from, diffTreeIsEquals) 149 } else { 150 c, err = merkletrie.DiffTree(from, to, diffTreeIsEquals) 151 } 152 153 if err != nil { 154 return nil, err 155 } 156 157 if excludeIgnoredChanges { 158 return w.excludeIgnoredChanges(c), nil 159 } 160 return c, nil 161} 162 163func (w *Worktree) excludeIgnoredChanges(changes merkletrie.Changes) merkletrie.Changes { 164 patterns, err := gitignore.ReadPatterns(w.Filesystem, nil) 165 if err != nil { 166 return changes 167 } 168 169 patterns = append(patterns, w.Excludes...) 170 171 if len(patterns) == 0 { 172 return changes 173 } 174 175 m := gitignore.NewMatcher(patterns) 176 177 var res merkletrie.Changes 178 for _, ch := range changes { 179 var path []string 180 for _, n := range ch.To { 181 path = append(path, n.Name()) 182 } 183 if len(path) == 0 { 184 for _, n := range ch.From { 185 path = append(path, n.Name()) 186 } 187 } 188 if len(path) != 0 { 189 isDir := (len(ch.To) > 0 && ch.To.IsDir()) || (len(ch.From) > 0 && ch.From.IsDir()) 190 if m.Match(path, isDir) { 191 if len(ch.From) == 0 { 192 continue 193 } 194 } 195 } 196 res = append(res, ch) 197 } 198 return res 199} 200 201func (w *Worktree) getSubmodulesStatus() (map[string]plumbing.Hash, error) { 202 o := map[string]plumbing.Hash{} 203 204 sub, err := w.Submodules() 205 if err != nil { 206 return nil, err 207 } 208 209 status, err := sub.Status() 210 if err != nil { 211 return nil, err 212 } 213 214 for _, s := range status { 215 if s.Current.IsZero() { 216 o[s.Path] = s.Expected 217 continue 218 } 219 220 o[s.Path] = s.Current 221 } 222 223 return o, nil 224} 225 226func (w *Worktree) diffCommitWithStaging(commit plumbing.Hash, reverse bool) (merkletrie.Changes, error) { 227 var t *object.Tree 228 if !commit.IsZero() { 229 c, err := w.r.CommitObject(commit) 230 if err != nil { 231 return nil, err 232 } 233 234 t, err = c.Tree() 235 if err != nil { 236 return nil, err 237 } 238 } 239 240 return w.diffTreeWithStaging(t, reverse) 241} 242 243func (w *Worktree) diffTreeWithStaging(t *object.Tree, reverse bool) (merkletrie.Changes, error) { 244 var from noder.Noder 245 if t != nil { 246 from = object.NewTreeRootNode(t) 247 } 248 249 idx, err := w.r.Storer.Index() 250 if err != nil { 251 return nil, err 252 } 253 254 to := mindex.NewRootNode(idx) 255 256 if reverse { 257 return merkletrie.DiffTree(to, from, diffTreeIsEquals) 258 } 259 260 return merkletrie.DiffTree(from, to, diffTreeIsEquals) 261} 262 263var emptyNoderHash = make([]byte, 24) 264 265// diffTreeIsEquals is a implementation of noder.Equals, used to compare 266// noder.Noder, it compare the content and the length of the hashes. 267// 268// Since some of the noder.Noder implementations doesn't compute a hash for 269// some directories, if any of the hashes is a 24-byte slice of zero values 270// the comparison is not done and the hashes are take as different. 271func diffTreeIsEquals(a, b noder.Hasher) bool { 272 hashA := a.Hash() 273 hashB := b.Hash() 274 275 if bytes.Equal(hashA, emptyNoderHash) || bytes.Equal(hashB, emptyNoderHash) { 276 return false 277 } 278 279 return bytes.Equal(hashA, hashB) 280} 281 282// Add adds the file contents of a file in the worktree to the index. if the 283// file is already staged in the index no error is returned. If a file deleted 284// from the Workspace is given, the file is removed from the index. If a 285// directory given, adds the files and all his sub-directories recursively in 286// the worktree to the index. If any of the files is already staged in the index 287// no error is returned. When path is a file, the blob.Hash is returned. 288func (w *Worktree) Add(path string) (plumbing.Hash, error) { 289 // TODO(mcuadros): deprecate in favor of AddWithOption in v6. 290 return w.doAdd(path, make([]gitignore.Pattern, 0), false) 291} 292 293func (w *Worktree) doAddDirectory(idx *index.Index, s Status, directory string, ignorePattern []gitignore.Pattern) (added bool, err error) { 294 if len(ignorePattern) > 0 { 295 m := gitignore.NewMatcher(ignorePattern) 296 matchPath := strings.Split(directory, string(os.PathSeparator)) 297 if m.Match(matchPath, true) { 298 // ignore 299 return false, nil 300 } 301 } 302 303 directory = filepath.ToSlash(filepath.Clean(directory)) 304 305 for name := range s { 306 if !isPathInDirectory(name, directory) { 307 continue 308 } 309 310 var a bool 311 a, _, err = w.doAddFile(idx, s, name, ignorePattern) 312 if err != nil { 313 return 314 } 315 316 added = added || a 317 } 318 319 return 320} 321 322func isPathInDirectory(path, directory string) bool { 323 return directory == "." || strings.HasPrefix(path, directory+"/") 324} 325 326// AddWithOptions file contents to the index, updates the index using the 327// current content found in the working tree, to prepare the content staged for 328// the next commit. 329// 330// It typically adds the current content of existing paths as a whole, but with 331// some options it can also be used to add content with only part of the changes 332// made to the working tree files applied, or remove paths that do not exist in 333// the working tree anymore. 334func (w *Worktree) AddWithOptions(opts *AddOptions) error { 335 if err := opts.Validate(w.r); err != nil { 336 return err 337 } 338 339 if opts.All { 340 _, err := w.doAdd(".", w.Excludes, false) 341 return err 342 } 343 344 if opts.Glob != "" { 345 return w.AddGlob(opts.Glob) 346 } 347 348 _, err := w.doAdd(opts.Path, make([]gitignore.Pattern, 0), opts.SkipStatus) 349 return err 350} 351 352func (w *Worktree) doAdd(path string, ignorePattern []gitignore.Pattern, skipStatus bool) (plumbing.Hash, error) { 353 idx, err := w.r.Storer.Index() 354 if err != nil { 355 return plumbing.ZeroHash, err 356 } 357 358 var h plumbing.Hash 359 var added bool 360 361 fi, err := w.Filesystem.Lstat(path) 362 363 // status is required for doAddDirectory 364 var s Status 365 var err2 error 366 if !skipStatus || fi == nil || fi.IsDir() { 367 s, err2 = w.Status() 368 if err2 != nil { 369 return plumbing.ZeroHash, err2 370 } 371 } 372 373 path = filepath.Clean(path) 374 375 if err != nil || !fi.IsDir() { 376 added, h, err = w.doAddFile(idx, s, path, ignorePattern) 377 } else { 378 added, err = w.doAddDirectory(idx, s, path, ignorePattern) 379 } 380 381 if err != nil { 382 return h, err 383 } 384 385 if !added { 386 return h, nil 387 } 388 389 return h, w.r.Storer.SetIndex(idx) 390} 391 392// AddGlob adds all paths, matching pattern, to the index. If pattern matches a 393// directory path, all directory contents are added to the index recursively. No 394// error is returned if all matching paths are already staged in index. 395func (w *Worktree) AddGlob(pattern string) error { 396 // TODO(mcuadros): deprecate in favor of AddWithOption in v6. 397 files, err := util.Glob(w.Filesystem, pattern) 398 if err != nil { 399 return err 400 } 401 402 if len(files) == 0 { 403 return ErrGlobNoMatches 404 } 405 406 s, err := w.Status() 407 if err != nil { 408 return err 409 } 410 411 idx, err := w.r.Storer.Index() 412 if err != nil { 413 return err 414 } 415 416 var saveIndex bool 417 for _, file := range files { 418 fi, err := w.Filesystem.Lstat(file) 419 if err != nil { 420 return err 421 } 422 423 var added bool 424 if fi.IsDir() { 425 added, err = w.doAddDirectory(idx, s, file, make([]gitignore.Pattern, 0)) 426 } else { 427 added, _, err = w.doAddFile(idx, s, file, make([]gitignore.Pattern, 0)) 428 } 429 430 if err != nil { 431 return err 432 } 433 434 if !saveIndex && added { 435 saveIndex = true 436 } 437 } 438 439 if saveIndex { 440 return w.r.Storer.SetIndex(idx) 441 } 442 443 return nil 444} 445 446// doAddFile create a new blob from path and update the index, added is true if 447// the file added is different from the index. 448// if s status is nil will skip the status check and update the index anyway 449func (w *Worktree) doAddFile(idx *index.Index, s Status, path string, ignorePattern []gitignore.Pattern) (added bool, h plumbing.Hash, err error) { 450 if s != nil && s.File(path).Worktree == Unmodified { 451 return false, h, nil 452 } 453 if len(ignorePattern) > 0 { 454 m := gitignore.NewMatcher(ignorePattern) 455 matchPath := strings.Split(path, string(os.PathSeparator)) 456 if m.Match(matchPath, true) { 457 // ignore 458 return false, h, nil 459 } 460 } 461 462 h, err = w.copyFileToStorage(path) 463 if err != nil { 464 if os.IsNotExist(err) { 465 added = true 466 h, err = w.deleteFromIndex(idx, path) 467 } 468 469 return 470 } 471 472 if err := w.addOrUpdateFileToIndex(idx, path, h); err != nil { 473 return false, h, err 474 } 475 476 return true, h, err 477} 478 479func (w *Worktree) copyFileToStorage(path string) (hash plumbing.Hash, err error) { 480 fi, err := w.Filesystem.Lstat(path) 481 if err != nil { 482 return plumbing.ZeroHash, err 483 } 484 485 obj := w.r.Storer.NewEncodedObject() 486 obj.SetType(plumbing.BlobObject) 487 obj.SetSize(fi.Size()) 488 489 writer, err := obj.Writer() 490 if err != nil { 491 return plumbing.ZeroHash, err 492 } 493 494 defer ioutil.CheckClose(writer, &err) 495 496 if fi.Mode()&os.ModeSymlink != 0 { 497 err = w.fillEncodedObjectFromSymlink(writer, path, fi) 498 } else { 499 err = w.fillEncodedObjectFromFile(writer, path, fi) 500 } 501 502 if err != nil { 503 return plumbing.ZeroHash, err 504 } 505 506 return w.r.Storer.SetEncodedObject(obj) 507} 508 509func (w *Worktree) fillEncodedObjectFromFile(dst io.Writer, path string, _ os.FileInfo) (err error) { 510 src, err := w.Filesystem.Open(path) 511 if err != nil { 512 return err 513 } 514 515 defer ioutil.CheckClose(src, &err) 516 517 if _, err := io.Copy(dst, src); err != nil { 518 return err 519 } 520 521 return err 522} 523 524func (w *Worktree) fillEncodedObjectFromSymlink(dst io.Writer, path string, _ os.FileInfo) error { 525 target, err := w.Filesystem.Readlink(path) 526 if err != nil { 527 return err 528 } 529 530 _, err = dst.Write([]byte(target)) 531 return err 532} 533 534func (w *Worktree) addOrUpdateFileToIndex(idx *index.Index, filename string, h plumbing.Hash) error { 535 e, err := idx.Entry(filename) 536 if err != nil && err != index.ErrEntryNotFound { 537 return err 538 } 539 540 if err == index.ErrEntryNotFound { 541 return w.doAddFileToIndex(idx, filename, h) 542 } 543 544 return w.doUpdateFileToIndex(e, filename, h) 545} 546 547func (w *Worktree) doAddFileToIndex(idx *index.Index, filename string, h plumbing.Hash) error { 548 return w.doUpdateFileToIndex(idx.Add(filename), filename, h) 549} 550 551func (w *Worktree) doUpdateFileToIndex(e *index.Entry, filename string, h plumbing.Hash) error { 552 info, err := w.Filesystem.Lstat(filename) 553 if err != nil { 554 return err 555 } 556 557 e.Hash = h 558 e.ModifiedAt = info.ModTime() 559 e.Mode, err = filemode.NewFromOSFileMode(info.Mode()) 560 if err != nil { 561 return err 562 } 563 564 // The entry size must always reflect the current state, otherwise 565 // it will cause go-git's Worktree.Status() to divert from "git status". 566 // The size of a symlink is the length of the path to the target. 567 // The size of Regular and Executable files is the size of the files. 568 e.Size = uint32(info.Size()) 569 570 fillSystemInfo(e, info.Sys()) 571 return nil 572} 573 574// Remove removes files from the working tree and from the index. 575func (w *Worktree) Remove(path string) (plumbing.Hash, error) { 576 // TODO(mcuadros): remove plumbing.Hash from signature at v5. 577 idx, err := w.r.Storer.Index() 578 if err != nil { 579 return plumbing.ZeroHash, err 580 } 581 582 var h plumbing.Hash 583 584 fi, err := w.Filesystem.Lstat(path) 585 if err != nil || !fi.IsDir() { 586 h, err = w.doRemoveFile(idx, path) 587 } else { 588 _, err = w.doRemoveDirectory(idx, path) 589 } 590 if err != nil { 591 return h, err 592 } 593 594 return h, w.r.Storer.SetIndex(idx) 595} 596 597func (w *Worktree) doRemoveDirectory(idx *index.Index, directory string) (removed bool, err error) { 598 files, err := w.Filesystem.ReadDir(directory) 599 if err != nil { 600 return false, err 601 } 602 603 for _, file := range files { 604 name := path.Join(directory, file.Name()) 605 606 var r bool 607 if file.IsDir() { 608 r, err = w.doRemoveDirectory(idx, name) 609 } else { 610 _, err = w.doRemoveFile(idx, name) 611 if err == index.ErrEntryNotFound { 612 err = nil 613 } 614 } 615 616 if err != nil { 617 return 618 } 619 620 if !removed && r { 621 removed = true 622 } 623 } 624 625 err = w.removeEmptyDirectory(directory) 626 return 627} 628 629func (w *Worktree) removeEmptyDirectory(path string) error { 630 files, err := w.Filesystem.ReadDir(path) 631 if err != nil { 632 return err 633 } 634 635 if len(files) != 0 { 636 return nil 637 } 638 639 return w.Filesystem.Remove(path) 640} 641 642func (w *Worktree) doRemoveFile(idx *index.Index, path string) (plumbing.Hash, error) { 643 hash, err := w.deleteFromIndex(idx, path) 644 if err != nil { 645 return plumbing.ZeroHash, err 646 } 647 648 return hash, w.deleteFromFilesystem(path) 649} 650 651func (w *Worktree) deleteFromIndex(idx *index.Index, path string) (plumbing.Hash, error) { 652 e, err := idx.Remove(path) 653 if err != nil { 654 return plumbing.ZeroHash, err 655 } 656 657 return e.Hash, nil 658} 659 660func (w *Worktree) deleteFromFilesystem(path string) error { 661 err := w.Filesystem.Remove(path) 662 if os.IsNotExist(err) { 663 return nil 664 } 665 666 return err 667} 668 669// RemoveGlob removes all paths, matching pattern, from the index. If pattern 670// matches a directory path, all directory contents are removed from the index 671// recursively. 672func (w *Worktree) RemoveGlob(pattern string) error { 673 idx, err := w.r.Storer.Index() 674 if err != nil { 675 return err 676 } 677 678 entries, err := idx.Glob(pattern) 679 if err != nil { 680 return err 681 } 682 683 for _, e := range entries { 684 file := filepath.FromSlash(e.Name) 685 if _, err := w.Filesystem.Lstat(file); err != nil && !os.IsNotExist(err) { 686 return err 687 } 688 689 if _, err := w.doRemoveFile(idx, file); err != nil { 690 return err 691 } 692 693 dir, _ := filepath.Split(file) 694 if err := w.removeEmptyDirectory(dir); err != nil { 695 return err 696 } 697 } 698 699 return w.r.Storer.SetIndex(idx) 700} 701 702// Move moves or rename a file in the worktree and the index, directories are 703// not supported. 704func (w *Worktree) Move(from, to string) (plumbing.Hash, error) { 705 // TODO(mcuadros): support directories and/or implement support for glob 706 if _, err := w.Filesystem.Lstat(from); err != nil { 707 return plumbing.ZeroHash, err 708 } 709 710 if _, err := w.Filesystem.Lstat(to); err == nil { 711 return plumbing.ZeroHash, ErrDestinationExists 712 } 713 714 idx, err := w.r.Storer.Index() 715 if err != nil { 716 return plumbing.ZeroHash, err 717 } 718 719 hash, err := w.deleteFromIndex(idx, from) 720 if err != nil { 721 return plumbing.ZeroHash, err 722 } 723 724 if err := w.Filesystem.Rename(from, to); err != nil { 725 return hash, err 726 } 727 728 if err := w.addOrUpdateFileToIndex(idx, to, hash); err != nil { 729 return hash, err 730 } 731 732 return hash, w.r.Storer.SetIndex(idx) 733}