github.com/kilpkonn/gtm-enhanced@v1.3.5/scm/git.go (about) 1 // Copyright 2016 Michael Schenk. All rights reserved. 2 // Use of this source code is governed by a MIT-style 3 // license that can be found in the LICENSE file. 4 5 package scm 6 7 import ( 8 "errors" 9 "fmt" 10 "io/ioutil" 11 "os" 12 "os/exec" 13 "path/filepath" 14 "regexp" 15 "runtime" 16 "strings" 17 "time" 18 19 "github.com/git-time-metric/gtm/util" 20 "github.com/libgit2/git2go" 21 ) 22 23 // Workdir returns the working directory for a repo 24 func Workdir(gitRepoPath string) (string, error) { 25 defer util.Profile()() 26 util.Debug.Print("gitRepoPath:", gitRepoPath) 27 28 repo, err := git.OpenRepository(gitRepoPath) 29 if err != nil { 30 return "", err 31 } 32 defer repo.Free() 33 34 workDir := filepath.Clean(repo.Workdir()) 35 util.Debug.Print("workDir:", workDir) 36 37 return workDir, nil 38 } 39 40 // GitRepoPath discovers .git directory for a repo 41 func GitRepoPath(path ...string) (string, error) { 42 defer util.Profile()() 43 var ( 44 wd string 45 gitRepoPath string 46 err error 47 ) 48 if len(path) > 0 { 49 wd = path[0] 50 } else { 51 wd, err = os.Getwd() 52 if err != nil { 53 return "", err 54 } 55 } 56 //TODO: benchmark the call to git.Discover 57 //TODO: optionally print result with -debug flag 58 gitRepoPath, err = git.Discover(wd, false, []string{}) 59 if err != nil { 60 return "", err 61 } 62 63 return filepath.Clean(gitRepoPath), nil 64 } 65 66 // CommitLimiter struct filter commits by criteria 67 type CommitLimiter struct { 68 Max int 69 DateRange util.DateRange 70 Before time.Time 71 After time.Time 72 Author string 73 Message string 74 HasMax bool 75 HasBefore bool 76 HasAfter bool 77 HasAuthor bool 78 HasMessage bool 79 } 80 81 // NewCommitLimiter returns a new initialize CommitLimiter struct 82 func NewCommitLimiter( 83 max int, fromDateStr, toDateStr, author, message string, 84 today, yesterday, thisWeek, lastWeek, 85 thisMonth, lastMonth, thisYear, lastYear bool) (CommitLimiter, error) { 86 87 const dateFormat = "2006-01-02" 88 89 fromDateStr = strings.TrimSpace(fromDateStr) 90 toDateStr = strings.TrimSpace(toDateStr) 91 author = strings.TrimSpace(author) 92 message = strings.TrimSpace(message) 93 94 cnt := func(vals []bool) int { 95 var c int 96 for _, v := range vals { 97 if v { 98 c++ 99 } 100 } 101 return c 102 }([]bool{ 103 fromDateStr != "" || toDateStr != "", 104 today, yesterday, thisWeek, lastWeek, thisMonth, lastMonth, thisYear, lastYear}) 105 106 if cnt > 1 { 107 return CommitLimiter{}, fmt.Errorf("Using multiple temporal flags is not allowed") 108 } 109 110 var ( 111 err error 112 dateRange util.DateRange 113 ) 114 115 switch { 116 case fromDateStr != "" || toDateStr != "": 117 fromDate := time.Time{} 118 toDate := time.Time{} 119 120 if fromDateStr != "" { 121 fromDate, err = time.Parse(dateFormat, fromDateStr) 122 if err != nil { 123 return CommitLimiter{}, err 124 } 125 } 126 if toDateStr != "" { 127 toDate, err = time.Parse(dateFormat, toDateStr) 128 if err != nil { 129 return CommitLimiter{}, err 130 } 131 } 132 dateRange = util.DateRange{Start: fromDate, End: toDate} 133 134 case today: 135 dateRange = util.TodayRange() 136 case yesterday: 137 dateRange = util.YesterdayRange() 138 case thisWeek: 139 dateRange = util.ThisWeekRange() 140 case lastWeek: 141 dateRange = util.LastWeekRange() 142 case thisMonth: 143 dateRange = util.ThisMonthRange() 144 case lastMonth: 145 dateRange = util.LastMonthRange() 146 case thisYear: 147 dateRange = util.ThisYearRange() 148 case lastYear: 149 dateRange = util.LastYearRange() 150 } 151 152 hasMax := max > 0 153 hasAuthor := author != "" 154 hasMessage := message != "" 155 156 if !(hasMax || dateRange.IsSet() || hasAuthor || hasMessage) { 157 // if no limits set default to max of one result 158 hasMax = true 159 max = 1 160 } 161 162 return CommitLimiter{ 163 DateRange: dateRange, 164 Max: max, 165 Author: author, 166 Message: message, 167 HasMax: hasMax, 168 HasAuthor: hasAuthor, 169 HasMessage: hasMessage, 170 }, nil 171 } 172 173 func (m CommitLimiter) filter(c *git.Commit, cnt int) (bool, bool, error) { 174 if m.HasMax && m.Max == cnt { 175 return false, true, nil 176 } 177 178 if m.DateRange.IsSet() && !m.DateRange.Within(c.Author().When) { 179 return false, false, nil 180 } 181 182 if m.HasAuthor && !strings.Contains(c.Author().Name, m.Author) { 183 return false, false, nil 184 } 185 186 if m.HasMessage && !(strings.Contains(c.Summary(), m.Message) || strings.Contains(c.Message(), m.Message)) { 187 return false, false, nil 188 } 189 190 return true, false, nil 191 } 192 193 // CommitIDs returns commit SHA1 IDs starting from the head up to the limit 194 func CommitIDs(limiter CommitLimiter, wd ...string) ([]string, error) { 195 var ( 196 repo *git.Repository 197 cnt int 198 w *git.RevWalk 199 err error 200 ) 201 commits := []string{} 202 203 if len(wd) > 0 { 204 repo, err = openRepository(wd[0]) 205 } else { 206 repo, err = openRepository() 207 } 208 209 if err != nil { 210 return commits, err 211 } 212 defer repo.Free() 213 214 w, err = repo.Walk() 215 if err != nil { 216 return commits, err 217 } 218 defer w.Free() 219 220 err = w.PushHead() 221 if err != nil { 222 return commits, err 223 } 224 225 var filterError error 226 227 err = w.Iterate( 228 func(commit *git.Commit) bool { 229 include, done, err := limiter.filter(commit, cnt) 230 if err != nil { 231 filterError = err 232 return false 233 } 234 if done { 235 return false 236 } 237 if include { 238 commits = append(commits, commit.Object.Id().String()) 239 cnt++ 240 } 241 return true 242 }) 243 244 if filterError != nil { 245 return commits, filterError 246 } 247 if err != nil { 248 return commits, err 249 } 250 251 return commits, nil 252 } 253 254 // Commit contains commit details 255 type Commit struct { 256 ID string 257 OID *git.Oid 258 Summary string 259 Message string 260 Author string 261 Email string 262 When time.Time 263 Files []string 264 Stats CommitStats 265 } 266 267 // CommitStats contains the files changed and their stats 268 type CommitStats struct { 269 Files []string 270 Insertions int 271 Deletions int 272 FilesChanged int 273 } 274 275 // ChangeRatePerHour calculates the rate change per hour 276 func (c CommitStats) ChangeRatePerHour(seconds int) float64 { 277 if seconds == 0 { 278 return 0 279 } 280 return (float64(c.Insertions+c.Deletions) / float64(seconds)) * 3600 281 } 282 283 // DiffParentCommit compares commit to it's parent and returns their stats 284 func DiffParentCommit(childCommit *git.Commit) (CommitStats, error) { 285 defer util.Profile()() 286 287 childTree, err := childCommit.Tree() 288 if err != nil { 289 return CommitStats{}, err 290 } 291 defer childTree.Free() 292 293 if childCommit.ParentCount() == 0 { 294 // there is no parent commit, should be the first commit in the repo? 295 296 path := "" 297 fileCnt := 0 298 files := []string{} 299 300 err := childTree.Walk( 301 func(s string, entry *git.TreeEntry) int { 302 switch entry.Filemode { 303 case git.FilemodeTree: 304 // directory where file entry is located 305 path = filepath.ToSlash(entry.Name) 306 default: 307 files = append(files, filepath.Join(path, entry.Name)) 308 fileCnt++ 309 } 310 return 0 311 }) 312 313 if err != nil { 314 return CommitStats{}, err 315 } 316 317 return CommitStats{ 318 Insertions: fileCnt, 319 Deletions: 0, 320 Files: files, 321 FilesChanged: fileCnt, 322 }, nil 323 } 324 325 parentTree, err := childCommit.Parent(0).Tree() 326 if err != nil { 327 return CommitStats{}, err 328 } 329 defer parentTree.Free() 330 331 options, err := git.DefaultDiffOptions() 332 if err != nil { 333 return CommitStats{}, err 334 } 335 336 diff, err := childCommit.Owner().DiffTreeToTree(parentTree, childTree, &options) 337 if err != nil { 338 return CommitStats{}, err 339 } 340 defer func() { 341 if err := diff.Free(); err != nil { 342 fmt.Printf("Unable to free diff, %s\n", err) 343 } 344 }() 345 346 files := []string{} 347 err = diff.ForEach( 348 func(delta git.DiffDelta, progress float64) (git.DiffForEachHunkCallback, error) { 349 // these should only be files that have changed 350 351 files = append(files, filepath.ToSlash(delta.NewFile.Path)) 352 353 return func(hunk git.DiffHunk) (git.DiffForEachLineCallback, error) { 354 return func(line git.DiffLine) error { 355 return nil 356 }, nil 357 }, nil 358 }, git.DiffDetailFiles) 359 360 if err != nil { 361 return CommitStats{}, err 362 } 363 364 stats, err := diff.Stats() 365 if err != nil { 366 return CommitStats{}, err 367 } 368 defer func() { 369 if err := stats.Free(); err != nil { 370 fmt.Printf("Unable to free stats, %s\n", err) 371 } 372 }() 373 374 return CommitStats{ 375 Insertions: stats.Insertions(), 376 Deletions: stats.Deletions(), 377 Files: files, 378 FilesChanged: stats.FilesChanged(), 379 }, err 380 } 381 382 // HeadCommit returns the latest commit 383 func HeadCommit(wd ...string) (Commit, error) { 384 var ( 385 repo *git.Repository 386 err error 387 ) 388 commit := Commit{} 389 390 if len(wd) > 0 { 391 repo, err = openRepository(wd[0]) 392 } else { 393 repo, err = openRepository() 394 } 395 if err != nil { 396 return commit, err 397 } 398 defer repo.Free() 399 400 headCommit, err := lookupHeadCommit(repo) 401 if err != nil { 402 if err == ErrHeadUnborn { 403 return commit, nil 404 } 405 return commit, err 406 } 407 defer headCommit.Free() 408 409 commitStats, err := DiffParentCommit(headCommit) 410 if err != nil { 411 return commit, err 412 } 413 414 return Commit{ 415 ID: headCommit.Object.Id().String(), 416 OID: headCommit.Object.Id(), 417 Summary: headCommit.Summary(), 418 Message: headCommit.Message(), 419 Author: headCommit.Author().Name, 420 Email: headCommit.Author().Email, 421 When: headCommit.Author().When, 422 Stats: commitStats, 423 }, nil 424 } 425 426 // CreateNote creates a git note associated with the head commit 427 func CreateNote(noteTxt string, nameSpace string, wd ...string) error { 428 defer util.Profile()() 429 430 var ( 431 repo *git.Repository 432 err error 433 ) 434 435 if len(wd) > 0 { 436 repo, err = openRepository(wd[0]) 437 } else { 438 repo, err = openRepository() 439 } 440 if err != nil { 441 return err 442 } 443 defer repo.Free() 444 445 headCommit, err := lookupHeadCommit(repo) 446 if err != nil { 447 return err 448 } 449 450 sig := &git.Signature{ 451 Name: headCommit.Author().Name, 452 Email: headCommit.Author().Email, 453 When: headCommit.Author().When, 454 } 455 456 _, err = repo.Notes.Create("refs/notes/"+nameSpace, sig, sig, headCommit.Id(), noteTxt, false) 457 458 return err 459 } 460 461 // CommitNote contains a git note's details 462 type CommitNote struct { 463 ID string 464 OID *git.Oid 465 Summary string 466 Message string 467 Author string 468 Email string 469 When time.Time 470 Note string 471 Stats CommitStats 472 } 473 474 // ReadNote returns a commit note for the SHA1 commit id 475 func ReadNote(commitID string, nameSpace string, calcStats bool, wd ...string) (CommitNote, error) { 476 var ( 477 err error 478 repo *git.Repository 479 commit *git.Commit 480 n *git.Note 481 ) 482 483 if len(wd) > 0 { 484 repo, err = openRepository(wd[0]) 485 } else { 486 repo, err = openRepository() 487 } 488 489 if err != nil { 490 return CommitNote{}, err 491 } 492 493 defer func() { 494 if commit != nil { 495 commit.Free() 496 } 497 if n != nil { 498 if err := n.Free(); err != nil { 499 fmt.Printf("Unable to free note, %s\n", err) 500 } 501 } 502 repo.Free() 503 }() 504 505 id, err := git.NewOid(commitID) 506 if err != nil { 507 return CommitNote{}, err 508 } 509 510 commit, err = repo.LookupCommit(id) 511 if err != nil { 512 return CommitNote{}, err 513 } 514 515 var noteTxt string 516 n, err = repo.Notes.Read("refs/notes/"+nameSpace, id) 517 if err != nil { 518 noteTxt = "" 519 } else { 520 noteTxt = n.Message() 521 } 522 523 stats := CommitStats{} 524 if calcStats { 525 stats, err = DiffParentCommit(commit) 526 if err != nil { 527 return CommitNote{}, err 528 } 529 } 530 531 return CommitNote{ 532 ID: commit.Object.Id().String(), 533 OID: commit.Object.Id(), 534 Summary: commit.Summary(), 535 Message: commit.Message(), 536 Author: commit.Author().Name, 537 Email: commit.Author().Email, 538 When: commit.Author().When, 539 Note: noteTxt, 540 Stats: stats, 541 }, nil 542 } 543 544 // ConfigSet persists git configuration settings 545 func ConfigSet(settings map[string]string, wd ...string) error { 546 var ( 547 err error 548 repo *git.Repository 549 cfg *git.Config 550 ) 551 552 if len(wd) > 0 { 553 repo, err = openRepository(wd[0]) 554 } else { 555 repo, err = openRepository() 556 } 557 if err != nil { 558 return err 559 } 560 561 cfg, err = repo.Config() 562 defer cfg.Free() 563 if err != nil { 564 return err 565 } 566 567 for k, v := range settings { 568 err = cfg.SetString(k, v) 569 if err != nil { 570 return err 571 } 572 } 573 return nil 574 } 575 576 // ConfigRemove removes git configuration settings 577 func ConfigRemove(settings map[string]string, wd ...string) error { 578 var ( 579 err error 580 repo *git.Repository 581 cfg *git.Config 582 ) 583 584 if len(wd) > 0 { 585 repo, err = openRepository(wd[0]) 586 } else { 587 repo, err = openRepository() 588 } 589 if err != nil { 590 return err 591 } 592 593 cfg, err = repo.Config() 594 defer cfg.Free() 595 if err != nil { 596 return err 597 } 598 599 for k := range settings { 600 err = cfg.Delete(k) 601 if err != nil { 602 return err 603 } 604 } 605 return nil 606 } 607 608 // GitHook is the Command with options to be added/removed from a git hook 609 // Exe is the executable file name for Linux/MacOS 610 // RE is the regex to match on for the command 611 type GitHook struct { 612 Exe string 613 Command string 614 RE *regexp.Regexp 615 } 616 617 func (g GitHook) getCommandPath() string { 618 // save current dir & change to root 619 // to guarantee we get the full path 620 wd, err := os.Getwd() 621 if err != nil { 622 fmt.Printf("Unable to get working directory, %s\n", err) 623 } 624 defer func() { 625 if err := os.Chdir(wd); err != nil { 626 fmt.Printf("Unable to change back to working dir, %s\n", err) 627 } 628 }() 629 if err := os.Chdir(string(filepath.Separator)); err != nil { 630 fmt.Printf("Unable to change to root directory, %s\n", err) 631 } 632 633 p, err := exec.LookPath(g.getExeForOS()) 634 if err != nil { 635 return g.Command 636 } 637 if runtime.GOOS == "windows" { 638 // put "" around file path 639 return strings.Replace(g.Command, g.Exe, fmt.Sprintf("%s || \"%s\"", g.Command, p), 1) 640 } 641 return strings.Replace(g.Command, g.Exe, fmt.Sprintf("%s || %s", g.Command, p), 1) 642 } 643 644 func (g GitHook) getExeForOS() string { 645 if runtime.GOOS == "windows" { 646 return fmt.Sprintf("gtm.%s", "exe") 647 } 648 return g.Exe 649 } 650 651 // SetHooks creates git hooks 652 func SetHooks(hooks map[string]GitHook, wd ...string) error { 653 const shebang = "#!/bin/sh" 654 for ghfile, hook := range hooks { 655 var ( 656 p string 657 err error 658 ) 659 660 if len(wd) > 0 { 661 p = wd[0] 662 } else { 663 664 p, err = os.Getwd() 665 if err != nil { 666 return err 667 } 668 } 669 fp := filepath.Join(p, "hooks", ghfile) 670 hooksDir := filepath.Join(p, "hooks") 671 672 var output string 673 674 if _, err := os.Stat(hooksDir); os.IsNotExist(err) { 675 if err := os.MkdirAll(hooksDir, 0700); err != nil { 676 return err 677 } 678 } 679 680 if _, err := os.Stat(fp); !os.IsNotExist(err) { 681 b, err := ioutil.ReadFile(fp) 682 if err != nil { 683 return err 684 } 685 output = string(b) 686 } 687 688 if !strings.Contains(output, shebang) { 689 output = fmt.Sprintf("%s\n%s", shebang, output) 690 } 691 692 if hook.RE.MatchString(output) { 693 output = hook.RE.ReplaceAllString(output, hook.getCommandPath()) 694 } else { 695 output = fmt.Sprintf("%s\n%s", output, hook.getCommandPath()) 696 } 697 698 if err = ioutil.WriteFile(fp, []byte(output), 0755); err != nil { 699 return err 700 } 701 702 if err := os.Chmod(fp, 0755); err != nil { 703 return err 704 } 705 } 706 707 return nil 708 } 709 710 // RemoveHooks remove matching git hook commands 711 func RemoveHooks(hooks map[string]GitHook, p string) error { 712 713 for ghfile, hook := range hooks { 714 fp := filepath.Join(p, "hooks", ghfile) 715 if _, err := os.Stat(fp); os.IsNotExist(err) { 716 continue 717 } 718 719 b, err := ioutil.ReadFile(fp) 720 if err != nil { 721 return err 722 } 723 output := string(b) 724 725 if hook.RE.MatchString(output) { 726 output := hook.RE.ReplaceAllString(output, "") 727 i := strings.LastIndexAny(output, "\n") 728 if i > -1 { 729 output = output[0:i] 730 } 731 if err = ioutil.WriteFile(fp, []byte(output), 0755); err != nil { 732 return err 733 } 734 } 735 } 736 737 return nil 738 } 739 740 // IgnoreSet persists paths/files to ignore for a git repo 741 func IgnoreSet(ignore string, wd ...string) error { 742 var ( 743 p string 744 err error 745 ) 746 747 if len(wd) > 0 { 748 p = wd[0] 749 } else { 750 p, err = os.Getwd() 751 if err != nil { 752 return err 753 } 754 } 755 756 fp := filepath.Join(p, ".gitignore") 757 758 var output string 759 760 data, err := ioutil.ReadFile(fp) 761 if err == nil { 762 output = string(data) 763 764 lines := strings.Split(output, "\n") 765 for _, line := range lines { 766 if strings.TrimSpace(line) == ignore { 767 return nil 768 } 769 } 770 } else if !os.IsNotExist(err) { 771 return fmt.Errorf("can't read %s: %s", fp, err) 772 } 773 774 if len(output) > 0 && !strings.HasSuffix(output, "\n") { 775 output += "\n" 776 } 777 778 output += ignore + "\n" 779 780 if err = ioutil.WriteFile(fp, []byte(output), 0644); err != nil { 781 return fmt.Errorf("can't write %s: %s", fp, err) 782 } 783 784 return nil 785 } 786 787 // IgnoreRemove removes paths/files ignored for a git repo 788 func IgnoreRemove(ignore string, wd ...string) error { 789 var ( 790 p string 791 err error 792 ) 793 794 if len(wd) > 0 { 795 p = wd[0] 796 } else { 797 p, err = os.Getwd() 798 if err != nil { 799 return err 800 } 801 } 802 fp := filepath.Join(p, ".gitignore") 803 804 if _, err := os.Stat(fp); os.IsNotExist(err) { 805 return fmt.Errorf("Unable to remove %s from .gitignore, %s not found", ignore, fp) 806 } 807 b, err := ioutil.ReadFile(fp) 808 if err != nil { 809 return err 810 } 811 output := string(b) 812 if strings.Contains(output, ignore+"\n") { 813 output = strings.Replace(output, ignore+"\n", "", 1) 814 if err = ioutil.WriteFile(fp, []byte(output), 0644); err != nil { 815 return err 816 } 817 } 818 return nil 819 } 820 821 func openRepository(wd ...string) (*git.Repository, error) { 822 var ( 823 p string 824 err error 825 ) 826 827 if len(wd) > 0 { 828 p, err = GitRepoPath(wd[0]) 829 } else { 830 p, err = GitRepoPath() 831 } 832 if err != nil { 833 return nil, err 834 } 835 836 repo, err := git.OpenRepository(p) 837 return repo, err 838 } 839 840 var ( 841 // ErrHeadUnborn is raised when there are no commits yet in the git repo 842 ErrHeadUnborn = errors.New("Head commit not found") 843 ) 844 845 func lookupHeadCommit(repo *git.Repository) (*git.Commit, error) { 846 847 headUnborn, err := repo.IsHeadUnborn() 848 if err != nil { 849 return nil, err 850 } 851 if headUnborn { 852 return nil, ErrHeadUnborn 853 } 854 855 headRef, err := repo.Head() 856 if err != nil { 857 return nil, err 858 } 859 defer headRef.Free() 860 861 commit, err := repo.LookupCommit(headRef.Target()) 862 if err != nil { 863 return nil, err 864 } 865 866 return commit, nil 867 } 868 869 // Status contains the git file statuses 870 type Status struct { 871 Files []fileStatus 872 } 873 874 // NewStatus create a Status struct for a git repo 875 func NewStatus(wd ...string) (Status, error) { 876 defer util.Profile()() 877 878 var ( 879 repo *git.Repository 880 err error 881 ) 882 status := Status{} 883 884 if len(wd) > 0 { 885 repo, err = openRepository(wd[0]) 886 } else { 887 repo, err = openRepository() 888 } 889 if err != nil { 890 return status, err 891 } 892 defer repo.Free() 893 894 //TODO: research what status options to set 895 opts := &git.StatusOptions{} 896 opts.Show = git.StatusShowIndexAndWorkdir 897 opts.Flags = git.StatusOptIncludeUntracked | git.StatusOptRenamesHeadToIndex | git.StatusOptSortCaseSensitively 898 statusList, err := repo.StatusList(opts) 899 900 if err != nil { 901 return status, err 902 } 903 defer statusList.Free() 904 905 cnt, err := statusList.EntryCount() 906 if err != nil { 907 return status, err 908 } 909 910 for i := 0; i < cnt; i++ { 911 entry, err := statusList.ByIndex(i) 912 if err != nil { 913 return status, err 914 } 915 status.AddFile(entry) 916 } 917 918 return status, nil 919 } 920 921 // AddFile adds a StatusEntry for each file in working and staging directories 922 func (s *Status) AddFile(e git.StatusEntry) { 923 var path string 924 if e.Status == git.StatusIndexNew || 925 e.Status == git.StatusIndexModified || 926 e.Status == git.StatusIndexDeleted || 927 e.Status == git.StatusIndexRenamed || 928 e.Status == git.StatusIndexTypeChange { 929 path = filepath.ToSlash(e.HeadToIndex.NewFile.Path) 930 } else { 931 path = filepath.ToSlash(e.IndexToWorkdir.NewFile.Path) 932 } 933 s.Files = append(s.Files, fileStatus{Path: path, Status: e.Status}) 934 } 935 936 // HasStaged returns true if there are any files in staging 937 func (s *Status) HasStaged() bool { 938 for _, f := range s.Files { 939 if f.InStaging() { 940 return true 941 } 942 } 943 return false 944 } 945 946 // IsModified returns true if the file is modified in either working or staging 947 func (s *Status) IsModified(path string, staging bool) bool { 948 path = filepath.ToSlash(path) 949 for _, f := range s.Files { 950 if path == f.Path && f.InStaging() == staging { 951 return f.IsModified() 952 } 953 } 954 return false 955 } 956 957 // IsTracked returns true if file is tracked by the git repo 958 func (s *Status) IsTracked(path string) bool { 959 path = filepath.ToSlash(path) 960 for _, f := range s.Files { 961 if path == f.Path { 962 return f.IsTracked() 963 } 964 } 965 return false 966 } 967 968 type fileStatus struct { 969 Status git.Status 970 Path string 971 } 972 973 // InStaging returns true if the file is in staging 974 func (f fileStatus) InStaging() bool { 975 return f.Status == git.StatusIndexNew || 976 f.Status == git.StatusIndexModified || 977 f.Status == git.StatusIndexDeleted || 978 f.Status == git.StatusIndexRenamed || 979 f.Status == git.StatusIndexTypeChange 980 } 981 982 // InWorking returns true if the file is in working 983 func (f fileStatus) InWorking() bool { 984 return f.Status == git.StatusWtModified || 985 f.Status == git.StatusWtDeleted || 986 f.Status == git.StatusWtRenamed || 987 f.Status == git.StatusWtTypeChange 988 } 989 990 // IsTracked returns true if the file is tracked by git 991 func (f fileStatus) IsTracked() bool { 992 return f.Status != git.StatusIgnored && 993 f.Status != git.StatusWtNew 994 } 995 996 // IsModified returns true if the file has been modified 997 func (f fileStatus) IsModified() bool { 998 return f.InStaging() || f.InWorking() 999 }