github.com/developest/gtm-enhanced@v1.0.4-0.20220111132249-cc80a3372c3f/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/DEVELOPEST/gtm-core/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 Subdir string 75 HasMax bool 76 HasBefore bool 77 HasAfter bool 78 HasAuthor bool 79 HasMessage bool 80 HasSubdir bool 81 } 82 83 // NewCommitLimiter returns a new initialize CommitLimiter struct 84 func NewCommitLimiter( 85 max int, fromDateStr, toDateStr, author, message string, 86 today, yesterday, thisWeek, lastWeek, 87 thisMonth, lastMonth, thisYear, lastYear bool) (CommitLimiter, error) { 88 89 const dateFormat = "2006-01-02" 90 91 fromDateStr = strings.TrimSpace(fromDateStr) 92 toDateStr = strings.TrimSpace(toDateStr) 93 author = strings.TrimSpace(author) 94 message = strings.TrimSpace(message) 95 96 cnt := func(vals []bool) int { 97 var c int 98 for _, v := range vals { 99 if v { 100 c++ 101 } 102 } 103 return c 104 }([]bool{ 105 fromDateStr != "" || toDateStr != "", 106 today, yesterday, thisWeek, lastWeek, thisMonth, lastMonth, thisYear, lastYear}) 107 108 if cnt > 1 { 109 return CommitLimiter{}, fmt.Errorf("Using multiple temporal flags is not allowed") 110 } 111 112 var ( 113 err error 114 dateRange util.DateRange 115 ) 116 117 switch { 118 case fromDateStr != "" || toDateStr != "": 119 fromDate := time.Time{} 120 toDate := time.Time{} 121 122 if fromDateStr != "" { 123 fromDate, err = time.Parse(dateFormat, fromDateStr) 124 if err != nil { 125 return CommitLimiter{}, err 126 } 127 } 128 if toDateStr != "" { 129 toDate, err = time.Parse(dateFormat, toDateStr) 130 if err != nil { 131 return CommitLimiter{}, err 132 } 133 } 134 dateRange = util.DateRange{Start: fromDate, End: toDate} 135 136 case today: 137 dateRange = util.TodayRange() 138 case yesterday: 139 dateRange = util.YesterdayRange() 140 case thisWeek: 141 dateRange = util.ThisWeekRange() 142 case lastWeek: 143 dateRange = util.LastWeekRange() 144 case thisMonth: 145 dateRange = util.ThisMonthRange() 146 case lastMonth: 147 dateRange = util.LastMonthRange() 148 case thisYear: 149 dateRange = util.ThisYearRange() 150 case lastYear: 151 dateRange = util.LastYearRange() 152 } 153 154 hasAuthor := author != "" 155 hasMessage := message != "" 156 hasMax := max > 0 157 158 return CommitLimiter{ 159 DateRange: dateRange, 160 Max: max, 161 Author: author, 162 Message: message, 163 HasMax: hasMax, 164 HasAuthor: hasAuthor, 165 HasMessage: hasMessage, 166 }, nil 167 } 168 169 func (m CommitLimiter) filter(c *git.Commit, cnt int) (bool, bool, error) { 170 if m.HasMax && m.Max == cnt { 171 return false, true, nil 172 } 173 174 if m.DateRange.IsSet() && !m.DateRange.Within(c.Author().When) { 175 return false, false, nil 176 } 177 178 if m.HasAuthor && !strings.Contains(c.Author().Name, m.Author) { 179 return false, false, nil 180 } 181 182 if m.HasMessage && 183 !(strings.Contains(c.Summary(), m.Message) || strings.Contains(c.Message(), m.Message)) { 184 return false, false, nil 185 } 186 187 return true, false, nil 188 } 189 190 // CommitIDs returns commit SHA1 IDs starting from the head up to the limit 191 func CommitIDs(limiter CommitLimiter, wd ...string) ([]string, error) { 192 var ( 193 repo *git.Repository 194 cnt int 195 w *git.RevWalk 196 err error 197 ) 198 var commits []string 199 200 if len(wd) > 0 { 201 repo, err = openRepository(wd[0]) 202 } else { 203 repo, err = openRepository() 204 } 205 206 if err != nil { 207 return commits, err 208 } 209 defer repo.Free() 210 211 w, err = repo.Walk() 212 if err != nil { 213 return commits, err 214 } 215 defer w.Free() 216 217 err = w.PushHead() 218 if err != nil { 219 return commits, err 220 } 221 222 var filterError error 223 224 err = w.Iterate( 225 func(commit *git.Commit) bool { 226 include, done, err := limiter.filter(commit, cnt) 227 if err != nil { 228 filterError = err 229 return false 230 } 231 if done { 232 return false 233 } 234 if include { 235 commits = append(commits, commit.Object.Id().String()) 236 cnt++ 237 } 238 return true 239 }) 240 241 if filterError != nil { 242 return commits, filterError 243 } 244 if err != nil { 245 return commits, err 246 } 247 248 return commits, nil 249 } 250 251 // Commit contains commit details 252 type Commit struct { 253 ID string 254 OID *git.Oid 255 Summary string 256 Message string 257 Author string 258 Email string 259 When time.Time 260 Files []string 261 Stats CommitStats 262 } 263 264 // CommitStats contains the files changed and their stats 265 type CommitStats struct { 266 Files []string 267 Insertions int 268 Deletions int 269 FilesChanged int 270 } 271 272 // ChangeRatePerHour calculates the rate change per hour 273 func (c CommitStats) ChangeRatePerHour(seconds int) float64 { 274 if seconds == 0 { 275 return 0 276 } 277 return (float64(c.Insertions+c.Deletions) / float64(seconds)) * 3600 278 } 279 280 // CurrentBranch return current git branch name 281 func CurrentBranch(wd ...string) string { 282 var ( 283 repo *git.Repository 284 err error 285 head *git.Reference 286 branch string 287 ) 288 289 if len(wd) > 0 { 290 repo, err = openRepository(wd[0]) 291 } else { 292 repo, err = openRepository() 293 } 294 if err != nil { 295 return "" 296 } 297 defer repo.Free() 298 299 if head, err = repo.Head(); err != nil { 300 return "" 301 } 302 303 if branch, err = head.Branch().Name(); err != nil { 304 return "" 305 } 306 307 return branch 308 } 309 310 // DiffParentCommit compares commit to it's parent and returns their stats 311 func DiffParentCommit(childCommit *git.Commit) (CommitStats, error) { 312 defer util.Profile()() 313 314 childTree, err := childCommit.Tree() 315 if err != nil { 316 return CommitStats{}, err 317 } 318 defer childTree.Free() 319 320 if childCommit.ParentCount() == 0 { 321 // there is no parent commit, should be the first commit in the repo? 322 323 path := "" 324 fileCnt := 0 325 var files []string 326 327 err := childTree.Walk( 328 func(s string, entry *git.TreeEntry) error { 329 switch entry.Filemode { 330 case git.FilemodeTree: 331 // directory where file entry is located 332 path = filepath.ToSlash(entry.Name) 333 default: 334 files = append(files, filepath.Join(path, entry.Name)) 335 fileCnt++ 336 } 337 return nil 338 }) 339 340 if err != nil { 341 return CommitStats{}, err 342 } 343 344 return CommitStats{ 345 Insertions: fileCnt, 346 Deletions: 0, 347 Files: files, 348 FilesChanged: fileCnt, 349 }, nil 350 } 351 352 parentTree, err := childCommit.Parent(0).Tree() 353 if err != nil { 354 return CommitStats{}, err 355 } 356 defer parentTree.Free() 357 358 options, err := git.DefaultDiffOptions() 359 if err != nil { 360 return CommitStats{}, err 361 } 362 363 diff, err := childCommit.Owner().DiffTreeToTree(parentTree, childTree, &options) 364 if err != nil { 365 return CommitStats{}, err 366 } 367 defer func() { 368 if err := diff.Free(); err != nil { 369 fmt.Printf("Unable to free diff, %s\n", err) 370 } 371 }() 372 373 var files []string 374 err = diff.ForEach( 375 func(delta git.DiffDelta, progress float64) (git.DiffForEachHunkCallback, error) { 376 // these should only be files that have changed 377 378 files = append(files, filepath.ToSlash(delta.NewFile.Path)) 379 380 return func(hunk git.DiffHunk) (git.DiffForEachLineCallback, error) { 381 return func(line git.DiffLine) error { 382 return nil 383 }, nil 384 }, nil 385 }, git.DiffDetailFiles) 386 387 if err != nil { 388 return CommitStats{}, err 389 } 390 391 stats, err := diff.Stats() 392 if err != nil { 393 return CommitStats{}, err 394 } 395 defer func() { 396 if err := stats.Free(); err != nil { 397 fmt.Printf("Unable to free stats, %s\n", err) 398 } 399 }() 400 401 return CommitStats{ 402 Insertions: stats.Insertions(), 403 Deletions: stats.Deletions(), 404 Files: files, 405 FilesChanged: stats.FilesChanged(), 406 }, err 407 } 408 409 // HeadCommit returns the latest commit 410 func HeadCommit(wd ...string) (Commit, error) { 411 var ( 412 repo *git.Repository 413 err error 414 ) 415 commit := Commit{} 416 417 if len(wd) > 0 { 418 repo, err = openRepository(wd[0]) 419 } else { 420 repo, err = openRepository() 421 } 422 if err != nil { 423 return commit, err 424 } 425 defer repo.Free() 426 427 headCommit, err := lookupCommit(repo) 428 if err != nil { 429 if err == ErrHeadUnborn { 430 return commit, nil 431 } 432 return commit, err 433 } 434 defer headCommit.Free() 435 436 commitStats, err := DiffParentCommit(headCommit) 437 if err != nil { 438 return commit, err 439 } 440 441 return Commit{ 442 ID: headCommit.Object.Id().String(), 443 OID: headCommit.Object.Id(), 444 Summary: headCommit.Summary(), 445 Message: headCommit.Message(), 446 Author: headCommit.Author().Name, 447 Email: headCommit.Author().Email, 448 When: headCommit.Author().When, 449 Stats: commitStats, 450 }, nil 451 } 452 453 // CreateNote creates a git note associated with the head commit 454 func CreateNote(noteTxt, nameSpace, commitHash string, wd ...string) error { 455 defer util.Profile()() 456 457 var ( 458 repo *git.Repository 459 err error 460 prevNote *git.Note 461 commit *git.Commit 462 ) 463 464 if len(wd) > 0 { 465 repo, err = openRepository(wd[0]) 466 } else { 467 repo, err = openRepository() 468 } 469 if err != nil { 470 return err 471 } 472 defer repo.Free() 473 474 if commitHash != "" { 475 476 } 477 if commitHash != "" { 478 commit, err = lookupCommit(repo, commitHash) 479 } else { 480 commit, err = lookupCommit(repo) 481 } 482 if err != nil { 483 return err 484 } 485 486 sig := &git.Signature{ 487 Name: commit.Author().Name, 488 Email: commit.Author().Email, 489 When: commit.Author().When, 490 } 491 492 prevNote, err = repo.Notes.Read("refs/notes/"+nameSpace, commit.Id()) 493 494 if prevNote != nil { 495 noteTxt += "\n" + prevNote.Message() 496 if err := repo.Notes.Remove( 497 "refs/notes/"+nameSpace, 498 prevNote.Author(), 499 prevNote.Committer(), 500 commit.Id()); err != nil { 501 return err 502 } 503 504 if err := prevNote.Free(); err != nil { 505 return err 506 } 507 } 508 509 _, err = repo.Notes.Create("refs/notes/"+nameSpace, sig, sig, commit.Id(), noteTxt, false) 510 511 return err 512 } 513 514 func RemoveNote(nameSpace, commitHash string, wd ...string) error { 515 var ( 516 repo *git.Repository 517 err error 518 commit *git.Commit 519 ) 520 521 if len(wd) > 0 { 522 repo, err = openRepository(wd[0]) 523 } else { 524 repo, err = openRepository() 525 } 526 if err != nil { 527 return err 528 } 529 defer repo.Free() 530 531 commit, err = lookupCommit(repo, commitHash) 532 533 if err != nil { 534 return err 535 } 536 537 sig := &git.Signature{ 538 Name: commit.Author().Name, 539 Email: commit.Author().Email, 540 When: commit.Author().When, 541 } 542 543 err = repo.Notes.Remove("refs/notes/"+nameSpace, sig, sig, commit.Id()) 544 return err 545 } 546 547 // CommitNote contains a git note's details 548 type CommitNote struct { 549 ID string 550 OID *git.Oid 551 Summary string 552 Message string 553 Author string 554 Email string 555 When time.Time 556 Note string 557 Stats CommitStats 558 } 559 560 // ReadNote returns a commit note for the SHA1 commit id, 561 // tries to fetch squashed commits notes as well by message 562 func ReadNote(commitID string, nameSpace string, calcStats bool, wd ...string) (CommitNote, error) { 563 var ( 564 err error 565 repo *git.Repository 566 commit *git.Commit 567 n *git.Note 568 notes [][]string 569 ) 570 571 if len(wd) > 0 { 572 repo, err = openRepository(wd[0]) 573 } else { 574 repo, err = openRepository() 575 } 576 577 if err != nil { 578 return CommitNote{}, err 579 } 580 581 defer func() { 582 if commit != nil { 583 commit.Free() 584 } 585 if n != nil { 586 if err := n.Free(); err != nil { 587 fmt.Printf("Unable to free note, %s\n", err) 588 } 589 } 590 repo.Free() 591 }() 592 593 id, err := git.NewOid(commitID) 594 if err != nil { 595 return CommitNote{}, err 596 } 597 598 commit, err = repo.LookupCommit(id) 599 if err != nil { 600 return CommitNote{}, err 601 } 602 603 r := regexp.MustCompile(`commit\s+([\dabcdef]*)\r?\n`) 604 msg := commit.Message() 605 notes = r.FindAllStringSubmatch(msg, -1) 606 607 var noteTxt string 608 n, err = repo.Notes.Read("refs/notes/"+nameSpace, id) 609 if err != nil { 610 noteTxt = "" 611 err = nil 612 } else { 613 noteTxt = n.Message() 614 } 615 616 for _, note := range notes { 617 noteID, err := git.NewOid(note[1]) 618 if err == nil { 619 n, err = repo.Notes.Read("refs/notes/"+nameSpace, noteID) 620 } 621 if err != nil { 622 continue 623 } 624 noteTxt += "\n" + n.Message() 625 626 } 627 628 stats := CommitStats{} 629 if calcStats { 630 stats, err = DiffParentCommit(commit) 631 if err != nil { 632 return CommitNote{}, err 633 } 634 } 635 636 return CommitNote{ 637 ID: commit.Object.Id().String(), 638 OID: commit.Object.Id(), 639 Summary: commit.Summary(), 640 Message: commit.Message(), 641 Author: commit.Author().Name, 642 Email: commit.Author().Email, 643 When: commit.Author().When, 644 Note: noteTxt, 645 Stats: stats, 646 }, nil 647 } 648 649 func RewriteNote(oldHash, newHash, nameSpace string, wd ...string) error { 650 oldNote, err := ReadNote(oldHash, nameSpace, true, wd...) 651 if err != nil { 652 return err 653 } 654 err = CreateNote(oldNote.Note, nameSpace, newHash, wd...) 655 if err != nil { 656 return err 657 } 658 return RemoveNote(nameSpace, oldHash, wd...) 659 } 660 661 // ConfigSet persists git configuration settings 662 func ConfigSet(settings map[string]string, wd ...string) error { 663 var ( 664 err error 665 repo *git.Repository 666 cfg *git.Config 667 ) 668 669 if len(wd) > 0 { 670 repo, err = openRepository(wd[0]) 671 } else { 672 repo, err = openRepository() 673 } 674 if err != nil { 675 return err 676 } 677 678 if cfg, err = repo.Config(); err != nil { 679 return err 680 } 681 defer cfg.Free() 682 683 for k, v := range settings { 684 err = cfg.SetString(k, v) 685 if err != nil { 686 return err 687 } 688 } 689 return nil 690 } 691 692 // ConfigRemove removes git configuration settings 693 func ConfigRemove(settings map[string]string, wd ...string) error { 694 var ( 695 err error 696 repo *git.Repository 697 cfg *git.Config 698 ) 699 700 if len(wd) > 0 { 701 repo, err = openRepository(wd[0]) 702 } else { 703 repo, err = openRepository() 704 } 705 if err != nil { 706 return err 707 } 708 709 if cfg, err = repo.Config(); err != nil { 710 return err 711 } 712 defer cfg.Free() 713 714 for k := range settings { 715 err = cfg.Delete(k) 716 if err != nil { 717 return err 718 } 719 } 720 return nil 721 } 722 723 func FetchRemotesAddRefSpecs(refSpecs []string, wd ...string) error { 724 var ( 725 err error 726 repo *git.Repository 727 remotes []string 728 remoteRepo *git.Remote 729 refs []string 730 added bool 731 ) 732 733 if len(wd) > 0 { 734 repo, err = openRepository(wd[0]) 735 } else { 736 repo, err = openRepository() 737 } 738 if err != nil { 739 return err 740 } 741 742 remotes, err = repo.Remotes.List() 743 if err != nil { 744 return err 745 } 746 747 for _, remote := range remotes { 748 for _, refSpec := range refSpecs { 749 remoteRepo, err = repo.Remotes.Lookup(remote) 750 if err == nil { 751 refs, err = remoteRepo.FetchRefspecs() 752 } 753 added = false 754 for _, ref := range refs { 755 added = added || ref == refSpec 756 } 757 if err == nil && !added { 758 err = repo.Remotes.AddFetch(remote, refSpec) 759 } 760 if err != nil { 761 fmt.Println("Error updating ref spec for: " + remote) 762 return err 763 } 764 remoteRepo.Free() 765 } 766 } 767 return nil 768 } 769 770 func FetchRemotesRemoveRefSpecs(refSpecs []string, wd ...string) error { 771 var ( 772 buffer []byte 773 err error 774 config string 775 ) 776 777 if len(wd) > 0 { 778 buffer, err = ioutil.ReadFile(wd[0] + "/config") 779 } else { 780 buffer, err = ioutil.ReadFile("/config") 781 } 782 if err != nil { 783 return err 784 } 785 config = string(buffer) 786 for _, ref := range refSpecs { 787 config = strings.Replace(config, "fetch = "+ref, "", -1) 788 } 789 if len(wd) > 0 { 790 err = ioutil.WriteFile(wd[0]+"/config", []byte(config), 0644) 791 } else { 792 err = ioutil.WriteFile("/config", []byte(config), 0644) 793 } 794 return err 795 } 796 797 // GitHook is the Command with options to be added/removed from a git hook 798 // Exe is the executable file name for Linux/MacOS 799 // RE is the regex to match on for the command 800 type GitHook struct { 801 Exe string 802 Command string 803 RE *regexp.Regexp 804 } 805 806 func (g GitHook) getCommandPath() string { 807 // save current dir & change to root 808 // to guarantee we get the full path 809 wd, err := os.Getwd() 810 if err != nil { 811 fmt.Printf("Unable to get working directory, %s\n", err) 812 } 813 defer func() { 814 if err := os.Chdir(wd); err != nil { 815 fmt.Printf("Unable to change back to working dir, %s\n", err) 816 } 817 }() 818 if err := os.Chdir(string(filepath.Separator)); err != nil { 819 fmt.Printf("Unable to change to root directory, %s\n", err) 820 } 821 822 p, err := exec.LookPath(g.getExeForOS()) 823 if err != nil { 824 return g.Command 825 } 826 if runtime.GOOS == "windows" { 827 // put "" around file path 828 return strings.Replace(g.Command, g.Exe, fmt.Sprintf("%s || \"%s\"", g.Command, p), 1) 829 } 830 return strings.Replace(g.Command, g.Exe, fmt.Sprintf("%s || %s", g.Command, p), 1) 831 } 832 833 func (g GitHook) getExeForOS() string { 834 if runtime.GOOS == "windows" { 835 return fmt.Sprintf("gtm.%s", "exe") 836 } 837 return g.Exe 838 } 839 840 // SetHooks creates git hooks 841 func SetHooks(hooks map[string]GitHook, wd ...string) error { 842 const shebang = "#!/bin/sh" 843 for ghfile, hook := range hooks { 844 var ( 845 p string 846 err error 847 ) 848 849 if len(wd) > 0 { 850 p = wd[0] 851 } else { 852 853 p, err = os.Getwd() 854 if err != nil { 855 return err 856 } 857 } 858 fp := filepath.Join(p, "hooks", ghfile) 859 hooksDir := filepath.Join(p, "hooks") 860 861 var output string 862 863 if _, err := os.Stat(hooksDir); os.IsNotExist(err) { 864 if err := os.MkdirAll(hooksDir, 0700); err != nil { 865 return err 866 } 867 } 868 869 if _, err := os.Stat(fp); !os.IsNotExist(err) { 870 b, err := ioutil.ReadFile(fp) 871 if err != nil { 872 return err 873 } 874 output = string(b) 875 } 876 877 if !strings.Contains(output, shebang) { 878 output = fmt.Sprintf("%s\n%s", shebang, output) 879 } 880 881 if hook.RE.MatchString(output) { 882 output = hook.RE.ReplaceAllString(output, hook.getCommandPath()) 883 } else { 884 output = fmt.Sprintf("%s\n%s", output, hook.getCommandPath()) 885 } 886 887 if err = ioutil.WriteFile(fp, []byte(output), 0755); err != nil { 888 return err 889 } 890 891 if err := os.Chmod(fp, 0755); err != nil { 892 return err 893 } 894 } 895 896 return nil 897 } 898 899 // RemoveHooks remove matching git hook commands 900 func RemoveHooks(hooks map[string]GitHook, p string) error { 901 902 for ghfile, hook := range hooks { 903 fp := filepath.Join(p, "hooks", ghfile) 904 if _, err := os.Stat(fp); os.IsNotExist(err) { 905 continue 906 } 907 908 b, err := ioutil.ReadFile(fp) 909 if err != nil { 910 return err 911 } 912 output := string(b) 913 914 if hook.RE.MatchString(output) { 915 output := hook.RE.ReplaceAllString(output, "") 916 i := strings.LastIndexAny(output, "\n") 917 if i > -1 { 918 output = output[0:i] 919 } 920 if err = ioutil.WriteFile(fp, []byte(output), 0755); err != nil { 921 return err 922 } 923 } 924 } 925 926 return nil 927 } 928 929 // IgnoreSet persists paths/files to ignore for a git repo 930 func IgnoreSet(ignore string, wd ...string) error { 931 var ( 932 p string 933 err error 934 ) 935 936 if len(wd) > 0 { 937 p = wd[0] 938 } else { 939 p, err = os.Getwd() 940 if err != nil { 941 return err 942 } 943 } 944 945 fp := filepath.Join(p, ".gitignore") 946 947 var output string 948 949 data, err := ioutil.ReadFile(fp) 950 if err == nil { 951 output = string(data) 952 953 lines := strings.Split(output, "\n") 954 for _, line := range lines { 955 if strings.TrimSpace(line) == ignore { 956 return nil 957 } 958 } 959 } else if !os.IsNotExist(err) { 960 return fmt.Errorf("can't read %s: %s", fp, err) 961 } 962 963 if len(output) > 0 && !strings.HasSuffix(output, "\n") { 964 output += "\n" 965 } 966 967 output += ignore + "\n" 968 969 if err = ioutil.WriteFile(fp, []byte(output), 0644); err != nil { 970 return fmt.Errorf("can't write %s: %s", fp, err) 971 } 972 973 return nil 974 } 975 976 // IgnoreRemove removes paths/files ignored for a git repo 977 func IgnoreRemove(ignore string, wd ...string) error { 978 var ( 979 p string 980 err error 981 ) 982 983 if len(wd) > 0 { 984 p = wd[0] 985 } else { 986 p, err = os.Getwd() 987 if err != nil { 988 return err 989 } 990 } 991 fp := filepath.Join(p, ".gitignore") 992 993 if _, err := os.Stat(fp); os.IsNotExist(err) { 994 return fmt.Errorf("Unable to remove %s from .gitignore, %s not found", ignore, fp) 995 } 996 b, err := ioutil.ReadFile(fp) 997 if err != nil { 998 return err 999 } 1000 output := string(b) 1001 if strings.Contains(output, ignore+"\n") { 1002 output = strings.Replace(output, ignore+"\n", "", 1) 1003 if err = ioutil.WriteFile(fp, []byte(output), 0644); err != nil { 1004 return err 1005 } 1006 } 1007 return nil 1008 } 1009 1010 func openRepository(wd ...string) (*git.Repository, error) { 1011 var ( 1012 p string 1013 err error 1014 ) 1015 1016 if len(wd) > 0 { 1017 p, err = GitRepoPath(wd[0]) 1018 } else { 1019 p, err = GitRepoPath() 1020 } 1021 if err != nil { 1022 return nil, err 1023 } 1024 1025 repo, err := git.OpenRepository(p) 1026 return repo, err 1027 } 1028 1029 var ( 1030 // ErrHeadUnborn is raised when there are no commits yet in the git repo 1031 ErrHeadUnborn = errors.New("Head commit not found") 1032 ) 1033 1034 func lookupCommit(repo *git.Repository, hash ...string) (*git.Commit, error) { 1035 var ( 1036 oid *git.Oid 1037 err error 1038 ) 1039 if len(hash) > 0 { 1040 oid, err = git.NewOid(hash[0]) 1041 if err != nil { 1042 return nil, err 1043 } 1044 } else { 1045 headUnborn, err := repo.IsHeadUnborn() 1046 if err != nil { 1047 return nil, err 1048 } 1049 if headUnborn { 1050 return nil, ErrHeadUnborn 1051 } 1052 1053 headRef, err := repo.Head() 1054 if err != nil { 1055 return nil, err 1056 } 1057 defer headRef.Free() 1058 oid = headRef.Target() 1059 } 1060 1061 commit, err := repo.LookupCommit(oid) 1062 if err != nil { 1063 return nil, err 1064 } 1065 1066 return commit, nil 1067 } 1068 1069 // Status contains the git file statuses 1070 type Status struct { 1071 Files []fileStatus 1072 } 1073 1074 // NewStatus create a Status struct for a git repo 1075 func NewStatus(wd ...string) (Status, error) { 1076 defer util.Profile()() 1077 1078 var ( 1079 repo *git.Repository 1080 err error 1081 ) 1082 status := Status{} 1083 1084 if len(wd) > 0 { 1085 repo, err = openRepository(wd[0]) 1086 } else { 1087 repo, err = openRepository() 1088 } 1089 if err != nil { 1090 return status, err 1091 } 1092 defer repo.Free() 1093 1094 //TODO: research what status options to set 1095 opts := &git.StatusOptions{} 1096 opts.Show = git.StatusShowIndexAndWorkdir 1097 opts.Flags = git.StatusOptIncludeUntracked | git.StatusOptRenamesHeadToIndex | git.StatusOptSortCaseSensitively 1098 statusList, err := repo.StatusList(opts) 1099 1100 if err != nil { 1101 return status, err 1102 } 1103 defer statusList.Free() 1104 1105 cnt, err := statusList.EntryCount() 1106 if err != nil { 1107 return status, err 1108 } 1109 1110 for i := 0; i < cnt; i++ { 1111 entry, err := statusList.ByIndex(i) 1112 if err != nil { 1113 return status, err 1114 } 1115 status.AddFile(entry) 1116 } 1117 1118 return status, nil 1119 } 1120 1121 // AddFile adds a StatusEntry for each file in working and staging directories 1122 func (s *Status) AddFile(e git.StatusEntry) { 1123 var path string 1124 if e.Status == git.StatusIndexNew || 1125 e.Status == git.StatusIndexModified || 1126 e.Status == git.StatusIndexDeleted || 1127 e.Status == git.StatusIndexRenamed || 1128 e.Status == git.StatusIndexTypeChange { 1129 path = filepath.ToSlash(e.HeadToIndex.NewFile.Path) 1130 } else { 1131 path = filepath.ToSlash(e.IndexToWorkdir.NewFile.Path) 1132 } 1133 s.Files = append(s.Files, fileStatus{Path: path, Status: e.Status}) 1134 } 1135 1136 // HasStaged returns true if there are any files in staging 1137 func (s *Status) HasStaged() bool { 1138 for _, f := range s.Files { 1139 if f.InStaging() { 1140 return true 1141 } 1142 } 1143 return false 1144 } 1145 1146 // IsModified returns true if the file is modified in either working or staging 1147 func (s *Status) IsModified(path string, staging bool) bool { 1148 path = filepath.ToSlash(path) 1149 for _, f := range s.Files { 1150 if path == f.Path && f.InStaging() == staging { 1151 return f.IsModified() 1152 } 1153 } 1154 return false 1155 } 1156 1157 // IsTracked returns true if file is tracked by the git repo 1158 func (s *Status) IsTracked(path string) bool { 1159 path = filepath.ToSlash(path) 1160 for _, f := range s.Files { 1161 if path == f.Path { 1162 return f.IsTracked() 1163 } 1164 } 1165 return false 1166 } 1167 1168 type fileStatus struct { 1169 Status git.Status 1170 Path string 1171 } 1172 1173 // InStaging returns true if the file is in staging 1174 func (f fileStatus) InStaging() bool { 1175 return f.Status == git.StatusIndexNew || 1176 f.Status == git.StatusIndexModified || 1177 f.Status == git.StatusIndexDeleted || 1178 f.Status == git.StatusIndexRenamed || 1179 f.Status == git.StatusIndexTypeChange 1180 } 1181 1182 // InWorking returns true if the file is in working 1183 func (f fileStatus) InWorking() bool { 1184 return f.Status == git.StatusWtModified || 1185 f.Status == git.StatusWtDeleted || 1186 f.Status == git.StatusWtRenamed || 1187 f.Status == git.StatusWtTypeChange 1188 } 1189 1190 // IsTracked returns true if the file is tracked by git 1191 func (f fileStatus) IsTracked() bool { 1192 return f.Status != git.StatusIgnored && 1193 f.Status != git.StatusWtNew 1194 } 1195 1196 // IsModified returns true if the file has been modified 1197 func (f fileStatus) IsModified() bool { 1198 return f.InStaging() || f.InWorking() 1199 }