Add convenience builder for git commands

This commit is contained in:
Jesse Duffield 2023-05-19 20:18:02 +10:00
parent 63ddc52a6b
commit 25f8b0337e
22 changed files with 711 additions and 386 deletions

View file

@ -1,7 +1,6 @@
package git_commands package git_commands
import ( import (
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -98,13 +97,15 @@ func (self *BisectCommands) GetInfo() *BisectInfo {
} }
func (self *BisectCommands) Reset() error { func (self *BisectCommands) Reset() error {
return self.cmd.New("git bisect reset").StreamOutput().Run() cmdStr := NewGitCmd("bisect").Arg("reset").ToString()
return self.cmd.New(cmdStr).StreamOutput().Run()
} }
func (self *BisectCommands) Mark(ref string, term string) error { func (self *BisectCommands) Mark(ref string, term string) error {
return self.cmd.New( cmdStr := NewGitCmd("bisect").Arg(term, ref).ToString()
fmt.Sprintf("git bisect %s %s", term, ref),
). return self.cmd.New(cmdStr).
IgnoreEmptyError(). IgnoreEmptyError().
StreamOutput(). StreamOutput().
Run() Run()
@ -115,7 +116,9 @@ func (self *BisectCommands) Skip(ref string) error {
} }
func (self *BisectCommands) Start() error { func (self *BisectCommands) Start() error {
return self.cmd.New("git bisect start").StreamOutput().Run() cmdStr := NewGitCmd("bisect").Arg("start").ToString()
return self.cmd.New(cmdStr).StreamOutput().Run()
} }
// tells us whether we've found our problem commit(s). We return a string slice of // tells us whether we've found our problem commit(s). We return a string slice of
@ -137,7 +140,8 @@ func (self *BisectCommands) IsDone() (bool, []string, error) {
done := false done := false
candidates := []string{} candidates := []string{}
err := self.cmd.New(fmt.Sprintf("git rev-list %s", newSha)).RunAndProcessLines(func(line string) (bool, error) { cmdStr := NewGitCmd("rev-list").Arg(newSha).ToString()
err := self.cmd.New(cmdStr).RunAndProcessLines(func(line string) (bool, error) {
sha := strings.TrimSpace(line) sha := strings.TrimSpace(line)
if status, ok := info.statusMap[sha]; ok { if status, ok := info.statusMap[sha]; ok {
@ -167,9 +171,11 @@ func (self *BisectCommands) IsDone() (bool, []string, error) {
// bisecting is actually a descendant of our current bisect commit. If it's not, we need to // bisecting is actually a descendant of our current bisect commit. If it's not, we need to
// render the commits from the bad commit. // render the commits from the bad commit.
func (self *BisectCommands) ReachableFromStart(bisectInfo *BisectInfo) bool { func (self *BisectCommands) ReachableFromStart(bisectInfo *BisectInfo) bool {
err := self.cmd.New( cmdStr := NewGitCmd("merge-base").
fmt.Sprintf("git merge-base --is-ancestor %s %s", bisectInfo.GetNewSha(), bisectInfo.GetStartSha()), Arg("--is-ancestor", bisectInfo.GetNewSha(), bisectInfo.GetStartSha()).
).DontLog().Run() ToString()
err := self.cmd.New(cmdStr).DontLog().Run()
return err == nil return err == nil
} }

View file

@ -20,12 +20,20 @@ func NewBranchCommands(gitCommon *GitCommon) *BranchCommands {
// New creates a new branch // New creates a new branch
func (self *BranchCommands) New(name string, base string) error { func (self *BranchCommands) New(name string, base string) error {
return self.cmd.New(fmt.Sprintf("git checkout -b %s %s", self.cmd.Quote(name), self.cmd.Quote(base))).Run() cmdStr := NewGitCmd("checkout").
Arg("-b", self.cmd.Quote(name), self.cmd.Quote(base)).
ToString()
return self.cmd.New(cmdStr).Run()
} }
// CurrentBranchInfo get the current branch information. // CurrentBranchInfo get the current branch information.
func (self *BranchCommands) CurrentBranchInfo() (BranchInfo, error) { func (self *BranchCommands) CurrentBranchInfo() (BranchInfo, error) {
branchName, err := self.cmd.New("git symbolic-ref --short HEAD").DontLog().RunWithOutput() branchName, err := self.cmd.New(
NewGitCmd("symbolic-ref").
Arg("--short", "HEAD").
ToString(),
).DontLog().RunWithOutput()
if err == nil && branchName != "HEAD\n" { if err == nil && branchName != "HEAD\n" {
trimmedBranchName := strings.TrimSpace(branchName) trimmedBranchName := strings.TrimSpace(branchName)
return BranchInfo{ return BranchInfo{
@ -34,7 +42,11 @@ func (self *BranchCommands) CurrentBranchInfo() (BranchInfo, error) {
DetachedHead: false, DetachedHead: false,
}, nil }, nil
} }
output, err := self.cmd.New(`git branch --points-at=HEAD --format="%(HEAD)%00%(objectname)%00%(refname)"`).DontLog().RunWithOutput() output, err := self.cmd.New(
NewGitCmd("branch").
Arg("--points-at=HEAD", "--format=\"%(HEAD)%00%(objectname)%00%(refname)\"").
ToString(),
).DontLog().RunWithOutput()
if err != nil { if err != nil {
return BranchInfo{}, err return BranchInfo{}, err
} }
@ -59,13 +71,12 @@ func (self *BranchCommands) CurrentBranchInfo() (BranchInfo, error) {
// Delete delete branch // Delete delete branch
func (self *BranchCommands) Delete(branch string, force bool) error { func (self *BranchCommands) Delete(branch string, force bool) error {
command := "git branch -d" cmdStr := NewGitCmd("branch").
ArgIfElse(force, "-D", "-d").
Arg(self.cmd.Quote(branch)).
ToString()
if force { return self.cmd.New(cmdStr).Run()
command = "git branch -D"
}
return self.cmd.New(fmt.Sprintf("%s %s", command, self.cmd.Quote(branch))).Run()
} }
// Checkout checks out a branch (or commit), with --force if you set the force arg to true // Checkout checks out a branch (or commit), with --force if you set the force arg to true
@ -75,12 +86,12 @@ type CheckoutOptions struct {
} }
func (self *BranchCommands) Checkout(branch string, options CheckoutOptions) error { func (self *BranchCommands) Checkout(branch string, options CheckoutOptions) error {
forceArg := "" cmdStr := NewGitCmd("checkout").
if options.Force { ArgIf(options.Force, "--force").
forceArg = " --force" Arg(self.cmd.Quote(branch)).
} ToString()
return self.cmd.New(fmt.Sprintf("git checkout%s %s", forceArg, self.cmd.Quote(branch))). return self.cmd.New(cmdStr).
// prevents git from prompting us for input which would freeze the program // prevents git from prompting us for input which would freeze the program
// TODO: see if this is actually needed here // TODO: see if this is actually needed here
AddEnvVars("GIT_TERMINAL_PROMPT=0"). AddEnvVars("GIT_TERMINAL_PROMPT=0").
@ -104,15 +115,27 @@ func (self *BranchCommands) GetGraphCmdObj(branchName string) oscommands.ICmdObj
} }
func (self *BranchCommands) SetCurrentBranchUpstream(remoteName string, remoteBranchName string) error { func (self *BranchCommands) SetCurrentBranchUpstream(remoteName string, remoteBranchName string) error {
return self.cmd.New(fmt.Sprintf("git branch --set-upstream-to=%s/%s", self.cmd.Quote(remoteName), self.cmd.Quote(remoteBranchName))).Run() cmdStr := NewGitCmd("branch").
Arg(fmt.Sprintf("--set-upstream-to=%s/%s", self.cmd.Quote(remoteName), self.cmd.Quote(remoteBranchName))).
ToString()
return self.cmd.New(cmdStr).Run()
} }
func (self *BranchCommands) SetUpstream(remoteName string, remoteBranchName string, branchName string) error { func (self *BranchCommands) SetUpstream(remoteName string, remoteBranchName string, branchName string) error {
return self.cmd.New(fmt.Sprintf("git branch --set-upstream-to=%s/%s %s", self.cmd.Quote(remoteName), self.cmd.Quote(remoteBranchName), self.cmd.Quote(branchName))).Run() cmdStr := NewGitCmd("branch").
Arg(fmt.Sprintf("--set-upstream-to=%s/%s", self.cmd.Quote(remoteName), self.cmd.Quote(remoteBranchName))).
Arg(self.cmd.Quote(branchName)).
ToString()
return self.cmd.New(cmdStr).Run()
} }
func (self *BranchCommands) UnsetUpstream(branchName string) error { func (self *BranchCommands) UnsetUpstream(branchName string) error {
return self.cmd.New(fmt.Sprintf("git branch --unset-upstream %s", self.cmd.Quote(branchName))).Run() cmdStr := NewGitCmd("branch").Arg("--unset-upstream", self.cmd.Quote(branchName)).
ToString()
return self.cmd.New(cmdStr).Run()
} }
func (self *BranchCommands) GetCurrentBranchUpstreamDifferenceCount() (string, string) { func (self *BranchCommands) GetCurrentBranchUpstreamDifferenceCount() (string, string) {
@ -126,29 +149,49 @@ func (self *BranchCommands) GetUpstreamDifferenceCount(branchName string) (strin
// GetCommitDifferences checks how many pushables/pullables there are for the // GetCommitDifferences checks how many pushables/pullables there are for the
// current branch // current branch
func (self *BranchCommands) GetCommitDifferences(from, to string) (string, string) { func (self *BranchCommands) GetCommitDifferences(from, to string) (string, string) {
command := "git rev-list %s..%s --count" pushableCount, err := self.countDifferences(to, from)
pushableCount, err := self.cmd.New(fmt.Sprintf(command, to, from)).DontLog().RunWithOutput()
if err != nil { if err != nil {
return "?", "?" return "?", "?"
} }
pullableCount, err := self.cmd.New(fmt.Sprintf(command, from, to)).DontLog().RunWithOutput() pullableCount, err := self.countDifferences(from, to)
if err != nil { if err != nil {
return "?", "?" return "?", "?"
} }
return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount) return strings.TrimSpace(pushableCount), strings.TrimSpace(pullableCount)
} }
func (self *BranchCommands) countDifferences(from, to string) (string, error) {
cmdStr := NewGitCmd("rev-list").
Arg(fmt.Sprintf("%s..%s", from, to)).
Arg("--count").
ToString()
return self.cmd.New(cmdStr).DontLog().RunWithOutput()
}
func (self *BranchCommands) IsHeadDetached() bool { func (self *BranchCommands) IsHeadDetached() bool {
err := self.cmd.New("git symbolic-ref -q HEAD").DontLog().Run() cmdStr := NewGitCmd("symbolic-ref").Arg("-q", "HEAD").ToString()
err := self.cmd.New(cmdStr).DontLog().Run()
return err != nil return err != nil
} }
func (self *BranchCommands) Rename(oldName string, newName string) error { func (self *BranchCommands) Rename(oldName string, newName string) error {
return self.cmd.New(fmt.Sprintf("git branch --move %s %s", self.cmd.Quote(oldName), self.cmd.Quote(newName))).Run() cmdStr := NewGitCmd("branch").
Arg("--move", self.cmd.Quote(oldName), self.cmd.Quote(newName)).
ToString()
return self.cmd.New(cmdStr).Run()
} }
func (self *BranchCommands) GetRawBranches() (string, error) { func (self *BranchCommands) GetRawBranches() (string, error) {
return self.cmd.New(`git for-each-ref --sort=-committerdate --format="%(HEAD)%00%(refname:short)%00%(upstream:short)%00%(upstream:track)" refs/heads`).DontLog().RunWithOutput() cmdStr := NewGitCmd("for-each-ref").
Arg("--sort=-committerdate").
Arg(`--format="%(HEAD)%00%(refname:short)%00%(upstream:short)%00%(upstream:track)"`).
Arg("refs/heads").
ToString()
return self.cmd.New(cmdStr).DontLog().RunWithOutput()
} }
type MergeOpts struct { type MergeOpts struct {
@ -156,15 +199,12 @@ type MergeOpts struct {
} }
func (self *BranchCommands) Merge(branchName string, opts MergeOpts) error { func (self *BranchCommands) Merge(branchName string, opts MergeOpts) error {
mergeArg := "" command := NewGitCmd("merge").
if self.UserConfig.Git.Merging.Args != "" { Arg("--no-edit").
mergeArg = " " + self.UserConfig.Git.Merging.Args ArgIf(self.UserConfig.Git.Merging.Args != "", self.UserConfig.Git.Merging.Args).
} ArgIf(opts.FastForwardOnly, "--ff-only").
Arg(self.cmd.Quote(branchName)).
command := fmt.Sprintf("git merge --no-edit%s %s", mergeArg, self.cmd.Quote(branchName)) ToString()
if opts.FastForwardOnly {
command = fmt.Sprintf("%s --ff-only", command)
}
return self.cmd.New(command).Run() return self.cmd.New(command).Run()
} }

View file

@ -22,18 +22,27 @@ func NewCommitCommands(gitCommon *GitCommon) *CommitCommands {
// ResetAuthor resets the author of the topmost commit // ResetAuthor resets the author of the topmost commit
func (self *CommitCommands) ResetAuthor() error { func (self *CommitCommands) ResetAuthor() error {
return self.cmd.New("git commit --allow-empty --only --no-edit --amend --reset-author").Run() cmdStr := NewGitCmd("commit").
Arg("--allow-empty", "--only", "--no-edit", "--amend", "--reset-author").
ToString()
return self.cmd.New(cmdStr).Run()
} }
// Sets the commit's author to the supplied value. Value is expected to be of the form 'Name <Email>' // Sets the commit's author to the supplied value. Value is expected to be of the form 'Name <Email>'
func (self *CommitCommands) SetAuthor(value string) error { func (self *CommitCommands) SetAuthor(value string) error {
commandStr := fmt.Sprintf("git commit --allow-empty --only --no-edit --amend --author=%s", self.cmd.Quote(value)) cmdStr := NewGitCmd("commit").
return self.cmd.New(commandStr).Run() Arg("--allow-empty", "--only", "--no-edit", "--amend", "--author="+self.cmd.Quote(value)).
ToString()
return self.cmd.New(cmdStr).Run()
} }
// ResetToCommit reset to commit // ResetToCommit reset to commit
func (self *CommitCommands) ResetToCommit(sha string, strength string, envVars []string) error { func (self *CommitCommands) ResetToCommit(sha string, strength string, envVars []string) error {
return self.cmd.New(fmt.Sprintf("git reset --%s %s", strength, sha)). cmdStr := NewGitCmd("reset").Arg("--"+strength, sha).ToString()
return self.cmd.New(cmdStr).
// prevents git from prompting us for input which would freeze the program // prevents git from prompting us for input which would freeze the program
// TODO: see if this is actually needed here // TODO: see if this is actually needed here
AddEnvVars("GIT_TERMINAL_PROMPT=0"). AddEnvVars("GIT_TERMINAL_PROMPT=0").
@ -45,38 +54,52 @@ func (self *CommitCommands) CommitCmdObj(message string) oscommands.ICmdObj {
messageArgs := self.commitMessageArgs(message) messageArgs := self.commitMessageArgs(message)
skipHookPrefix := self.UserConfig.Git.SkipHookPrefix skipHookPrefix := self.UserConfig.Git.SkipHookPrefix
noVerifyFlag := ""
if skipHookPrefix != "" && strings.HasPrefix(message, skipHookPrefix) {
noVerifyFlag = " --no-verify"
}
return self.cmd.New(fmt.Sprintf("git commit%s%s%s", noVerifyFlag, self.signoffFlag(), messageArgs)) cmdStr := NewGitCmd("commit").
ArgIf(skipHookPrefix != "" && strings.HasPrefix(message, skipHookPrefix), "--no-verify").
ArgIf(self.signoffFlag() != "", self.signoffFlag()).
Arg(messageArgs...).
ToString()
return self.cmd.New(cmdStr)
} }
// RewordLastCommit rewords the topmost commit with the given message // RewordLastCommit rewords the topmost commit with the given message
func (self *CommitCommands) RewordLastCommit(message string) error { func (self *CommitCommands) RewordLastCommit(message string) error {
messageArgs := self.commitMessageArgs(message) messageArgs := self.commitMessageArgs(message)
return self.cmd.New(fmt.Sprintf("git commit --allow-empty --amend --only%s", messageArgs)).Run()
cmdStr := NewGitCmd("commit").
Arg("--allow-empty", "--amend", "--only").
Arg(messageArgs...).
ToString()
return self.cmd.New(cmdStr).Run()
} }
func (self *CommitCommands) commitMessageArgs(message string) string { func (self *CommitCommands) commitMessageArgs(message string) []string {
msg, description, _ := strings.Cut(message, "\n") msg, description, _ := strings.Cut(message, "\n")
descriptionArgs := "" args := []string{"-m", self.cmd.Quote(msg)}
if description != "" { if description != "" {
descriptionArgs = fmt.Sprintf(" -m %s", self.cmd.Quote(description)) args = append(args, "-m", self.cmd.Quote(description))
} }
return fmt.Sprintf(" -m %s%s", self.cmd.Quote(msg), descriptionArgs) return args
} }
// runs git commit without the -m argument meaning it will invoke the user's editor // runs git commit without the -m argument meaning it will invoke the user's editor
func (self *CommitCommands) CommitEditorCmdObj() oscommands.ICmdObj { func (self *CommitCommands) CommitEditorCmdObj() oscommands.ICmdObj {
return self.cmd.New(fmt.Sprintf("git commit%s%s", self.signoffFlag(), self.verboseFlag())) cmdStr := NewGitCmd("commit").
ArgIf(self.signoffFlag() != "", self.signoffFlag()).
ArgIf(self.verboseFlag() != "", self.verboseFlag()).
ToString()
return self.cmd.New(cmdStr)
} }
func (self *CommitCommands) signoffFlag() string { func (self *CommitCommands) signoffFlag() string {
if self.UserConfig.Git.Commit.SignOff { if self.UserConfig.Git.Commit.SignOff {
return " --signoff" return "--signoff"
} else { } else {
return "" return ""
} }
@ -85,9 +108,9 @@ func (self *CommitCommands) signoffFlag() string {
func (self *CommitCommands) verboseFlag() string { func (self *CommitCommands) verboseFlag() string {
switch self.config.UserConfig.Git.Commit.Verbose { switch self.config.UserConfig.Git.Commit.Verbose {
case "always": case "always":
return " --verbose" return "--verbose"
case "never": case "never":
return " --no-verbose" return "--no-verbose"
default: default:
return "" return ""
} }
@ -95,19 +118,25 @@ func (self *CommitCommands) verboseFlag() string {
// Get the subject of the HEAD commit // Get the subject of the HEAD commit
func (self *CommitCommands) GetHeadCommitMessage() (string, error) { func (self *CommitCommands) GetHeadCommitMessage() (string, error) {
message, err := self.cmd.New("git log -1 --pretty=%s").DontLog().RunWithOutput() cmdStr := NewGitCmd("log").Arg("-1", "--pretty=%s").ToString()
message, err := self.cmd.New(cmdStr).DontLog().RunWithOutput()
return strings.TrimSpace(message), err return strings.TrimSpace(message), err
} }
func (self *CommitCommands) GetCommitMessage(commitSha string) (string, error) { func (self *CommitCommands) GetCommitMessage(commitSha string) (string, error) {
cmdStr := "git rev-list --format=%B --max-count=1 " + commitSha cmdStr := NewGitCmd("rev-list").
Arg("--format=%B", "--max-count=1", commitSha).
ToString()
messageWithHeader, err := self.cmd.New(cmdStr).DontLog().RunWithOutput() messageWithHeader, err := self.cmd.New(cmdStr).DontLog().RunWithOutput()
message := strings.Join(strings.SplitAfter(messageWithHeader, "\n")[1:], "") message := strings.Join(strings.SplitAfter(messageWithHeader, "\n")[1:], "")
return strings.TrimSpace(message), err return strings.TrimSpace(message), err
} }
func (self *CommitCommands) GetCommitDiff(commitSha string) (string, error) { func (self *CommitCommands) GetCommitDiff(commitSha string) (string, error) {
cmdStr := "git show --no-color " + commitSha cmdStr := NewGitCmd("show").Arg("--no-color", commitSha).ToString()
diff, err := self.cmd.New(cmdStr).DontLog().RunWithOutput() diff, err := self.cmd.New(cmdStr).DontLog().RunWithOutput()
return diff, err return diff, err
} }
@ -118,7 +147,10 @@ type Author struct {
} }
func (self *CommitCommands) GetCommitAuthor(commitSha string) (Author, error) { func (self *CommitCommands) GetCommitAuthor(commitSha string) (Author, error) {
cmdStr := "git show --no-patch --pretty=format:'%an%x00%ae' " + commitSha cmdStr := NewGitCmd("show").
Arg("--no-patch", "--pretty=format:'%an%x00%ae'", commitSha).
ToString()
output, err := self.cmd.New(cmdStr).DontLog().RunWithOutput() output, err := self.cmd.New(cmdStr).DontLog().RunWithOutput()
if err != nil { if err != nil {
return Author{}, err return Author{}, err
@ -138,15 +170,21 @@ func (self *CommitCommands) GetCommitMessageFirstLine(sha string) (string, error
} }
func (self *CommitCommands) GetCommitMessagesFirstLine(shas []string) (string, error) { func (self *CommitCommands) GetCommitMessagesFirstLine(shas []string) (string, error) {
return self.cmd.New( cmdStr := NewGitCmd("show").
fmt.Sprintf("git show --no-patch --pretty=format:%%s %s", strings.Join(shas, " ")), Arg("--no-patch", "--pretty=format:%s").
).DontLog().RunWithOutput() Arg(shas...).
ToString()
return self.cmd.New(cmdStr).DontLog().RunWithOutput()
} }
func (self *CommitCommands) GetCommitsOneline(shas []string) (string, error) { func (self *CommitCommands) GetCommitsOneline(shas []string) (string, error) {
return self.cmd.New( cmdStr := NewGitCmd("show").
fmt.Sprintf("git show --no-patch --oneline %s", strings.Join(shas, " ")), Arg("--no-patch", "--oneline").
).DontLog().RunWithOutput() Arg(shas...).
ToString()
return self.cmd.New(cmdStr).DontLog().RunWithOutput()
} }
// AmendHead amends HEAD with whatever is staged in your working tree // AmendHead amends HEAD with whatever is staged in your working tree
@ -155,42 +193,57 @@ func (self *CommitCommands) AmendHead() error {
} }
func (self *CommitCommands) AmendHeadCmdObj() oscommands.ICmdObj { func (self *CommitCommands) AmendHeadCmdObj() oscommands.ICmdObj {
return self.cmd.New("git commit --amend --no-edit --allow-empty") cmdStr := NewGitCmd("commit").
Arg("--amend", "--no-edit", "--allow-empty").
ToString()
return self.cmd.New(cmdStr)
} }
func (self *CommitCommands) ShowCmdObj(sha string, filterPath string, ignoreWhitespace bool) oscommands.ICmdObj { func (self *CommitCommands) ShowCmdObj(sha string, filterPath string, ignoreWhitespace bool) oscommands.ICmdObj {
contextSize := self.UserConfig.Git.DiffContextSize contextSize := self.UserConfig.Git.DiffContextSize
filterPathArg := ""
if filterPath != "" {
filterPathArg = fmt.Sprintf(" -- %s", self.cmd.Quote(filterPath))
}
ignoreWhitespaceArg := ""
if ignoreWhitespace {
ignoreWhitespaceArg = " --ignore-all-space"
}
cmdStr := fmt.Sprintf("git show --submodule --color=%s --unified=%d --stat -p %s%s%s", cmdStr := NewGitCmd("show").
self.UserConfig.Git.Paging.ColorArg, contextSize, sha, ignoreWhitespaceArg, filterPathArg) Arg("--submodule").
Arg("--color="+self.UserConfig.Git.Paging.ColorArg).
Arg(fmt.Sprintf("--unified=%d", contextSize)).
Arg("--stat").
Arg("-p").
Arg(sha).
ArgIf(ignoreWhitespace, "--ignore-all-space").
ArgIf(filterPath != "", "--", self.cmd.Quote(filterPath)).
ToString()
return self.cmd.New(cmdStr).DontLog() return self.cmd.New(cmdStr).DontLog()
} }
// Revert reverts the selected commit by sha // Revert reverts the selected commit by sha
func (self *CommitCommands) Revert(sha string) error { func (self *CommitCommands) Revert(sha string) error {
return self.cmd.New(fmt.Sprintf("git revert %s", sha)).Run() cmdStr := NewGitCmd("revert").Arg(sha).ToString()
return self.cmd.New(cmdStr).Run()
} }
func (self *CommitCommands) RevertMerge(sha string, parentNumber int) error { func (self *CommitCommands) RevertMerge(sha string, parentNumber int) error {
return self.cmd.New(fmt.Sprintf("git revert %s -m %d", sha, parentNumber)).Run() cmdStr := NewGitCmd("revert").Arg(sha, "-m", fmt.Sprintf("%d", parentNumber)).
ToString()
return self.cmd.New(cmdStr).Run()
} }
// CreateFixupCommit creates a commit that fixes up a previous commit // CreateFixupCommit creates a commit that fixes up a previous commit
func (self *CommitCommands) CreateFixupCommit(sha string) error { func (self *CommitCommands) CreateFixupCommit(sha string) error {
return self.cmd.New(fmt.Sprintf("git commit --fixup=%s", sha)).Run() cmdStr := NewGitCmd("commit").Arg("--fixup=" + sha).ToString()
return self.cmd.New(cmdStr).Run()
} }
// a value of 0 means the head commit, 1 is the parent commit, etc // a value of 0 means the head commit, 1 is the parent commit, etc
func (self *CommitCommands) GetCommitMessageFromHistory(value int) (string, error) { func (self *CommitCommands) GetCommitMessageFromHistory(value int) (string, error) {
hash, _ := self.cmd.New(fmt.Sprintf("git log -1 --skip=%d --pretty=%%H", value)).DontLog().RunWithOutput() cmdStr := NewGitCmd("log").Arg("-1", fmt.Sprintf("--skip=%d", value), "--pretty=%H").
ToString()
hash, _ := self.cmd.New(cmdStr).DontLog().RunWithOutput()
formattedHash := strings.TrimSpace(hash) formattedHash := strings.TrimSpace(hash)
if len(formattedHash) == 0 { if len(formattedHash) == 0 {
return "", ErrInvalidCommitIndex return "", ErrInvalidCommitIndex

View file

@ -1,7 +1,6 @@
package git_commands package git_commands
import ( import (
"fmt"
"strings" "strings"
"github.com/jesseduffield/generics/slices" "github.com/jesseduffield/generics/slices"
@ -25,12 +24,18 @@ func NewCommitFileLoader(common *common.Common, cmd oscommands.ICmdObjBuilder) *
// GetFilesInDiff get the specified commit files // GetFilesInDiff get the specified commit files
func (self *CommitFileLoader) GetFilesInDiff(from string, to string, reverse bool) ([]*models.CommitFile, error) { func (self *CommitFileLoader) GetFilesInDiff(from string, to string, reverse bool) ([]*models.CommitFile, error) {
reverseFlag := "" cmdStr := NewGitCmd("diff").
if reverse { Arg("--submodule").
reverseFlag = " -R " Arg("--no-ext-diff").
} Arg("--name-status").
Arg("-z").
Arg("--no-renames").
ArgIf(reverse, "-R").
Arg(from).
Arg(to).
ToString()
filenames, err := self.cmd.New(fmt.Sprintf("git diff --submodule --no-ext-diff --name-status -z --no-renames %s %s %s", reverseFlag, from, to)).DontLog().RunWithOutput() filenames, err := self.cmd.New(cmdStr).DontLog().RunWithOutput()
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -201,12 +201,11 @@ func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode
// note that we're not filtering these as we do non-rebasing commits just because // note that we're not filtering these as we do non-rebasing commits just because
// I suspect that will cause some damage // I suspect that will cause some damage
cmdObj := self.cmd.New( cmdObj := self.cmd.New(
fmt.Sprintf( NewGitCmd("show").
"git -c log.showSignature=false show %s --no-patch --oneline %s --abbrev=%d", Config("log.showSignature=false").
strings.Join(commitShas, " "), Arg("--no-patch", "--oneline", "--abbrev=20", prettyFormat).
prettyFormat, Arg(commitShas...).
20, ToString(),
),
).DontLog() ).DontLog()
fullCommits := map[string]*models.Commit{} fullCommits := map[string]*models.Commit{}
@ -375,8 +374,11 @@ func (self *CommitLoader) getMergeBase(refName string) string {
// We pass all configured main branches to the merge-base call; git will // We pass all configured main branches to the merge-base call; git will
// return the base commit for the closest one. // return the base commit for the closest one.
output, err := self.cmd.New(fmt.Sprintf("git merge-base %s %s",
self.cmd.Quote(refName), *self.quotedMainBranches)).DontLog().RunWithOutput() output, err := self.cmd.New(
NewGitCmd("merge-base").Arg(self.cmd.Quote(refName), *self.quotedMainBranches).
ToString(),
).DontLog().RunWithOutput()
if err != nil { if err != nil {
// If there's an error, it must be because one of the main branches that // If there's an error, it must be because one of the main branches that
// used to exist when we called getExistingMainBranches() was deleted // used to exist when we called getExistingMainBranches() was deleted
@ -391,7 +393,9 @@ func (self *CommitLoader) getExistingMainBranches() string {
lo.FilterMap(self.UserConfig.Git.MainBranches, lo.FilterMap(self.UserConfig.Git.MainBranches,
func(branchName string, _ int) (string, bool) { func(branchName string, _ int) (string, bool) {
quotedRef := self.cmd.Quote("refs/heads/" + branchName) quotedRef := self.cmd.Quote("refs/heads/" + branchName)
if err := self.cmd.New(fmt.Sprintf("git rev-parse --verify --quiet %s", quotedRef)).DontLog().Run(); err != nil { if err := self.cmd.New(
NewGitCmd("rev-parse").Arg("--verify", "--quiet", quotedRef).ToString(),
).DontLog().Run(); err != nil {
return "", false return "", false
} }
return quotedRef, true return quotedRef, true
@ -413,9 +417,10 @@ func ignoringWarnings(commandOutput string) string {
func (self *CommitLoader) getFirstPushedCommit(refName string) (string, error) { func (self *CommitLoader) getFirstPushedCommit(refName string) (string, error) {
output, err := self.cmd. output, err := self.cmd.
New( New(
fmt.Sprintf("git merge-base %s %s@{u}", NewGitCmd("merge-base").
self.cmd.Quote(refName), Arg(self.cmd.Quote(refName)).
self.cmd.Quote(strings.TrimPrefix(refName, "refs/heads/"))), Arg(self.cmd.Quote(strings.TrimPrefix(refName, "refs/heads/")) + "@{u}").
ToString(),
). ).
DontLog(). DontLog().
RunWithOutput() RunWithOutput()
@ -428,42 +433,23 @@ func (self *CommitLoader) getFirstPushedCommit(refName string) (string, error) {
// getLog gets the git log. // getLog gets the git log.
func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj { func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj {
limitFlag := ""
if opts.Limit {
limitFlag = " -300"
}
followFlag := ""
filterFlag := ""
if opts.FilterPath != "" {
followFlag = " --follow"
filterFlag = fmt.Sprintf(" %s", self.cmd.Quote(opts.FilterPath))
}
config := self.UserConfig.Git.Log config := self.UserConfig.Git.Log
orderFlag := "" cmdStr := NewGitCmd("log").
if config.Order != "default" { Arg(self.cmd.Quote(opts.RefName)).
orderFlag = " --" + config.Order ArgIf(config.Order != "default", "--"+config.Order).
} ArgIf(opts.All, "--all").
allFlag := "" Arg("--oneline").
if opts.All { Arg(prettyFormat).
allFlag = " --all" Arg("--abbrev=40").
} ArgIf(opts.Limit, "-300").
ArgIf(opts.FilterPath != "", "--follow").
Arg("--no-show-signature").
Arg("--").
ArgIf(opts.FilterPath != "", self.cmd.Quote(opts.FilterPath)).
ToString()
return self.cmd.New( return self.cmd.New(cmdStr).DontLog()
fmt.Sprintf(
"git log %s%s%s --oneline %s%s --abbrev=%d%s --no-show-signature --%s",
self.cmd.Quote(opts.RefName),
orderFlag,
allFlag,
prettyFormat,
limitFlag,
40,
followFlag,
filterFlag,
),
).DontLog()
} }
const prettyFormat = `--pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s"` const prettyFormat = `--pretty=format:"%H%x00%at%x00%aN%x00%ae%x00%d%x00%p%x00%s"`

View file

@ -42,7 +42,7 @@ func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File
} }
untrackedFilesArg := fmt.Sprintf("--untracked-files=%s", untrackedFilesSetting) untrackedFilesArg := fmt.Sprintf("--untracked-files=%s", untrackedFilesSetting)
statuses, err := self.GitStatus(GitStatusOptions{NoRenames: opts.NoRenames, UntrackedFilesArg: untrackedFilesArg}) statuses, err := self.gitStatus(GitStatusOptions{NoRenames: opts.NoRenames, UntrackedFilesArg: untrackedFilesArg})
if err != nil { if err != nil {
self.Log.Error(err) self.Log.Error(err)
} }
@ -81,13 +81,15 @@ type FileStatus struct {
PreviousName string PreviousName string
} }
func (c *FileLoader) GitStatus(opts GitStatusOptions) ([]FileStatus, error) { func (c *FileLoader) gitStatus(opts GitStatusOptions) ([]FileStatus, error) {
noRenamesFlag := "" cmdStr := NewGitCmd("status").
if opts.NoRenames { Arg(opts.UntrackedFilesArg).
noRenamesFlag = " --no-renames" Arg("--porcelain").
} Arg("-z").
ArgIf(opts.NoRenames, "--no-renames").
ToString()
statusLines, _, err := c.cmd.New(fmt.Sprintf("git status %s --porcelain -z%s", opts.UntrackedFilesArg, noRenamesFlag)).DontLog().RunWithOutputs() statusLines, _, err := c.cmd.New(cmdStr).DontLog().RunWithOutputs()
if err != nil { if err != nil {
return []FileStatus{}, err return []FileStatus{}, err
} }

View file

@ -34,6 +34,7 @@ func (self *FlowCommands) FinishCmdObj(branchName string) (oscommands.ICmdObj, e
branchType := "" branchType := ""
for _, line := range strings.Split(strings.TrimSpace(prefixes), "\n") { for _, line := range strings.Split(strings.TrimSpace(prefixes), "\n") {
if strings.HasPrefix(line, "gitflow.prefix.") && strings.HasSuffix(line, prefix) { if strings.HasPrefix(line, "gitflow.prefix.") && strings.HasSuffix(line, prefix) {
regex := regexp.MustCompile("gitflow.prefix.([^ ]*) .*") regex := regexp.MustCompile("gitflow.prefix.([^ ]*) .*")
matches := regex.FindAllStringSubmatch(line, 1) matches := regex.FindAllStringSubmatch(line, 1)
@ -48,9 +49,13 @@ func (self *FlowCommands) FinishCmdObj(branchName string) (oscommands.ICmdObj, e
return nil, errors.New(self.Tr.NotAGitFlowBranch) return nil, errors.New(self.Tr.NotAGitFlowBranch)
} }
return self.cmd.New("git flow " + branchType + " finish " + suffix), nil cmdStr := NewGitCmd("flow").Arg(branchType, "finish", suffix).ToString()
return self.cmd.New(cmdStr), nil
} }
func (self *FlowCommands) StartCmdObj(branchType string, name string) oscommands.ICmdObj { func (self *FlowCommands) StartCmdObj(branchType string, name string) oscommands.ICmdObj {
return self.cmd.New("git flow " + branchType + " start " + name) cmdStr := NewGitCmd("flow").Arg(branchType, "start", name).ToString()
return self.cmd.New(cmdStr)
} }

View file

@ -0,0 +1,54 @@
package git_commands
import "strings"
// convenience struct for building git commands. Especially useful when
// including conditional args
type GitCommandBuilder struct {
// command string
args []string
}
func NewGitCmd(command string) *GitCommandBuilder {
return &GitCommandBuilder{args: []string{command}}
}
func (self *GitCommandBuilder) Arg(args ...string) *GitCommandBuilder {
self.args = append(self.args, args...)
return self
}
func (self *GitCommandBuilder) ArgIf(condition bool, ifTrue ...string) *GitCommandBuilder {
if condition {
self.Arg(ifTrue...)
}
return self
}
func (self *GitCommandBuilder) ArgIfElse(condition bool, ifTrue string, ifFalse string) *GitCommandBuilder {
if condition {
return self.Arg(ifTrue)
} else {
return self.Arg(ifFalse)
}
}
func (self *GitCommandBuilder) Config(value string) *GitCommandBuilder {
// config settings come before the command
self.args = append([]string{"-c", value}, self.args...)
return self
}
func (self *GitCommandBuilder) RepoPath(value string) *GitCommandBuilder {
// repo path comes before the command
self.args = append([]string{"-C", value}, self.args...)
return self
}
func (self *GitCommandBuilder) ToString() string {
return "git " + strings.Join(self.args, " ")
}

View file

@ -0,0 +1,56 @@
package git_commands
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGitCommandBuilder(t *testing.T) {
scenarios := []struct {
input string
expected string
}{
{
input: NewGitCmd("push").
Arg("--force-with-lease").
Arg("--set-upstream").
Arg("origin").
Arg("master").
ToString(),
expected: "git push --force-with-lease --set-upstream origin master",
},
{
input: NewGitCmd("push").ArgIf(true, "--test").ToString(),
expected: "git push --test",
},
{
input: NewGitCmd("push").ArgIf(false, "--test").ToString(),
expected: "git push",
},
{
input: NewGitCmd("push").ArgIfElse(true, "-b", "-a").ToString(),
expected: "git push -b",
},
{
input: NewGitCmd("push").ArgIfElse(false, "-a", "-b").ToString(),
expected: "git push -b",
},
{
input: NewGitCmd("push").Arg("-a", "-b").ToString(),
expected: "git push -a -b",
},
{
input: NewGitCmd("push").Config("user.name=foo").Config("user.email=bar").ToString(),
expected: "git -c user.email=bar -c user.name=foo push",
},
{
input: NewGitCmd("push").RepoPath("a/b/c").ToString(),
expected: "git -C a/b/c push",
},
}
for _, s := range scenarios {
assert.Equal(t, s.input, s.expected)
}
}

View file

@ -267,5 +267,7 @@ func (self *PatchCommands) PullPatchIntoNewCommit(commits []*models.Commit, comm
// only some lines of a range of adjacent added lines. To solve this, we // only some lines of a range of adjacent added lines. To solve this, we
// get the diff of HEAD and the original commit and then apply that. // get the diff of HEAD and the original commit and then apply that.
func (self *PatchCommands) diffHeadAgainstCommit(commit *models.Commit) (string, error) { func (self *PatchCommands) diffHeadAgainstCommit(commit *models.Commit) (string, error) {
return self.cmd.New(fmt.Sprintf("git diff HEAD..%s", commit.Sha)).RunWithOutput() cmdStr := NewGitCmd("diff").Arg("HEAD.." + commit.Sha).ToString()
return self.cmd.New(cmdStr).RunWithOutput()
} }

View file

@ -176,23 +176,21 @@ type PrepareInteractiveRebaseCommandOpts struct {
func (self *RebaseCommands) PrepareInteractiveRebaseCommand(opts PrepareInteractiveRebaseCommandOpts) oscommands.ICmdObj { func (self *RebaseCommands) PrepareInteractiveRebaseCommand(opts PrepareInteractiveRebaseCommandOpts) oscommands.ICmdObj {
ex := oscommands.GetLazygitPath() ex := oscommands.GetLazygitPath()
cmdStr := NewGitCmd("rebase").
Arg("--interactive").
Arg("--autostash").
Arg("--keep-empty").
ArgIf(!self.version.IsOlderThan(2, 26, 0), "--empty=keep").
Arg("--no-autosquash").
ArgIf(!self.version.IsOlderThan(2, 22, 0), "--rebase-merges").
Arg(opts.baseShaOrRoot).
ToString()
debug := "FALSE" debug := "FALSE"
if self.Debug { if self.Debug {
debug = "TRUE" debug = "TRUE"
} }
emptyArg := " --empty=keep"
if self.version.IsOlderThan(2, 26, 0) {
emptyArg = ""
}
rebaseMergesArg := " --rebase-merges"
if self.version.IsOlderThan(2, 22, 0) {
rebaseMergesArg = ""
}
cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty%s --no-autosquash%s %s",
emptyArg, rebaseMergesArg, opts.baseShaOrRoot)
self.Log.WithField("command", cmdStr).Debug("RunCommand") self.Log.WithField("command", cmdStr).Debug("RunCommand")
cmdObj := self.cmd.New(cmdStr) cmdObj := self.cmd.New(cmdStr)
@ -228,7 +226,8 @@ func (self *RebaseCommands) AmendTo(commits []*models.Commit, commitIndex int) e
} }
// Get the sha of the commit we just created // Get the sha of the commit we just created
fixupSha, err := self.cmd.New("git rev-parse --verify HEAD").RunWithOutput() cmdStr := NewGitCmd("rev-parse").Arg("--verify", "HEAD").ToString()
fixupSha, err := self.cmd.New(cmdStr).RunWithOutput()
if err != nil { if err != nil {
return err return err
} }
@ -265,14 +264,11 @@ func (self *RebaseCommands) SquashAllAboveFixupCommits(commit *models.Commit) er
shaOrRoot = "--root" shaOrRoot = "--root"
} }
return self.runSkipEditorCommand( cmdStr := NewGitCmd("rebase").
self.cmd.New( Arg("--interactive", "--rebase-merges", "--autostash", "--autosquash", shaOrRoot).
fmt.Sprintf( ToString()
"git rebase --interactive --rebase-merges --autostash --autosquash %s",
shaOrRoot, return self.runSkipEditorCommand(self.cmd.New(cmdStr))
),
),
)
} }
// BeginInteractiveRebaseForCommit starts an interactive rebase to edit the current // BeginInteractiveRebaseForCommit starts an interactive rebase to edit the current
@ -308,7 +304,9 @@ func (self *RebaseCommands) RebaseBranch(branchName string) error {
} }
func (self *RebaseCommands) GenericMergeOrRebaseActionCmdObj(commandType string, command string) oscommands.ICmdObj { func (self *RebaseCommands) GenericMergeOrRebaseActionCmdObj(commandType string, command string) oscommands.ICmdObj {
return self.cmd.New("git " + commandType + " --" + command) cmdStr := NewGitCmd(commandType).Arg("--" + command).ToString()
return self.cmd.New(cmdStr)
} }
func (self *RebaseCommands) ContinueRebase() error { func (self *RebaseCommands) ContinueRebase() error {
@ -365,7 +363,9 @@ func (self *RebaseCommands) DiscardOldFileChanges(commits []*models.Commit, comm
} }
// check if file exists in previous commit (this command returns an error if the file doesn't exist) // check if file exists in previous commit (this command returns an error if the file doesn't exist)
if err := self.cmd.New("git cat-file -e HEAD^:" + self.cmd.Quote(fileName)).Run(); err != nil { cmdStr := NewGitCmd("cat-file").Arg("-e", "HEAD^:"+self.cmd.Quote(fileName)).ToString()
if err := self.cmd.New(cmdStr).Run(); err != nil {
if err := self.os.Remove(fileName); err != nil { if err := self.os.Remove(fileName); err != nil {
return err return err
} }

View file

@ -15,44 +15,52 @@ func NewRemoteCommands(gitCommon *GitCommon) *RemoteCommands {
} }
func (self *RemoteCommands) AddRemote(name string, url string) error { func (self *RemoteCommands) AddRemote(name string, url string) error {
return self.cmd. cmdStr := NewGitCmd("remote").
New(fmt.Sprintf("git remote add %s %s", self.cmd.Quote(name), self.cmd.Quote(url))). Arg("add", self.cmd.Quote(name), self.cmd.Quote(url)).
Run() ToString()
return self.cmd.New(cmdStr).Run()
} }
func (self *RemoteCommands) RemoveRemote(name string) error { func (self *RemoteCommands) RemoveRemote(name string) error {
return self.cmd. cmdStr := NewGitCmd("remote").
New(fmt.Sprintf("git remote remove %s", self.cmd.Quote(name))). Arg("remove", self.cmd.Quote(name)).
Run() ToString()
return self.cmd.New(cmdStr).Run()
} }
func (self *RemoteCommands) RenameRemote(oldRemoteName string, newRemoteName string) error { func (self *RemoteCommands) RenameRemote(oldRemoteName string, newRemoteName string) error {
return self.cmd. cmdStr := NewGitCmd("remote").
New(fmt.Sprintf("git remote rename %s %s", self.cmd.Quote(oldRemoteName), self.cmd.Quote(newRemoteName))). Arg("rename", self.cmd.Quote(oldRemoteName), self.cmd.Quote(newRemoteName)).
Run() ToString()
return self.cmd.New(cmdStr).Run()
} }
func (self *RemoteCommands) UpdateRemoteUrl(remoteName string, updatedUrl string) error { func (self *RemoteCommands) UpdateRemoteUrl(remoteName string, updatedUrl string) error {
return self.cmd. cmdStr := NewGitCmd("remote").
New(fmt.Sprintf("git remote set-url %s %s", self.cmd.Quote(remoteName), self.cmd.Quote(updatedUrl))). Arg("set-url", self.cmd.Quote(remoteName), self.cmd.Quote(updatedUrl)).
Run() ToString()
return self.cmd.New(cmdStr).Run()
} }
func (self *RemoteCommands) DeleteRemoteBranch(remoteName string, branchName string) error { func (self *RemoteCommands) DeleteRemoteBranch(remoteName string, branchName string) error {
command := fmt.Sprintf("git push %s --delete %s", self.cmd.Quote(remoteName), self.cmd.Quote(branchName)) cmdStr := NewGitCmd("push").
return self.cmd.New(command).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run() Arg(self.cmd.Quote(remoteName), "--delete", self.cmd.Quote(branchName)).
ToString()
return self.cmd.New(cmdStr).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
} }
// CheckRemoteBranchExists Returns remote branch // CheckRemoteBranchExists Returns remote branch
func (self *RemoteCommands) CheckRemoteBranchExists(branchName string) bool { func (self *RemoteCommands) CheckRemoteBranchExists(branchName string) bool {
_, err := self.cmd. cmdStr := NewGitCmd("show-ref").
New( Arg("--verify", "--", fmt.Sprintf("refs/remotes/origin/%s", self.cmd.Quote(branchName))).
fmt.Sprintf("git show-ref --verify -- refs/remotes/origin/%s", ToString()
self.cmd.Quote(branchName),
), _, err := self.cmd.New(cmdStr).DontLog().RunWithOutput()
).
DontLog().
RunWithOutput()
return err == nil return err == nil
} }

View file

@ -31,7 +31,8 @@ func NewRemoteLoader(
} }
func (self *RemoteLoader) GetRemotes() ([]*models.Remote, error) { func (self *RemoteLoader) GetRemotes() ([]*models.Remote, error) {
remoteBranchesStr, err := self.cmd.New("git branch -r").DontLog().RunWithOutput() cmdStr := NewGitCmd("branch").Arg("-r").ToString()
remoteBranchesStr, err := self.cmd.New(cmdStr).DontLog().RunWithOutput()
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -26,68 +26,94 @@ func NewStashCommands(
} }
func (self *StashCommands) DropNewest() error { func (self *StashCommands) DropNewest() error {
return self.cmd.New("git stash drop").Run() cmdStr := NewGitCmd("stash").Arg("drop").ToString()
return self.cmd.New(cmdStr).Run()
} }
func (self *StashCommands) Drop(index int) error { func (self *StashCommands) Drop(index int) error {
return self.cmd.New(fmt.Sprintf("git stash drop stash@{%d}", index)).Run() cmdStr := NewGitCmd("stash").Arg("drop", fmt.Sprintf("stash@{%d}", index)).
ToString()
return self.cmd.New(cmdStr).Run()
} }
func (self *StashCommands) Pop(index int) error { func (self *StashCommands) Pop(index int) error {
return self.cmd.New(fmt.Sprintf("git stash pop stash@{%d}", index)).Run() cmdStr := NewGitCmd("stash").Arg("pop", fmt.Sprintf("stash@{%d}", index)).
ToString()
return self.cmd.New(cmdStr).Run()
} }
func (self *StashCommands) Apply(index int) error { func (self *StashCommands) Apply(index int) error {
return self.cmd.New(fmt.Sprintf("git stash apply stash@{%d}", index)).Run() cmdStr := NewGitCmd("stash").Arg("apply", fmt.Sprintf("stash@{%d}", index)).
ToString()
return self.cmd.New(cmdStr).Run()
} }
// Save save stash // Save save stash
func (self *StashCommands) Save(message string) error { func (self *StashCommands) Save(message string) error {
return self.cmd.New("git stash save " + self.cmd.Quote(message)).Run() cmdStr := NewGitCmd("stash").Arg("save", self.cmd.Quote(message)).
ToString()
return self.cmd.New(cmdStr).Run()
} }
func (self *StashCommands) Store(sha string, message string) error { func (self *StashCommands) Store(sha string, message string) error {
trimmedMessage := strings.Trim(message, " \t") trimmedMessage := strings.Trim(message, " \t")
if len(trimmedMessage) > 0 {
return self.cmd.New(fmt.Sprintf("git stash store %s -m %s", self.cmd.Quote(sha), self.cmd.Quote(trimmedMessage))).Run() cmdStr := NewGitCmd("stash").Arg("store", self.cmd.Quote(sha)).
} ArgIf(trimmedMessage != "", "-m", self.cmd.Quote(trimmedMessage)).
return self.cmd.New(fmt.Sprintf("git stash store %s", self.cmd.Quote(sha))).Run() ToString()
return self.cmd.New(cmdStr).Run()
} }
func (self *StashCommands) Sha(index int) (string, error) { func (self *StashCommands) Sha(index int) (string, error) {
sha, _, err := self.cmd.New(fmt.Sprintf("git rev-parse refs/stash@{%d}", index)).DontLog().RunWithOutputs() cmdStr := NewGitCmd("rev-parse").
Arg(fmt.Sprintf("refs/stash@{%d}", index)).
ToString()
sha, _, err := self.cmd.New(cmdStr).DontLog().RunWithOutputs()
return strings.Trim(sha, "\r\n"), err return strings.Trim(sha, "\r\n"), err
} }
func (self *StashCommands) ShowStashEntryCmdObj(index int, ignoreWhitespace bool) oscommands.ICmdObj { func (self *StashCommands) ShowStashEntryCmdObj(index int, ignoreWhitespace bool) oscommands.ICmdObj {
ignoreWhitespaceFlag := "" cmdStr := NewGitCmd("stash").Arg("show").
if ignoreWhitespace { Arg("-p").
ignoreWhitespaceFlag = " --ignore-all-space" Arg("--stat").
} Arg(fmt.Sprintf("--color=%s", self.UserConfig.Git.Paging.ColorArg)).
Arg(fmt.Sprintf("--unified=%d", self.UserConfig.Git.DiffContextSize)).
cmdStr := fmt.Sprintf( ArgIf(ignoreWhitespace, "--ignore-all-space").
"git stash show -p --stat --color=%s --unified=%d%s stash@{%d}", Arg(fmt.Sprintf("stash@{%d}", index)).
self.UserConfig.Git.Paging.ColorArg, ToString()
self.UserConfig.Git.DiffContextSize,
ignoreWhitespaceFlag,
index,
)
return self.cmd.New(cmdStr).DontLog() return self.cmd.New(cmdStr).DontLog()
} }
func (self *StashCommands) StashAndKeepIndex(message string) error { func (self *StashCommands) StashAndKeepIndex(message string) error {
return self.cmd.New(fmt.Sprintf("git stash save %s --keep-index", self.cmd.Quote(message))).Run() cmdStr := NewGitCmd("stash").Arg("save", self.cmd.Quote(message), "--keep-index").
ToString()
return self.cmd.New(cmdStr).Run()
} }
func (self *StashCommands) StashUnstagedChanges(message string) error { func (self *StashCommands) StashUnstagedChanges(message string) error {
if err := self.cmd.New("git commit --no-verify -m \"[lazygit] stashing unstaged changes\"").Run(); err != nil { if err := self.cmd.New(
NewGitCmd("commit").
Arg("--no-verify", "-m", self.cmd.Quote("[lazygit] stashing unstaged changes")).
ToString(),
).Run(); err != nil {
return err return err
} }
if err := self.Save(message); err != nil { if err := self.Save(message); err != nil {
return err return err
} }
if err := self.cmd.New("git reset --soft HEAD^").Run(); err != nil {
if err := self.cmd.New(
NewGitCmd("reset").Arg("--soft", "HEAD^").ToString(),
).Run(); err != nil {
return err return err
} }
return nil return nil
@ -97,7 +123,9 @@ func (self *StashCommands) StashUnstagedChanges(message string) error {
// shoutouts to Joe on https://stackoverflow.com/questions/14759748/stashing-only-staged-changes-in-git-is-it-possible // shoutouts to Joe on https://stackoverflow.com/questions/14759748/stashing-only-staged-changes-in-git-is-it-possible
func (self *StashCommands) SaveStagedChanges(message string) error { func (self *StashCommands) SaveStagedChanges(message string) error {
// wrap in 'writing', which uses a mutex // wrap in 'writing', which uses a mutex
if err := self.cmd.New("git stash --keep-index").Run(); err != nil { if err := self.cmd.New(
NewGitCmd("stash").Arg("--keep-index").ToString(),
).Run(); err != nil {
return err return err
} }
@ -105,15 +133,22 @@ func (self *StashCommands) SaveStagedChanges(message string) error {
return err return err
} }
if err := self.cmd.New("git stash apply stash@{1}").Run(); err != nil { if err := self.cmd.New(
NewGitCmd("stash").Arg("apply", "stash@{1}").ToString(),
).Run(); err != nil {
return err return err
} }
if err := self.os.PipeCommands("git stash show -p", "git apply -R"); err != nil { if err := self.os.PipeCommands(
NewGitCmd("stash").Arg("show", "-p").ToString(),
NewGitCmd("apply").Arg("-R").ToString(),
); err != nil {
return err return err
} }
if err := self.cmd.New("git stash drop stash@{1}").Run(); err != nil { if err := self.cmd.New(
NewGitCmd("stash").Arg("drop", "stash@{1}").ToString(),
).Run(); err != nil {
return err return err
} }
@ -135,7 +170,10 @@ func (self *StashCommands) SaveStagedChanges(message string) error {
} }
func (self *StashCommands) StashIncludeUntrackedChanges(message string) error { func (self *StashCommands) StashIncludeUntrackedChanges(message string) error {
return self.cmd.New(fmt.Sprintf("git stash save %s --include-untracked", self.cmd.Quote(message))).Run() return self.cmd.New(
NewGitCmd("stash").Arg("save", self.cmd.Quote(message), "--include-untracked").
ToString(),
).Run()
} }
func (self *StashCommands) Rename(index int, message string) error { func (self *StashCommands) Rename(index int, message string) error {

View file

@ -32,7 +32,8 @@ func (self *StashLoader) GetStashEntries(filterPath string) []*models.StashEntry
return self.getUnfilteredStashEntries() return self.getUnfilteredStashEntries()
} }
rawString, err := self.cmd.New("git stash list --name-only").DontLog().RunWithOutput() cmdStr := NewGitCmd("stash").Arg("list", "--name-only").ToString()
rawString, err := self.cmd.New(cmdStr).DontLog().RunWithOutput()
if err != nil { if err != nil {
return self.getUnfilteredStashEntries() return self.getUnfilteredStashEntries()
} }
@ -65,7 +66,9 @@ outer:
} }
func (self *StashLoader) getUnfilteredStashEntries() []*models.StashEntry { func (self *StashLoader) getUnfilteredStashEntries() []*models.StashEntry {
rawString, _ := self.cmd.New("git stash list -z --pretty='%gs'").DontLog().RunWithOutput() cmdStr := NewGitCmd("stash").Arg("list", "-z", "--pretty='%gs'").ToString()
rawString, _ := self.cmd.New(cmdStr).DontLog().RunWithOutput()
return slices.MapWithIndex(utils.SplitNul(rawString), func(line string, index int) *models.StashEntry { return slices.MapWithIndex(utils.SplitNul(rawString), func(line string, index int) *models.StashEntry {
return self.stashEntryFromLine(line, index) return self.stashEntryFromLine(line, index)
}) })

View file

@ -56,7 +56,9 @@ func (self *StatusCommands) IsBareRepo() (bool, error) {
} }
func IsBareRepo(osCommand *oscommands.OSCommand) (bool, error) { func IsBareRepo(osCommand *oscommands.OSCommand) (bool, error) {
res, err := osCommand.Cmd.New("git rev-parse --is-bare-repository").DontLog().RunWithOutput() res, err := osCommand.Cmd.New(
NewGitCmd("rev-parse").Arg("--is-bare-repository").ToString(),
).DontLog().RunWithOutput()
if err != nil { if err != nil {
return false, err return false, err
} }

View file

@ -2,7 +2,6 @@ package git_commands
import ( import (
"bufio" "bufio"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
@ -82,38 +81,60 @@ func (self *SubmoduleCommands) Stash(submodule *models.SubmoduleConfig) error {
return nil return nil
} }
return self.cmd.New("git -C " + self.cmd.Quote(submodule.Path) + " stash --include-untracked").Run() cmdStr := NewGitCmd("stash").
RepoPath(self.cmd.Quote(submodule.Path)).
Arg("--include-untracked").
ToString()
return self.cmd.New(cmdStr).Run()
} }
func (self *SubmoduleCommands) Reset(submodule *models.SubmoduleConfig) error { func (self *SubmoduleCommands) Reset(submodule *models.SubmoduleConfig) error {
return self.cmd.New("git submodule update --init --force -- " + self.cmd.Quote(submodule.Path)).Run() cmdStr := NewGitCmd("submodule").
Arg("update", "--init", "--force", "--", self.cmd.Quote(submodule.Path)).
ToString()
return self.cmd.New(cmdStr).Run()
} }
func (self *SubmoduleCommands) UpdateAll() error { func (self *SubmoduleCommands) UpdateAll() error {
// not doing an --init here because the user probably doesn't want that // not doing an --init here because the user probably doesn't want that
return self.cmd.New("git submodule update --force").Run() cmdStr := NewGitCmd("submodule").Arg("update", "--force").ToString()
return self.cmd.New(cmdStr).Run()
} }
func (self *SubmoduleCommands) Delete(submodule *models.SubmoduleConfig) error { func (self *SubmoduleCommands) Delete(submodule *models.SubmoduleConfig) error {
// based on https://gist.github.com/myusuf3/7f645819ded92bda6677 // based on https://gist.github.com/myusuf3/7f645819ded92bda6677
if err := self.cmd.New("git submodule deinit --force -- " + self.cmd.Quote(submodule.Path)).Run(); err != nil { if err := self.cmd.New(
if strings.Contains(err.Error(), "did not match any file(s) known to git") { NewGitCmd("submodule").
if err := self.cmd.New("git config --file .gitmodules --remove-section submodule." + self.cmd.Quote(submodule.Name)).Run(); err != nil { Arg("deinit", "--force", "--", self.cmd.Quote(submodule.Path)).ToString(),
return err ).Run(); err != nil {
} if !strings.Contains(err.Error(), "did not match any file(s) known to git") {
return err
}
if err := self.cmd.New("git config --remove-section submodule." + self.cmd.Quote(submodule.Name)).Run(); err != nil { if err := self.cmd.New(
return err NewGitCmd("config").
} Arg("--file", ".gitmodules", "--remove-section", "submodule."+self.cmd.Quote(submodule.Path)).
ToString(),
).Run(); err != nil {
return err
}
// if there's an error here about it not existing then we'll just continue to do `git rm` if err := self.cmd.New(
} else { NewGitCmd("config").
Arg("--remove-section", "submodule."+self.cmd.Quote(submodule.Path)).
ToString(),
).Run(); err != nil {
return err return err
} }
} }
if err := self.cmd.New("git rm --force -r " + submodule.Path).Run(); err != nil { if err := self.cmd.New(
NewGitCmd("rm").Arg("--force", "-r", submodule.Path).ToString(),
).Run(); err != nil {
// if the directory isn't there then that's fine // if the directory isn't there then that's fine
self.Log.Error(err) self.Log.Error(err)
} }
@ -122,24 +143,35 @@ func (self *SubmoduleCommands) Delete(submodule *models.SubmoduleConfig) error {
} }
func (self *SubmoduleCommands) Add(name string, path string, url string) error { func (self *SubmoduleCommands) Add(name string, path string, url string) error {
return self.cmd. cmdStr := NewGitCmd("submodule").
New( Arg("add").
fmt.Sprintf( Arg("--force").
"git submodule add --force --name %s -- %s %s ", Arg("--name").
self.cmd.Quote(name), Arg(self.cmd.Quote(name)).
self.cmd.Quote(url), Arg("--").
self.cmd.Quote(path), Arg(self.cmd.Quote(url)).
)). Arg(self.cmd.Quote(path)).
Run() ToString()
return self.cmd.New(cmdStr).Run()
} }
func (self *SubmoduleCommands) UpdateUrl(name string, path string, newUrl string) error { func (self *SubmoduleCommands) UpdateUrl(name string, path string, newUrl string) error {
setUrlCmdStr := NewGitCmd("config").
Arg(
"--file", ".gitmodules", "submodule."+self.cmd.Quote(name)+".url", self.cmd.Quote(newUrl),
).
ToString()
// the set-url command is only for later git versions so we're doing it manually here // the set-url command is only for later git versions so we're doing it manually here
if err := self.cmd.New("git config --file .gitmodules submodule." + self.cmd.Quote(name) + ".url " + self.cmd.Quote(newUrl)).Run(); err != nil { if err := self.cmd.New(setUrlCmdStr).Run(); err != nil {
return err return err
} }
if err := self.cmd.New("git submodule sync -- " + self.cmd.Quote(path)).Run(); err != nil { syncCmdStr := NewGitCmd("submodule").Arg("sync", "--", self.cmd.Quote(path)).
ToString()
if err := self.cmd.New(syncCmdStr).Run(); err != nil {
return err return err
} }
@ -147,27 +179,45 @@ func (self *SubmoduleCommands) UpdateUrl(name string, path string, newUrl string
} }
func (self *SubmoduleCommands) Init(path string) error { func (self *SubmoduleCommands) Init(path string) error {
return self.cmd.New("git submodule init -- " + self.cmd.Quote(path)).Run() cmdStr := NewGitCmd("submodule").Arg("init", "--", self.cmd.Quote(path)).
ToString()
return self.cmd.New(cmdStr).Run()
} }
func (self *SubmoduleCommands) Update(path string) error { func (self *SubmoduleCommands) Update(path string) error {
return self.cmd.New("git submodule update --init -- " + self.cmd.Quote(path)).Run() cmdStr := NewGitCmd("submodule").Arg("update", "--init", "--", self.cmd.Quote(path)).
ToString()
return self.cmd.New(cmdStr).Run()
} }
func (self *SubmoduleCommands) BulkInitCmdObj() oscommands.ICmdObj { func (self *SubmoduleCommands) BulkInitCmdObj() oscommands.ICmdObj {
return self.cmd.New("git submodule init") cmdStr := NewGitCmd("submodule").Arg("init").
ToString()
return self.cmd.New(cmdStr)
} }
func (self *SubmoduleCommands) BulkUpdateCmdObj() oscommands.ICmdObj { func (self *SubmoduleCommands) BulkUpdateCmdObj() oscommands.ICmdObj {
return self.cmd.New("git submodule update") cmdStr := NewGitCmd("submodule").Arg("update").
ToString()
return self.cmd.New(cmdStr)
} }
func (self *SubmoduleCommands) ForceBulkUpdateCmdObj() oscommands.ICmdObj { func (self *SubmoduleCommands) ForceBulkUpdateCmdObj() oscommands.ICmdObj {
return self.cmd.New("git submodule update --force") cmdStr := NewGitCmd("submodule").Arg("update", "--force").
ToString()
return self.cmd.New(cmdStr)
} }
func (self *SubmoduleCommands) BulkDeinitCmdObj() oscommands.ICmdObj { func (self *SubmoduleCommands) BulkDeinitCmdObj() oscommands.ICmdObj {
return self.cmd.New("git submodule deinit --all --force") cmdStr := NewGitCmd("submodule").Arg("deinit", "--all", "--force").
ToString()
return self.cmd.New(cmdStr)
} }
func (self *SubmoduleCommands) ResetSubmodules(submodules []*models.SubmoduleConfig) error { func (self *SubmoduleCommands) ResetSubmodules(submodules []*models.SubmoduleConfig) error {

View file

@ -1,8 +1,6 @@
package git_commands package git_commands
import ( import (
"fmt"
"github.com/go-errors/errors" "github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands"
) )
@ -26,26 +24,16 @@ type PushOpts struct {
} }
func (self *SyncCommands) PushCmdObj(opts PushOpts) (oscommands.ICmdObj, error) { func (self *SyncCommands) PushCmdObj(opts PushOpts) (oscommands.ICmdObj, error) {
cmdStr := "git push" if opts.UpstreamBranch != "" && opts.UpstreamRemote == "" {
return nil, errors.New(self.Tr.MustSpecifyOriginError)
if opts.Force {
cmdStr += " --force-with-lease"
} }
if opts.SetUpstream { cmdStr := NewGitCmd("push").
cmdStr += " --set-upstream" ArgIf(opts.Force, "--force-with-lease").
} ArgIf(opts.SetUpstream, "--set-upstream").
ArgIf(opts.UpstreamRemote != "", self.cmd.Quote(opts.UpstreamRemote)).
if opts.UpstreamRemote != "" { ArgIf(opts.UpstreamBranch != "", self.cmd.Quote(opts.UpstreamBranch)).
cmdStr += " " + self.cmd.Quote(opts.UpstreamRemote) ToString()
}
if opts.UpstreamBranch != "" {
if opts.UpstreamRemote == "" {
return nil, errors.New(self.Tr.MustSpecifyOriginError)
}
cmdStr += " " + self.cmd.Quote(opts.UpstreamBranch)
}
cmdObj := self.cmd.New(cmdStr).PromptOnCredentialRequest().WithMutex(self.syncMutex) cmdObj := self.cmd.New(cmdStr).PromptOnCredentialRequest().WithMutex(self.syncMutex)
return cmdObj, nil return cmdObj, nil
@ -68,14 +56,10 @@ type FetchOptions struct {
// Fetch fetch git repo // Fetch fetch git repo
func (self *SyncCommands) Fetch(opts FetchOptions) error { func (self *SyncCommands) Fetch(opts FetchOptions) error {
cmdStr := "git fetch" cmdStr := NewGitCmd("fetch").
ArgIf(opts.RemoteName != "", self.cmd.Quote(opts.RemoteName)).
if opts.RemoteName != "" { ArgIf(opts.BranchName != "", self.cmd.Quote(opts.BranchName)).
cmdStr = fmt.Sprintf("%s %s", cmdStr, self.cmd.Quote(opts.RemoteName)) ToString()
}
if opts.BranchName != "" {
cmdStr = fmt.Sprintf("%s %s", cmdStr, self.cmd.Quote(opts.BranchName))
}
cmdObj := self.cmd.New(cmdStr) cmdObj := self.cmd.New(cmdStr)
if opts.Background { if opts.Background {
@ -93,18 +77,12 @@ type PullOptions struct {
} }
func (self *SyncCommands) Pull(opts PullOptions) error { func (self *SyncCommands) Pull(opts PullOptions) error {
cmdStr := "git pull --no-edit" cmdStr := NewGitCmd("pull").
Arg("--no-edit").
if opts.FastForwardOnly { ArgIf(opts.FastForwardOnly, "--ff-only").
cmdStr += " --ff-only" ArgIf(opts.RemoteName != "", self.cmd.Quote(opts.RemoteName)).
} ArgIf(opts.BranchName != "", self.cmd.Quote(opts.BranchName)).
ToString()
if opts.RemoteName != "" {
cmdStr = fmt.Sprintf("%s %s", cmdStr, self.cmd.Quote(opts.RemoteName))
}
if opts.BranchName != "" {
cmdStr = fmt.Sprintf("%s %s", cmdStr, self.cmd.Quote(opts.BranchName))
}
// setting GIT_SEQUENCE_EDITOR to ':' as a way of skipping it, in case the user // setting GIT_SEQUENCE_EDITOR to ':' as a way of skipping it, in case the user
// has 'pull.rebase = interactive' configured. // has 'pull.rebase = interactive' configured.
@ -112,11 +90,18 @@ func (self *SyncCommands) Pull(opts PullOptions) error {
} }
func (self *SyncCommands) FastForward(branchName string, remoteName string, remoteBranchName string) error { func (self *SyncCommands) FastForward(branchName string, remoteName string, remoteBranchName string) error {
cmdStr := fmt.Sprintf("git fetch %s %s:%s", self.cmd.Quote(remoteName), self.cmd.Quote(remoteBranchName), self.cmd.Quote(branchName)) cmdStr := NewGitCmd("fetch").
Arg(self.cmd.Quote(remoteName)).
Arg(self.cmd.Quote(remoteBranchName) + ":" + self.cmd.Quote(branchName)).
ToString()
return self.cmd.New(cmdStr).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run() return self.cmd.New(cmdStr).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
} }
func (self *SyncCommands) FetchRemote(remoteName string) error { func (self *SyncCommands) FetchRemote(remoteName string) error {
cmdStr := fmt.Sprintf("git fetch %s", self.cmd.Quote(remoteName)) cmdStr := NewGitCmd("fetch").
Arg(self.cmd.Quote(remoteName)).
ToString()
return self.cmd.New(cmdStr).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run() return self.cmd.New(cmdStr).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
} }

View file

@ -1,9 +1,5 @@
package git_commands package git_commands
import (
"fmt"
)
type TagCommands struct { type TagCommands struct {
*GitCommon *GitCommon
} }
@ -15,25 +11,32 @@ func NewTagCommands(gitCommon *GitCommon) *TagCommands {
} }
func (self *TagCommands) CreateLightweight(tagName string, ref string) error { func (self *TagCommands) CreateLightweight(tagName string, ref string) error {
if len(ref) > 0 { cmdStr := NewGitCmd("tag").Arg("--", self.cmd.Quote(tagName)).
return self.cmd.New(fmt.Sprintf("git tag -- %s %s", self.cmd.Quote(tagName), self.cmd.Quote(ref))).Run() ArgIf(len(ref) > 0, self.cmd.Quote(ref)).
} else { ToString()
return self.cmd.New(fmt.Sprintf("git tag -- %s", self.cmd.Quote(tagName))).Run()
} return self.cmd.New(cmdStr).Run()
} }
func (self *TagCommands) CreateAnnotated(tagName, ref, msg string) error { func (self *TagCommands) CreateAnnotated(tagName, ref, msg string) error {
if len(ref) > 0 { cmdStr := NewGitCmd("tag").Arg(self.cmd.Quote(tagName)).
return self.cmd.New(fmt.Sprintf("git tag %s %s -m %s", self.cmd.Quote(tagName), self.cmd.Quote(ref), self.cmd.Quote(msg))).Run() ArgIf(len(ref) > 0, self.cmd.Quote(ref)).
} else { Arg("-m", self.cmd.Quote(msg)).
return self.cmd.New(fmt.Sprintf("git tag %s -m %s", self.cmd.Quote(tagName), self.cmd.Quote(msg))).Run() ToString()
}
return self.cmd.New(cmdStr).Run()
} }
func (self *TagCommands) Delete(tagName string) error { func (self *TagCommands) Delete(tagName string) error {
return self.cmd.New(fmt.Sprintf("git tag -d %s", self.cmd.Quote(tagName))).Run() cmdStr := NewGitCmd("tag").Arg("-d", self.cmd.Quote(tagName)).
ToString()
return self.cmd.New(cmdStr).Run()
} }
func (self *TagCommands) Push(remoteName string, tagName string) error { func (self *TagCommands) Push(remoteName string, tagName string) error {
return self.cmd.New(fmt.Sprintf("git push %s tag %s", self.cmd.Quote(remoteName), self.cmd.Quote(tagName))).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run() cmdStr := NewGitCmd("push").Arg(self.cmd.Quote(remoteName), "tag", self.cmd.Quote(tagName)).
ToString()
return self.cmd.New(cmdStr).PromptOnCredentialRequest().WithMutex(self.syncMutex).Run()
} }

View file

@ -28,7 +28,8 @@ func NewTagLoader(
func (self *TagLoader) GetTags() ([]*models.Tag, error) { func (self *TagLoader) GetTags() ([]*models.Tag, error) {
// get remote branches, sorted by creation date (descending) // get remote branches, sorted by creation date (descending)
// see: https://git-scm.com/docs/git-tag#Documentation/git-tag.txt---sortltkeygt // see: https://git-scm.com/docs/git-tag#Documentation/git-tag.txt---sortltkeygt
tagsOutput, err := self.cmd.New(`git tag --list -n --sort=-creatordate`).DontLog().RunWithOutput() cmdStr := NewGitCmd("tag").Arg("--list", "-n", "--sort=-creatordate").ToString()
tagsOutput, err := self.cmd.New(cmdStr).DontLog().RunWithOutput()
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -15,7 +15,7 @@ type GitVersion struct {
} }
func GetGitVersion(osCommand *oscommands.OSCommand) (*GitVersion, error) { func GetGitVersion(osCommand *oscommands.OSCommand) (*GitVersion, error) {
versionStr, _, err := osCommand.Cmd.New("git --version").RunWithOutputs() versionStr, _, err := osCommand.Cmd.New(NewGitCmd("--version").ToString()).RunWithOutputs()
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -4,7 +4,6 @@ import (
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"time" "time"
"github.com/go-errors/errors" "github.com/go-errors/errors"
@ -33,7 +32,7 @@ func NewWorkingTreeCommands(
} }
func (self *WorkingTreeCommands) OpenMergeToolCmdObj() oscommands.ICmdObj { func (self *WorkingTreeCommands) OpenMergeToolCmdObj() oscommands.ICmdObj {
return self.cmd.New("git mergetool") return self.cmd.New(NewGitCmd("mergetool").ToString())
} }
func (self *WorkingTreeCommands) OpenMergeTool() error { func (self *WorkingTreeCommands) OpenMergeTool() error {
@ -49,30 +48,37 @@ func (self *WorkingTreeCommands) StageFiles(paths []string) error {
quotedPaths := slices.Map(paths, func(path string) string { quotedPaths := slices.Map(paths, func(path string) string {
return self.cmd.Quote(path) return self.cmd.Quote(path)
}) })
return self.cmd.New(fmt.Sprintf("git add -- %s", strings.Join(quotedPaths, " "))).Run()
cmdStr := NewGitCmd("add").Arg("--").Arg(quotedPaths...).ToString()
return self.cmd.New(cmdStr).Run()
} }
// StageAll stages all files // StageAll stages all files
func (self *WorkingTreeCommands) StageAll() error { func (self *WorkingTreeCommands) StageAll() error {
return self.cmd.New("git add -A").Run() cmdStr := NewGitCmd("add").Arg("-A").ToString()
return self.cmd.New(cmdStr).Run()
} }
// UnstageAll unstages all files // UnstageAll unstages all files
func (self *WorkingTreeCommands) UnstageAll() error { func (self *WorkingTreeCommands) UnstageAll() error {
return self.cmd.New("git reset").Run() return self.cmd.New(NewGitCmd("reset").ToString()).Run()
} }
// UnStageFile unstages a file // UnStageFile unstages a file
// we accept an array of filenames for the cases where a file has been renamed i.e. // we accept an array of filenames for the cases where a file has been renamed i.e.
// we accept the current name and the previous name // we accept the current name and the previous name
func (self *WorkingTreeCommands) UnStageFile(fileNames []string, reset bool) error { func (self *WorkingTreeCommands) UnStageFile(fileNames []string, reset bool) error {
command := "git rm --cached --force -- %s"
if reset {
command = "git reset HEAD -- %s"
}
for _, name := range fileNames { for _, name := range fileNames {
err := self.cmd.New(fmt.Sprintf(command, self.cmd.Quote(name))).Run() var cmdStr string
if reset {
cmdStr = NewGitCmd("reset").Arg("HEAD", "--", self.cmd.Quote(name)).ToString()
} else {
cmdStr = NewGitCmd("rm").Arg("--cached", "--force", "--", self.cmd.Quote(name)).ToString()
}
err := self.cmd.New(cmdStr).Run()
if err != nil { if err != nil {
return err return err
} }
@ -137,22 +143,31 @@ func (self *WorkingTreeCommands) DiscardAllFileChanges(file *models.File) error
quotedFileName := self.cmd.Quote(file.Name) quotedFileName := self.cmd.Quote(file.Name)
if file.ShortStatus == "AA" { if file.ShortStatus == "AA" {
if err := self.cmd.New("git checkout --ours -- " + quotedFileName).Run(); err != nil { if err := self.cmd.New(
NewGitCmd("checkout").Arg("--ours", "--", quotedFileName).ToString(),
).Run(); err != nil {
return err return err
} }
if err := self.cmd.New("git add -- " + quotedFileName).Run(); err != nil {
if err := self.cmd.New(
NewGitCmd("add").Arg("--", quotedFileName).ToString(),
).Run(); err != nil {
return err return err
} }
return nil return nil
} }
if file.ShortStatus == "DU" { if file.ShortStatus == "DU" {
return self.cmd.New("git rm -- " + quotedFileName).Run() return self.cmd.New(
NewGitCmd("rm").Arg("rm", "--", quotedFileName).ToString(),
).Run()
} }
// if the file isn't tracked, we assume you want to delete it // if the file isn't tracked, we assume you want to delete it
if file.HasStagedChanges || file.HasMergeConflicts { if file.HasStagedChanges || file.HasMergeConflicts {
if err := self.cmd.New("git reset -- " + quotedFileName).Run(); err != nil { if err := self.cmd.New(
NewGitCmd("reset").Arg("--", quotedFileName).ToString(),
).Run(); err != nil {
return err return err
} }
} }
@ -184,7 +199,8 @@ func (self *WorkingTreeCommands) DiscardUnstagedDirChanges(node IFileNode) error
} }
quotedPath := self.cmd.Quote(node.GetPath()) quotedPath := self.cmd.Quote(node.GetPath())
if err := self.cmd.New("git checkout -- " + quotedPath).Run(); err != nil { cmdStr := NewGitCmd("checkout").Arg("--", quotedPath).ToString()
if err := self.cmd.New(cmdStr).Run(); err != nil {
return err return err
} }
@ -209,7 +225,8 @@ func (self *WorkingTreeCommands) RemoveUntrackedDirFiles(node IFileNode) error {
// DiscardUnstagedFileChanges directly // DiscardUnstagedFileChanges directly
func (self *WorkingTreeCommands) DiscardUnstagedFileChanges(file *models.File) error { func (self *WorkingTreeCommands) DiscardUnstagedFileChanges(file *models.File) error {
quotedFileName := self.cmd.Quote(file.Name) quotedFileName := self.cmd.Quote(file.Name)
return self.cmd.New("git checkout -- " + quotedFileName).Run() cmdStr := NewGitCmd("checkout").Arg("--", quotedFileName).ToString()
return self.cmd.New(cmdStr).Run()
} }
// Ignore adds a file to the gitignore for the repo // Ignore adds a file to the gitignore for the repo
@ -230,30 +247,28 @@ func (self *WorkingTreeCommands) WorktreeFileDiff(file *models.File, plain bool,
} }
func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain bool, cached bool, ignoreWhitespace bool) oscommands.ICmdObj { func (self *WorkingTreeCommands) WorktreeFileDiffCmdObj(node models.IFile, plain bool, cached bool, ignoreWhitespace bool) oscommands.ICmdObj {
cachedArg := ""
trackedArg := "--"
colorArg := self.UserConfig.Git.Paging.ColorArg colorArg := self.UserConfig.Git.Paging.ColorArg
quotedPath := self.cmd.Quote(node.GetPath())
quotedPrevPath := ""
ignoreWhitespaceArg := ""
contextSize := self.UserConfig.Git.DiffContextSize
if cached {
cachedArg = " --cached"
}
if !node.GetIsTracked() && !node.GetHasStagedChanges() && !cached && node.GetIsFile() {
trackedArg = "--no-index -- /dev/null"
}
if plain { if plain {
colorArg = "never" colorArg = "never"
} }
if ignoreWhitespace {
ignoreWhitespaceArg = " --ignore-all-space"
}
if prevPath := node.GetPreviousPath(); prevPath != "" {
quotedPrevPath = " " + self.cmd.Quote(prevPath)
}
cmdStr := fmt.Sprintf("git diff --submodule --no-ext-diff --unified=%d --color=%s%s%s %s %s%s", contextSize, colorArg, ignoreWhitespaceArg, cachedArg, trackedArg, quotedPath, quotedPrevPath) contextSize := self.UserConfig.Git.DiffContextSize
prevPath := node.GetPreviousPath()
noIndex := !node.GetIsTracked() && !node.GetHasStagedChanges() && !cached && node.GetIsFile()
cmdStr := NewGitCmd("diff").
Arg("--submodule").
Arg("--no-ext-diff").
Arg(fmt.Sprintf("--unified=%d", contextSize)).
Arg(fmt.Sprintf("--color=%s", colorArg)).
ArgIf(ignoreWhitespace, "--ignore-all-space").
ArgIf(cached, "--cached").
ArgIf(noIndex, "--no-index").
Arg("--").
ArgIf(noIndex, "/dev/null").
Arg(self.cmd.Quote(node.GetPath())).
ArgIf(prevPath != "", self.cmd.Quote(prevPath)).
ToString()
return self.cmd.New(cmdStr).DontLog() return self.cmd.New(cmdStr).DontLog()
} }
@ -296,49 +311,59 @@ func (self *WorkingTreeCommands) ShowFileDiff(from string, to string, reverse bo
func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reverse bool, fileName string, plain bool, func (self *WorkingTreeCommands) ShowFileDiffCmdObj(from string, to string, reverse bool, fileName string, plain bool,
ignoreWhitespace bool, ignoreWhitespace bool,
) oscommands.ICmdObj { ) oscommands.ICmdObj {
colorArg := self.UserConfig.Git.Paging.ColorArg
contextSize := self.UserConfig.Git.DiffContextSize contextSize := self.UserConfig.Git.DiffContextSize
colorArg := self.UserConfig.Git.Paging.ColorArg
if plain { if plain {
colorArg = "never" colorArg = "never"
} }
reverseFlag := "" cmdStr := NewGitCmd("diff").
if reverse { Arg("--submodule").
reverseFlag = " -R" Arg("--no-ext-diff").
} Arg(fmt.Sprintf("--unified=%d", contextSize)).
Arg("--no-renames").
Arg(fmt.Sprintf("--color=%s", colorArg)).
Arg(from).
Arg(to).
ArgIf(reverse, "-R").
ArgIf(ignoreWhitespace, "--ignore-all-space").
Arg("--").
Arg(self.cmd.Quote(fileName)).
ToString()
ignoreWhitespaceFlag := "" return self.cmd.New(cmdStr).DontLog()
if ignoreWhitespace {
ignoreWhitespaceFlag = " --ignore-all-space"
}
return self.cmd.
New(
fmt.Sprintf(
"git diff --submodule --no-ext-diff --unified=%d --no-renames --color=%s%s%s%s%s -- %s",
contextSize, colorArg, pad(from), pad(to), reverseFlag, ignoreWhitespaceFlag, self.cmd.Quote(fileName)),
).
DontLog()
} }
// CheckoutFile checks out the file for the given commit // CheckoutFile checks out the file for the given commit
func (self *WorkingTreeCommands) CheckoutFile(commitSha, fileName string) error { func (self *WorkingTreeCommands) CheckoutFile(commitSha, fileName string) error {
return self.cmd.New(fmt.Sprintf("git checkout %s -- %s", commitSha, self.cmd.Quote(fileName))).Run() cmdStr := NewGitCmd("checkout").Arg(commitSha, "--", self.cmd.Quote(fileName)).
ToString()
return self.cmd.New(cmdStr).Run()
} }
// DiscardAnyUnstagedFileChanges discards any unstages file changes via `git checkout -- .` // DiscardAnyUnstagedFileChanges discards any unstaged file changes via `git checkout -- .`
func (self *WorkingTreeCommands) DiscardAnyUnstagedFileChanges() error { func (self *WorkingTreeCommands) DiscardAnyUnstagedFileChanges() error {
return self.cmd.New("git checkout -- .").Run() cmdStr := NewGitCmd("checkout").Arg("--", ".").
ToString()
return self.cmd.New(cmdStr).Run()
} }
// RemoveTrackedFiles will delete the given file(s) even if they are currently tracked // RemoveTrackedFiles will delete the given file(s) even if they are currently tracked
func (self *WorkingTreeCommands) RemoveTrackedFiles(name string) error { func (self *WorkingTreeCommands) RemoveTrackedFiles(name string) error {
return self.cmd.New("git rm -r --cached -- " + self.cmd.Quote(name)).Run() cmdStr := NewGitCmd("rm").Arg("-r", "--cached", "--", self.cmd.Quote(name)).
ToString()
return self.cmd.New(cmdStr).Run()
} }
// RemoveUntrackedFiles runs `git clean -fd` // RemoveUntrackedFiles runs `git clean -fd`
func (self *WorkingTreeCommands) RemoveUntrackedFiles() error { func (self *WorkingTreeCommands) RemoveUntrackedFiles() error {
return self.cmd.New("git clean -fd").Run() cmdStr := NewGitCmd("clean").Arg("-fd").ToString()
return self.cmd.New(cmdStr).Run()
} }
// ResetAndClean removes all unstaged changes and removes all untracked files // ResetAndClean removes all unstaged changes and removes all untracked files
@ -363,23 +388,23 @@ func (self *WorkingTreeCommands) ResetAndClean() error {
// ResetHardHead runs `git reset --hard` // ResetHardHead runs `git reset --hard`
func (self *WorkingTreeCommands) ResetHard(ref string) error { func (self *WorkingTreeCommands) ResetHard(ref string) error {
return self.cmd.New("git reset --hard " + self.cmd.Quote(ref)).Run() cmdStr := NewGitCmd("reset").Arg("--hard", self.cmd.Quote(ref)).
ToString()
return self.cmd.New(cmdStr).Run()
} }
// ResetSoft runs `git reset --soft HEAD` // ResetSoft runs `git reset --soft HEAD`
func (self *WorkingTreeCommands) ResetSoft(ref string) error { func (self *WorkingTreeCommands) ResetSoft(ref string) error {
return self.cmd.New("git reset --soft " + self.cmd.Quote(ref)).Run() cmdStr := NewGitCmd("reset").Arg("--soft", self.cmd.Quote(ref)).
ToString()
return self.cmd.New(cmdStr).Run()
} }
func (self *WorkingTreeCommands) ResetMixed(ref string) error { func (self *WorkingTreeCommands) ResetMixed(ref string) error {
return self.cmd.New("git reset --mixed " + self.cmd.Quote(ref)).Run() cmdStr := NewGitCmd("reset").Arg("--mixed", self.cmd.Quote(ref)).
} ToString()
// so that we don't have unnecessary space in our commands we use this helper function to prepend spaces to args so that in the format string we can go '%s%s%s' and if any args are missing we won't have gaps. return self.cmd.New(cmdStr).Run()
func pad(str string) string {
if str == "" {
return ""
}
return " " + str
} }