v.io/jiri@v0.0.0-20160715023856-abfb8b131290/gitutil/git.go (about)

     1  // Copyright 2015 The Vanadium Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package gitutil
     6  
     7  import (
     8  	"bytes"
     9  	"fmt"
    10  	"os"
    11  	"os/exec"
    12  	"path/filepath"
    13  	"strconv"
    14  	"strings"
    15  
    16  	"v.io/jiri/runutil"
    17  )
    18  
    19  // PlatformSpecificGitArgs returns a git command line with platform specific,
    20  // if any, modifications. The code is duplicated here because of the dependency
    21  // structure in the jiri tool.
    22  // TODO(cnicolaou,bprosnitz): remove this once ssl certs are installed.
    23  func platformSpecificGitArgs(args ...string) []string {
    24  	if os.Getenv("FNL_SYSTEM") != "" {
    25  		// TODO(bprosnitz) Remove this after certificates are installed on FNL
    26  		// Disable SSL verification because certificates are not present on FNL.func
    27  		return append([]string{"-c", "http.sslVerify=false"}, args...)
    28  	}
    29  	return args
    30  }
    31  
    32  type GitError struct {
    33  	args        []string
    34  	output      string
    35  	errorOutput string
    36  }
    37  
    38  func Error(output, errorOutput string, args ...string) GitError {
    39  	return GitError{
    40  		args:        args,
    41  		output:      output,
    42  		errorOutput: errorOutput,
    43  	}
    44  }
    45  
    46  func (ge GitError) Error() string {
    47  	result := "'git "
    48  	result += strings.Join(ge.args, " ")
    49  	result += "' failed:\n"
    50  	result += ge.errorOutput
    51  	return result
    52  }
    53  
    54  type Git struct {
    55  	s       runutil.Sequence
    56  	opts    map[string]string
    57  	rootDir string
    58  }
    59  
    60  type gitOpt interface {
    61  	gitOpt()
    62  }
    63  type AuthorDateOpt string
    64  type CommitterDateOpt string
    65  type RootDirOpt string
    66  
    67  func (AuthorDateOpt) gitOpt()    {}
    68  func (CommitterDateOpt) gitOpt() {}
    69  func (RootDirOpt) gitOpt()       {}
    70  
    71  // New is the Git factory.
    72  func New(s runutil.Sequence, opts ...gitOpt) *Git {
    73  	rootDir := ""
    74  	env := map[string]string{}
    75  	for _, opt := range opts {
    76  		switch typedOpt := opt.(type) {
    77  		case AuthorDateOpt:
    78  			env["GIT_AUTHOR_DATE"] = string(typedOpt)
    79  		case CommitterDateOpt:
    80  			env["GIT_COMMITTER_DATE"] = string(typedOpt)
    81  		case RootDirOpt:
    82  			rootDir = string(typedOpt)
    83  		}
    84  	}
    85  	return &Git{
    86  		s:       s,
    87  		opts:    env,
    88  		rootDir: rootDir,
    89  	}
    90  }
    91  
    92  // Add adds a file to staging.
    93  func (g *Git) Add(file string) error {
    94  	return g.run("add", file)
    95  }
    96  
    97  // AddRemote adds a new remote with the given name and path.
    98  func (g *Git) AddRemote(name, path string) error {
    99  	return g.run("remote", "add", name, path)
   100  }
   101  
   102  // BranchExists tests whether a branch with the given name exists in
   103  // the local repository.
   104  func (g *Git) BranchExists(branch string) bool {
   105  	return g.run("show-branch", branch) == nil
   106  }
   107  
   108  // BranchesDiffer tests whether two branches have any changes between them.
   109  func (g *Git) BranchesDiffer(branch1, branch2 string) (bool, error) {
   110  	out, err := g.runOutput("--no-pager", "diff", "--name-only", branch1+".."+branch2)
   111  	if err != nil {
   112  		return false, err
   113  	}
   114  	// If output is empty, then there is no difference.
   115  	if len(out) == 0 {
   116  		return false, nil
   117  	}
   118  	// Otherwise there is a difference.
   119  	return true, nil
   120  }
   121  
   122  // CheckoutBranch checks out the given branch.
   123  func (g *Git) CheckoutBranch(branch string, opts ...CheckoutOpt) error {
   124  	args := []string{"checkout"}
   125  	force := false
   126  	for _, opt := range opts {
   127  		switch typedOpt := opt.(type) {
   128  		case ForceOpt:
   129  			force = bool(typedOpt)
   130  		}
   131  	}
   132  	if force {
   133  		args = append(args, "-f")
   134  	}
   135  	args = append(args, branch)
   136  	return g.run(args...)
   137  }
   138  
   139  // Clone clones the given repository to the given local path.
   140  func (g *Git) Clone(repo, path string) error {
   141  	return g.run("clone", repo, path)
   142  }
   143  
   144  // CloneRecursive clones the given repository recursively to the given local path.
   145  func (g *Git) CloneRecursive(repo, path string) error {
   146  	return g.run("clone", "--recursive", repo, path)
   147  }
   148  
   149  // Commit commits all files in staging with an empty message.
   150  func (g *Git) Commit() error {
   151  	return g.run("commit", "--allow-empty", "--allow-empty-message", "--no-edit")
   152  }
   153  
   154  // CommitAmend amends the previous commit with the currently staged
   155  // changes. Empty commits are allowed.
   156  func (g *Git) CommitAmend() error {
   157  	return g.run("commit", "--amend", "--allow-empty", "--no-edit")
   158  }
   159  
   160  // CommitAmendWithMessage amends the previous commit with the
   161  // currently staged changes, and the given message. Empty commits are
   162  // allowed.
   163  func (g *Git) CommitAmendWithMessage(message string) error {
   164  	return g.run("commit", "--amend", "--allow-empty", "-m", message)
   165  }
   166  
   167  // CommitAndEdit commits all files in staging and allows the user to
   168  // edit the commit message.
   169  func (g *Git) CommitAndEdit() error {
   170  	args := []string{"commit", "--allow-empty"}
   171  	return g.runInteractive(args...)
   172  }
   173  
   174  // CommitFile commits the given file with the given commit message.
   175  func (g *Git) CommitFile(fileName, message string) error {
   176  	if err := g.Add(fileName); err != nil {
   177  		return err
   178  	}
   179  	return g.CommitWithMessage(message)
   180  }
   181  
   182  // CommitMessages returns the concatenation of all commit messages on
   183  // <branch> that are not also on <baseBranch>.
   184  func (g *Git) CommitMessages(branch, baseBranch string) (string, error) {
   185  	out, err := g.runOutput("log", "--no-merges", baseBranch+".."+branch)
   186  	if err != nil {
   187  		return "", err
   188  	}
   189  	return strings.Join(out, "\n"), nil
   190  }
   191  
   192  // CommitNoVerify commits all files in staging with the given
   193  // message and skips all git-hooks.
   194  func (g *Git) CommitNoVerify(message string) error {
   195  	return g.run("commit", "--allow-empty", "--allow-empty-message", "--no-verify", "-m", message)
   196  }
   197  
   198  // CommitWithMessage commits all files in staging with the given
   199  // message.
   200  func (g *Git) CommitWithMessage(message string) error {
   201  	return g.run("commit", "--allow-empty", "--allow-empty-message", "-m", message)
   202  }
   203  
   204  // CommitWithMessage commits all files in staging and allows the user
   205  // to edit the commit message. The given message will be used as the
   206  // default.
   207  func (g *Git) CommitWithMessageAndEdit(message string) error {
   208  	args := []string{"commit", "--allow-empty", "-e", "-m", message}
   209  	return g.runInteractive(args...)
   210  }
   211  
   212  // Committers returns a list of committers for the current repository
   213  // along with the number of their commits.
   214  func (g *Git) Committers() ([]string, error) {
   215  	out, err := g.runOutput("shortlog", "-s", "-n", "-e")
   216  	if err != nil {
   217  		return nil, err
   218  	}
   219  	return out, nil
   220  }
   221  
   222  // CountCommits returns the number of commits on <branch> that are not
   223  // on <base>.
   224  func (g *Git) CountCommits(branch, base string) (int, error) {
   225  	args := []string{"rev-list", "--count", branch}
   226  	if base != "" {
   227  		args = append(args, "^"+base)
   228  	}
   229  	args = append(args, "--")
   230  	out, err := g.runOutput(args...)
   231  	if err != nil {
   232  		return 0, err
   233  	}
   234  	if got, want := len(out), 1; got != want {
   235  		return 0, fmt.Errorf("unexpected length of %v: got %v, want %v", out, got, want)
   236  	}
   237  	count, err := strconv.Atoi(out[0])
   238  	if err != nil {
   239  		return 0, fmt.Errorf("Atoi(%v) failed: %v", out[0], err)
   240  	}
   241  	return count, nil
   242  }
   243  
   244  // CreateBranch creates a new branch with the given name.
   245  func (g *Git) CreateBranch(branch string) error {
   246  	return g.run("branch", branch)
   247  }
   248  
   249  // CreateAndCheckoutBranch creates a new branch with the given name
   250  // and checks it out.
   251  func (g *Git) CreateAndCheckoutBranch(branch string) error {
   252  	return g.run("checkout", "-b", branch)
   253  }
   254  
   255  // CreateBranchWithUpstream creates a new branch and sets the upstream
   256  // repository to the given upstream.
   257  func (g *Git) CreateBranchWithUpstream(branch, upstream string) error {
   258  	return g.run("branch", branch, upstream)
   259  }
   260  
   261  // CurrentBranchName returns the name of the current branch.
   262  func (g *Git) CurrentBranchName() (string, error) {
   263  	out, err := g.runOutput("rev-parse", "--abbrev-ref", "HEAD")
   264  	if err != nil {
   265  		return "", err
   266  	}
   267  	if got, want := len(out), 1; got != want {
   268  		return "", fmt.Errorf("unexpected length of %v: got %v, want %v", out, got, want)
   269  	}
   270  	return out[0], nil
   271  }
   272  
   273  // CurrentRevision returns the current revision.
   274  func (g *Git) CurrentRevision() (string, error) {
   275  	return g.CurrentRevisionOfBranch("HEAD")
   276  }
   277  
   278  // CurrentRevisionOfBranch returns the current revision of the given branch.
   279  func (g *Git) CurrentRevisionOfBranch(branch string) (string, error) {
   280  	out, err := g.runOutput("rev-parse", branch)
   281  	if err != nil {
   282  		return "", err
   283  	}
   284  	if got, want := len(out), 1; got != want {
   285  		return "", fmt.Errorf("unexpected length of %v: got %v, want %v", out, got, want)
   286  	}
   287  	return out[0], nil
   288  }
   289  
   290  // DeleteBranch deletes the given branch.
   291  func (g *Git) DeleteBranch(branch string, opts ...DeleteBranchOpt) error {
   292  	args := []string{"branch"}
   293  	force := false
   294  	for _, opt := range opts {
   295  		switch typedOpt := opt.(type) {
   296  		case ForceOpt:
   297  			force = bool(typedOpt)
   298  		}
   299  	}
   300  	if force {
   301  		args = append(args, "-D")
   302  	} else {
   303  		args = append(args, "-d")
   304  	}
   305  	args = append(args, branch)
   306  	return g.run(args...)
   307  }
   308  
   309  // DirExistsOnBranch returns true if a directory with the given name
   310  // exists on the branch.  If branch is empty it defaults to "master".
   311  func (g *Git) DirExistsOnBranch(dir, branch string) bool {
   312  	if dir == "." {
   313  		dir = ""
   314  	}
   315  	if branch == "" {
   316  		branch = "master"
   317  	}
   318  	args := []string{"ls-tree", "-d", branch + ":" + dir}
   319  	return g.run(args...) == nil
   320  }
   321  
   322  // Fetch fetches refs and tags from the given remote.
   323  func (g *Git) Fetch(remote string, opts ...FetchOpt) error {
   324  	return g.FetchRefspec(remote, "", opts...)
   325  }
   326  
   327  // FetchRefspec fetches refs and tags from the given remote for a particular refspec.
   328  func (g *Git) FetchRefspec(remote, refspec string, opts ...FetchOpt) error {
   329  	args := []string{"fetch"}
   330  	tags := false
   331  	for _, opt := range opts {
   332  		switch typedOpt := opt.(type) {
   333  		case TagsOpt:
   334  			tags = bool(typedOpt)
   335  		}
   336  	}
   337  	if tags {
   338  		args = append(args, "--tags")
   339  	}
   340  
   341  	args = append(args, remote)
   342  	if refspec != "" {
   343  		args = append(args, refspec)
   344  	}
   345  
   346  	return g.run(args...)
   347  }
   348  
   349  // FilesWithUncommittedChanges returns the list of files that have
   350  // uncommitted changes.
   351  func (g *Git) FilesWithUncommittedChanges() ([]string, error) {
   352  	out, err := g.runOutput("diff", "--name-only", "--no-ext-diff")
   353  	if err != nil {
   354  		return nil, err
   355  	}
   356  	out2, err := g.runOutput("diff", "--cached", "--name-only", "--no-ext-diff")
   357  	if err != nil {
   358  		return nil, err
   359  	}
   360  	return append(out, out2...), nil
   361  }
   362  
   363  // GetBranches returns a slice of the local branches of the current
   364  // repository, followed by the name of the current branch. The
   365  // behavior can be customized by providing optional arguments
   366  // (e.g. --merged).
   367  func (g *Git) GetBranches(args ...string) ([]string, string, error) {
   368  	args = append([]string{"branch"}, args...)
   369  	out, err := g.runOutput(args...)
   370  	if err != nil {
   371  		return nil, "", err
   372  	}
   373  	branches, current := []string{}, ""
   374  	for _, branch := range out {
   375  		if strings.HasPrefix(branch, "*") {
   376  			branch = strings.TrimSpace(strings.TrimPrefix(branch, "*"))
   377  			current = branch
   378  		}
   379  		branches = append(branches, strings.TrimSpace(branch))
   380  	}
   381  	return branches, current, nil
   382  }
   383  
   384  // HasUncommittedChanges checks whether the current branch contains
   385  // any uncommitted changes.
   386  func (g *Git) HasUncommittedChanges() (bool, error) {
   387  	out, err := g.FilesWithUncommittedChanges()
   388  	if err != nil {
   389  		return false, err
   390  	}
   391  	return len(out) != 0, nil
   392  }
   393  
   394  // HasUntrackedFiles checks whether the current branch contains any
   395  // untracked files.
   396  func (g *Git) HasUntrackedFiles() (bool, error) {
   397  	out, err := g.UntrackedFiles()
   398  	if err != nil {
   399  		return false, err
   400  	}
   401  	return len(out) != 0, nil
   402  }
   403  
   404  // Init initializes a new git repository.
   405  func (g *Git) Init(path string) error {
   406  	return g.run("init", path)
   407  }
   408  
   409  // IsFileCommitted tests whether the given file has been committed to
   410  // the repository.
   411  func (g *Git) IsFileCommitted(file string) bool {
   412  	// Check if file is still in staging enviroment.
   413  	if out, _ := g.runOutput("status", "--porcelain", file); len(out) > 0 {
   414  		return false
   415  	}
   416  	// Check if file is unknown to git.
   417  	return g.run("ls-files", file, "--error-unmatch") == nil
   418  }
   419  
   420  // LatestCommitMessage returns the latest commit message on the
   421  // current branch.
   422  func (g *Git) LatestCommitMessage() (string, error) {
   423  	out, err := g.runOutput("log", "-n", "1", "--format=format:%B")
   424  	if err != nil {
   425  		return "", err
   426  	}
   427  	return strings.Join(out, "\n"), nil
   428  }
   429  
   430  // Log returns a list of commits on <branch> that are not on <base>,
   431  // using the specified format.
   432  func (g *Git) Log(branch, base, format string) ([][]string, error) {
   433  	n, err := g.CountCommits(branch, base)
   434  	if err != nil {
   435  		return nil, err
   436  	}
   437  	result := [][]string{}
   438  	for i := 0; i < n; i++ {
   439  		skipArg := fmt.Sprintf("--skip=%d", i)
   440  		formatArg := fmt.Sprintf("--format=%s", format)
   441  		branchArg := fmt.Sprintf("%v..%v", base, branch)
   442  		out, err := g.runOutput("log", "-1", skipArg, formatArg, branchArg)
   443  		if err != nil {
   444  			return nil, err
   445  		}
   446  		result = append(result, out)
   447  	}
   448  	return result, nil
   449  }
   450  
   451  // Merge merges all commits from <branch> to the current branch. If
   452  // <squash> is set, then all merged commits are squashed into a single
   453  // commit.
   454  func (g *Git) Merge(branch string, opts ...MergeOpt) error {
   455  	args := []string{"merge"}
   456  	squash := false
   457  	strategy := ""
   458  	resetOnFailure := true
   459  	for _, opt := range opts {
   460  		switch typedOpt := opt.(type) {
   461  		case SquashOpt:
   462  			squash = bool(typedOpt)
   463  		case StrategyOpt:
   464  			strategy = string(typedOpt)
   465  		case ResetOnFailureOpt:
   466  			resetOnFailure = bool(typedOpt)
   467  		}
   468  	}
   469  	if squash {
   470  		args = append(args, "--squash")
   471  	} else {
   472  		args = append(args, "--no-squash")
   473  	}
   474  	if strategy != "" {
   475  		args = append(args, fmt.Sprintf("--strategy=%v", strategy))
   476  	}
   477  	args = append(args, branch)
   478  	if out, err := g.runOutput(args...); err != nil {
   479  		if resetOnFailure {
   480  			if err2 := g.run("reset", "--merge"); err2 != nil {
   481  				return fmt.Errorf("%v\nCould not git reset while recovering from error: %v", err, err2)
   482  			}
   483  		}
   484  		return fmt.Errorf("%v\n%v", err, strings.Join(out, "\n"))
   485  	}
   486  	return nil
   487  }
   488  
   489  // MergeInProgress returns a boolean flag that indicates if a merge
   490  // operation is in progress for the current repository.
   491  func (g *Git) MergeInProgress() (bool, error) {
   492  	repoRoot, err := g.TopLevel()
   493  	if err != nil {
   494  		return false, err
   495  	}
   496  	mergeFile := filepath.Join(repoRoot, ".git", "MERGE_HEAD")
   497  	if _, err := g.s.Stat(mergeFile); err != nil {
   498  		if runutil.IsNotExist(err) {
   499  			return false, nil
   500  		}
   501  		return false, err
   502  	}
   503  	return true, nil
   504  }
   505  
   506  // ModifiedFiles returns a slice of filenames that have changed
   507  // between <baseBranch> and <currentBranch>.
   508  func (g *Git) ModifiedFiles(baseBranch, currentBranch string) ([]string, error) {
   509  	out, err := g.runOutput("diff", "--name-only", baseBranch+".."+currentBranch)
   510  	if err != nil {
   511  		return nil, err
   512  	}
   513  	return out, nil
   514  }
   515  
   516  // Pull pulls the given branch from the given remote.
   517  func (g *Git) Pull(remote, branch string) error {
   518  	if out, err := g.runOutput("pull", remote, branch); err != nil {
   519  		g.run("reset", "--merge")
   520  		return fmt.Errorf("%v\n%v", err, strings.Join(out, "\n"))
   521  	}
   522  	major, minor, err := g.Version()
   523  	if err != nil {
   524  		return err
   525  	}
   526  	// Starting with git 1.8, "git pull <remote> <branch>" does not
   527  	// create the branch "<remote>/<branch>" locally. To avoid the need
   528  	// to account for this, run "git pull", which fails but creates the
   529  	// missing branch, for git 1.7 and older.
   530  	if major < 2 && minor < 8 {
   531  		// This command is expected to fail (with desirable side effects).
   532  		// Use exec.Command instead of runner to prevent this failure from
   533  		// showing up in the console and confusing people.
   534  		command := exec.Command("git", "pull")
   535  		command.Run()
   536  	}
   537  	return nil
   538  }
   539  
   540  // Push pushes the given branch to the given remote.
   541  func (g *Git) Push(remote, branch string, opts ...PushOpt) error {
   542  	args := []string{"push"}
   543  	force := false
   544  	verify := true
   545  	// TODO(youngseokyoon): consider making followTags option default to true, after verifying that
   546  	// it works well for the madb repository.
   547  	followTags := false
   548  	for _, opt := range opts {
   549  		switch typedOpt := opt.(type) {
   550  		case ForceOpt:
   551  			force = bool(typedOpt)
   552  		case VerifyOpt:
   553  			verify = bool(typedOpt)
   554  		case FollowTagsOpt:
   555  			followTags = bool(typedOpt)
   556  		}
   557  	}
   558  	if force {
   559  		args = append(args, "--force")
   560  	}
   561  	if verify {
   562  		args = append(args, "--verify")
   563  	} else {
   564  		args = append(args, "--no-verify")
   565  	}
   566  	if followTags {
   567  		args = append(args, "--follow-tags")
   568  	}
   569  	args = append(args, remote, branch)
   570  	return g.run(args...)
   571  }
   572  
   573  // Rebase rebases to a particular upstream branch.
   574  func (g *Git) Rebase(upstream string) error {
   575  	return g.run("rebase", upstream)
   576  }
   577  
   578  // RebaseAbort aborts an in-progress rebase operation.
   579  func (g *Git) RebaseAbort() error {
   580  	return g.run("rebase", "--abort")
   581  }
   582  
   583  // Remove removes the given files.
   584  func (g *Git) Remove(fileNames ...string) error {
   585  	args := []string{"rm"}
   586  	args = append(args, fileNames...)
   587  	return g.run(args...)
   588  }
   589  
   590  // RemoteUrl gets the url of the remote with the given name.
   591  func (g *Git) RemoteUrl(name string) (string, error) {
   592  	configKey := fmt.Sprintf("remote.%s.url", name)
   593  	out, err := g.runOutput("config", "--get", configKey)
   594  	if err != nil {
   595  		return "", err
   596  	}
   597  	if got, want := len(out), 1; got != want {
   598  		return "", fmt.Errorf("RemoteUrl: unexpected length of remotes %v: got %v, want %v", out, got, want)
   599  	}
   600  	return out[0], nil
   601  }
   602  
   603  // RemoveUntrackedFiles removes untracked files and directories.
   604  func (g *Git) RemoveUntrackedFiles() error {
   605  	return g.run("clean", "-d", "-f")
   606  }
   607  
   608  // Reset resets the current branch to the target, discarding any
   609  // uncommitted changes.
   610  func (g *Git) Reset(target string, opts ...ResetOpt) error {
   611  	args := []string{"reset"}
   612  	mode := "hard"
   613  	for _, opt := range opts {
   614  		switch typedOpt := opt.(type) {
   615  		case ModeOpt:
   616  			mode = string(typedOpt)
   617  		}
   618  	}
   619  	args = append(args, fmt.Sprintf("--%v", mode), target, "--")
   620  	return g.run(args...)
   621  }
   622  
   623  // SetRemoteUrl sets the url of the remote with given name to the given url.
   624  func (g *Git) SetRemoteUrl(name, url string) error {
   625  	return g.run("remote", "set-url", name, url)
   626  }
   627  
   628  // Stash attempts to stash any unsaved changes. It returns true if
   629  // anything was actually stashed, otherwise false. An error is
   630  // returned if the stash command fails.
   631  func (g *Git) Stash() (bool, error) {
   632  	oldSize, err := g.StashSize()
   633  	if err != nil {
   634  		return false, err
   635  	}
   636  	if err := g.run("stash", "save"); err != nil {
   637  		return false, err
   638  	}
   639  	newSize, err := g.StashSize()
   640  	if err != nil {
   641  		return false, err
   642  	}
   643  	return newSize > oldSize, nil
   644  }
   645  
   646  // StashSize returns the size of the stash stack.
   647  func (g *Git) StashSize() (int, error) {
   648  	out, err := g.runOutput("stash", "list")
   649  	if err != nil {
   650  		return 0, err
   651  	}
   652  	// If output is empty, then stash is empty.
   653  	if len(out) == 0 {
   654  		return 0, nil
   655  	}
   656  	// Otherwise, stash size is the length of the output.
   657  	return len(out), nil
   658  }
   659  
   660  // StashPop pops the stash into the current working tree.
   661  func (g *Git) StashPop() error {
   662  	return g.run("stash", "pop")
   663  }
   664  
   665  // TopLevel returns the top level path of the current repository.
   666  func (g *Git) TopLevel() (string, error) {
   667  	// TODO(sadovsky): If g.rootDir is set, perhaps simply return that?
   668  	out, err := g.runOutput("rev-parse", "--show-toplevel")
   669  	if err != nil {
   670  		return "", err
   671  	}
   672  	return strings.Join(out, "\n"), nil
   673  }
   674  
   675  // TrackedFiles returns the list of files that are tracked.
   676  func (g *Git) TrackedFiles() ([]string, error) {
   677  	out, err := g.runOutput("ls-files")
   678  	if err != nil {
   679  		return nil, err
   680  	}
   681  	return out, nil
   682  }
   683  
   684  // UntrackedFiles returns the list of files that are not tracked.
   685  func (g *Git) UntrackedFiles() ([]string, error) {
   686  	out, err := g.runOutput("ls-files", "--others", "--directory", "--exclude-standard")
   687  	if err != nil {
   688  		return nil, err
   689  	}
   690  	return out, nil
   691  }
   692  
   693  // Version returns the major and minor git version.
   694  func (g *Git) Version() (int, int, error) {
   695  	out, err := g.runOutput("version")
   696  	if err != nil {
   697  		return 0, 0, err
   698  	}
   699  	if got, want := len(out), 1; got != want {
   700  		return 0, 0, fmt.Errorf("unexpected length of %v: got %v, want %v", out, got, want)
   701  	}
   702  	words := strings.Split(out[0], " ")
   703  	if got, want := len(words), 3; got < want {
   704  		return 0, 0, fmt.Errorf("unexpected length of %v: got %v, want at least %v", words, got, want)
   705  	}
   706  	version := strings.Split(words[2], ".")
   707  	if got, want := len(version), 3; got < want {
   708  		return 0, 0, fmt.Errorf("unexpected length of %v: got %v, want at least %v", version, got, want)
   709  	}
   710  	major, err := strconv.Atoi(version[0])
   711  	if err != nil {
   712  		return 0, 0, fmt.Errorf("failed parsing %q to integer", major)
   713  	}
   714  	minor, err := strconv.Atoi(version[1])
   715  	if err != nil {
   716  		return 0, 0, fmt.Errorf("failed parsing %q to integer", minor)
   717  	}
   718  	return major, minor, nil
   719  }
   720  
   721  func (g *Git) run(args ...string) error {
   722  	var stdout, stderr bytes.Buffer
   723  	capture := func(s runutil.Sequence) runutil.Sequence { return s.Capture(&stdout, &stderr) }
   724  	if err := g.runWithFn(capture, args...); err != nil {
   725  		return Error(stdout.String(), stderr.String(), args...)
   726  	}
   727  	return nil
   728  }
   729  
   730  func trimOutput(o string) []string {
   731  	output := strings.TrimSpace(o)
   732  	if len(output) == 0 {
   733  		return nil
   734  	}
   735  	return strings.Split(output, "\n")
   736  }
   737  
   738  func (g *Git) runOutput(args ...string) ([]string, error) {
   739  	var stdout, stderr bytes.Buffer
   740  	fn := func(s runutil.Sequence) runutil.Sequence { return s.Capture(&stdout, &stderr) }
   741  	if err := g.runWithFn(fn, args...); err != nil {
   742  		return nil, Error(stdout.String(), stderr.String(), args...)
   743  	}
   744  	return trimOutput(stdout.String()), nil
   745  }
   746  
   747  func (g *Git) runInteractive(args ...string) error {
   748  	var stderr bytes.Buffer
   749  	// In order for the editing to work correctly with
   750  	// terminal-based editors, notably "vim", use os.Stdout.
   751  	capture := func(s runutil.Sequence) runutil.Sequence { return s.Capture(os.Stdout, &stderr) }
   752  	if err := g.runWithFn(capture, args...); err != nil {
   753  		return Error("", stderr.String(), args...)
   754  	}
   755  	return nil
   756  }
   757  
   758  func (g *Git) runWithFn(fn func(s runutil.Sequence) runutil.Sequence, args ...string) error {
   759  	g.s.Dir(g.rootDir)
   760  	args = platformSpecificGitArgs(args...)
   761  	if fn == nil {
   762  		fn = func(s runutil.Sequence) runutil.Sequence { return s }
   763  	}
   764  	return fn(g.s).Env(g.opts).Last("git", args...)
   765  }
   766  
   767  // Committer encapsulates the process of create a commit.
   768  type Committer struct {
   769  	commit            func() error
   770  	commitWithMessage func(message string) error
   771  }
   772  
   773  // Commit creates a commit.
   774  func (c *Committer) Commit(message string) error {
   775  	if len(message) == 0 {
   776  		// No commit message supplied, let git supply one.
   777  		return c.commit()
   778  	}
   779  	return c.commitWithMessage(message)
   780  }
   781  
   782  // NewCommitter is the Committer factory. The boolean <edit> flag
   783  // determines whether the commit commands should prompt users to edit
   784  // the commit message. This flag enables automated testing.
   785  func (g *Git) NewCommitter(edit bool) *Committer {
   786  	if edit {
   787  		return &Committer{
   788  			commit:            g.CommitAndEdit,
   789  			commitWithMessage: g.CommitWithMessageAndEdit,
   790  		}
   791  	} else {
   792  		return &Committer{
   793  			commit:            g.Commit,
   794  			commitWithMessage: g.CommitWithMessage,
   795  		}
   796  	}
   797  }