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