diff options
author | Jesse Duffield <jessedduffield@gmail.com> | 2020-09-29 20:03:39 +1000 |
---|---|---|
committer | Jesse Duffield <jessedduffield@gmail.com> | 2020-09-29 20:48:49 +1000 |
commit | 72af7e41778bca93d82fa668641f515fba1d92bc (patch) | |
tree | 7e755e857be72205ee99641d5eb5d4556151ad8f /pkg/commands/git.go | |
parent | 1767f91047a35318f6b1e469199c8a7f547f2afc (diff) |
factor out code from git.go
Diffstat (limited to 'pkg/commands/git.go')
-rw-r--r-- | pkg/commands/git.go | 1141 |
1 files changed, 50 insertions, 1091 deletions
diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 278b1d557..ab2819f75 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -1,17 +1,10 @@ package commands import ( - "fmt" "io/ioutil" "os" - "os/exec" "path/filepath" - "regexp" - "strconv" "strings" - "time" - - "github.com/mgutz/str" "github.com/go-errors/errors" @@ -21,7 +14,6 @@ import ( "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/env" "github.com/jesseduffield/lazygit/pkg/i18n" - "github.com/jesseduffield/lazygit/pkg/models" "github.com/jesseduffield/lazygit/pkg/utils" "github.com/sirupsen/logrus" gitconfig "github.com/tcnksm/go-gitconfig" @@ -33,83 +25,6 @@ import ( // and returns '264fc6f5' as the second match const CurrentBranchNameRegex = `(?m)^\*.*?([^ ]*?)\)?$` -func verifyInGitRepo(runCmd func(string, ...interface{}) error) error { - return runCmd("git status") -} - -func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir func(string) error) error { - gitDir := env.GetGitDirEnv() - if gitDir != "" { - // we've been given the git directory explicitly so no need to navigate to it - _, err := stat(gitDir) - if err != nil { - return utils.WrapError(err) - } - - return nil - } - - // we haven't been given the git dir explicitly so we assume it's in the current working directory as `.git/` (or an ancestor directory) - - for { - _, err := stat(".git") - - if err == nil { - return nil - } - - if !os.IsNotExist(err) { - return utils.WrapError(err) - } - - if err = chdir(".."); err != nil { - return utils.WrapError(err) - } - } -} - -// resolvePath takes a path containing a symlink and returns the true path -func resolvePath(path string) (string, error) { - l, err := os.Lstat(path) - if err != nil { - return "", err - } - - if l.Mode()&os.ModeSymlink == 0 { - return path, nil - } - - return filepath.EvalSymlinks(path) -} - -func setupRepository(openGitRepository func(string) (*gogit.Repository, error), sLocalize func(string) string) (*gogit.Repository, error) { - unresolvedPath := env.GetGitDirEnv() - if unresolvedPath == "" { - var err error - unresolvedPath, err = os.Getwd() - if err != nil { - return nil, err - } - } - - path, err := resolvePath(unresolvedPath) - if err != nil { - return nil, err - } - - repository, err := openGitRepository(path) - - if err != nil { - if strings.Contains(err.Error(), `unquoted '\' must be followed by new line`) { - return nil, errors.New(sLocalize("GitconfigParseErr")) - } - - return nil, err - } - - return repository, err -} - // GitCommand is our main git interface type GitCommand struct { Log *logrus.Entry @@ -176,1060 +91,104 @@ func NewGitCommand(log *logrus.Entry, osCommand *oscommands.OSCommand, tr *i18n. return gitCommand, nil } -func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filename string) ([]byte, error)) (string, error) { - if env.GetGitDirEnv() != "" { - return env.GetGitDirEnv(), nil - } - - f, err := stat(".git") - if err != nil { - return "", err - } - - if f.IsDir() { - return ".git", nil - } - - fileBytes, err := readFile(".git") - if err != nil { - return "", err - } - fileContent := string(fileBytes) - if !strings.HasPrefix(fileContent, "gitdir: ") { - 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") - } - return strings.TrimSpace(strings.TrimPrefix(fileContent, "gitdir: ")), nil -} - -// GetStashEntryDiff stash diff -func (c *GitCommand) ShowStashEntryCmdStr(index int) string { - return fmt.Sprintf("git stash show -p --stat --color=%s stash@{%d}", c.colorArg(), index) -} - -// GetStatusFiles git status files -type GetStatusFileOptions struct { - NoRenames bool -} - -func (c *GitCommand) GetConfigValue(key string) string { - output, _ := c.OSCommand.RunCommandWithOutput("git config --get %s", key) - // looks like this returns an error if there is no matching value which we're okay with - return strings.TrimSpace(output) -} - -// StashDo modify stash -func (c *GitCommand) StashDo(index int, method string) error { - return c.OSCommand.RunCommand("git stash %s stash@{%d}", method, index) -} - -// StashSave save stash -// TODO: before calling this, check if there is anything to save -func (c *GitCommand) StashSave(message string) error { - return c.OSCommand.RunCommand("git stash save %s", c.OSCommand.Quote(message)) -} - -func includesInt(list []int, a int) bool { - for _, b := range list { - if b == a { - return true - } - } - return false +func verifyInGitRepo(runCmd func(string, ...interface{}) error) error { + return runCmd("git status") } -// ResetAndClean removes all unstaged changes and removes all untracked files -func (c *GitCommand) ResetAndClean() error { - submoduleConfigs, err := c.GetSubmoduleConfigs() - if err != nil { - return err - } - - if len(submoduleConfigs) > 0 { - for _, config := range submoduleConfigs { - if err := c.SubmoduleStash(config); err != nil { - return err - } - } - - if err := c.SubmoduleUpdateAll(); err != nil { - return err +func navigateToRepoRootDirectory(stat func(string) (os.FileInfo, error), chdir func(string) error) error { + gitDir := env.GetGitDirEnv() + if gitDir != "" { + // we've been given the git directory explicitly so no need to navigate to it + _, err := stat(gitDir) + if err != nil { + return utils.WrapError(err) } - } - - if err := c.ResetHard("HEAD"); err != nil { - return err - } - - return c.RemoveUntrackedFiles() -} - -func (c *GitCommand) GetCurrentBranchUpstreamDifferenceCount() (string, string) { - return c.GetCommitDifferences("HEAD", "HEAD@{u}") -} - -func (c *GitCommand) GetBranchUpstreamDifferenceCount(branchName string) (string, string) { - return c.GetCommitDifferences(branchName, branchName+"@{u}") -} - -// GetCommitDifferences checks how many pushables/pullables there are for the -// current branch -func (c *GitCommand) GetCommitDifferences(from, to string) (string, string) { - command := "git rev-list %s..%s --count" - pushableCount, err := c.OSCommand.RunCommandWithOutput(command, to, from) - if err != nil { - return "?", "?" - } - pullableCount, err := c.OSCommand.RunCommandWithOutput(command, from, to) - if err != nil { - return "?", "?" - } - return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount) -} - -// RenameCommit renames the topmost commit with the given name -func (c *GitCommand) RenameCommit(name string) error { - return c.OSCommand.RunCommand("git commit --allow-empty --amend -m %s", c.OSCommand.Quote(name)) -} -// RebaseBranch interactive rebases onto a branch -func (c *GitCommand) RebaseBranch(branchName string) error { - cmd, err := c.PrepareInteractiveRebaseCommand(branchName, "", false) - if err != nil { - return err + return nil } - return c.OSCommand.RunPreparedCommand(cmd) -} - -type FetchOptions struct { - PromptUserForCredential func(string) string - RemoteName string - BranchName string -} - -// Fetch fetch git repo -func (c *GitCommand) Fetch(opts FetchOptions) error { - command := "git fetch" + // we haven't been given the git dir explicitly so we assume it's in the current working directory as `.git/` (or an ancestor directory) - if opts.RemoteName != "" { - command = fmt.Sprintf("%s %s", command, opts.RemoteName) - } - if opts.BranchName != "" { - command = fmt.Sprintf("%s %s", command, opts.BranchName) - } + for { + _, err := stat(".git") - return c.OSCommand.DetectUnamePass(command, func(question string) string { - if opts.PromptUserForCredential != nil { - return opts.PromptUserForCredential(question) + if err == nil { + return nil } - return "\n" - }) -} - -// ResetToCommit reset to commit -func (c *GitCommand) ResetToCommit(sha string, strength string, options oscommands.RunCommandOptions) error { - return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git reset --%s %s", strength, sha), options) -} -// NewBranch create new branch -func (c *GitCommand) NewBranch(name string, base string) error { - return c.OSCommand.RunCommand("git checkout -b %s %s", name, base) -} - -// CurrentBranchName get the current branch name and displayname. -// the first returned string is the name and the second is the displayname -// e.g. name is 123asdf and displayname is '(HEAD detached at 123asdf)' -func (c *GitCommand) CurrentBranchName() (string, string, error) { - branchName, err := c.OSCommand.RunCommandWithOutput("git symbolic-ref --short HEAD") - if err == nil && branchName != "HEAD\n" { - trimmedBranchName := strings.TrimSpace(branchName) - return trimmedBranchName, trimmedBranchName, nil - } - output, err := c.OSCommand.RunCommandWithOutput("git branch --contains") - if err != nil { - return "", "", err - } - for _, line := range utils.SplitLines(output) { - re := regexp.MustCompile(CurrentBranchNameRegex) - match := re.FindStringSubmatch(line) - if len(match) > 0 { - branchName = match[1] - displayBranchName := match[0][2:] - return branchName, displayBranchName, nil + if !os.IsNotExist(err) { + return utils.WrapError(err) } - } - return "HEAD", "HEAD", nil -} - -// DeleteBranch delete branch -func (c *GitCommand) DeleteBranch(branch string, force bool) error { - command := "git branch -d" - - if force { - command = "git branch -D" - } - - return c.OSCommand.RunCommand("%s %s", command, branch) -} - -type MergeOpts struct { - FastForwardOnly bool -} - -// Merge merge -func (c *GitCommand) Merge(branchName string, opts MergeOpts) error { - mergeArgs := c.Config.GetUserConfig().GetString("git.merging.args") - - command := fmt.Sprintf("git merge --no-edit %s %s", mergeArgs, branchName) - if opts.FastForwardOnly { - command = fmt.Sprintf("%s --ff-only", command) - } - - return c.OSCommand.RunCommand(command) -} - -// AbortMerge abort merge -func (c *GitCommand) AbortMerge() error { - return c.OSCommand.RunCommand("git merge --abort") -} - -// usingGpg tells us whether the user has gpg enabled so that we can know -// whether we need to run a subprocess to allow them to enter their password -func (c *GitCommand) usingGpg() bool { - overrideGpg := c.Config.GetUserConfig().GetBool("git.overrideGpg") - if overrideGpg { - return false - } - - gpgsign, _ := c.getLocalGitConfig("commit.gpgsign") - if gpgsign == "" { - gpgsign, _ = c.getGlobalGitConfig("commit.gpgsign") - } - value := strings.ToLower(gpgsign) - return value == "true" || value == "1" || value == "yes" || value == "on" -} - -// Commit commits to git -func (c *GitCommand) Commit(message string, flags string) (*exec.Cmd, error) { - command := fmt.Sprintf("git commit %s -m %s", flags, strconv.Quote(message)) - if c.usingGpg() { - return c.OSCommand.ShellCommandFromString(command), nil - } - - return nil, c.OSCommand.RunCommand(command) -} - -// Get the subject of the HEAD commit -func (c *GitCommand) GetHeadCommitMessage() (string, error) { - cmdStr := "git log -1 --pretty=%s" - message, err := c.OSCommand.RunCommandWithOutput(cmdStr) - return strings.TrimSpace(message), err -} - -func (c *GitCommand) GetCommitMessage(commitSha string) (string, error) { - cmdStr := "git rev-list --format=%B --max-count=1 " + commitSha - messageWithHeader, err := c.OSCommand.RunCommandWithOutput(cmdStr) - message := strings.Join(strings.SplitAfter(messageWithHeader, "\n")[1:], "\n") - return strings.TrimSpace(message), err -} - -// AmendHead amends HEAD with whatever is staged in your working tree -func (c *GitCommand) AmendHead() (*exec.Cmd, error) { - command := "git commit --amend --no-edit --allow-empty" - if c.usingGpg() { - return c.OSCommand.ShellCommandFromString(command), nil - } - - return nil, c.OSCommand.RunCommand(command) -} - -// Push pushes to a branch -func (c *GitCommand) Push(branchName string, force bool, upstream string, args string, promptUserForCredential func(string) string) error { - forceFlag := "" - if force { - forceFlag = "--force-with-lease" - } - - setUpstreamArg := "" - if upstream != "" { - setUpstreamArg = "--set-upstream " + upstream - } - - cmd := fmt.Sprintf("git push --follow-tags %s %s %s", forceFlag, setUpstreamArg, args) - return c.OSCommand.DetectUnamePass(cmd, promptUserForCredential) -} - -// CatFile obtains the content of a file -func (c *GitCommand) CatFile(fileName string) (string, error) { - return c.OSCommand.RunCommandWithOutput("%s %s", c.OSCommand.Platform.CatCmd, c.OSCommand.Quote(fileName)) -} - -// StageFile stages a file -func (c *GitCommand) StageFile(fileName string) error { - // renamed files look like "file1 -> file2" - fileNames := strings.Split(fileName, " -> ") - return c.OSCommand.RunCommand("git add %s", c.OSCommand.Quote(fileNames[len(fileNames)-1])) -} - -// StageAll stages all files -func (c *GitCommand) StageAll() error { - return c.OSCommand.RunCommand("git add -A") -} - -// UnstageAll stages all files -func (c *GitCommand) UnstageAll() error { - return c.OSCommand.RunCommand("git reset") -} - -// UnStageFile unstages a file -func (c *GitCommand) UnStageFile(fileName string, tracked bool) error { - command := "git rm --cached %s" - if tracked { - command = "git reset HEAD %s" - } - - // renamed files look like "file1 -> file2" - fileNames := strings.Split(fileName, " -> ") - for _, name := range fileNames { - if err := c.OSCommand.RunCommand(command, c.OSCommand.Quote(name)); err != nil { - return err + if err = chdir(".."); err != nil { + return utils.WrapError(err) } } - return nil } -// IsInMergeState states whether we are still mid-merge -func (c *GitCommand) IsInMergeState() (bool, error) { - return c.OSCommand.FileExists(filepath.Join(c.DotGitDir, "MERGE_HEAD")) -} - -// RebaseMode returns "" for non-rebase mode, "normal" for normal rebase -// and "interactive" for interactive rebase -func (c *GitCommand) RebaseMode() (string, error) { - exists, err := c.OSCommand.FileExists(filepath.Join(c.DotGitDir, "rebase-apply")) +// resolvePath takes a path containing a symlink and returns the true path +func resolvePath(path string) (string, error) { + l, err := os.Lstat(path) if err != nil { return "", err } - if exists { - return "normal", nil - } - exists, err = c.OSCommand.FileExists(filepath.Join(c.DotGitDir, "rebase-merge")) - if exists { - return "interactive", err - } else { - return "", err - } -} - -func (c *GitCommand) BeforeAndAfterFileForRename(file *models.File) (*models.File, *models.File, error) { - - if !file.IsRename() { - return nil, nil, errors.New("Expected renamed file") - } - - // we've got a file that represents a rename from one file to another. Unfortunately - // our File abstraction fails to consider this case, so here we will refetch - // all files, passing the --no-renames flag and then recursively call the function - // again for the before file and after file. At some point we should fix the abstraction itself - split := strings.Split(file.Name, " -> ") - filesWithoutRenames := c.GetStatusFiles(GetStatusFileOptions{NoRenames: true}) - var beforeFile *models.File - var afterFile *models.File - for _, f := range filesWithoutRenames { - if f.Name == split[0] { - beforeFile = f - } - if f.Name == split[1] { - afterFile = f - } - } - - if beforeFile == nil || afterFile == nil { - return nil, nil, errors.New("Could not find deleted file or new file for file rename") - } - - if beforeFile.IsRename() || afterFile.IsRename() { - // probably won't happen but we want to ensure we don't get an infinite loop - return nil, nil, errors.New("Nested rename found") + if l.Mode()&os.ModeSymlink == 0 { + return path, nil } - return beforeFile, afterFile, nil + return filepath.EvalSymlinks(path) } -// DiscardAllFileChanges directly -func (c *GitCommand) DiscardAllFileChanges(file *models.File) error { - if file.IsRename() { - beforeFile, afterFile, err := c.BeforeAndAfterFileForRename(file) +func setupRepository(openGitRepository func(string) (*gogit.Repository, error), sLocalize func(string) string) (*gogit.Repository, error) { + unresolvedPath := env.GetGitDirEnv() + if unresolvedPath == "" { + var err error + unresolvedPath, err = os.Getwd() if err != nil { - return err - } - - if err := c.DiscardAllFileChanges(beforeFile); err != nil { - return err - } - - if err := c.DiscardAllFileChanges(afterFile); err != nil { - return err - } - - return nil - } - - // if the file isn't tracked, we assume you want to delete it - quotedFileName := c.OSCommand.Quote(file.Name) - if file.HasStagedChanges || file.HasMergeConflicts { - if err := c.OSCommand.RunCommand("git reset -- %s", quotedFileName); err != nil { - return err - } - } - - if !file.Tracked { - return c.removeFile(file.Name) - } - return c.DiscardUnstagedFileChanges(file) -} - -// DiscardUnstagedFileChanges directly -func (c *GitCommand) DiscardUnstagedFileChanges(file *models.File) error { - quotedFileName := c.OSCommand.Quote(file.Name) - return c.OSCommand.RunCommand("git checkout -- %s", quotedFileName) -} - -// Checkout checks out a branch (or commit), with --force if you set the force arg to true -type CheckoutOptions struct { - Force bool - EnvVars []string -} - -func (c *GitCommand) Checkout(branch string, options CheckoutOptions) error { - forceArg := "" - if options.Force { - forceArg = "--force " - } - return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git checkout %s %s", forceArg, branch), oscommands.RunCommandOptions{EnvVars: options.EnvVars}) -} - -// PrepareCommitAmendSubProcess prepares a subprocess for `git commit --amend --allow-empty` -func (c *GitCommand) PrepareCommitAmendSubProcess() *exec.Cmd { - return c.OSCommand.PrepareSubProcess("git", "commit", "--amend", "--allow-empty") -} - -// GetBranchGraph gets the color-formatted graph of the log for the given branch -// Currently it limits the result to 100 commits, but when we get async stuff -// working we can do lazy loading -func (c *GitCommand) GetBranchGraph(branchName string) (string, error) { - cmdStr := c.GetBranchGraphCmdStr(branchName) - return c.OSCommand.RunCommandWithOutput(cmdStr) -} - -func (c *GitCommand) GetUpstreamForBranch(branchName string) (string, error) { - output, err := c.OSCommand.RunCommandWithOutput("git rev-parse --abbrev-ref --symbolic-full-name %s@{u}", branchName) - return strings.TrimSpace(output), err -} - -// Ignore adds a file to the gitignore for the repo -func (c *GitCommand) Ignore(filename string) error { - return c.OSCommand.AppendLineToFile(".gitignore", filename) -} - -func (c *GitCommand) ShowCmdStr(sha string, filterPath string) string { - filterPathArg := "" - if filterPath != "" { - filterPathArg = fmt.Sprintf(" -- %s", c.OSCommand.Quote(filterPath)) - } - return fmt.Sprintf("git show --submodule --color=%s --no-renames --stat -p %s %s", c.colorArg(), sha, filterPathArg) -} - -func (c *GitCommand) GetBranchGraphCmdStr(branchName string) string { - branchLogCmdTemplate := c.Config.GetUserConfig().GetString("git.branchLogCmd") - templateValues := map[string]string{ - "branchName": branchName, - } - return utils.ResolvePlaceholderString(branchLogCmdTemplate, templateValues) -} - -// GetRemoteURL returns current repo remote url -func (c *GitCommand) GetRemoteURL() string { - url, _ := c.OSCommand.RunCommandWithOutput("git config --get remote.origin.url") - return utils.TrimTrailingNewline(url) -} - -// CheckRemoteBranchExists Returns remote branch -func (c *GitCommand) CheckRemoteBranchExists(branch *models.Branch) bool { - _, err := c.OSCommand.RunCommandWithOutput( - "git show-ref --verify -- refs/remotes/origin/%s", - branch.Name, - ) - - return err == nil -} - -// WorktreeFileDiff returns the diff of a file -func (c *GitCommand) WorktreeFileDiff(file *models.File, plain bool, cached bool) string { - // for now we assume an error means the file was deleted - s, _ := c.OSCommand.RunCommandWithOutput(c.WorktreeFileDiffCmdStr(file, plain, cached)) - return s -} - -func (c *GitCommand) WorktreeFileDiffCmdStr(file *models.File, plain bool, cached bool) string { - cachedArg := "" - trackedArg := "--" - colorArg := c.colorArg() - split := strings.Split(file.Name, " -> ") // in case of a renamed file we get the new filename - fileName := c.OSCommand.Quote(split[len(split)-1]) - if cached { - cachedArg = "--cached" - } - if !file.Tracked && !file.HasStagedChanges && !cached { - trackedArg = "--no-index /dev/null" - } - if plain { - colorArg = "never" - } - - return fmt.Sprintf("git diff --submodule --no-ext-diff --color=%s %s %s %s", colorArg, cachedArg, trackedArg, fileName) -} - -func (c *GitCommand) ApplyPatch(patch string, flags ...string) error { - filepath := filepath.Join(c.Config.GetUserConfigDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch") - c.Log.Infof("saving temporary patch to %s", filepath) - if err := c.OSCommand.CreateFileWithContent(filepath, patch); err != nil { - return err - } - - flagStr := "" - for _, flag := range flags { - flagStr += " --" + flag - } - - return c.OSCommand.RunCommand("git apply %s %s", flagStr, c.OSCommand.Quote(filepath)) -} - -func (c *GitCommand) FastForward(branchName string, remoteName string, remoteBranchName string, promptUserForCredential func(string) string) error { - command := fmt.Sprintf("git fetch %s %s:%s", remoteName, remoteBranchName, branchName) - return c.OSCommand.DetectUnamePass(command, promptUserForCredential) -} - -func (c *GitCommand) RunSkipEditorCommand(command string) error { - cmd := c.OSCommand.ExecutableFromString(command) - lazyGitPath := c.OSCommand.GetLazygitPath() - cmd.Env = append( - cmd.Env, - "LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY", - "GIT_EDITOR="+lazyGitPath, - "EDITOR="+lazyGitPath, - "VISUAL="+lazyGitPath, - ) - return c.OSCommand.RunExecutable(cmd) -} - -// GenericMerge takes a commandType of "merge" or "rebase" and a command of "abort", "skip" or "continue" -// By default we skip the editor in the case where a commit will be made -func (c *GitCommand) GenericMerge(commandType string, command string) error { - err := c.RunSkipEditorCommand( - fmt.Sprintf( - "git %s --%s", - commandType, - command, - ), - ) - if err != nil { - if !strings.Contains(err.Error(), "no rebase in progress") { - return err + return nil, err } - c.Log.Warn(err) } - // sometimes we need to do a sequence of things in a rebase but the user needs to - // fix merge conflicts along the way. When this happens we queue up the next step - // so that after the next successful rebase continue we can continue from where we left off - if commandType == "rebase" && command == "continue" && c.onSuccessfulContinue != nil { - f := c.onSuccessfulContinue - c.onSuccessfulContinue = nil - return f() - } - if command == "abort" { - c.onSuccessfulContinue = nil - } - return nil -} - -func (c *GitCommand) RewordCommit(commits []*models.Commit, index int) (*exec.Cmd, error) { - todo, sha, err := c.GenerateGenericRebaseTodo(commits, index, "reword") + path, err := resolvePath(unresolvedPath) if err != nil { return nil, err } - return c.PrepareInteractiveRebaseCommand(sha, todo, false) -} - -func (c *GitCommand) MoveCommitDown(commits []*models.Commit, index int) error { - // we must ensure that we have at least two commits after the selected one - if len(commits) <= index+2 { - // assuming they aren't picking the bottom commit - return errors.New(c.Tr.SLocalize("NoRoom")) - } - - todo := "" - orderedCommits := append(commits[0:index], commits[index+1], commits[index]) - for _, commit := range orderedCommits { - todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo - } - - cmd, err := c.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todo, true) - if err != nil { - return err - } - - return c.OSCommand.RunPreparedCommand(cmd) -} - -func (c *GitCommand) InteractiveRebase(commits []*models.Commit, index int, action string) error { - todo, sha, err := c.GenerateGenericRebaseTodo(commits, index, action) - if err != nil { - return err - } - - cmd, err := c.PrepareInteractiveRebaseCommand(sha, todo, true) - if err != nil { - return err - } - - return c.OSCommand.RunPreparedCommand(cmd) -} - -// PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase -// we tell git to run lazygit to edit the todo list, and we pass the client -// lazygit a todo string to write to the todo file -func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string, overrideEditor bool) (*exec.Cmd, error) { - ex := c.OSCommand.GetLazygitPath() - - debug := "FALSE" - if c.OSCommand.Config.GetDebug() { - debug = "TRUE" - } - - cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty %s", baseSha) - c.Log.WithField("command", cmdStr).Info("RunCommand") - splitCmd := str.ToArgv(cmdStr) - - cmd := c.OSCommand.Command(splitCmd[0], splitCmd[1:]...) - - gitSequenceEditor := ex - if todo == "" { - gitSequenceEditor = "true" - } - - cmd.Env = os.Environ() - cmd.Env = append( - cmd.Env, - "LAZYGIT_CLIENT_COMMAND=INTERACTIVE_REBASE", - "LAZYGIT_REBASE_TODO="+todo, - "DEBUG="+debug, - "LANG=en_US.UTF-8", // Force using EN as language - "LC_ALL=en_US.UTF-8", // Force using EN as language - "GIT_SEQUENCE_EDITOR="+gitSequenceEditor, - ) - - if overrideEditor { - cmd.Env = append(cmd.Env, "GIT_EDITOR="+ex) - } - - return cmd, nil -} - -func (c *GitCommand) HardReset(baseSha string) error { - return c.OSCommand.RunCommand("git reset --hard " + baseSha) -} - -func (c *GitCommand) SoftReset(baseSha string) error { - return c.OSCommand.RunCommand("git reset --soft " + baseSha) -} - -func (c *GitCommand) GenerateGenericRebaseTodo(commits []*models.Commit, actionIndex int, action string) (string, string, error) { - baseIndex := actionIndex + 1 - - if len(commits) <= baseIndex { - return "", "", errors.New(c.Tr.SLocalize("CannotRebaseOntoFirstCommit")) - } - - if action == "squash" || action == "fixup" { - baseIndex++ - - if len(commits) <= baseIndex { - return "", "", errors.New(c.Tr.SLocalize("CannotSquashOntoSecondCommit")) - } - } - - todo := "" - for i, commit := range commits[0:baseIndex] { - var commitAction string - if i == actionIndex { - commitAction = action - } else if commit.IsMerge { - // your typical interactive rebase will actually drop merge commits by default. Damn git CLI, you scary! - // doing this means we don't need to worry about rebasing over merges which always causes problems. - // you typically shouldn't be doing rebases that pass over merge commits anyway. - commitAction = "drop" - } else { - commitAction = "pick" - } - todo = commitAction + " " + commit.Sha + " " + commit.Name + "\n" + todo - } - - return todo, commits[baseIndex].Sha, nil -} - -// AmendTo amends the given commit with whatever files are staged -func (c *GitCommand) AmendTo(sha string) error { - if err := c.CreateFixupCommit(sha); err != nil { - return err - } - - return c.SquashAllAboveFixupCommits(sha) -} - -// EditRebaseTodo sets the action at a given index in the git-rebase-todo file -func (c *GitCommand) EditRebaseTodo(index int, action string) error { - fileName := filepath.Join(c.DotGitDir, "rebase-merge/git-rebase-todo") - bytes, err := ioutil.ReadFile(fileName) - if err != nil { - return err - } - - content := strings.Split(string(bytes), "\n") - commitCount := c.getTodoCommitCount(content) - - // we have the most recent commit at the bottom whereas the todo file has - // it at the bottom, so we need to subtract our index from the commit count - contentIndex := commitCount - 1 - index - splitLine := strings.Split(content[contentIndex], " ") - content[contentIndex] = action + " " + strings.Join(splitLine[1:], " ") - result := strings.Join(content, "\n") - - return ioutil.WriteFile(fileName, []byte(result), 0644) -} - -func (c *GitCommand) getTodoCommitCount(content []string) int { - // count lines that are not blank and are not comments - commitCount := 0 - for _, line := range content { - if line != "" && !strings.HasPrefix(line, "#") { - commitCount++ - } - } - return commitCount -} - -// MoveTodoDown moves a rebase todo item down by one position -func (c *GitCommand) MoveTodoDown(index int) error { - fileName := filepath.Join(c.DotGitDir, "rebase-merge/git-rebase-todo") - bytes, err := ioutil.ReadFile(fileName) - if err != nil { - return err - } - - content := strings.Split(string(bytes), "\n") - commitCount := c.getTodoCommitCount(content) - contentIndex := commitCount - 1 - index - - rearrangedContent := append(content[0:contentIndex-1], content[contentIndex], content[contentIndex-1]) - rearrangedContent = append(rearrangedContent, content[contentIndex+1:]...) - result := strings.Join(rearrangedContent, "\n") - - return ioutil.WriteFile(fileName, []byte(result), 0644) -} - -// Revert reverts the selected commit by sha -func (c *GitCommand) Revert(sha string) error { - return c.OSCommand.RunCommand("git revert %s", sha) -} - -// CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD -func (c *GitCommand) CherryPickCommits(commits []*models.Commit) error { - todo := "" - for _, commit := range commits { - todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo - } + repository, err := openGitRepository(path) - cmd, err := c.PrepareInteractiveRebaseCommand("HEAD", todo, false) if err != nil { - return err - } - - return c.OSCommand.RunPreparedCommand(cmd) -} - -// ShowFileDiff get the diff of specified from and to. Typically this will be used for a single commit so it'll be 123abc^..123abc -// but when we're in diff mode it could be any 'from' to any 'to'. The reverse flag is also here thanks to diff mode. -func (c *GitCommand) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool) (string, error) { - cmdStr := c.ShowFileDiffCmdStr(from, to, reverse, fileName, plain) - return c.OSCommand.RunCommandWithOutput(cmdStr) -} - -func (c *GitCommand) ShowFileDiffCmdStr(from string, to string, reverse bool, fileName string, plain bool) string { - colorArg := c.colorArg() - if plain { - colorArg = "never" - } - - reverseFlag := "" - if reverse { - reverseFlag = " -R " - } - - return fmt.Sprintf("git diff --submodule --no-ext-diff --no-renames --color=%s %s %s %s -- %s", colorArg, from, to, reverseFlag, fileName) -} - -// CheckoutFile checks out the file for the given commit -func (c *GitCommand) CheckoutFile(commitSha, fileName string) error { - return c.OSCommand.RunCommand("git checkout %s %s", commitSha, fileName) -} - -// DiscardOldFileChanges discards changes to a file from an old commit -func (c *GitCommand) DiscardOldFileChanges(commits []*models.Commit, commitIndex int, fileName string) error { - if err := c.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil { - return err - } - - // check if file exists in previous commit (this command returns an error if the file doesn't exist) - if err := c.OSCommand.RunCommand("git cat-file -e HEAD^:%s", fileName); err != nil { - if err := c.OSCommand.Remove(fileName); err != nil { - return err - } - if err := c.StageFile(fileName); err != nil { - return err + if strings.Contains(err.Error(), `unquoted '\' must be followed by new line`) { + return nil, errors.New(sLocalize("GitconfigParseErr")) } - } else if err := c.CheckoutFile("HEAD^", fileName); err != nil { - return err - } - // amend the commit - cmd, err := c.AmendHead() - if cmd != nil { - return errors.New("received unexpected pointer to cmd") - } - if err != nil { - return err - } - - // continue - return c.GenericMerge("rebase", "continue") -} - |