github.com/andrewrech/lazygit@v0.8.1/pkg/commands/git.go (about)

     1  package commands
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"os"
     7  	"os/exec"
     8  	"strings"
     9  
    10  	"github.com/mgutz/str"
    11  
    12  	"github.com/go-errors/errors"
    13  
    14  	"github.com/jesseduffield/lazygit/pkg/config"
    15  	"github.com/jesseduffield/lazygit/pkg/i18n"
    16  	"github.com/jesseduffield/lazygit/pkg/utils"
    17  	"github.com/sirupsen/logrus"
    18  	gitconfig "github.com/tcnksm/go-gitconfig"
    19  	gogit "gopkg.in/src-d/go-git.v4"
    20  )
    21  
    22  func verifyInGitRepo(runCmd func(string) error) error {
    23  	return runCmd("git status")
    24  }
    25  
    26  func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir func(string) error) error {
    27  	for {
    28  		_, err := stat(".git")
    29  
    30  		if err == nil {
    31  			return nil
    32  		}
    33  
    34  		if !os.IsNotExist(err) {
    35  			return WrapError(err)
    36  		}
    37  
    38  		if err = chdir(".."); err != nil {
    39  			return WrapError(err)
    40  		}
    41  	}
    42  }
    43  
    44  func setupRepositoryAndWorktree(openGitRepository func(string) (*gogit.Repository, error), sLocalize func(string) string) (repository *gogit.Repository, worktree *gogit.Worktree, err error) {
    45  	repository, err = openGitRepository(".")
    46  
    47  	if err != nil {
    48  		if strings.Contains(err.Error(), `unquoted '\' must be followed by new line`) {
    49  			return nil, nil, errors.New(sLocalize("GitconfigParseErr"))
    50  		}
    51  
    52  		return
    53  	}
    54  
    55  	worktree, err = repository.Worktree()
    56  
    57  	if err != nil {
    58  		return
    59  	}
    60  
    61  	return
    62  }
    63  
    64  // GitCommand is our main git interface
    65  type GitCommand struct {
    66  	Log                *logrus.Entry
    67  	OSCommand          *OSCommand
    68  	Worktree           *gogit.Worktree
    69  	Repo               *gogit.Repository
    70  	Tr                 *i18n.Localizer
    71  	Config             config.AppConfigurer
    72  	getGlobalGitConfig func(string) (string, error)
    73  	getLocalGitConfig  func(string) (string, error)
    74  	removeFile         func(string) error
    75  	DotGitDir          string
    76  }
    77  
    78  // NewGitCommand it runs git commands
    79  func NewGitCommand(log *logrus.Entry, osCommand *OSCommand, tr *i18n.Localizer, config config.AppConfigurer) (*GitCommand, error) {
    80  	var worktree *gogit.Worktree
    81  	var repo *gogit.Repository
    82  
    83  	fs := []func() error{
    84  		func() error {
    85  			return verifyInGitRepo(osCommand.RunCommand)
    86  		},
    87  		func() error {
    88  			return navigateToRepoRootDirectory(os.Stat, os.Chdir)
    89  		},
    90  		func() error {
    91  			var err error
    92  			repo, worktree, err = setupRepositoryAndWorktree(gogit.PlainOpen, tr.SLocalize)
    93  			return err
    94  		},
    95  	}
    96  
    97  	for _, f := range fs {
    98  		if err := f(); err != nil {
    99  			return nil, err
   100  		}
   101  	}
   102  
   103  	dotGitDir, err := findDotGitDir(os.Stat, ioutil.ReadFile)
   104  	if err != nil {
   105  		return nil, err
   106  	}
   107  
   108  	return &GitCommand{
   109  		Log:                log,
   110  		OSCommand:          osCommand,
   111  		Tr:                 tr,
   112  		Worktree:           worktree,
   113  		Repo:               repo,
   114  		Config:             config,
   115  		getGlobalGitConfig: gitconfig.Global,
   116  		getLocalGitConfig:  gitconfig.Local,
   117  		removeFile:         os.RemoveAll,
   118  		DotGitDir:          dotGitDir,
   119  	}, nil
   120  }
   121  
   122  func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filename string) ([]byte, error)) (string, error) {
   123  	f, err := stat(".git")
   124  	if err != nil {
   125  		return "", err
   126  	}
   127  
   128  	if f.IsDir() {
   129  		return ".git", nil
   130  	}
   131  
   132  	fileBytes, err := readFile(".git")
   133  	if err != nil {
   134  		return "", err
   135  	}
   136  	fileContent := string(fileBytes)
   137  	if !strings.HasPrefix(fileContent, "gitdir: ") {
   138  		return "", errors.New(".git is a file which suggests we are in a submodule but the file's contents do not contain a gitdir pointing to the actual .git directory")
   139  	}
   140  	return strings.TrimSpace(strings.TrimPrefix(fileContent, "gitdir: ")), nil
   141  }
   142  
   143  // GetStashEntries stash entryies
   144  func (c *GitCommand) GetStashEntries() []*StashEntry {
   145  	rawString, _ := c.OSCommand.RunCommandWithOutput("git stash list --pretty='%gs'")
   146  	stashEntries := []*StashEntry{}
   147  	for i, line := range utils.SplitLines(rawString) {
   148  		stashEntries = append(stashEntries, stashEntryFromLine(line, i))
   149  	}
   150  	return stashEntries
   151  }
   152  
   153  func stashEntryFromLine(line string, index int) *StashEntry {
   154  	return &StashEntry{
   155  		Name:          line,
   156  		Index:         index,
   157  		DisplayString: line,
   158  	}
   159  }
   160  
   161  // GetStashEntryDiff stash diff
   162  func (c *GitCommand) GetStashEntryDiff(index int) (string, error) {
   163  	return c.OSCommand.RunCommandWithOutput("git stash show -p --color stash@{" + fmt.Sprint(index) + "}")
   164  }
   165  
   166  // GetStatusFiles git status files
   167  func (c *GitCommand) GetStatusFiles() []*File {
   168  	statusOutput, _ := c.GitStatus()
   169  	statusStrings := utils.SplitLines(statusOutput)
   170  	files := []*File{}
   171  
   172  	for _, statusString := range statusStrings {
   173  		change := statusString[0:2]
   174  		stagedChange := change[0:1]
   175  		unstagedChange := statusString[1:2]
   176  		filename := c.OSCommand.Unquote(statusString[3:])
   177  		_, untracked := map[string]bool{"??": true, "A ": true, "AM": true}[change]
   178  		_, hasNoStagedChanges := map[string]bool{" ": true, "U": true, "?": true}[stagedChange]
   179  
   180  		file := &File{
   181  			Name:                    filename,
   182  			DisplayString:           statusString,
   183  			HasStagedChanges:        !hasNoStagedChanges,
   184  			HasUnstagedChanges:      unstagedChange != " ",
   185  			Tracked:                 !untracked,
   186  			Deleted:                 unstagedChange == "D" || stagedChange == "D",
   187  			HasMergeConflicts:       change == "UU" || change == "AA" || change == "DU",
   188  			HasInlineMergeConflicts: change == "UU" || change == "AA",
   189  			Type:                    c.OSCommand.FileType(filename),
   190  			ShortStatus:             change,
   191  		}
   192  		files = append(files, file)
   193  	}
   194  	return files
   195  }
   196  
   197  // StashDo modify stash
   198  func (c *GitCommand) StashDo(index int, method string) error {
   199  	return c.OSCommand.RunCommand(fmt.Sprintf("git stash %s stash@{%d}", method, index))
   200  }
   201  
   202  // StashSave save stash
   203  // TODO: before calling this, check if there is anything to save
   204  func (c *GitCommand) StashSave(message string) error {
   205  	return c.OSCommand.RunCommand(fmt.Sprintf("git stash save %s", c.OSCommand.Quote(message)))
   206  }
   207  
   208  // MergeStatusFiles merge status files
   209  func (c *GitCommand) MergeStatusFiles(oldFiles, newFiles []*File) []*File {
   210  	if len(oldFiles) == 0 {
   211  		return newFiles
   212  	}
   213  
   214  	appendedIndexes := []int{}
   215  
   216  	// retain position of files we already could see
   217  	result := []*File{}
   218  	for _, oldFile := range oldFiles {
   219  		for newIndex, newFile := range newFiles {
   220  			if oldFile.Name == newFile.Name {
   221  				result = append(result, newFile)
   222  				appendedIndexes = append(appendedIndexes, newIndex)
   223  				break
   224  			}
   225  		}
   226  	}
   227  
   228  	// append any new files to the end
   229  	for index, newFile := range newFiles {
   230  		if !includesInt(appendedIndexes, index) {
   231  			result = append(result, newFile)
   232  		}
   233  	}
   234  
   235  	return result
   236  }
   237  
   238  func includesInt(list []int, a int) bool {
   239  	for _, b := range list {
   240  		if b == a {
   241  			return true
   242  		}
   243  	}
   244  	return false
   245  }
   246  
   247  // ResetAndClean removes all unstaged changes and removes all untracked files
   248  func (c *GitCommand) ResetAndClean() error {
   249  	if err := c.ResetHardHead(); err != nil {
   250  		return err
   251  	}
   252  
   253  	return c.RemoveUntrackedFiles()
   254  }
   255  
   256  func (c *GitCommand) GetCurrentBranchUpstreamDifferenceCount() (string, string) {
   257  	return c.GetCommitDifferences("HEAD", "@{u}")
   258  }
   259  
   260  func (c *GitCommand) GetBranchUpstreamDifferenceCount(branchName string) (string, string) {
   261  	upstream := "origin" // hardcoded for now
   262  	return c.GetCommitDifferences(branchName, fmt.Sprintf("%s/%s", upstream, branchName))
   263  }
   264  
   265  // GetCommitDifferences checks how many pushables/pullables there are for the
   266  // current branch
   267  func (c *GitCommand) GetCommitDifferences(from, to string) (string, string) {
   268  	command := "git rev-list %s..%s --count"
   269  	pushableCount, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf(command, to, from))
   270  	if err != nil {
   271  		return "?", "?"
   272  	}
   273  	pullableCount, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf(command, from, to))
   274  	if err != nil {
   275  		return "?", "?"
   276  	}
   277  	return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount)
   278  }
   279  
   280  // RenameCommit renames the topmost commit with the given name
   281  func (c *GitCommand) RenameCommit(name string) error {
   282  	return c.OSCommand.RunCommand(fmt.Sprintf("git commit --allow-empty --amend -m %s", c.OSCommand.Quote(name)))
   283  }
   284  
   285  // RebaseBranch interactive rebases onto a branch
   286  func (c *GitCommand) RebaseBranch(branchName string) error {
   287  	cmd, err := c.PrepareInteractiveRebaseCommand(branchName, "", false)
   288  	if err != nil {
   289  		return err
   290  	}
   291  
   292  	return c.OSCommand.RunPreparedCommand(cmd)
   293  }
   294  
   295  // Fetch fetch git repo
   296  func (c *GitCommand) Fetch(unamePassQuestion func(string) string, canAskForCredentials bool) error {
   297  	return c.OSCommand.DetectUnamePass("git fetch", func(question string) string {
   298  		if canAskForCredentials {
   299  			return unamePassQuestion(question)
   300  		}
   301  		return "\n"
   302  	})
   303  }
   304  
   305  // ResetToCommit reset to commit
   306  func (c *GitCommand) ResetToCommit(sha string, strength string) error {
   307  	return c.OSCommand.RunCommand(fmt.Sprintf("git reset --%s %s", strength, sha))
   308  }
   309  
   310  // NewBranch create new branch
   311  func (c *GitCommand) NewBranch(name string) error {
   312  	return c.OSCommand.RunCommand(fmt.Sprintf("git checkout -b %s", name))
   313  }
   314  
   315  // CurrentBranchName is a function.
   316  func (c *GitCommand) CurrentBranchName() (string, error) {
   317  	branchName, err := c.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD")
   318  	if err != nil {
   319  		branchName, err = c.OSCommand.RunCommandWithOutput("git rev-parse --short HEAD")
   320  		if err != nil {
   321  			return "", err
   322  		}
   323  	}
   324  	return utils.TrimTrailingNewline(branchName), nil
   325  }
   326  
   327  // DeleteBranch delete branch
   328  func (c *GitCommand) DeleteBranch(branch string, force bool) error {
   329  	command := "git branch -d"
   330  
   331  	if force {
   332  		command = "git branch -D"
   333  	}
   334  
   335  	return c.OSCommand.RunCommand(fmt.Sprintf("%s %s", command, branch))
   336  }
   337  
   338  // ListStash list stash
   339  func (c *GitCommand) ListStash() (string, error) {
   340  	return c.OSCommand.RunCommandWithOutput("git stash list")
   341  }
   342  
   343  // Merge merge
   344  func (c *GitCommand) Merge(branchName string) error {
   345  	return c.OSCommand.RunCommand(fmt.Sprintf("git merge --no-edit %s", branchName))
   346  }
   347  
   348  // AbortMerge abort merge
   349  func (c *GitCommand) AbortMerge() error {
   350  	return c.OSCommand.RunCommand("git merge --abort")
   351  }
   352  
   353  // usingGpg tells us whether the user has gpg enabled so that we can know
   354  // whether we need to run a subprocess to allow them to enter their password
   355  func (c *GitCommand) usingGpg() bool {
   356  	gpgsign, _ := c.getLocalGitConfig("commit.gpgsign")
   357  	if gpgsign == "" {
   358  		gpgsign, _ = c.getGlobalGitConfig("commit.gpgsign")
   359  	}
   360  	value := strings.ToLower(gpgsign)
   361  
   362  	return value == "true" || value == "1" || value == "yes" || value == "on"
   363  }
   364  
   365  // Commit commits to git
   366  func (c *GitCommand) Commit(message string, flags string) (*exec.Cmd, error) {
   367  	command := fmt.Sprintf("git commit %s -m %s", flags, c.OSCommand.Quote(message))
   368  	if c.usingGpg() {
   369  		return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command), nil
   370  	}
   371  
   372  	return nil, c.OSCommand.RunCommand(command)
   373  }
   374  
   375  // AmendHead amends HEAD with whatever is staged in your working tree
   376  func (c *GitCommand) AmendHead() (*exec.Cmd, error) {
   377  	command := "git commit --amend --no-edit"
   378  	if c.usingGpg() {
   379  		return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command), nil
   380  	}
   381  
   382  	return nil, c.OSCommand.RunCommand(command)
   383  }
   384  
   385  // Pull pulls from repo
   386  func (c *GitCommand) Pull(ask func(string) string) error {
   387  	return c.OSCommand.DetectUnamePass("git pull --no-edit", ask)
   388  }
   389  
   390  // Push pushes to a branch
   391  func (c *GitCommand) Push(branchName string, force bool, ask func(string) string) error {
   392  	forceFlag := ""
   393  	if force {
   394  		forceFlag = "--force-with-lease "
   395  	}
   396  
   397  	cmd := fmt.Sprintf("git push %s-u origin %s", forceFlag, branchName)
   398  	return c.OSCommand.DetectUnamePass(cmd, ask)
   399  }
   400  
   401  // CatFile obtains the content of a file
   402  func (c *GitCommand) CatFile(fileName string) (string, error) {
   403  	return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("cat %s", c.OSCommand.Quote(fileName)))
   404  }
   405  
   406  // StageFile stages a file
   407  func (c *GitCommand) StageFile(fileName string) error {
   408  	return c.OSCommand.RunCommand(fmt.Sprintf("git add %s", c.OSCommand.Quote(fileName)))
   409  }
   410  
   411  // StageAll stages all files
   412  func (c *GitCommand) StageAll() error {
   413  	return c.OSCommand.RunCommand("git add -A")
   414  }
   415  
   416  // UnstageAll stages all files
   417  func (c *GitCommand) UnstageAll() error {
   418  	return c.OSCommand.RunCommand("git reset")
   419  }
   420  
   421  // UnStageFile unstages a file
   422  func (c *GitCommand) UnStageFile(fileName string, tracked bool) error {
   423  	command := "git rm --cached %s"
   424  	if tracked {
   425  		command = "git reset HEAD %s"
   426  	}
   427  
   428  	// renamed files look like "file1 -> file2"
   429  	fileNames := strings.Split(fileName, " -> ")
   430  	for _, name := range fileNames {
   431  		if err := c.OSCommand.RunCommand(fmt.Sprintf(command, c.OSCommand.Quote(name))); err != nil {
   432  			return err
   433  		}
   434  	}
   435  	return nil
   436  }
   437  
   438  // GitStatus returns the plaintext short status of the repo
   439  func (c *GitCommand) GitStatus() (string, error) {
   440  	return c.OSCommand.RunCommandWithOutput("git status --untracked-files=all --porcelain")
   441  }
   442  
   443  // IsInMergeState states whether we are still mid-merge
   444  func (c *GitCommand) IsInMergeState() (bool, error) {
   445  	output, err := c.OSCommand.RunCommandWithOutput("git status --untracked-files=all")
   446  	if err != nil {
   447  		return false, err
   448  	}
   449  	return strings.Contains(output, "conclude merge") || strings.Contains(output, "unmerged paths"), nil
   450  }
   451  
   452  // RebaseMode returns "" for non-rebase mode, "normal" for normal rebase
   453  // and "interactive" for interactive rebase
   454  func (c *GitCommand) RebaseMode() (string, error) {
   455  	exists, err := c.OSCommand.FileExists(fmt.Sprintf("%s/rebase-apply", c.DotGitDir))
   456  	if err != nil {
   457  		return "", err
   458  	}
   459  	if exists {
   460  		return "normal", nil
   461  	}
   462  	exists, err = c.OSCommand.FileExists(fmt.Sprintf("%s/rebase-merge", c.DotGitDir))
   463  	if exists {
   464  		return "interactive", err
   465  	} else {
   466  		return "", err
   467  	}
   468  }
   469  
   470  // DiscardAllFileChanges directly
   471  func (c *GitCommand) DiscardAllFileChanges(file *File) error {
   472  	// if the file isn't tracked, we assume you want to delete it
   473  	quotedFileName := c.OSCommand.Quote(file.Name)
   474  	if file.HasStagedChanges {
   475  		if err := c.OSCommand.RunCommand(fmt.Sprintf("git reset -- %s", quotedFileName)); err != nil {
   476  			return err
   477  		}
   478  	}
   479  	if !file.Tracked {
   480  		return c.removeFile(file.Name)
   481  	}
   482  	return c.DiscardUnstagedFileChanges(file)
   483  }
   484  
   485  // DiscardUnstagedFileChanges directly
   486  func (c *GitCommand) DiscardUnstagedFileChanges(file *File) error {
   487  	quotedFileName := c.OSCommand.Quote(file.Name)
   488  	return c.OSCommand.RunCommand(fmt.Sprintf("git checkout -- %s", quotedFileName))
   489  }
   490  
   491  // Checkout checks out a branch, with --force if you set the force arg to true
   492  func (c *GitCommand) Checkout(branch string, force bool) error {
   493  	forceArg := ""
   494  	if force {
   495  		forceArg = "--force "
   496  	}
   497  	return c.OSCommand.RunCommand(fmt.Sprintf("git checkout %s %s", forceArg, branch))
   498  }
   499  
   500  // AddPatch prepares a subprocess for adding a patch by patch
   501  // this will eventually be swapped out for a better solution inside the Gui
   502  func (c *GitCommand) AddPatch(filename string) *exec.Cmd {
   503  	return c.OSCommand.PrepareSubProcess("git", "add", "--patch", c.OSCommand.Quote(filename))
   504  }
   505  
   506  // PrepareCommitSubProcess prepares a subprocess for `git commit`
   507  func (c *GitCommand) PrepareCommitSubProcess() *exec.Cmd {
   508  	return c.OSCommand.PrepareSubProcess("git", "commit")
   509  }
   510  
   511  // PrepareCommitAmendSubProcess prepares a subprocess for `git commit --amend --allow-empty`
   512  func (c *GitCommand) PrepareCommitAmendSubProcess() *exec.Cmd {
   513  	return c.OSCommand.PrepareSubProcess("git", "commit", "--amend", "--allow-empty")
   514  }
   515  
   516  // GetBranchGraph gets the color-formatted graph of the log for the given branch
   517  // Currently it limits the result to 100 commits, but when we get async stuff
   518  // working we can do lazy loading
   519  func (c *GitCommand) GetBranchGraph(branchName string) (string, error) {
   520  	return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git log --graph --color --abbrev-commit --decorate --date=relative --pretty=medium -100 %s", branchName))
   521  }
   522  
   523  // Ignore adds a file to the gitignore for the repo
   524  func (c *GitCommand) Ignore(filename string) error {
   525  	return c.OSCommand.AppendLineToFile(".gitignore", filename)
   526  }
   527  
   528  // Show shows the diff of a commit
   529  func (c *GitCommand) Show(sha string) (string, error) {
   530  	show, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git show --color %s", sha))
   531  	if err != nil {
   532  		return "", err
   533  	}
   534  
   535  	// if this is a merge commit, we need to go a step further and get the diff between the two branches we merged
   536  	revList, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git rev-list -1 --merges %s^...%s", sha, sha))
   537  	if err != nil {
   538  		// turns out we get an error here when it's the first commit. We'll just return the original show
   539  		return show, nil
   540  	}
   541  	if len(revList) == 0 {
   542  		return show, nil
   543  	}
   544  
   545  	// we want to pull out 1a6a69a and 3b51d7c from this:
   546  	// commit ccc771d8b13d5b0d4635db4463556366470fd4f6
   547  	// Merge: 1a6a69a 3b51d7c
   548  	lines := utils.SplitLines(show)
   549  	if len(lines) < 2 {
   550  		return show, nil
   551  	}
   552  
   553  	secondLineWords := strings.Split(lines[1], " ")
   554  	if len(secondLineWords) < 3 {
   555  		return show, nil
   556  	}
   557  
   558  	mergeDiff, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git diff --color %s...%s", secondLineWords[1], secondLineWords[2]))
   559  	if err != nil {
   560  		return "", err
   561  	}
   562  	return show + mergeDiff, nil
   563  }
   564  
   565  // GetRemoteURL returns current repo remote url
   566  func (c *GitCommand) GetRemoteURL() string {
   567  	url, _ := c.OSCommand.RunCommandWithOutput("git config --get remote.origin.url")
   568  	return utils.TrimTrailingNewline(url)
   569  }
   570  
   571  // CheckRemoteBranchExists Returns remote branch
   572  func (c *GitCommand) CheckRemoteBranchExists(branch *Branch) bool {
   573  	_, err := c.OSCommand.RunCommandWithOutput(fmt.Sprintf(
   574  		"git show-ref --verify -- refs/remotes/origin/%s",
   575  		branch.Name,
   576  	))
   577  
   578  	return err == nil
   579  }
   580  
   581  // Diff returns the diff of a file
   582  func (c *GitCommand) Diff(file *File, plain bool) string {
   583  	cachedArg := ""
   584  	trackedArg := "--"
   585  	colorArg := "--color"
   586  	split := strings.Split(file.Name, " -> ") // in case of a renamed file we get the new filename
   587  	fileName := c.OSCommand.Quote(split[len(split)-1])
   588  	if file.HasStagedChanges && !file.HasUnstagedChanges {
   589  		cachedArg = "--cached"
   590  	}
   591  	if !file.Tracked && !file.HasStagedChanges {
   592  		trackedArg = "--no-index /dev/null"
   593  	}
   594  	if plain {
   595  		colorArg = ""
   596  	}
   597  
   598  	command := fmt.Sprintf("git diff %s %s %s %s", colorArg, cachedArg, trackedArg, fileName)
   599  
   600  	// for now we assume an error means the file was deleted
   601  	s, _ := c.OSCommand.RunCommandWithOutput(command)
   602  	return s
   603  }
   604  
   605  func (c *GitCommand) ApplyPatch(patch string) (string, error) {
   606  	filename, err := c.OSCommand.CreateTempFile("patch", patch)
   607  	if err != nil {
   608  		c.Log.Error(err)
   609  		return "", err
   610  	}
   611  
   612  	defer func() { _ = c.OSCommand.Remove(filename) }()
   613  
   614  	return c.OSCommand.RunCommandWithOutput(fmt.Sprintf("git apply --cached %s", c.OSCommand.Quote(filename)))
   615  }
   616  
   617  func (c *GitCommand) FastForward(branchName string) error {
   618  	upstream := "origin" // hardcoding for now
   619  	return c.OSCommand.RunCommand(fmt.Sprintf("git fetch %s %s:%s", upstream, branchName, branchName))
   620  }
   621  
   622  func (c *GitCommand) RunSkipEditorCommand(command string) error {
   623  	cmd := c.OSCommand.ExecutableFromString(command)
   624  	cmd.Env = append(
   625  		cmd.Env,
   626  		"LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY",
   627  		"EDITOR="+c.OSCommand.GetLazygitPath(),
   628  	)
   629  	return c.OSCommand.RunExecutable(cmd)
   630  }
   631  
   632  // GenericMerge takes a commandType of "merge" or "rebase" and a command of "abort", "skip" or "continue"
   633  // By default we skip the editor in the case where a commit will be made
   634  func (c *GitCommand) GenericMerge(commandType string, command string) error {
   635  	return c.RunSkipEditorCommand(
   636  		fmt.Sprintf(
   637  			"git %s --%s",
   638  			commandType,
   639  			command,
   640  		),
   641  	)
   642  }
   643  
   644  func (c *GitCommand) RewordCommit(commits []*Commit, index int) (*exec.Cmd, error) {
   645  	todo, sha, err := c.GenerateGenericRebaseTodo(commits, index, "reword")
   646  	if err != nil {
   647  		return nil, err
   648  	}
   649  
   650  	return c.PrepareInteractiveRebaseCommand(sha, todo, false)
   651  }
   652  
   653  func (c *GitCommand) MoveCommitDown(commits []*Commit, index int) error {
   654  	// we must ensure that we have at least two commits after the selected one
   655  	if len(commits) <= index+2 {
   656  		// assuming they aren't picking the bottom commit
   657  		return errors.New(c.Tr.SLocalize("NoRoom"))
   658  	}
   659  
   660  	todo := ""
   661  	orderedCommits := append(commits[0:index], commits[index+1], commits[index])
   662  	for _, commit := range orderedCommits {
   663  		todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo
   664  	}
   665  
   666  	cmd, err := c.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todo, true)
   667  	if err != nil {
   668  		return err
   669  	}
   670  
   671  	return c.OSCommand.RunPreparedCommand(cmd)
   672  }
   673  
   674  func (c *GitCommand) InteractiveRebase(commits []*Commit, index int, action string) error {
   675  	todo, sha, err := c.GenerateGenericRebaseTodo(commits, index, action)
   676  	if err != nil {
   677  		return err
   678  	}
   679  
   680  	cmd, err := c.PrepareInteractiveRebaseCommand(sha, todo, true)
   681  	if err != nil {
   682  		return err
   683  	}
   684  
   685  	return c.OSCommand.RunPreparedCommand(cmd)
   686  }
   687  
   688  // PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase
   689  // we tell git to run lazygit to edit the todo list, and we pass the client
   690  // lazygit a todo string to write to the todo file
   691  func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string, overrideEditor bool) (*exec.Cmd, error) {
   692  	ex := c.OSCommand.GetLazygitPath()
   693  
   694  	debug := "FALSE"
   695  	if c.OSCommand.Config.GetDebug() == true {
   696  		debug = "TRUE"
   697  	}
   698  
   699  	splitCmd := str.ToArgv(fmt.Sprintf("git rebase --interactive --autostash %s", baseSha))
   700  
   701  	cmd := c.OSCommand.command(splitCmd[0], splitCmd[1:]...)
   702  
   703  	gitSequenceEditor := ex
   704  	if todo == "" {
   705  		gitSequenceEditor = "true"
   706  	}
   707  
   708  	cmd.Env = os.Environ()
   709  	cmd.Env = append(
   710  		cmd.Env,
   711  		"LAZYGIT_CLIENT_COMMAND=INTERACTIVE_REBASE",
   712  		"LAZYGIT_REBASE_TODO="+todo,
   713  		"DEBUG="+debug,
   714  		"LANG=en_US.UTF-8",   // Force using EN as language
   715  		"LC_ALL=en_US.UTF-8", // Force using EN as language
   716  		"GIT_SEQUENCE_EDITOR="+gitSequenceEditor,
   717  	)
   718  
   719  	if overrideEditor {
   720  		cmd.Env = append(cmd.Env, "EDITOR="+ex)
   721  	}
   722  
   723  	return cmd, nil
   724  }
   725  
   726  func (c *GitCommand) HardReset(baseSha string) error {
   727  	return c.OSCommand.RunCommand("git reset --hard " + baseSha)
   728  }
   729  
   730  func (c *GitCommand) SoftReset(baseSha string) error {
   731  	return c.OSCommand.RunCommand("git reset --soft " + baseSha)
   732  }
   733  
   734  func (c *GitCommand) GenerateGenericRebaseTodo(commits []*Commit, actionIndex int, action string) (string, string, error) {
   735  	baseIndex := actionIndex + 1
   736  
   737  	if len(commits) <= baseIndex {
   738  		return "", "", errors.New(c.Tr.SLocalize("CannotRebaseOntoFirstCommit"))
   739  	}
   740  
   741  	if action == "squash" || action == "fixup" {
   742  		baseIndex++
   743  
   744  		if len(commits) <= baseIndex {
   745  			return "", "", errors.New(c.Tr.SLocalize("CannotSquashOntoSecondCommit"))
   746  		}
   747  	}
   748  
   749  	todo := ""
   750  	for i, commit := range commits[0:baseIndex] {
   751  		a := "pick"
   752  		if i == actionIndex {
   753  			a = action
   754  		}
   755  		todo = a + " " + commit.Sha + " " + commit.Name + "\n" + todo
   756  	}
   757  
   758  	return todo, commits[baseIndex].Sha, nil
   759  }
   760  
   761  // AmendTo amends the given commit with whatever files are staged
   762  func (c *GitCommand) AmendTo(sha string) error {
   763  	if err := c.CreateFixupCommit(sha); err != nil {
   764  		return err
   765  	}
   766  
   767  	return c.SquashAllAboveFixupCommits(sha)
   768  }
   769  
   770  // EditRebaseTodo sets the action at a given index in the git-rebase-todo file
   771  func (c *GitCommand) EditRebaseTodo(index int, action string) error {
   772  	fileName := fmt.Sprintf("%s/rebase-merge/git-rebase-todo", c.DotGitDir)
   773  	bytes, err := ioutil.ReadFile(fileName)
   774  	if err != nil {
   775  		return err
   776  	}
   777  
   778  	content := strings.Split(string(bytes), "\n")
   779  	commitCount := c.getTodoCommitCount(content)
   780  
   781  	// we have the most recent commit at the bottom whereas the todo file has
   782  	// it at the bottom, so we need to subtract our index from the commit count
   783  	contentIndex := commitCount - 1 - index
   784  	splitLine := strings.Split(content[contentIndex], " ")
   785  	content[contentIndex] = action + " " + strings.Join(splitLine[1:], " ")
   786  	result := strings.Join(content, "\n")
   787  
   788  	return ioutil.WriteFile(fileName, []byte(result), 0644)
   789  }
   790  
   791  func (c *GitCommand) getTodoCommitCount(content []string) int {
   792  	// count lines that are not blank and are not comments
   793  	commitCount := 0
   794  	for _, line := range content {
   795  		if line != "" && !strings.HasPrefix(line, "#") {
   796  			commitCount++
   797  		}
   798  	}
   799  	return commitCount
   800  }
   801  
   802  // MoveTodoDown moves a rebase todo item down by one position
   803  func (c *GitCommand) MoveTodoDown(index int) error {
   804  	fileName := fmt.Sprintf("%s/rebase-merge/git-rebase-todo", c.DotGitDir)
   805  	bytes, err := ioutil.ReadFile(fileName)
   806  	if err != nil {
   807  		return err
   808  	}
   809  
   810  	content := strings.Split(string(bytes), "\n")
   811  	commitCount := c.getTodoCommitCount(content)
   812  	contentIndex := commitCount - 1 - index
   813  
   814  	rearrangedContent := append(content[0:contentIndex-1], content[contentIndex], content[contentIndex-1])
   815  	rearrangedContent = append(rearrangedContent, content[contentIndex+1:]...)
   816  	result := strings.Join(rearrangedContent, "\n")
   817  
   818  	return ioutil.WriteFile(fileName, []byte(result), 0644)
   819  }
   820  
   821  // Revert reverts the selected commit by sha
   822  func (c *GitCommand) Revert(sha string) error {
   823  	return c.OSCommand.RunCommand(fmt.Sprintf("git revert %s", sha))
   824  }
   825  
   826  // CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD
   827  func (c *GitCommand) CherryPickCommits(commits []*Commit) error {
   828  	todo := ""
   829  	for _, commit := range commits {
   830  		todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo
   831  	}
   832  
   833  	cmd, err := c.PrepareInteractiveRebaseCommand("HEAD", todo, false)
   834  	if err != nil {
   835  		return err
   836  	}
   837  
   838  	return c.OSCommand.RunPreparedCommand(cmd)
   839  }
   840  
   841  // GetCommitFiles get the specified commit files
   842  func (c *GitCommand) GetCommitFiles(commitSha string) ([]*CommitFile, error) {
   843  	cmd := fmt.Sprintf("git show --pretty= --name-only %s", commitSha)
   844  	files, err := c.OSCommand.RunCommandWithOutput(cmd)
   845  	if err != nil {
   846  		return nil, err
   847  	}
   848  
   849  	commitFiles := make([]*CommitFile, 0)
   850  
   851  	for _, file := range strings.Split(strings.TrimRight(files, "\n"), "\n") {
   852  		commitFiles = append(commitFiles, &CommitFile{
   853  			Sha:           commitSha,
   854  			Name:          file,
   855  			DisplayString: file,
   856  		})
   857  	}
   858  
   859  	return commitFiles, nil
   860  }
   861  
   862  // ShowCommitFile get the diff of specified commit file
   863  func (c *GitCommand) ShowCommitFile(commitSha, fileName string) (string, error) {
   864  	cmd := fmt.Sprintf("git show --color %s -- %s", commitSha, fileName)
   865  	return c.OSCommand.RunCommandWithOutput(cmd)
   866  }
   867  
   868  // CheckoutFile checks out the file for the given commit
   869  func (c *GitCommand) CheckoutFile(commitSha, fileName string) error {
   870  	cmd := fmt.Sprintf("git checkout %s %s", commitSha, fileName)
   871  	return c.OSCommand.RunCommand(cmd)
   872  }
   873  
   874  // DiscardOldFileChanges discards changes to a file from an old commit
   875  func (c *GitCommand) DiscardOldFileChanges(commits []*Commit, commitIndex int, fileName string) error {
   876  	if len(commits)-1 < commitIndex {
   877  		return errors.New("index outside of range of commits")
   878  	}
   879  
   880  	// we can make this GPG thing possible it just means we need to do this in two parts:
   881  	// one where we handle the possibility of a credential request, and the other
   882  	// where we continue the rebase
   883  	if c.usingGpg() {
   884  		return errors.New(c.Tr.SLocalize("DisabledForGPG"))
   885  	}
   886  
   887  	todo, sha, err := c.GenerateGenericRebaseTodo(commits, commitIndex, "edit")
   888  	if err != nil {
   889  		return err
   890  	}
   891  
   892  	cmd, err := c.PrepareInteractiveRebaseCommand(sha, todo, true)
   893  	if err != nil {
   894  		return err
   895  	}
   896  
   897  	if err := c.OSCommand.RunPreparedCommand(cmd); err != nil {
   898  		return err
   899  	}
   900  
   901  	// check if file exists in previous commit (this command returns an error if the file doesn't exist)
   902  	if err := c.OSCommand.RunCommand(fmt.Sprintf("git cat-file -e HEAD^:%s", fileName)); err != nil {
   903  		if err := c.OSCommand.Remove(fileName); err != nil {
   904  			return err
   905  		}
   906  		if err := c.StageFile(fileName); err != nil {
   907  			return err
   908  		}
   909  	} else {
   910  		if err := c.CheckoutFile("HEAD^", fileName); err != nil {
   911  			return err
   912  		}
   913  	}
   914  
   915  	// amend the commit
   916  	cmd, err = c.AmendHead()
   917  	if cmd != nil {
   918  		return errors.New("received unexpected pointer to cmd")
   919  	}
   920  	if err != nil {
   921  		return err
   922  	}
   923  
   924  	// continue
   925  	return c.GenericMerge("rebase", "continue")
   926  }
   927  
   928  // DiscardAnyUnstagedFileChanges discards any unstages file changes via `git checkout -- .`
   929  func (c *GitCommand) DiscardAnyUnstagedFileChanges() error {
   930  	return c.OSCommand.RunCommand("git checkout -- .")
   931  }
   932  
   933  // RemoveUntrackedFiles runs `git clean -fd`
   934  func (c *GitCommand) RemoveUntrackedFiles() error {
   935  	return c.OSCommand.RunCommand("git clean -fd")
   936  }
   937  
   938  // ResetHardHead runs `git reset --hard HEAD`
   939  func (c *GitCommand) ResetHardHead() error {
   940  	return c.OSCommand.RunCommand("git reset --hard HEAD")
   941  }
   942  
   943  // ResetSoftHead runs `git reset --soft HEAD`
   944  func (c *GitCommand) ResetSoftHead() error {
   945  	return c.OSCommand.RunCommand("git reset --soft HEAD")
   946  }
   947  
   948  // DiffCommits show diff between commits
   949  func (c *GitCommand) DiffCommits(sha1, sha2 string) (string, error) {
   950  	cmd := fmt.Sprintf("git diff --color %s %s", sha1, sha2)
   951  	return c.OSCommand.RunCommandWithOutput(cmd)
   952  }
   953  
   954  // CreateFixupCommit creates a commit that fixes up a previous commit
   955  func (c *GitCommand) CreateFixupCommit(sha string) error {
   956  	cmd := fmt.Sprintf("git commit --fixup=%s", sha)
   957  	return c.OSCommand.RunCommand(cmd)
   958  }
   959  
   960  // SquashAllAboveFixupCommits squashes all fixup! commits above the given one
   961  func (c *GitCommand) SquashAllAboveFixupCommits(sha string) error {
   962  	return c.RunSkipEditorCommand(
   963  		fmt.Sprintf(
   964  			"git rebase --interactive --autostash --autosquash %s^",
   965  			sha,
   966  		),
   967  	)
   968  }
   969  
   970  // StashSaveStagedChanges stashes only the currently staged changes. This takes a few steps
   971  // shoutouts to Joe on https://stackoverflow.com/questions/14759748/stashing-only-staged-changes-in-git-is-it-possible
   972  func (c *GitCommand) StashSaveStagedChanges(message string) error {
   973  
   974  	if err := c.OSCommand.RunCommand("git stash --keep-index"); err != nil {
   975  		return err
   976  	}
   977  
   978  	if err := c.StashSave(message); err != nil {
   979  		return err
   980  	}
   981  
   982  	if err := c.OSCommand.RunCommand("git stash apply stash@{1}"); err != nil {
   983  		return err
   984  	}
   985  
   986  	if err := c.OSCommand.PipeCommands("git stash show -p", "git apply -R"); err != nil {
   987  		return err
   988  	}
   989  
   990  	if err := c.OSCommand.RunCommand("git stash drop stash@{1}"); err != nil {
   991  		return err
   992  	}
   993  
   994  	// if you had staged an untracked file, that will now appear as 'AD' in git status
   995  	// meaning it's deleted in your working tree but added in your index. Given that it's
   996  	// now safely stashed, we need to remove it.
   997  	files := c.GetStatusFiles()
   998  	for _, file := range files {
   999  		if file.ShortStatus == "AD" {
  1000  			if err := c.UnStageFile(file.Name, false); err != nil {
  1001  				return err
  1002  			}
  1003  		}
  1004  	}
  1005  
  1006  	return nil
  1007  }