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  }