1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-08-10 22:42:00 +02:00

Merge pull request #2645 from jesseduffield/convenient-git-command-building

This commit is contained in:
Jesse Duffield
2023-05-20 20:58:18 +10:00
committed by GitHub
32 changed files with 1034 additions and 527 deletions

View File

@@ -117,8 +117,7 @@ func NewGitCommandAux(
workingTreeCommands := git_commands.NewWorkingTreeCommands(gitCommon, submoduleCommands, fileLoader) workingTreeCommands := git_commands.NewWorkingTreeCommands(gitCommon, submoduleCommands, fileLoader)
rebaseCommands := git_commands.NewRebaseCommands(gitCommon, commitCommands, workingTreeCommands) rebaseCommands := git_commands.NewRebaseCommands(gitCommon, commitCommands, workingTreeCommands)
stashCommands := git_commands.NewStashCommands(gitCommon, fileLoader, workingTreeCommands) stashCommands := git_commands.NewStashCommands(gitCommon, fileLoader, workingTreeCommands)
// TODO: have patch builder take workingTreeCommands in its entirety patchBuilder := patch.NewPatchBuilder(cmn.Log,
patchBuilder := patch.NewPatchBuilder(cmn.Log, workingTreeCommands.ApplyPatch,
func(from string, to string, reverse bool, filename string, plain bool) (string, error) { func(from string, to string, reverse bool, filename string, plain bool) (string, error) {
// TODO: make patch builder take Gui.IgnoreWhitespaceInDiffView into // TODO: make patch builder take Gui.IgnoreWhitespaceInDiffView into
// account. For now we just pass false. // account. For now we just pass false.

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

@@ -5,6 +5,7 @@ import (
"github.com/go-errors/errors" "github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@@ -99,12 +100,53 @@ func TestBranchDeleteBranch(t *testing.T) {
} }
func TestBranchMerge(t *testing.T) { func TestBranchMerge(t *testing.T) {
runner := oscommands.NewFakeRunner(t). scenarios := []struct {
Expect(`git merge --no-edit "test"`, "", nil) testName string
instance := buildBranchCommands(commonDeps{runner: runner}) userConfig *config.UserConfig
opts MergeOpts
branchName string
expected string
}{
{
testName: "basic",
userConfig: &config.UserConfig{},
opts: MergeOpts{},
branchName: "mybranch",
expected: `git merge --no-edit "mybranch"`,
},
{
testName: "merging args",
userConfig: &config.UserConfig{
Git: config.GitConfig{
Merging: config.MergingConfig{
Args: "--merging-args", // it's up to the user what they put here
},
},
},
opts: MergeOpts{},
branchName: "mybranch",
expected: `git merge --no-edit --merging-args "mybranch"`,
},
{
testName: "fast forward only",
userConfig: &config.UserConfig{},
opts: MergeOpts{FastForwardOnly: true},
branchName: "mybranch",
expected: `git merge --no-edit --ff-only "mybranch"`,
},
}
assert.NoError(t, instance.Merge("test", MergeOpts{})) for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
runner := oscommands.NewFakeRunner(t).
Expect(s.expected, "", nil)
instance := buildBranchCommands(commonDeps{runner: runner, userConfig: s.userConfig})
assert.NoError(t, instance.Merge(s.branchName, s.opts))
runner.CheckForMissingCalls() runner.CheckForMissingCalls()
})
}
} }
func TestBranchCheckout(t *testing.T) { func TestBranchCheckout(t *testing.T) {

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,33 +54,47 @@ 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 {
@@ -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

@@ -7,6 +7,7 @@ import (
gogit "github.com/jesseduffield/go-git/v5" gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/git_config" "github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/common" "github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
@@ -117,6 +118,26 @@ func buildWorkingTreeCommands(deps commonDeps) *WorkingTreeCommands {
return NewWorkingTreeCommands(gitCommon, submoduleCommands, fileLoader) return NewWorkingTreeCommands(gitCommon, submoduleCommands, fileLoader)
} }
func buildPatchCommands(deps commonDeps) *PatchCommands {
gitCommon := buildGitCommon(deps)
rebaseCommands := buildRebaseCommands(deps)
commitCommands := buildCommitCommands(deps)
statusCommands := buildStatusCommands(deps)
stashCommands := buildStashCommands(deps)
loadFileFn := func(from string, to string, reverse bool, filename string, plain bool) (string, error) {
return "", nil
}
patchBuilder := patch.NewPatchBuilder(gitCommon.Log, loadFileFn)
return NewPatchCommands(gitCommon, rebaseCommands, commitCommands, statusCommands, stashCommands, patchBuilder)
}
func buildStatusCommands(deps commonDeps) *StatusCommands {
gitCommon := buildGitCommon(deps)
return NewStatusCommands(gitCommon)
}
func buildStashCommands(deps commonDeps) *StashCommands { func buildStashCommands(deps commonDeps) *StashCommands {
gitCommon := buildGitCommon(deps) gitCommon := buildGitCommon(deps)
fileLoader := buildFileLoader(gitCommon) fileLoader := buildFileLoader(gitCommon)
@@ -150,3 +171,9 @@ func buildBranchCommands(deps commonDeps) *BranchCommands {
return NewBranchCommands(gitCommon) return NewBranchCommands(gitCommon)
} }
func buildFlowCommands(deps commonDeps) *FlowCommands {
gitCommon := buildGitCommon(deps)
return NewFlowCommands(gitCommon)
}

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,92 @@
package git_commands
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/stretchr/testify/assert"
)
func TestStartCmdObj(t *testing.T) {
scenarios := []struct {
testName string
branchType string
name string
expected string
}{
{
testName: "basic",
branchType: "feature",
name: "test",
expected: "git flow feature start test",
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildFlowCommands(commonDeps{})
assert.Equal(t,
instance.StartCmdObj(s.branchType, s.name).ToString(),
s.expected,
)
})
}
}
func TestFinishCmdObj(t *testing.T) {
scenarios := []struct {
testName string
branchName string
expected string
expectedError string
gitConfigMockResponses map[string]string
}{
{
testName: "not a git flow branch",
branchName: "mybranch",
expected: "",
expectedError: "This does not seem to be a git flow branch",
gitConfigMockResponses: nil,
},
{
testName: "feature branch without config",
branchName: "feature/mybranch",
expected: "",
expectedError: "This does not seem to be a git flow branch",
gitConfigMockResponses: nil,
},
{
testName: "feature branch with config",
branchName: "feature/mybranch",
expected: "git flow feature finish mybranch",
expectedError: "",
gitConfigMockResponses: map[string]string{
"--local --get-regexp gitflow.prefix": "gitflow.prefix.feature feature/",
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildFlowCommands(commonDeps{
gitConfig: git_config.NewFakeGitConfig(s.gitConfigMockResponses),
})
cmd, err := instance.FinishCmdObj(s.branchName)
if s.expectedError != "" {
if err == nil {
t.Errorf("Expected error, got nil")
} else {
assert.Equal(t, err.Error(), s.expectedError)
}
} else {
assert.NoError(t, err)
assert.Equal(t, cmd.ToString(), s.expected)
}
})
}
}

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

@@ -2,6 +2,8 @@ package git_commands
import ( import (
"fmt" "fmt"
"path/filepath"
"time"
"github.com/fsmiamoto/git-todo-parser/todo" "github.com/fsmiamoto/git-todo-parser/todo"
"github.com/go-errors/errors" "github.com/go-errors/errors"
@@ -9,6 +11,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/patch" "github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums" "github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/utils"
) )
type PatchCommands struct { type PatchCommands struct {
@@ -39,6 +42,53 @@ func NewPatchCommands(
} }
} }
type ApplyPatchOpts struct {
ThreeWay bool
Cached bool
Index bool
Reverse bool
}
func (self *PatchCommands) ApplyCustomPatch(reverse bool) error {
patch := self.PatchBuilder.PatchToApply(reverse)
return self.ApplyPatch(patch, ApplyPatchOpts{
Index: true,
ThreeWay: true,
Reverse: reverse,
})
}
func (self *PatchCommands) ApplyPatch(patch string, opts ApplyPatchOpts) error {
filepath, err := self.SaveTemporaryPatch(patch)
if err != nil {
return err
}
return self.applyPatchFile(filepath, opts)
}
func (self *PatchCommands) applyPatchFile(filepath string, opts ApplyPatchOpts) error {
cmdStr := NewGitCmd("apply").
ArgIf(opts.ThreeWay, "--3way").
ArgIf(opts.Cached, "--cached").
ArgIf(opts.Index, "--index").
ArgIf(opts.Reverse, "--reverse").
Arg(self.cmd.Quote(filepath)).
ToString()
return self.cmd.New(cmdStr).Run()
}
func (self *PatchCommands) SaveTemporaryPatch(patch string) (string, error) {
filepath := filepath.Join(self.os.GetTempDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch")
self.Log.Infof("saving temporary patch to %s", filepath)
if err := self.os.CreateFileWithContent(filepath, patch); err != nil {
return "", err
}
return filepath, nil
}
// DeletePatchesFromCommit applies a patch in reverse for a commit // DeletePatchesFromCommit applies a patch in reverse for a commit
func (self *PatchCommands) DeletePatchesFromCommit(commits []*models.Commit, commitIndex int) error { func (self *PatchCommands) DeletePatchesFromCommit(commits []*models.Commit, commitIndex int) error {
if err := self.rebase.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil { if err := self.rebase.BeginInteractiveRebaseForCommit(commits, commitIndex); err != nil {
@@ -46,7 +96,7 @@ func (self *PatchCommands) DeletePatchesFromCommit(commits []*models.Commit, com
} }
// apply each patch in reverse // apply each patch in reverse
if err := self.PatchBuilder.ApplyPatches(true); err != nil { if err := self.ApplyCustomPatch(true); err != nil {
_ = self.rebase.AbortRebase() _ = self.rebase.AbortRebase()
return err return err
} }
@@ -72,7 +122,7 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
} }
// apply each patch forward // apply each patch forward
if err := self.PatchBuilder.ApplyPatches(false); err != nil { if err := self.ApplyCustomPatch(false); err != nil {
// Don't abort the rebase here; this might cause conflicts, so give // Don't abort the rebase here; this might cause conflicts, so give
// the user a chance to resolve them // the user a chance to resolve them
return err return err
@@ -121,7 +171,7 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
} }
// apply each patch in reverse // apply each patch in reverse
if err := self.PatchBuilder.ApplyPatches(true); err != nil { if err := self.ApplyCustomPatch(true); err != nil {
_ = self.rebase.AbortRebase() _ = self.rebase.AbortRebase()
return err return err
} }
@@ -144,7 +194,7 @@ func (self *PatchCommands) MovePatchToSelectedCommit(commits []*models.Commit, s
self.rebase.onSuccessfulContinue = func() error { self.rebase.onSuccessfulContinue = func() error {
// now we should be up to the destination, so let's apply forward these patches to that. // now we should be up to the destination, so let's apply forward these patches to that.
// ideally we would ensure we're on the right commit but I'm not sure if that check is necessary // ideally we would ensure we're on the right commit but I'm not sure if that check is necessary
if err := self.rebase.workingTree.ApplyPatch(patch, "index", "3way"); err != nil { if err := self.ApplyPatch(patch, ApplyPatchOpts{Index: true, ThreeWay: true}); err != nil {
// Don't abort the rebase here; this might cause conflicts, so give // Don't abort the rebase here; this might cause conflicts, so give
// the user a chance to resolve them // the user a chance to resolve them
return err return err
@@ -177,7 +227,7 @@ func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitId
return err return err
} }
if err := self.PatchBuilder.ApplyPatches(true); err != nil { if err := self.ApplyCustomPatch(true); err != nil {
if self.status.WorkingTreeState() == enums.REBASE_MODE_REBASING { if self.status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
_ = self.rebase.AbortRebase() _ = self.rebase.AbortRebase()
} }
@@ -201,7 +251,7 @@ func (self *PatchCommands) MovePatchIntoIndex(commits []*models.Commit, commitId
self.rebase.onSuccessfulContinue = func() error { self.rebase.onSuccessfulContinue = func() error {
// add patches to index // add patches to index
if err := self.rebase.workingTree.ApplyPatch(patch, "index", "3way"); err != nil { if err := self.ApplyPatch(patch, ApplyPatchOpts{Index: true, ThreeWay: true}); err != nil {
if self.status.WorkingTreeState() == enums.REBASE_MODE_REBASING { if self.status.WorkingTreeState() == enums.REBASE_MODE_REBASING {
_ = self.rebase.AbortRebase() _ = self.rebase.AbortRebase()
} }
@@ -226,7 +276,7 @@ func (self *PatchCommands) PullPatchIntoNewCommit(commits []*models.Commit, comm
return err return err
} }
if err := self.PatchBuilder.ApplyPatches(true); err != nil { if err := self.ApplyCustomPatch(true); err != nil {
_ = self.rebase.AbortRebase() _ = self.rebase.AbortRebase()
return err return err
} }
@@ -242,7 +292,7 @@ func (self *PatchCommands) PullPatchIntoNewCommit(commits []*models.Commit, comm
return err return err
} }
if err := self.rebase.workingTree.ApplyPatch(patch, "index", "3way"); err != nil { if err := self.ApplyPatch(patch, ApplyPatchOpts{Index: true, ThreeWay: true}); err != nil {
_ = self.rebase.AbortRebase() _ = self.rebase.AbortRebase()
return err return err
} }
@@ -267,5 +317,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

@@ -0,0 +1,66 @@
package git_commands
import (
"fmt"
"os"
"regexp"
"testing"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/stretchr/testify/assert"
)
func TestWorkingTreeApplyPatch(t *testing.T) {
type scenario struct {
testName string
runner *oscommands.FakeCmdObjRunner
test func(error)
}
expectFn := func(regexStr string, errToReturn error) func(cmdObj oscommands.ICmdObj) (string, error) {
return func(cmdObj oscommands.ICmdObj) (string, error) {
re := regexp.MustCompile(regexStr)
cmdStr := cmdObj.ToString()
matches := re.FindStringSubmatch(cmdStr)
assert.Equal(t, 2, len(matches), fmt.Sprintf("unexpected command: %s", cmdStr))
filename := matches[1]
content, err := os.ReadFile(filename)
assert.NoError(t, err)
assert.Equal(t, "test", string(content))
return "", errToReturn
}
}
scenarios := []scenario{
{
testName: "valid case",
runner: oscommands.NewFakeRunner(t).
ExpectFunc(expectFn(`git apply --cached "(.*)"`, nil)),
test: func(err error) {
assert.NoError(t, err)
},
},
{
testName: "command returns error",
runner: oscommands.NewFakeRunner(t).
ExpectFunc(expectFn(`git apply --cached "(.*)"`, errors.New("error"))),
test: func(err error) {
assert.Error(t, err)
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildPatchCommands(commonDeps{runner: s.runner})
s.test(instance.ApplyPatch("test", ApplyPatchOpts{Cached: true}))
s.runner.CheckForMissingCalls()
})
}
}

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(),
).Run(); err != nil {
if !strings.Contains(err.Error(), "did not match any file(s) known to git") {
return err 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(
NewGitCmd("config").
Arg("--file", ".gitmodules", "--remove-section", "submodule."+self.cmd.Quote(submodule.Path)).
ToString(),
).Run(); err != nil {
return err 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 == "" {
if opts.Force {
cmdStr += " --force-with-lease"
}
if opts.SetUpstream {
cmdStr += " --set-upstream"
}
if opts.UpstreamRemote != "" {
cmdStr += " " + self.cmd.Quote(opts.UpstreamRemote)
}
if opts.UpstreamBranch != "" {
if opts.UpstreamRemote == "" {
return nil, errors.New(self.Tr.MustSpecifyOriginError) return nil, errors.New(self.Tr.MustSpecifyOriginError)
} }
cmdStr += " " + self.cmd.Quote(opts.UpstreamBranch)
} cmdStr := NewGitCmd("push").
ArgIf(opts.Force, "--force-with-lease").
ArgIf(opts.SetUpstream, "--set-upstream").
ArgIf(opts.UpstreamRemote != "", self.cmd.Quote(opts.UpstreamRemote)).
ArgIf(opts.UpstreamBranch != "", self.cmd.Quote(opts.UpstreamBranch)).
ToString()
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

@@ -3,15 +3,11 @@ package git_commands
import ( import (
"fmt" "fmt"
"os" "os"
"path/filepath"
"strings"
"time"
"github.com/go-errors/errors" "github.com/go-errors/errors"
"github.com/jesseduffield/generics/slices" "github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
) )
type WorkingTreeCommands struct { type WorkingTreeCommands struct {
@@ -33,7 +29,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 +45,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" for _, name := range fileNames {
var cmdStr string
if reset { if reset {
command = "git reset HEAD -- %s" cmdStr = NewGitCmd("reset").Arg("HEAD", "--", self.cmd.Quote(name)).ToString()
} else {
cmdStr = NewGitCmd("rm").Arg("--cached", "--force", "--", self.cmd.Quote(name)).ToString()
} }
for _, name := range fileNames { err := self.cmd.New(cmdStr).Run()
err := self.cmd.New(fmt.Sprintf(command, self.cmd.Quote(name))).Run()
if err != nil { if err != nil {
return err return err
} }
@@ -137,22 +140,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 +196,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 +222,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,61 +244,32 @@ 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()
} }
func (self *WorkingTreeCommands) ApplyPatch(patch string, flags ...string) error {
filepath, err := self.SaveTemporaryPatch(patch)
if err != nil {
return err
}
return self.ApplyPatchFile(filepath, flags...)
}
func (self *WorkingTreeCommands) ApplyPatchFile(filepath string, flags ...string) error {
flagStr := ""
for _, flag := range flags {
flagStr += " --" + flag
}
return self.cmd.New(fmt.Sprintf("git apply%s %s", flagStr, self.cmd.Quote(filepath))).Run()
}
func (self *WorkingTreeCommands) SaveTemporaryPatch(patch string) (string, error) {
filepath := filepath.Join(self.os.GetTempDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch")
self.Log.Infof("saving temporary patch to %s", filepath)
if err := self.os.CreateFileWithContent(filepath, patch); err != nil {
return "", err
}
return filepath, nil
}
// ShowFileDiff get the diff of specified from and to. Typically this will be used for a single commit so it'll be 123abc^..123abc // 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. // 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 (self *WorkingTreeCommands) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool, func (self *WorkingTreeCommands) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool,
@@ -296,49 +281,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 +358,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
} }

View File

@@ -2,8 +2,6 @@ package git_commands
import ( import (
"fmt" "fmt"
"os"
"regexp"
"testing" "testing"
"github.com/go-errors/errors" "github.com/go-errors/errors"
@@ -430,60 +428,6 @@ func TestWorkingTreeCheckoutFile(t *testing.T) {
} }
} }
func TestWorkingTreeApplyPatch(t *testing.T) {
type scenario struct {
testName string
runner *oscommands.FakeCmdObjRunner
test func(error)
}
expectFn := func(regexStr string, errToReturn error) func(cmdObj oscommands.ICmdObj) (string, error) {
return func(cmdObj oscommands.ICmdObj) (string, error) {
re := regexp.MustCompile(regexStr)
cmdStr := cmdObj.ToString()
matches := re.FindStringSubmatch(cmdStr)
assert.Equal(t, 2, len(matches), fmt.Sprintf("unexpected command: %s", cmdStr))
filename := matches[1]
content, err := os.ReadFile(filename)
assert.NoError(t, err)
assert.Equal(t, "test", string(content))
return "", errToReturn
}
}
scenarios := []scenario{
{
testName: "valid case",
runner: oscommands.NewFakeRunner(t).
ExpectFunc(expectFn(`git apply --cached "(.*)"`, nil)),
test: func(err error) {
assert.NoError(t, err)
},
},
{
testName: "command returns error",
runner: oscommands.NewFakeRunner(t).
ExpectFunc(expectFn(`git apply --cached "(.*)"`, errors.New("error"))),
test: func(err error) {
assert.Error(t, err)
},
},
}
for _, s := range scenarios {
s := s
t.Run(s.testName, func(t *testing.T) {
instance := buildWorkingTreeCommands(commonDeps{runner: s.runner})
s.test(instance.ApplyPatch("test", "cached"))
s.runner.CheckForMissingCalls()
})
}
}
func TestWorkingTreeDiscardUnstagedFileChanges(t *testing.T) { func TestWorkingTreeDiscardUnstagedFileChanges(t *testing.T) {
type scenario struct { type scenario struct {
testName string testName string

View File

@@ -29,7 +29,6 @@ type fileInfo struct {
} }
type ( type (
applyPatchFunc func(patch string, flags ...string) error
loadFileDiffFunc func(from string, to string, reverse bool, filename string, plain bool) (string, error) loadFileDiffFunc func(from string, to string, reverse bool, filename string, plain bool) (string, error)
) )
@@ -47,17 +46,14 @@ type PatchBuilder struct {
// fileInfoMap starts empty but you add files to it as you go along // fileInfoMap starts empty but you add files to it as you go along
fileInfoMap map[string]*fileInfo fileInfoMap map[string]*fileInfo
Log *logrus.Entry Log *logrus.Entry
applyPatch applyPatchFunc
// loadFileDiff loads the diff of a file, for a given to (typically a commit SHA) // loadFileDiff loads the diff of a file, for a given to (typically a commit SHA)
loadFileDiff loadFileDiffFunc loadFileDiff loadFileDiffFunc
} }
// NewPatchBuilder returns a new PatchBuilder func NewPatchBuilder(log *logrus.Entry, loadFileDiff loadFileDiffFunc) *PatchBuilder {
func NewPatchBuilder(log *logrus.Entry, applyPatch applyPatchFunc, loadFileDiff loadFileDiffFunc) *PatchBuilder {
return &PatchBuilder{ return &PatchBuilder{
Log: log, Log: log,
applyPatch: applyPatch,
loadFileDiff: loadFileDiff, loadFileDiff: loadFileDiff,
} }
} }
@@ -70,6 +66,20 @@ func (p *PatchBuilder) Start(from, to string, reverse bool, canRebase bool) {
p.fileInfoMap = map[string]*fileInfo{} p.fileInfoMap = map[string]*fileInfo{}
} }
func (p *PatchBuilder) PatchToApply(reverse bool) string {
patch := ""
for filename, info := range p.fileInfoMap {
if info.mode == UNSELECTED {
continue
}
patch += p.RenderPatchForFile(filename, true, reverse)
}
return patch
}
func (p *PatchBuilder) addFileWhole(info *fileInfo) { func (p *PatchBuilder) addFileWhole(info *fileInfo) {
info.mode = WHOLE info.mode = WHOLE
lineCount := len(strings.Split(info.diff, "\n")) lineCount := len(strings.Split(info.diff, "\n"))
@@ -234,25 +244,6 @@ func (p *PatchBuilder) GetFileIncLineIndices(filename string) ([]int, error) {
return info.includedLineIndices, nil return info.includedLineIndices, nil
} }
func (p *PatchBuilder) ApplyPatches(reverse bool) error {
patch := ""
applyFlags := []string{"index", "3way"}
if reverse {
applyFlags = append(applyFlags, "reverse")
}
for filename, info := range p.fileInfoMap {
if info.mode == UNSELECTED {
continue
}
patch += p.RenderPatchForFile(filename, true, reverse)
}
return p.applyPatch(patch, applyFlags...)
}
// clears the patch // clears the patch
func (p *PatchBuilder) Reset() { func (p *PatchBuilder) Reset() {
p.To = "" p.To = ""

View File

@@ -199,7 +199,7 @@ func (self *CustomPatchOptionsMenuAction) handleApplyPatch(reverse bool) error {
action = "Apply patch in reverse" action = "Apply patch in reverse"
} }
self.c.LogAction(action) self.c.LogAction(action)
if err := self.c.Git().Patch.PatchBuilder.ApplyPatches(reverse); err != nil { if err := self.c.Git().Patch.ApplyCustomPatch(reverse); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC}) return self.c.Refresh(types.RefreshOptions{Mode: types.ASYNC})

View File

@@ -4,6 +4,7 @@ import (
"strings" "strings"
"github.com/jesseduffield/gocui" "github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/patch" "github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
) )
@@ -213,15 +214,14 @@ func (self *StagingController) applySelection(reverse bool) error {
// apply the patch then refresh this panel // apply the patch then refresh this panel
// create a new temp file with the patch, then call git apply with that patch // create a new temp file with the patch, then call git apply with that patch
applyFlags := []string{}
if reverse {
applyFlags = append(applyFlags, "reverse")
}
if !reverse || self.staged {
applyFlags = append(applyFlags, "cached")
}
self.c.LogAction(self.c.Tr.Actions.ApplyPatch) self.c.LogAction(self.c.Tr.Actions.ApplyPatch)
err := self.c.Git().WorkingTree.ApplyPatch(patchToApply, applyFlags...) err := self.c.Git().Patch.ApplyPatch(
patchToApply,
git_commands.ApplyPatchOpts{
Reverse: reverse,
Cached: !reverse || self.staged,
},
)
if err != nil { if err != nil {
return self.c.Error(err) return self.c.Error(err)
} }
@@ -262,7 +262,7 @@ func (self *StagingController) editHunk() error {
}). }).
FormatPlain() FormatPlain()
patchFilepath, err := self.c.Git().WorkingTree.SaveTemporaryPatch(patchText) patchFilepath, err := self.c.Git().Patch.SaveTemporaryPatch(patchText)
if err != nil { if err != nil {
return err return err
} }
@@ -289,11 +289,13 @@ func (self *StagingController) editHunk() error {
}). }).
FormatPlain() FormatPlain()
applyFlags := []string{"cached"} if err := self.c.Git().Patch.ApplyPatch(
if self.staged { newPatchText,
applyFlags = append(applyFlags, "reverse") git_commands.ApplyPatchOpts{
} Reverse: self.staged,
if err := self.c.Git().WorkingTree.ApplyPatch(newPatchText, applyFlags...); err != nil { Cached: true,
},
); err != nil {
return self.c.Error(err) return self.c.Error(err)
} }

View File

@@ -136,7 +136,6 @@ M file1
} }
patchBuilder := patch.NewPatchBuilder( patchBuilder := patch.NewPatchBuilder(
utils.NewDummyLog(), utils.NewDummyLog(),
func(patch string, flags ...string) error { return nil },
func(from string, to string, reverse bool, filename string, plain bool) (string, error) { func(from string, to string, reverse bool, filename string, plain bool) (string, error) {
return "", nil return "", nil
}, },