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  }