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 | |
parent | 1767f91047a35318f6b1e469199c8a7f547f2afc (diff) |
factor out code from git.go
Diffstat (limited to 'pkg')
-rw-r--r-- | pkg/commands/branches.go | 157 | ||||
-rw-r--r-- | pkg/commands/commits.go | 93 | ||||
-rw-r--r-- | pkg/commands/config.go | 52 | ||||
-rw-r--r-- | pkg/commands/files.go | 270 | ||||
-rw-r--r-- | pkg/commands/git.go | 1141 | ||||
-rw-r--r-- | pkg/commands/git_test.go | 2 | ||||
-rw-r--r-- | pkg/commands/loading_branches.go (renamed from pkg/commands/branch_list_builder.go) | 0 | ||||
-rw-r--r-- | pkg/commands/loading_commits.go (renamed from pkg/commands/commit_list_builder.go) | 0 | ||||
-rw-r--r-- | pkg/commands/loading_commits_test.go (renamed from pkg/commands/commit_list_builder_test.go) | 0 | ||||
-rw-r--r-- | pkg/commands/loading_files.go | 9 | ||||
-rw-r--r-- | pkg/commands/patch_rebases.go | 28 | ||||
-rw-r--r-- | pkg/commands/rebasing.go | 287 | ||||
-rw-r--r-- | pkg/commands/remotes.go | 40 | ||||
-rw-r--r-- | pkg/commands/stash_entries.go | 58 | ||||
-rw-r--r-- | pkg/commands/status.go | 48 | ||||
-rw-r--r-- | pkg/commands/sync.go | 74 | ||||
-rw-r--r-- | pkg/commands/tags.go | 13 | ||||
-rw-r--r-- | pkg/gui/rebase_options_panel.go | 2 | ||||
-rw-r--r-- | pkg/gui/remotes_panel.go | 6 |
19 files changed, 1168 insertions, 1112 deletions
diff --git a/pkg/commands/branches.go b/pkg/commands/branches.go new file mode 100644 index 000000000..2da076e3a --- /dev/null +++ b/pkg/commands/branches.go @@ -0,0 +1,157 @@ +package commands + +import ( + "fmt" + "regexp" + "strings" + + "github.com/jesseduffield/lazygit/pkg/commands/oscommands" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +// 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 + } + } + 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) +} + +// 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}) +} + +// 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 +} + +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) +} + +func (c *GitCommand) SetUpstreamBranch(upstream string) error { + return c.OSCommand.RunCommand("git branch -u %s", upstream) +} + +func (c *GitCommand) SetBranchUpstream(remoteName string, remoteBranchName string, branchName string) error { + return c.OSCommand.RunCommand("git branch --set-upstream-to=%s/%s %s", remoteName, remoteBranchName, branchName) +} + +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) +} + +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") +} + +func (c *GitCommand) IsHeadDetached() bool { + err := c.OSCommand.RunCommand("git symbolic-ref -q HEAD") + return err != nil +} + +// ResetHardHead runs `git reset --hard` +func (c *GitCommand) ResetHard(ref string) error { + return c.OSCommand.RunCommand("git reset --hard " + ref) +} + +// ResetSoft runs `git reset --soft HEAD` +func (c *GitCommand) ResetSoft(ref string) error { + return c.OSCommand.RunCommand("git reset --soft " + ref) +} + +func (c *GitCommand) RenameBranch(oldName string, newName string) error { + return c.OSCommand.RunCommand("git branch --move %s %s", oldName, newName) +} diff --git a/pkg/commands/commits.go b/pkg/commands/commits.go new file mode 100644 index 000000000..7fc4cc2a9 --- /dev/null +++ b/pkg/commands/commits.go @@ -0,0 +1,93 @@ +package commands + +import ( + "fmt" + "os/exec" + "strconv" + "strings" + + "github.com/jesseduffield/lazygit/pkg/commands/oscommands" + "github.com/jesseduffield/lazygit/pkg/models" +) + +// 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)) +} + +// 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) +} + +// 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) +} + +// 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") +} + +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) +} + +// 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 + } + + cmd, err := c.PrepareInteractiveRebaseCommand("HEAD", todo, false) + if err != nil { + return err + } + + return c.OSCommand.RunPreparedCommand(cmd) +} + +// CreateFixupCommit creates a commit that fixes up a previous commit +func (c *GitCommand) CreateFixupCommit(sha string) error { + return c.OSCommand.RunCommand("git commit --fixup=%s", sha) +} diff --git a/pkg/commands/config.go b/pkg/commands/config.go new file mode 100644 index 000000000..018de2724 --- /dev/null +++ b/pkg/commands/config.go @@ -0,0 +1,52 @@ +package commands + +import ( + "os" + "strconv" + "strings" + + "github.com/jesseduffield/lazygit/pkg/utils" +) + +func (c *GitCommand) ConfiguredPager() string { + if os.Getenv("GIT_PAGER") != "" { + return os.Getenv("GIT_PAGER") + } + if os.Getenv("PAGER") != "" { + return os.Getenv("PAGER") + } + output, err := c.OSCommand.RunCommandWithOutput("git config --get-all core.pager") + if err != nil { + return "" + } + trimmedOutput := strings.TrimSpace(output) + return strings.Split(trimmedOutput, "\n")[0] +} + +func (c *GitCommand) GetPager(width int) string { + useConfig := c.Config.GetUserConfig().GetBool("git.paging.useConfig") + if useConfig { + pager := c.ConfiguredPager() + return strings.Split(pager, "| less")[0] + } + + templateValues := map[string]string{ + "columnWidth": strconv.Itoa(width/2 - 6), + } + + pagerTemplate := c.Config.GetUserConfig().GetString("git.paging.pager") + return utils.ResolvePlaceholderString(pagerTemplate, templateValues) +} + +func (c *GitCommand) colorArg() string { + return c.Config.GetUserConfig().GetString("git.paging.colorArg") +} + +func (c *GitCommand) GetConfigValue(key string) string { + output, err := c.OSCommand.RunCommandWithOutput("git config --get %s", key) + if err != nil { + // looks like this returns an error if there is no matching value which we're okay with + return "" + } + return strings.TrimSpace(output) +} diff --git a/pkg/commands/files.go b/pkg/commands/files.go new file mode 100644 index 000000000..e52de446b --- /dev/null +++ b/pkg/commands/files.go @@ -0,0 +1,270 @@ +package commands + +import ( + "fmt" + "path/filepath" + "strings" + "time" + + "github.com/go-errors/errors" + "github.com/jesseduffield/lazygit/pkg/models" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +// 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 + } + } + return nil +} + +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") + } + + return beforeFile, afterFile, nil +} + +// DiscardAllFileChanges directly +func (c *GitCommand) DiscardAllFileChanges(file *models.File) error { + if file.IsRename() { + beforeFile, afterFile, err := c.BeforeAndAfterFileForRename(file) + 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) +} + +// Ignore adds a file to the gitignore for the repo +func (c *GitCommand) Ignore(filename string) error { + return c.OSCommand.AppendLineToFile(".gitignore", filename) +} + +// 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)) +} + +// 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 + } + } 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.GenericMergeOrRebaseAction("rebase", "continue") +} + +// DiscardAnyUnstagedFileChanges discards any unstages file changes via `git checkout -- .` +func (c *GitCommand) DiscardAnyUnstagedFileChanges() error { + return c.OSCommand.RunCommand("git checkout -- .") +} + +// RemoveTrackedFiles will delete the given file(s) even if they are currently tracked +func (c *GitCommand) RemoveTrackedFiles(name string) error { + return c.OSCommand.RunCommand("git rm -r --cached %s", name) +} + +// RemoveUntrackedFiles runs `git clean -fd` +func (c *GitCommand) RemoveUntrackedFiles() error { + return c.OSCommand.RunCommand("git clean -fd") +} + +// 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 + } + } + + if err := c.ResetHard("HEAD"); err != nil { + return err + } + + return c.RemoveUntrackedFiles() +} 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" |