Compare commits

...

27 Commits

Author SHA1 Message Date
Jesse Duffield
d9548b5d00 start breaking up git struct 2022-01-02 10:34:33 +11:00
Jesse Duffield
e3da89efb7 do dependency injection up front and in one place 2022-01-02 10:21:32 +11:00
Jesse Duffield
19d2994af8 no more mocking command 2021-12-31 10:46:34 +11:00
Jesse Duffield
ad4fd67db3 WIP 2021-12-31 10:44:47 +11:00
Jesse Duffield
5f36eb507a more test refactoring 2021-12-31 10:24:53 +11:00
Jesse Duffield
1caec9114c refactor sync test 2021-12-31 10:04:32 +11:00
Jesse Duffield
e3af0ed43a refactor files_test.go 2021-12-31 10:04:25 +11:00
Jesse Duffield
18c39c5f24 stash and tags loaders 2021-12-30 17:48:47 +11:00
Jesse Duffield
07afb5359e move remotes loader into loaders package 2021-12-30 17:36:21 +11:00
Jesse Duffield
3f1cda88ed move reflog commit loader into loaders package 2021-12-30 17:25:36 +11:00
Jesse Duffield
edd43bcbeb WIP 2021-12-30 17:19:01 +11:00
Jesse Duffield
061db91002 WIP 2021-12-30 13:44:41 +11:00
Jesse Duffield
2455faec1b more refactoring 2021-12-30 13:35:10 +11:00
Jesse Duffield
6fddf2aa8b WIP 2021-12-30 13:13:25 +11:00
Jesse Duffield
c24bb11141 updating specs 2021-12-30 13:11:58 +11:00
Jesse Duffield
0eea75e8c6 better typing for rebase mode 2021-12-30 12:11:57 +11:00
Jesse Duffield
b21997d6b4 fix logging 2021-12-30 11:35:15 +11:00
Jesse Duffield
e94e8fc5b6 refactor 2021-12-30 11:22:29 +11:00
Jesse Duffield
d8084cd558 WIP 2021-12-30 10:44:08 +11:00
Jesse Duffield
65f910ebd8 WIP 2021-12-30 10:41:15 +11:00
Jesse Duffield
26c07b1ab3 WIP 2021-12-29 15:05:52 +11:00
Jesse Duffield
3d4470a6c0 WIP 2021-12-29 14:33:38 +11:00
Jesse Duffield
053a66a7be refactoring the config struct 2021-12-29 12:03:35 +11:00
Jesse Duffield
985fe482e8 align Gui struct with GitCommand 2021-12-29 11:50:20 +11:00
Jesse Duffield
4ab4af441e no more config in git command struct 2021-12-29 11:41:33 +11:00
Jesse Duffield
bdc54a5deb introduce Common struct for passing around common stuff 2021-12-29 11:37:15 +11:00
Jesse Duffield
d913c04109 WIP 2021-12-29 09:14:39 +11:00
100 changed files with 2694 additions and 2639 deletions

View File

@@ -17,6 +17,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/env"
"github.com/jesseduffield/lazygit/pkg/gui"
@@ -27,14 +28,12 @@ import (
// App struct
type App struct {
closers []io.Closer
*common.Common
closers []io.Closer
Config config.AppConfigurer
Log *logrus.Entry
OSCommand *oscommands.OSCommand
GitCommand *commands.GitCommand
Gui *gui.Gui
Tr *i18n.TranslationSet
Updater *updates.Updater // may only need this on the Gui
ClientContext string
}
@@ -97,27 +96,35 @@ func newLogger(config config.AppConfigurer) *logrus.Entry {
// NewApp bootstrap a new application
func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
userConfig := config.GetUserConfig()
app := &App{
closers: []io.Closer{},
Config: config,
}
var err error
app.Log = newLogger(config)
app.Tr, err = i18n.NewTranslationSetFromConfig(app.Log, config.GetUserConfig().Gui.Language)
log := newLogger(config)
tr, err := i18n.NewTranslationSetFromConfig(log, userConfig.Gui.Language)
if err != nil {
return app, err
}
app.Common = &common.Common{
Log: log,
Tr: tr,
UserConfig: userConfig,
Debug: config.GetDebug(),
}
// if we are being called in 'demon' mode, we can just return here
app.ClientContext = os.Getenv("LAZYGIT_CLIENT_COMMAND")
if app.ClientContext != "" {
return app, nil
}
app.OSCommand = oscommands.NewOSCommand(app.Log, config)
app.OSCommand = oscommands.NewOSCommand(app.Common)
app.Updater, err = updates.NewUpdater(app.Log, config, app.OSCommand, app.Tr)
app.Updater, err = updates.NewUpdater(app.Common, config, app.OSCommand)
if err != nil {
return app, err
}
@@ -128,17 +135,15 @@ func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
}
app.GitCommand, err = commands.NewGitCommand(
app.Log,
app.Common,
app.OSCommand,
app.Tr,
app.Config,
git_config.NewStdCachedGitConfig(app.Log),
)
if err != nil {
return app, err
}
app.Gui, err = gui.NewGui(app.Log, app.GitCommand, app.OSCommand, app.Tr, config, app.Updater, filterPath, showRecentRepos)
app.Gui, err = gui.NewGui(app.Common, app.GitCommand, app.OSCommand, config, app.Updater, filterPath, showRecentRepos)
if err != nil {
return app, err
}
@@ -146,7 +151,7 @@ func NewApp(config config.AppConfigurer, filterPath string) (*App, error) {
}
func (app *App) validateGitVersion() error {
output, err := app.OSCommand.RunCommandWithOutput("git --version")
output, err := app.OSCommand.Cmd.New("git --version").RunWithOutput()
// if we get an error anywhere here we'll show the same status
minVersionError := errors.New(app.Tr.MinGitVersionError)
if err != nil {
@@ -203,7 +208,7 @@ func (app *App) setupRepo() (bool, error) {
}
shouldInitRepo := true
notARepository := app.Config.GetUserConfig().NotARepository
notARepository := app.UserConfig.NotARepository
if notARepository == "prompt" {
// Offer to initialize a new repository in current directory.
fmt.Print(app.Tr.CreateRepo)
@@ -231,7 +236,7 @@ func (app *App) setupRepo() (bool, error) {
os.Exit(1)
}
if err := app.OSCommand.RunCommand("git init"); err != nil {
if err := app.OSCommand.Cmd.New("git init").Run(); err != nil {
return false, err
}
}

View File

@@ -11,19 +11,19 @@ import (
// NewBranch create new branch
func (c *GitCommand) NewBranch(name string, base string) error {
return c.RunCommand("git checkout -b %s %s", c.OSCommand.Quote(name), c.OSCommand.Quote(base))
return c.Cmd.New(fmt.Sprintf("git checkout -b %s %s", c.OSCommand.Quote(name), c.OSCommand.Quote(base))).Run()
}
// CurrentBranchName get the current branch name and displayname.
// the first returned string is the name and the second is the displayname
// e.g. name is 123asdf and displayname is '(HEAD detached at 123asdf)'
func (c *GitCommand) CurrentBranchName() (string, string, error) {
branchName, err := c.RunCommandWithOutput("git symbolic-ref --short HEAD")
branchName, err := c.Cmd.New("git symbolic-ref --short HEAD").RunWithOutput()
if err == nil && branchName != "HEAD\n" {
trimmedBranchName := strings.TrimSpace(branchName)
return trimmedBranchName, trimmedBranchName, nil
}
output, err := c.RunCommandWithOutput("git branch --contains")
output, err := c.Cmd.New("git branch --contains").RunWithOutput()
if err != nil {
return "", "", err
}
@@ -47,7 +47,7 @@ func (c *GitCommand) DeleteBranch(branch string, force bool) error {
command = "git branch -D"
}
return c.OSCommand.RunCommand("%s %s", command, c.OSCommand.Quote(branch))
return c.Cmd.New(fmt.Sprintf("%s %s", command, c.OSCommand.Quote(branch))).Run()
}
// Checkout checks out a branch (or commit), with --force if you set the force arg to true
@@ -61,36 +61,41 @@ func (c *GitCommand) Checkout(branch string, options CheckoutOptions) error {
if options.Force {
forceArg = " --force"
}
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git checkout%s %s", forceArg, c.OSCommand.Quote(branch)), oscommands.RunCommandOptions{EnvVars: options.EnvVars})
return c.Cmd.New(fmt.Sprintf("git checkout%s %s", forceArg, c.OSCommand.Quote(branch))).
// prevents git from prompting us for input which would freeze the program
// TODO: see if this is actually needed here
AddEnvVars("GIT_TERMINAL_PROMPT=0").
AddEnvVars(options.EnvVars...).
Run()
}
// GetBranchGraph gets the color-formatted graph of the log for the given branch
// Currently it limits the result to 100 commits, but when we get async stuff
// working we can do lazy loading
func (c *GitCommand) GetBranchGraph(branchName string) (string, error) {
cmdStr := c.GetBranchGraphCmdStr(branchName)
return c.OSCommand.RunCommandWithOutput(cmdStr)
return c.GetBranchGraphCmdObj(branchName).RunWithOutput()
}
func (c *GitCommand) GetUpstreamForBranch(branchName string) (string, error) {
output, err := c.RunCommandWithOutput("git rev-parse --abbrev-ref --symbolic-full-name %s@{u}", c.OSCommand.Quote(branchName))
output, err := c.Cmd.New(fmt.Sprintf("git rev-parse --abbrev-ref --symbolic-full-name %s@{u}", c.OSCommand.Quote(branchName))).RunWithOutput()
return strings.TrimSpace(output), err
}
func (c *GitCommand) GetBranchGraphCmdStr(branchName string) string {
branchLogCmdTemplate := c.Config.GetUserConfig().Git.BranchLogCmd
func (c *GitCommand) GetBranchGraphCmdObj(branchName string) oscommands.ICmdObj {
branchLogCmdTemplate := c.UserConfig.Git.BranchLogCmd
templateValues := map[string]string{
"branchName": c.OSCommand.Quote(branchName),
}
return utils.ResolvePlaceholderString(branchLogCmdTemplate, templateValues)
return c.Cmd.New(utils.ResolvePlaceholderString(branchLogCmdTemplate, templateValues))
}
func (c *GitCommand) SetUpstreamBranch(upstream string) error {
return c.RunCommand("git branch -u %s", c.OSCommand.Quote(upstream))
return c.Cmd.New("git branch -u " + c.OSCommand.Quote(upstream)).Run()
}
func (c *GitCommand) SetBranchUpstream(remoteName string, remoteBranchName string, branchName string) error {
return c.RunCommand("git branch --set-upstream-to=%s/%s %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(remoteBranchName), c.OSCommand.Quote(branchName))
return c.Cmd.New(fmt.Sprintf("git branch --set-upstream-to=%s/%s %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(remoteBranchName), c.OSCommand.Quote(branchName))).Run()
}
func (c *GitCommand) GetCurrentBranchUpstreamDifferenceCount() (string, string) {
@@ -105,11 +110,11 @@ func (c *GitCommand) GetBranchUpstreamDifferenceCount(branchName string) (string
// current branch
func (c *GitCommand) GetCommitDifferences(from, to string) (string, string) {
command := "git rev-list %s..%s --count"
pushableCount, err := c.OSCommand.RunCommandWithOutput(command, to, from)
pushableCount, err := c.Cmd.New(fmt.Sprintf(command, to, from)).RunWithOutput()
if err != nil {
return "?", "?"
}
pullableCount, err := c.OSCommand.RunCommandWithOutput(command, from, to)
pullableCount, err := c.Cmd.New(fmt.Sprintf(command, from, to)).RunWithOutput()
if err != nil {
return "?", "?"
}
@@ -122,40 +127,47 @@ type MergeOpts struct {
// Merge merge
func (c *GitCommand) Merge(branchName string, opts MergeOpts) error {
mergeArgs := c.Config.GetUserConfig().Git.Merging.Args
mergeArg := ""
if c.UserConfig.Git.Merging.Args != "" {
mergeArg = " " + c.UserConfig.Git.Merging.Args
}
command := fmt.Sprintf("git merge --no-edit %s %s", mergeArgs, c.OSCommand.Quote(branchName))
command := fmt.Sprintf("git merge --no-edit%s %s", mergeArg, c.OSCommand.Quote(branchName))
if opts.FastForwardOnly {
command = fmt.Sprintf("%s --ff-only", command)
}
return c.OSCommand.RunCommand(command)
return c.OSCommand.Cmd.New(command).Run()
}
// AbortMerge abort merge
func (c *GitCommand) AbortMerge() error {
return c.RunCommand("git merge --abort")
return c.Cmd.New("git merge --abort").Run()
}
func (c *GitCommand) IsHeadDetached() bool {
err := c.RunCommand("git symbolic-ref -q HEAD")
err := c.Cmd.New("git symbolic-ref -q HEAD").Run()
return err != nil
}
// ResetHardHead runs `git reset --hard`
func (c *GitCommand) ResetHard(ref string) error {
return c.RunCommand("git reset --hard " + c.OSCommand.Quote(ref))
return c.Cmd.New("git reset --hard " + c.OSCommand.Quote(ref)).Run()
}
// ResetSoft runs `git reset --soft HEAD`
func (c *GitCommand) ResetSoft(ref string) error {
return c.RunCommand("git reset --soft " + c.OSCommand.Quote(ref))
return c.Cmd.New("git reset --soft " + c.OSCommand.Quote(ref)).Run()
}
func (c *GitCommand) ResetMixed(ref string) error {
return c.RunCommand("git reset --mixed " + c.OSCommand.Quote(ref))
return c.Cmd.New("git reset --mixed " + c.OSCommand.Quote(ref)).Run()
}
func (c *GitCommand) RenameBranch(oldName string, newName string) error {
return c.RunCommand("git branch --move %s %s", c.OSCommand.Quote(oldName), c.OSCommand.Quote(newName))
return c.Cmd.New(fmt.Sprintf("git branch --move %s %s", c.OSCommand.Quote(oldName), c.OSCommand.Quote(newName))).Run()
}
func (c *GitCommand) GetRawBranches() (string, error) {
return c.Cmd.New(`git for-each-ref --sort=-committerdate --format="%(HEAD)|%(refname:short)|%(upstream:short)|%(upstream:track)" refs/heads`).RunWithOutput()
}

View File

@@ -1,120 +1,85 @@
package commands
import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/jesseduffield/lazygit/pkg/test"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/stretchr/testify/assert"
)
// TestGitCommandGetCommitDifferences is a function.
func TestGitCommandGetCommitDifferences(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(string, string)
testName string
runner *oscommands.FakeCmdObjRunner
expectedPushables string
expectedPullables string
}
scenarios := []scenario{
{
"Can't retrieve pushable count",
func(string, ...string) *exec.Cmd {
return secureexec.Command("test")
},
func(pushableCount string, pullableCount string) {
assert.EqualValues(t, "?", pushableCount)
assert.EqualValues(t, "?", pullableCount)
},
oscommands.NewFakeRunner(t).
Expect("git rev-list @{u}..HEAD --count", "", errors.New("error")),
"?", "?",
},
{
"Can't retrieve pullable count",
func(cmd string, args ...string) *exec.Cmd {
if args[1] == "HEAD..@{u}" {
return secureexec.Command("test")
}
return secureexec.Command("echo")
},
func(pushableCount string, pullableCount string) {
assert.EqualValues(t, "?", pushableCount)
assert.EqualValues(t, "?", pullableCount)
},
oscommands.NewFakeRunner(t).
Expect("git rev-list @{u}..HEAD --count", "1\n", nil).
Expect("git rev-list HEAD..@{u} --count", "", errors.New("error")),
"?", "?",
},
{
"Retrieve pullable and pushable count",
func(cmd string, args ...string) *exec.Cmd {
if args[1] == "HEAD..@{u}" {
return secureexec.Command("echo", "10")
}
return secureexec.Command("echo", "11")
},
func(pushableCount string, pullableCount string) {
assert.EqualValues(t, "11", pushableCount)
assert.EqualValues(t, "10", pullableCount)
},
oscommands.NewFakeRunner(t).
Expect("git rev-list @{u}..HEAD --count", "1\n", nil).
Expect("git rev-list HEAD..@{u} --count", "2\n", nil),
"1", "2",
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
s.test(gitCmd.GetCommitDifferences("HEAD", "@{u}"))
gitCmd := NewDummyGitCommandWithRunner(s.runner)
pushables, pullables := gitCmd.GetCommitDifferences("HEAD", "@{u}")
assert.EqualValues(t, s.expectedPushables, pushables)
assert.EqualValues(t, s.expectedPullables, pullables)
s.runner.CheckForMissingCalls()
})
}
}
// TestGitCommandNewBranch is a function.
func TestGitCommandNewBranch(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"checkout", "-b", "test", "master"}, args)
return secureexec.Command("echo")
}
runner := oscommands.NewFakeRunner(t).
Expect(`git checkout -b "test" "master"`, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner)
assert.NoError(t, gitCmd.NewBranch("test", "master"))
runner.CheckForMissingCalls()
}
// TestGitCommandDeleteBranch is a function.
func TestGitCommandDeleteBranch(t *testing.T) {
type scenario struct {
testName string
branch string
force bool
command func(string, ...string) *exec.Cmd
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
"Delete a branch",
"test",
false,
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"branch", "-d", "test"}, args)
return secureexec.Command("echo")
},
oscommands.NewFakeRunner(t).Expect(`git branch -d "test"`, "", nil),
func(err error) {
assert.NoError(t, err)
},
},
{
"Force delete a branch",
"test",
true,
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"branch", "-D", "test"}, args)
return secureexec.Command("echo")
},
oscommands.NewFakeRunner(t).Expect(`git branch -D "test"`, "", nil),
func(err error) {
assert.NoError(t, err)
},
@@ -123,31 +88,27 @@ func TestGitCommandDeleteBranch(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
s.test(gitCmd.DeleteBranch(s.branch, s.force))
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.DeleteBranch("test", s.force))
s.runner.CheckForMissingCalls()
})
}
}
// TestGitCommandMerge is a function.
func TestGitCommandMerge(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"merge", "--no-edit", "test"}, args)
return secureexec.Command("echo")
}
runner := oscommands.NewFakeRunner(t).
Expect(`git merge --no-edit "test"`, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner)
assert.NoError(t, gitCmd.Merge("test", MergeOpts{}))
runner.CheckForMissingCalls()
}
// TestGitCommandCheckout is a function.
func TestGitCommandCheckout(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
runner *oscommands.FakeCmdObjRunner
test func(error)
force bool
}
@@ -155,12 +116,7 @@ func TestGitCommandCheckout(t *testing.T) {
scenarios := []scenario{
{
"Checkout",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"checkout", "test"}, args)
return secureexec.Command("echo")
},
oscommands.NewFakeRunner(t).Expect(`git checkout "test"`, "", nil),
func(err error) {
assert.NoError(t, err)
},
@@ -168,12 +124,7 @@ func TestGitCommandCheckout(t *testing.T) {
},
{
"Checkout forced",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"checkout", "--force", "test"}, args)
return secureexec.Command("echo")
},
oscommands.NewFakeRunner(t).Expect(`git checkout --force "test"`, "", nil),
func(err error) {
assert.NoError(t, err)
},
@@ -183,52 +134,43 @@ func TestGitCommandCheckout(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.Checkout("test", CheckoutOptions{Force: s.force}))
s.runner.CheckForMissingCalls()
})
}
}
// TestGitCommandGetBranchGraph is a function.
func TestGitCommandGetBranchGraph(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"log", "--graph", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium", "test", "--"}, args)
return secureexec.Command("echo")
}
runner := oscommands.NewFakeRunner(t).ExpectArgs([]string{
"git", "log", "--graph", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium", "test", "--",
}, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner)
_, err := gitCmd.GetBranchGraph("test")
assert.NoError(t, err)
}
func TestGitCommandGetAllBranchGraph(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"log", "--graph", "--all", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium"}, args)
return secureexec.Command("echo")
}
cmdStr := gitCmd.Config.GetUserConfig().Git.AllBranchesLogCmd
_, err := gitCmd.OSCommand.RunCommandWithOutput(cmdStr)
runner := oscommands.NewFakeRunner(t).ExpectArgs([]string{
"git", "log", "--graph", "--all", "--color=always", "--abbrev-commit", "--decorate", "--date=relative", "--pretty=medium",
}, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner)
cmdStr := gitCmd.UserConfig.Git.AllBranchesLogCmd
_, err := gitCmd.Cmd.New(cmdStr).RunWithOutput()
assert.NoError(t, err)
}
// TestGitCommandCurrentBranchName is a function.
func TestGitCommandCurrentBranchName(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
runner *oscommands.FakeCmdObjRunner
test func(string, string, error)
}
scenarios := []scenario{
{
"says we are on the master branch if we are",
func(cmd string, args ...string) *exec.Cmd {
assert.Equal(t, "git", cmd)
return secureexec.Command("echo", "master")
},
oscommands.NewFakeRunner(t).Expect(`git symbolic-ref --short HEAD`, "master", nil),
func(name string, displayname string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "master", name)
@@ -237,20 +179,9 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
},
{
"falls back to git `git branch --contains` if symbolic-ref fails",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return secureexec.Command("test")
case "branch":
assert.EqualValues(t, []string{"branch", "--contains"}, args)
return secureexec.Command("echo", "* master")
}
return nil
},
oscommands.NewFakeRunner(t).
Expect(`git symbolic-ref --short HEAD`, "", errors.New("error")).
Expect(`git branch --contains`, "* master", nil),
func(name string, displayname string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "master", name)
@@ -259,20 +190,9 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
},
{
"handles a detached head",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return secureexec.Command("test")
case "branch":
assert.EqualValues(t, []string{"branch", "--contains"}, args)
return secureexec.Command("echo", "* (HEAD detached at 123abcd)")
}
return nil
},
oscommands.NewFakeRunner(t).
Expect(`git symbolic-ref --short HEAD`, "", errors.New("error")).
Expect(`git branch --contains`, "* (HEAD detached at 123abcd)", nil),
func(name string, displayname string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "123abcd", name)
@@ -281,10 +201,9 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
},
{
"bubbles up error if there is one",
func(cmd string, args ...string) *exec.Cmd {
assert.Equal(t, "git", cmd)
return secureexec.Command("test")
},
oscommands.NewFakeRunner(t).
Expect(`git symbolic-ref --short HEAD`, "", errors.New("error")).
Expect(`git branch --contains`, "", errors.New("error")),
func(name string, displayname string, err error) {
assert.Error(t, err)
assert.EqualValues(t, "", name)
@@ -295,19 +214,18 @@ func TestGitCommandCurrentBranchName(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.CurrentBranchName())
s.runner.CheckForMissingCalls()
})
}
}
// TestGitCommandResetHard is a function.
func TestGitCommandResetHard(t *testing.T) {
type scenario struct {
testName string
ref string
command func(string, ...string) *exec.Cmd
runner *oscommands.FakeCmdObjRunner
test func(error)
}
@@ -315,23 +233,17 @@ func TestGitCommandResetHard(t *testing.T) {
{
"valid case",
"HEAD",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: `git reset --hard HEAD`,
Replace: "echo",
},
}),
oscommands.NewFakeRunner(t).
Expect(`git reset --hard "HEAD"`, "", nil),
func(err error) {
assert.NoError(t, err)
},
},
}
gitCmd := NewDummyGitCommand()
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd.OSCommand.Command = s.command
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.ResetHard(s.ref))
})
}

View File

@@ -10,15 +10,20 @@ import (
// RenameCommit renames the topmost commit with the given name
func (c *GitCommand) RenameCommit(name string) error {
return c.RunCommand("git commit --allow-empty --amend --only -m %s", c.OSCommand.Quote(name))
return c.Cmd.New("git commit --allow-empty --amend --only -m " + c.OSCommand.Quote(name)).Run()
}
// ResetToCommit reset to commit
func (c *GitCommand) ResetToCommit(sha string, strength string, options oscommands.RunCommandOptions) error {
return c.OSCommand.RunCommandWithOptions(fmt.Sprintf("git reset --%s %s", strength, sha), options)
func (c *GitCommand) ResetToCommit(sha string, strength string, envVars []string) error {
return c.Cmd.New(fmt.Sprintf("git reset --%s %s", strength, sha)).
// prevents git from prompting us for input which would freeze the program
// TODO: see if this is actually needed here
AddEnvVars("GIT_TERMINAL_PROMPT=0").
AddEnvVars(envVars...).
Run()
}
func (c *GitCommand) CommitCmdStr(message string, flags string) string {
func (c *GitCommand) CommitCmdObj(message string, flags string) oscommands.ICmdObj {
splitMessage := strings.Split(message, "\n")
lineArgs := ""
for _, line := range splitMessage {
@@ -30,52 +35,53 @@ func (c *GitCommand) CommitCmdStr(message string, flags string) string {
flagsStr = fmt.Sprintf(" %s", flags)
}
return fmt.Sprintf("git commit%s%s", flagsStr, lineArgs)
return c.Cmd.New(fmt.Sprintf("git commit%s%s", flagsStr, lineArgs))
}
// Get the subject of the HEAD commit
func (c *GitCommand) GetHeadCommitMessage() (string, error) {
cmdStr := "git log -1 --pretty=%s"
message, err := c.OSCommand.RunCommandWithOutput(cmdStr)
message, err := c.Cmd.New("git log -1 --pretty=%s").RunWithOutput()
return strings.TrimSpace(message), err
}
func (c *GitCommand) GetCommitMessage(commitSha string) (string, error) {
cmdStr := "git rev-list --format=%B --max-count=1 " + commitSha
messageWithHeader, err := c.OSCommand.RunCommandWithOutput(cmdStr)
messageWithHeader, err := c.Cmd.New(cmdStr).RunWithOutput()
message := strings.Join(strings.SplitAfter(messageWithHeader, "\n")[1:], "\n")
return strings.TrimSpace(message), err
}
func (c *GitCommand) GetCommitMessageFirstLine(sha string) (string, error) {
return c.RunCommandWithOutput("git show --no-patch --pretty=format:%%s %s", sha)
return c.Cmd.New(fmt.Sprintf("git show --no-patch --pretty=format:%%s %s", sha)).RunWithOutput()
}
// AmendHead amends HEAD with whatever is staged in your working tree
func (c *GitCommand) AmendHead() error {
return c.OSCommand.RunCommand(c.AmendHeadCmdStr())
return c.AmendHeadCmdObj().Run()
}
func (c *GitCommand) AmendHeadCmdStr() string {
return "git commit --amend --no-edit --allow-empty"
func (c *GitCommand) AmendHeadCmdObj() oscommands.ICmdObj {
return c.Cmd.New("git commit --amend --no-edit --allow-empty")
}
func (c *GitCommand) ShowCmdStr(sha string, filterPath string) string {
contextSize := c.Config.GetUserConfig().Git.DiffContextSize
func (c *GitCommand) ShowCmdObj(sha string, filterPath string) oscommands.ICmdObj {
contextSize := c.UserConfig.Git.DiffContextSize
filterPathArg := ""
if filterPath != "" {
filterPathArg = fmt.Sprintf(" -- %s", c.OSCommand.Quote(filterPath))
}
return fmt.Sprintf("git show --submodule --color=%s --unified=%d --no-renames --stat -p %s %s", c.colorArg(), contextSize, sha, filterPathArg)
cmdStr := fmt.Sprintf("git show --submodule --color=%s --unified=%d --no-renames --stat -p %s %s", c.colorArg(), contextSize, sha, filterPathArg)
return c.Cmd.New(cmdStr)
}
// Revert reverts the selected commit by sha
func (c *GitCommand) Revert(sha string) error {
return c.RunCommand("git revert %s", sha)
return c.Cmd.New(fmt.Sprintf("git revert %s", sha)).Run()
}
func (c *GitCommand) RevertMerge(sha string, parentNumber int) error {
return c.RunCommand("git revert %s -m %d", sha, parentNumber)
return c.Cmd.New(fmt.Sprintf("git revert %s -m %d", sha, parentNumber)).Run()
}
// CherryPickCommits begins an interactive rebase with the given shas being cherry picked onto HEAD
@@ -85,15 +91,15 @@ func (c *GitCommand) CherryPickCommits(commits []*models.Commit) error {
todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo
}
cmd, err := c.PrepareInteractiveRebaseCommand("HEAD", todo, false)
cmdObj, err := c.PrepareInteractiveRebaseCommand("HEAD", todo, false)
if err != nil {
return err
}
return c.OSCommand.RunPreparedCommand(cmd)
return cmdObj.Run()
}
// CreateFixupCommit creates a commit that fixes up a previous commit
func (c *GitCommand) CreateFixupCommit(sha string) error {
return c.RunCommand("git commit --fixup=%s", sha)
return c.Cmd.New(fmt.Sprintf("git commit --fixup=%s", sha)).Run()
}

View File

@@ -1,43 +1,31 @@
package commands
import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/jesseduffield/lazygit/pkg/test"
"github.com/stretchr/testify/assert"
)
// TestGitCommandRenameCommit is a function.
func TestGitCommandRenameCommit(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"commit", "--allow-empty", "--amend", "--only", "-m", "test"}, args)
return secureexec.Command("echo")
}
runner := oscommands.NewFakeRunner(t).
ExpectArgs([]string{"git", "commit", "--allow-empty", "--amend", "--only", "-m", "test"}, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner)
assert.NoError(t, gitCmd.RenameCommit("test"))
runner.CheckForMissingCalls()
}
// TestGitCommandResetToCommit is a function.
func TestGitCommandResetToCommit(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"reset", "--hard", "78976bc"}, args)
runner := oscommands.NewFakeRunner(t).
ExpectArgs([]string{"git", "reset", "--hard", "78976bc"}, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner)
return secureexec.Command("echo")
}
assert.NoError(t, gitCmd.ResetToCommit("78976bc", "hard", oscommands.RunCommandOptions{}))
assert.NoError(t, gitCmd.ResetToCommit("78976bc", "hard", []string{}))
runner.CheckForMissingCalls()
}
// TestGitCommandCommitStr is a function.
func TestGitCommandCommitStr(t *testing.T) {
func TestGitCommandCommitObj(t *testing.T) {
gitCmd := NewDummyGitCommand()
type scenario struct {
@@ -70,49 +58,42 @@ func TestGitCommandCommitStr(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
cmdStr := gitCmd.CommitCmdStr(s.message, s.flags)
cmdStr := gitCmd.CommitCmdObj(s.message, s.flags).ToString()
assert.Equal(t, s.expected, cmdStr)
})
}
}
// TestGitCommandCreateFixupCommit is a function.
func TestGitCommandCreateFixupCommit(t *testing.T) {
type scenario struct {
testName string
sha string
command func(string, ...string) *exec.Cmd
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
"valid case",
"12345",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: `git commit --fixup=12345`,
Replace: "echo",
},
}),
func(err error) {
testName: "valid case",
sha: "12345",
runner: oscommands.NewFakeRunner(t).
Expect(`git commit --fixup=12345`, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
}
gitCmd := NewDummyGitCommand()
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd.OSCommand.Command = s.command
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.CreateFixupCommit(s.sha))
s.runner.CheckForMissingCalls()
})
}
}
// TestGitCommandShowCmdStr is a function.
func TestGitCommandShowCmdStr(t *testing.T) {
func TestGitCommandShowCmdObj(t *testing.T) {
type scenario struct {
testName string
filterPath string
@@ -145,8 +126,8 @@ func TestGitCommandShowCmdStr(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd.Config.GetUserConfig().Git.DiffContextSize = s.contextSize
cmdStr := gitCmd.ShowCmdStr("1234567890", s.filterPath)
gitCmd.UserConfig.Git.DiffContextSize = s.contextSize
cmdStr := gitCmd.ShowCmdObj("1234567890", s.filterPath).ToString()
assert.Equal(t, s.expected, cmdStr)
})
}

View File

@@ -20,7 +20,7 @@ func (c *GitCommand) ConfiguredPager() string {
}
func (c *GitCommand) GetPager(width int) string {
useConfig := c.Config.GetUserConfig().Git.Paging.UseConfig
useConfig := c.UserConfig.Git.Paging.UseConfig
if useConfig {
pager := c.ConfiguredPager()
return strings.Split(pager, "| less")[0]
@@ -30,18 +30,18 @@ func (c *GitCommand) GetPager(width int) string {
"columnWidth": strconv.Itoa(width/2 - 6),
}
pagerTemplate := c.Config.GetUserConfig().Git.Paging.Pager
pagerTemplate := c.UserConfig.Git.Paging.Pager
return utils.ResolvePlaceholderString(pagerTemplate, templateValues)
}
func (c *GitCommand) colorArg() string {
return c.Config.GetUserConfig().Git.Paging.ColorArg
return c.UserConfig.Git.Paging.ColorArg
}
// UsingGpg tells us whether the user has gpg enabled so that we can know
// whether we need to run a subprocess to allow them to enter their password
func (c *GitCommand) UsingGpg() bool {
overrideGpg := c.Config.GetUserConfig().Git.OverrideGpg
overrideGpg := c.UserConfig.Git.OverrideGpg
if overrideGpg {
return false
}

View File

@@ -6,8 +6,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -18,13 +16,19 @@ func NewDummyGitCommand() *GitCommand {
// NewDummyGitCommandWithOSCommand creates a new dummy GitCommand for testing
func NewDummyGitCommandWithOSCommand(osCommand *oscommands.OSCommand) *GitCommand {
newAppConfig := config.NewDummyAppConfig()
return &GitCommand{
Log: utils.NewDummyLog(),
Common: utils.NewDummyCommon(),
OSCommand: osCommand,
Tr: i18n.NewTranslationSet(utils.NewDummyLog(), newAppConfig.GetUserConfig().Gui.Language),
Config: newAppConfig,
GitConfig: git_config.NewFakeGitConfig(map[string]string{}),
GetCmdWriter: func() io.Writer { return ioutil.Discard },
}
}
func NewDummyGitCommandWithRunner(runner oscommands.ICmdObjRunner) *GitCommand {
builder := oscommands.NewDummyCmdObjBuilder(runner)
gitCommand := NewDummyGitCommand()
gitCommand.Cmd = builder
gitCommand.OSCommand.Cmd = builder
return gitCommand
}

View File

@@ -9,7 +9,9 @@ import (
"time"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -23,27 +25,27 @@ func (c *GitCommand) CatFile(fileName string) (string, error) {
return string(buf), nil
}
func (c *GitCommand) OpenMergeToolCmd() string {
return "git mergetool"
func (c *GitCommand) OpenMergeToolCmdObj() oscommands.ICmdObj {
return c.Cmd.New("git mergetool")
}
func (c *GitCommand) OpenMergeTool() error {
return c.OSCommand.RunCommand("git mergetool")
return c.OpenMergeToolCmdObj().Run()
}
// StageFile stages a file
func (c *GitCommand) StageFile(fileName string) error {
return c.RunCommand("git add -- %s", c.OSCommand.Quote(fileName))
return c.Cmd.New("git add -- " + c.OSCommand.Quote(fileName)).Run()
}
// StageAll stages all files
func (c *GitCommand) StageAll() error {
return c.RunCommand("git add -A")
return c.Cmd.New("git add -A").Run()
}
// UnstageAll unstages all files
func (c *GitCommand) UnstageAll() error {
return c.RunCommand("git reset")
return c.Cmd.New("git reset").Run()
}
// UnStageFile unstages a file
@@ -56,7 +58,8 @@ func (c *GitCommand) UnStageFile(fileNames []string, reset bool) error {
}
for _, name := range fileNames {
if err := c.OSCommand.RunCommand(command, c.OSCommand.Quote(name)); err != nil {
err := c.Cmd.New(fmt.Sprintf(command, c.OSCommand.Quote(name))).Run()
if err != nil {
return err
}
}
@@ -73,7 +76,10 @@ func (c *GitCommand) BeforeAndAfterFileForRename(file *models.File) (*models.Fil
// all files, passing the --no-renames flag and then recursively call the function
// again for the before file and after file.
filesWithoutRenames := c.GetStatusFiles(GetStatusFileOptions{NoRenames: true})
filesWithoutRenames := loaders.
NewFileLoader(c.Common, c.Cmd, c.GitConfig).
GetStatusFiles(loaders.GetStatusFileOptions{NoRenames: true})
var beforeFile *models.File
var afterFile *models.File
for _, f := range filesWithoutRenames {
@@ -120,22 +126,22 @@ func (c *GitCommand) DiscardAllFileChanges(file *models.File) error {
quotedFileName := c.OSCommand.Quote(file.Name)
if file.ShortStatus == "AA" {
if err := c.RunCommand("git checkout --ours -- %s", quotedFileName); err != nil {
if err := c.Cmd.New("git checkout --ours -- " + quotedFileName).Run(); err != nil {
return err
}
if err := c.RunCommand("git add -- %s", quotedFileName); err != nil {
if err := c.Cmd.New("git add -- " + quotedFileName).Run(); err != nil {
return err
}
return nil
}
if file.ShortStatus == "DU" {
return c.RunCommand("git rm -- %s", quotedFileName)
return c.Cmd.New("git rm -- " + quotedFileName).Run()
}
// if the file isn't tracked, we assume you want to delete it
if file.HasStagedChanges || file.HasMergeConflicts {
if err := c.RunCommand("git reset -- %s", quotedFileName); err != nil {
if err := c.Cmd.New("git reset -- " + quotedFileName).Run(); err != nil {
return err
}
}
@@ -161,7 +167,7 @@ func (c *GitCommand) DiscardUnstagedDirChanges(node *filetree.FileNode) error {
}
quotedPath := c.OSCommand.Quote(node.GetPath())
if err := c.RunCommand("git checkout -- %s", quotedPath); err != nil {
if err := c.Cmd.New("git checkout -- " + quotedPath).Run(); err != nil {
return err
}
@@ -186,7 +192,7 @@ func (c *GitCommand) RemoveUntrackedDirFiles(node *filetree.FileNode) error {
// DiscardUnstagedFileChanges directly
func (c *GitCommand) DiscardUnstagedFileChanges(file *models.File) error {
quotedFileName := c.OSCommand.Quote(file.Name)
return c.RunCommand("git checkout -- %s", quotedFileName)
return c.Cmd.New("git checkout -- " + quotedFileName).Run()
}
// Ignore adds a file to the gitignore for the repo
@@ -197,17 +203,17 @@ func (c *GitCommand) Ignore(filename string) error {
// WorktreeFileDiff returns the diff of a file
func (c *GitCommand) WorktreeFileDiff(file *models.File, plain bool, cached bool, ignoreWhitespace bool) string {
// for now we assume an error means the file was deleted
s, _ := c.OSCommand.RunCommandWithOutput(c.WorktreeFileDiffCmdStr(file, plain, cached, ignoreWhitespace))
s, _ := c.WorktreeFileDiffCmdObj(file, plain, cached, ignoreWhitespace).RunWithOutput()
return s
}
func (c *GitCommand) WorktreeFileDiffCmdStr(node models.IFile, plain bool, cached bool, ignoreWhitespace bool) string {
func (c *GitCommand) WorktreeFileDiffCmdObj(node models.IFile, plain bool, cached bool, ignoreWhitespace bool) oscommands.ICmdObj {
cachedArg := ""
trackedArg := "--"
colorArg := c.colorArg()
quotedPath := c.OSCommand.Quote(node.GetPath())
ignoreWhitespaceArg := ""
contextSize := c.Config.GetUserConfig().Git.DiffContextSize
contextSize := c.UserConfig.Git.DiffContextSize
if cached {
cachedArg = "--cached"
}
@@ -221,11 +227,13 @@ func (c *GitCommand) WorktreeFileDiffCmdStr(node models.IFile, plain bool, cache
ignoreWhitespaceArg = "--ignore-all-space"
}
return fmt.Sprintf("git diff --submodule --no-ext-diff --unified=%d --color=%s %s %s %s %s", contextSize, colorArg, ignoreWhitespaceArg, cachedArg, trackedArg, quotedPath)
cmdStr := fmt.Sprintf("git diff --submodule --no-ext-diff --unified=%d --color=%s %s %s %s %s", contextSize, colorArg, ignoreWhitespaceArg, cachedArg, trackedArg, quotedPath)
return c.Cmd.New(cmdStr)
}
func (c *GitCommand) ApplyPatch(patch string, flags ...string) error {
filepath := filepath.Join(c.Config.GetTempDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch")
filepath := filepath.Join(oscommands.GetTempDir(), utils.GetCurrentRepoName(), time.Now().Format("Jan _2 15.04.05.000000000")+".patch")
c.Log.Infof("saving temporary patch to %s", filepath)
if err := c.OSCommand.CreateFileWithContent(filepath, patch); err != nil {
return err
@@ -236,19 +244,18 @@ func (c *GitCommand) ApplyPatch(patch string, flags ...string) error {
flagStr += " --" + flag
}
return c.RunCommand("git apply %s %s", flagStr, c.OSCommand.Quote(filepath))
return c.Cmd.New(fmt.Sprintf("git apply%s %s", flagStr, c.OSCommand.Quote(filepath))).Run()
}
// ShowFileDiff get the diff of specified from and to. Typically this will be used for a single commit so it'll be 123abc^..123abc
// but when we're in diff mode it could be any 'from' to any 'to'. The reverse flag is also here thanks to diff mode.
func (c *GitCommand) ShowFileDiff(from string, to string, reverse bool, fileName string, plain bool) (string, error) {
cmdStr := c.ShowFileDiffCmdStr(from, to, reverse, fileName, plain)
return c.OSCommand.RunCommandWithOutput(cmdStr)
return c.ShowFileDiffCmdObj(from, to, reverse, fileName, plain).RunWithOutput()
}
func (c *GitCommand) ShowFileDiffCmdStr(from string, to string, reverse bool, fileName string, plain bool) string {
func (c *GitCommand) ShowFileDiffCmdObj(from string, to string, reverse bool, fileName string, plain bool) oscommands.ICmdObj {
colorArg := c.colorArg()
contextSize := c.Config.GetUserConfig().Git.DiffContextSize
contextSize := c.UserConfig.Git.DiffContextSize
if plain {
colorArg = "never"
}
@@ -258,12 +265,12 @@ func (c *GitCommand) ShowFileDiffCmdStr(from string, to string, reverse bool, fi
reverseFlag = " -R "
}
return fmt.Sprintf("git diff --submodule --no-ext-diff --unified=%d --no-renames --color=%s %s %s %s -- %s", contextSize, colorArg, from, to, reverseFlag, c.OSCommand.Quote(fileName))
return c.Cmd.New(fmt.Sprintf("git diff --submodule --no-ext-diff --unified=%d --no-renames --color=%s %s %s %s -- %s", contextSize, colorArg, from, to, reverseFlag, c.OSCommand.Quote(fileName)))
}
// CheckoutFile checks out the file for the given commit
func (c *GitCommand) CheckoutFile(commitSha, fileName string) error {
return c.RunCommand("git checkout %s -- %s", commitSha, c.OSCommand.Quote(fileName))
return c.Cmd.New(fmt.Sprintf("git checkout %s -- %s", commitSha, c.OSCommand.Quote(fileName))).Run()
}
// DiscardOldFileChanges discards changes to a file from an old commit
@@ -273,7 +280,7 @@ func (c *GitCommand) DiscardOldFileChanges(commits []*models.Commit, commitIndex
}
// check if file exists in previous commit (this command returns an error if the file doesn't exist)
if err := c.RunCommand("git cat-file -e HEAD^:%s", c.OSCommand.Quote(fileName)); err != nil {
if err := c.Cmd.New("git cat-file -e HEAD^:" + c.OSCommand.Quote(fileName)).Run(); err != nil {
if err := c.OSCommand.Remove(fileName); err != nil {
return err
}
@@ -296,28 +303,28 @@ func (c *GitCommand) DiscardOldFileChanges(commits []*models.Commit, commitIndex
// DiscardAnyUnstagedFileChanges discards any unstages file changes via `git checkout -- .`
func (c *GitCommand) DiscardAnyUnstagedFileChanges() error {
return c.RunCommand("git checkout -- .")
return c.Cmd.New("git checkout -- .").Run()
}
// RemoveTrackedFiles will delete the given file(s) even if they are currently tracked
func (c *GitCommand) RemoveTrackedFiles(name string) error {
return c.RunCommand("git rm -r --cached -- %s", c.OSCommand.Quote(name))
return c.Cmd.New("git rm -r --cached -- " + c.OSCommand.Quote(name)).Run()
}
// RemoveUntrackedFiles runs `git clean -fd`
func (c *GitCommand) RemoveUntrackedFiles() error {
return c.RunCommand("git clean -fd")
return c.Cmd.New("git clean -fd").Run()
}
// ResetAndClean removes all unstaged changes and removes all untracked files
func (c *GitCommand) ResetAndClean() error {
submoduleConfigs, err := c.GetSubmoduleConfigs()
submoduleConfigs, err := c.Submodules.GetConfigs()
if err != nil {
return err
}
if len(submoduleConfigs) > 0 {
if err := c.ResetSubmodules(submoduleConfigs); err != nil {
if err := c.Submodules.ResetSubmodules(submoduleConfigs); err != nil {
return err
}
}
@@ -330,7 +337,7 @@ func (c *GitCommand) ResetAndClean() error {
}
func (c *GitCommand) EditFileCmdStr(filename string, lineNumber int) (string, error) {
editor := c.Config.GetUserConfig().OS.EditCommand
editor := c.UserConfig.OS.EditCommand
if editor == "" {
editor = c.GitConfig.Get("core.editor")
@@ -346,7 +353,7 @@ func (c *GitCommand) EditFileCmdStr(filename string, lineNumber int) (string, er
editor = c.OSCommand.Getenv("EDITOR")
}
if editor == "" {
if err := c.OSCommand.RunCommand("which vi"); err == nil {
if err := c.OSCommand.Cmd.New("which vi").Run(); err == nil {
editor = "vi"
}
}
@@ -360,6 +367,6 @@ func (c *GitCommand) EditFileCmdStr(filename string, lineNumber int) (string, er
"line": strconv.Itoa(lineNumber),
}
editCmdTemplate := c.Config.GetUserConfig().OS.EditCommandTemplate
editCmdTemplate := c.UserConfig.OS.EditCommandTemplate
return utils.ResolvePlaceholderString(editCmdTemplate, templateValues), nil
}

File diff suppressed because it is too large Load Diff

View File

@@ -6,19 +6,17 @@ import (
"os"
"path/filepath"
"strings"
"time"
"github.com/go-errors/errors"
gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/env"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
// this takes something like:
@@ -27,17 +25,27 @@ import (
// and returns '264fc6f5' as the second match
const CurrentBranchNameRegex = `(?m)^\*.*?([^ ]*?)\)?$`
type Loaders struct {
Commits *loaders.CommitLoader
Branches *loaders.BranchLoader
Files *loaders.FileLoader
CommitFiles *loaders.CommitFileLoader
Remotes *loaders.RemoteLoader
ReflogCommits *loaders.ReflogCommitLoader
Stash *loaders.StashLoader
Tags *loaders.TagLoader
}
// GitCommand is our main git interface
type GitCommand struct {
Log *logrus.Entry
*common.Common
OSCommand *oscommands.OSCommand
Repo *gogit.Repository
Tr *i18n.TranslationSet
Config config.AppConfigurer
DotGitDir string
onSuccessfulContinue func() error
PatchManager *patch.PatchManager
GitConfig git_config.IGitConfig
Loaders Loaders
// Push to current determines whether the user has configured to push to the remote branch of the same name as the current or not
PushToCurrent bool
@@ -46,14 +54,16 @@ type GitCommand struct {
// Coincidentally at the moment it's the same view that OnRunCommand logs to
// but that need not always be the case.
GetCmdWriter func() io.Writer
Cmd oscommands.ICmdObjBuilder
Submodules SubmoduleCommands
}
// NewGitCommand it runs git commands
func NewGitCommand(
log *logrus.Entry,
cmn *common.Common,
osCommand *oscommands.OSCommand,
tr *i18n.TranslationSet,
config config.AppConfigurer,
gitConfig git_config.IGitConfig,
) (*GitCommand, error) {
var repo *gogit.Repository
@@ -65,7 +75,7 @@ func NewGitCommand(
}
var err error
if repo, err = setupRepository(gogit.PlainOpen, tr.GitconfigParseErr); err != nil {
if repo, err = setupRepository(gogit.PlainOpen, cmn.Tr.GitconfigParseErr); err != nil {
return nil, err
}
@@ -74,19 +84,33 @@ func NewGitCommand(
return nil, err
}
cmd := NewGitCmdObjBuilder(cmn.Log, osCommand.Cmd)
gitCommand := &GitCommand{
Log: log,
Common: cmn,
OSCommand: osCommand,
Tr: tr,
Repo: repo,
Config: config,
DotGitDir: dotGitDir,
PushToCurrent: pushToCurrent,
GitConfig: gitConfig,
GetCmdWriter: func() io.Writer { return ioutil.Discard },
Cmd: cmd,
}
gitCommand.PatchManager = patch.NewPatchManager(log, gitCommand.ApplyPatch, gitCommand.ShowFileDiff)
gitCommand.Loaders = Loaders{
Commits: loaders.NewCommitLoader(cmn, gitCommand),
Branches: loaders.NewBranchLoader(cmn, gitCommand),
Files: loaders.NewFileLoader(cmn, cmd, gitConfig),
CommitFiles: loaders.NewCommitFileLoader(cmn, cmd),
Remotes: loaders.NewRemoteLoader(cmn, cmd, gitCommand.Repo.Remotes),
ReflogCommits: loaders.NewReflogCommitLoader(cmn, cmd),
Stash: loaders.NewStashLoader(cmn, cmd),
Tags: loaders.NewTagLoader(cmn, cmd),
}
gitCommand.Submodules = NewSubmoduleCommands(cmn, cmd, dotGitDir)
gitCommand.PatchManager = patch.NewPatchManager(gitCommand.Log, gitCommand.ApplyPatch, gitCommand.ShowFileDiff)
return gitCommand, nil
}
@@ -103,6 +127,8 @@ func (c *GitCommand) WithSpan(span string) *GitCommand {
*newGitCommand = *c
newGitCommand.OSCommand = c.OSCommand.WithSpan(span)
newGitCommand.Cmd = NewGitCmdObjBuilder(c.Log, newGitCommand.OSCommand.Cmd)
// NOTE: unlike the other things here which create shallow clones, this will
// actually update the PatchManager on the original struct to have the new span.
// This means each time we call ApplyPatch in PatchManager, we need to ensure
@@ -223,38 +249,13 @@ func findDotGitDir(stat func(string) (os.FileInfo, error), readFile func(filenam
}
func VerifyInGitRepo(osCommand *oscommands.OSCommand) error {
return osCommand.RunCommand("git rev-parse --git-dir")
return osCommand.Cmd.New("git rev-parse --git-dir").Run()
}
func (c *GitCommand) RunCommand(formatString string, formatArgs ...interface{}) error {
_, err := c.RunCommandWithOutput(formatString, formatArgs...)
return err
func (c *GitCommand) GetDotGitDir() string {
return c.DotGitDir
}
func (c *GitCommand) RunCommandWithOutput(formatString string, formatArgs ...interface{}) (string, error) {
// TODO: have this retry logic in other places we run the command
waitTime := 50 * time.Millisecond
retryCount := 5
attempt := 0
for {
output, err := c.OSCommand.RunCommandWithOutput(formatString, formatArgs...)
if err != nil {
// if we have an error based on the index lock, we should wait a bit and then retry
if strings.Contains(output, ".git/index.lock") {
c.Log.Error(output)
c.Log.Info("index.lock prevented command from running. Retrying command after a small wait")
attempt++
time.Sleep(waitTime)
if attempt < retryCount {
continue
}
}
}
return output, err
}
}
func (c *GitCommand) NewCmdObjFromStr(cmdStr string) oscommands.ICmdObj {
return c.OSCommand.NewCmdObjFromStr(cmdStr).AddEnvVars("GIT_OPTIONAL_LOCKS=0")
func (c *GitCommand) GetCmd() oscommands.ICmdObjBuilder {
return c.Cmd
}

View File

@@ -0,0 +1,47 @@
package commands
import (
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/sirupsen/logrus"
)
// all we're doing here is wrapping the default command object builder with
// some git-specific stuff: e.g. adding a git-specific env var
type gitCmdObjBuilder struct {
innerBuilder *oscommands.CmdObjBuilder
}
var _ oscommands.ICmdObjBuilder = &gitCmdObjBuilder{}
func NewGitCmdObjBuilder(log *logrus.Entry, innerBuilder *oscommands.CmdObjBuilder) *gitCmdObjBuilder {
// the price of having a convenient interface where we can say .New(...).Run() is that our builder now depends on our runner, so when we want to wrap the default builder/runner in new functionality we need to jump through some hoops. We could avoid the use of a decorator function here by just exporting the runner field on the default builder but that would be misleading because we don't want anybody using that to run commands (i.e. we want there to be a single API used across the codebase)
updatedBuilder := innerBuilder.CloneWithNewRunner(func(runner oscommands.ICmdObjRunner) oscommands.ICmdObjRunner {
return &gitCmdObjRunner{
log: log,
innerRunner: runner,
}
})
return &gitCmdObjBuilder{
innerBuilder: updatedBuilder,
}
}
var defaultEnvVar = "GIT_OPTIONAL_LOCKS=0"
func (self *gitCmdObjBuilder) New(cmdStr string) oscommands.ICmdObj {
return self.innerBuilder.New(cmdStr).AddEnvVars(defaultEnvVar)
}
func (self *gitCmdObjBuilder) NewFromArgs(args []string) oscommands.ICmdObj {
return self.innerBuilder.NewFromArgs(args).AddEnvVars(defaultEnvVar)
}
func (self *gitCmdObjBuilder) NewShell(cmdStr string) oscommands.ICmdObj {
return self.innerBuilder.NewShell(cmdStr).AddEnvVars(defaultEnvVar)
}
func (self *gitCmdObjBuilder) Quote(str string) string {
return self.innerBuilder.Quote(str)
}

View File

@@ -0,0 +1,49 @@
package commands
import (
"strings"
"time"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/sirupsen/logrus"
)
// here we're wrapping the default command runner in some git-specific stuff e.g. retry logic if we get an error due to the presence of .git/index.lock
type gitCmdObjRunner struct {
log *logrus.Entry
innerRunner oscommands.ICmdObjRunner
}
func (self *gitCmdObjRunner) Run(cmdObj oscommands.ICmdObj) error {
_, err := self.RunWithOutput(cmdObj)
return err
}
func (self *gitCmdObjRunner) RunWithOutput(cmdObj oscommands.ICmdObj) (string, error) {
// TODO: have this retry logic in other places we run the command
waitTime := 50 * time.Millisecond
retryCount := 5
attempt := 0
for {
output, err := self.innerRunner.RunWithOutput(cmdObj)
if err != nil {
// if we have an error based on the index lock, we should wait a bit and then retry
if strings.Contains(output, ".git/index.lock") {
self.log.Error(output)
self.log.Info("index.lock prevented command from running. Retrying command after a small wait")
attempt++
time.Sleep(waitTime)
if attempt < retryCount {
continue
}
}
}
return output, err
}
}
func (self *gitCmdObjRunner) RunAndProcessLines(cmdObj oscommands.ICmdObj, onLine func(line string) (bool, error)) error {
return self.innerRunner.RunAndProcessLines(cmdObj, onLine)
}

View File

@@ -10,8 +10,6 @@ import (
gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
@@ -210,8 +208,7 @@ func TestNewGitCommand(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
s.setup()
newAppConfig := config.NewDummyAppConfig()
s.test(NewGitCommand(utils.NewDummyLog(), oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog(), newAppConfig.GetUserConfig().Gui.Language), newAppConfig, git_config.NewFakeGitConfig(nil)))
s.test(NewGitCommand(utils.NewDummyCommon(), oscommands.NewDummyOSCommand(), git_config.NewFakeGitConfig(nil)))
})
}
}

View File

@@ -1,12 +1,12 @@
package commands
package loaders
import (
"regexp"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
// context:
@@ -20,25 +20,76 @@ import (
// if we find out we need to use one of these functions in the git.go file, we
// can just pull them out of here and put them there and then call them from in here
// BranchListBuilder returns a list of Branch objects for the current repo
type BranchListBuilder struct {
Log *logrus.Entry
GitCommand *GitCommand
ReflogCommits []*models.Commit
// BranchLoader returns a list of Branch objects for the current repo
type BranchLoader struct {
*common.Common
getRawBranches func() (string, error)
getCurrentBranchName func() (string, string, error)
}
// NewBranchListBuilder builds a new branch list builder
func NewBranchListBuilder(log *logrus.Entry, gitCommand *GitCommand, reflogCommits []*models.Commit) (*BranchListBuilder, error) {
return &BranchListBuilder{
Log: log,
GitCommand: gitCommand,
ReflogCommits: reflogCommits,
}, nil
type BranchLoaderGitCommand interface {
GetRawBranches() (string, error)
CurrentBranchName() (string, string, error)
}
func (b *BranchListBuilder) obtainBranches() []*models.Branch {
cmdStr := `git for-each-ref --sort=-committerdate --format="%(HEAD)|%(refname:short)|%(upstream:short)|%(upstream:track)" refs/heads`
output, err := b.GitCommand.OSCommand.RunCommandWithOutput(cmdStr)
func NewBranchLoader(
cmn *common.Common,
gitCommand BranchLoaderGitCommand,
) *BranchLoader {
return &BranchLoader{
Common: cmn,
getRawBranches: gitCommand.GetRawBranches,
getCurrentBranchName: gitCommand.CurrentBranchName,
}
}
// Load the list of branches for the current repo
func (self *BranchLoader) Load(reflogCommits []*models.Commit) []*models.Branch {
branches := self.obtainBranches()
reflogBranches := self.obtainReflogBranches(reflogCommits)
// loop through reflog branches. If there is a match, merge them, then remove it from the branches and keep it in the reflog branches
branchesWithRecency := make([]*models.Branch, 0)
outer:
for _, reflogBranch := range reflogBranches {
for j, branch := range branches {
if branch.Head {
continue
}
if strings.EqualFold(reflogBranch.Name, branch.Name) {
branch.Recency = reflogBranch.Recency
branchesWithRecency = append(branchesWithRecency, branch)
branches = append(branches[0:j], branches[j+1:]...)
continue outer
}
}
}
branches = append(branchesWithRecency, branches...)
foundHead := false
for i, branch := range branches {
if branch.Head {
foundHead = true
branch.Recency = " *"
branches = append(branches[0:i], branches[i+1:]...)
branches = append([]*models.Branch{branch}, branches...)
break
}
}
if !foundHead {
currentBranchName, currentBranchDisplayName, err := self.getCurrentBranchName()
if err != nil {
panic(err)
}
branches = append([]*models.Branch{{Name: currentBranchName, DisplayName: currentBranchDisplayName, Head: true, Recency: " *"}}, branches...)
}
return branches
}
func (self *BranchLoader) obtainBranches() []*models.Branch {
output, err := self.getRawBranches()
if err != nil {
panic(err)
}
@@ -98,58 +149,13 @@ func (b *BranchListBuilder) obtainBranches() []*models.Branch {
return branches
}
// Build the list of branches for the current repo
func (b *BranchListBuilder) Build() []*models.Branch {
branches := b.obtainBranches()
reflogBranches := b.obtainReflogBranches()
// loop through reflog branches. If there is a match, merge them, then remove it from the branches and keep it in the reflog branches
branchesWithRecency := make([]*models.Branch, 0)
outer:
for _, reflogBranch := range reflogBranches {
for j, branch := range branches {
if branch.Head {
continue
}
if strings.EqualFold(reflogBranch.Name, branch.Name) {
branch.Recency = reflogBranch.Recency
branchesWithRecency = append(branchesWithRecency, branch)
branches = append(branches[0:j], branches[j+1:]...)
continue outer
}
}
}
branches = append(branchesWithRecency, branches...)
foundHead := false
for i, branch := range branches {
if branch.Head {
foundHead = true
branch.Recency = " *"
branches = append(branches[0:i], branches[i+1:]...)
branches = append([]*models.Branch{branch}, branches...)
break
}
}
if !foundHead {
currentBranchName, currentBranchDisplayName, err := b.GitCommand.CurrentBranchName()
if err != nil {
panic(err)
}
branches = append([]*models.Branch{{Name: currentBranchName, DisplayName: currentBranchDisplayName, Head: true, Recency: " *"}}, branches...)
}
return branches
}
// TODO: only look at the new reflog commits, and otherwise store the recencies in
// int form against the branch to recalculate the time ago
func (b *BranchListBuilder) obtainReflogBranches() []*models.Branch {
func (self *BranchLoader) obtainReflogBranches(reflogCommits []*models.Commit) []*models.Branch {
foundBranchesMap := map[string]bool{}
re := regexp.MustCompile(`checkout: moving from ([\S]+) to ([\S]+)`)
reflogBranches := make([]*models.Branch, 0, len(b.ReflogCommits))
for _, commit := range b.ReflogCommits {
reflogBranches := make([]*models.Branch, 0, len(reflogCommits))
for _, commit := range reflogCommits {
if match := re.FindStringSubmatch(commit.Name); len(match) == 3 {
recency := utils.UnixToTimeAgo(commit.UnixTimestamp)
for _, branchName := range match[1:] {

View File

@@ -0,0 +1,57 @@
package loaders
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
)
type CommitFileLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
}
func NewCommitFileLoader(common *common.Common, cmd oscommands.ICmdObjBuilder) *CommitFileLoader {
return &CommitFileLoader{
Common: common,
cmd: cmd,
}
}
// GetFilesInDiff get the specified commit files
func (self *CommitFileLoader) GetFilesInDiff(from string, to string, reverse bool) ([]*models.CommitFile, error) {
reverseFlag := ""
if reverse {
reverseFlag = " -R "
}
filenames, err := self.cmd.New(fmt.Sprintf("git diff --submodule --no-ext-diff --name-status -z --no-renames %s %s %s", reverseFlag, from, to)).RunWithOutput()
if err != nil {
return nil, err
}
return self.getCommitFilesFromFilenames(filenames), nil
}
// filenames string is something like "file1\nfile2\nfile3"
func (self *CommitFileLoader) getCommitFilesFromFilenames(filenames string) []*models.CommitFile {
commitFiles := make([]*models.CommitFile, 0)
lines := strings.Split(strings.TrimRight(filenames, "\x00"), "\x00")
n := len(lines)
for i := 0; i < n-1; i += 2 {
// typical result looks like 'A my_file' meaning my_file was added
changeStatus := lines[i]
name := lines[i+1]
commitFiles = append(commitFiles, &models.CommitFile{
Name: name,
ChangeStatus: changeStatus,
})
}
return commitFiles
}

View File

@@ -1,10 +1,9 @@
package commands
package loaders
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"regexp"
"strconv"
@@ -12,9 +11,9 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/sirupsen/logrus"
)
// context:
@@ -23,39 +22,146 @@ import (
// be processed as part of a rebase (these won't appear in git log but we
// grab them from the rebase-related files in the .git directory to show them
// if we find out we need to use one of these functions in the git.go file, we
// can just pull them out of here and put them there and then call them from in here
const SEPARATION_CHAR = "|"
// CommitListBuilder returns a list of Branch objects for the current repo
type CommitListBuilder struct {
Log *logrus.Entry
GitCommand *GitCommand
OSCommand *oscommands.OSCommand
Tr *i18n.TranslationSet
// CommitLoader returns a list of Commit objects for the current repo
type CommitLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
getCurrentBranchName func() (string, string, error)
getRebaseMode func() (enums.RebaseMode, error)
readFile func(filename string) ([]byte, error)
walkFiles func(root string, fn filepath.WalkFunc) error
dotGitDir string
}
// NewCommitListBuilder builds a new commit list builder
func NewCommitListBuilder(
log *logrus.Entry,
gitCommand *GitCommand,
osCommand *oscommands.OSCommand,
tr *i18n.TranslationSet,
) *CommitListBuilder {
return &CommitListBuilder{
Log: log,
GitCommand: gitCommand,
OSCommand: osCommand,
Tr: tr,
type CommitLoaderGitCommand interface {
CurrentBranchName() (string, string, error)
RebaseMode() (enums.RebaseMode, error)
GetCmd() oscommands.ICmdObjBuilder
GetDotGitDir() string
}
// making our dependencies explicit for the sake of easier testing
func NewCommitLoader(
cmn *common.Common,
gitCommand CommitLoaderGitCommand,
) *CommitLoader {
return &CommitLoader{
Common: cmn,
cmd: gitCommand.GetCmd(),
getCurrentBranchName: gitCommand.CurrentBranchName,
getRebaseMode: gitCommand.RebaseMode,
readFile: ioutil.ReadFile,
walkFiles: filepath.Walk,
dotGitDir: gitCommand.GetDotGitDir(),
}
}
type GetCommitsOptions struct {
Limit bool
FilterPath string
IncludeRebaseCommits bool
RefName string // e.g. "HEAD" or "my_branch"
// determines if we show the whole git graph i.e. pass the '--all' flag
All bool
}
// GetCommits obtains the commits of the current branch
func (self *CommitLoader) GetCommits(opts GetCommitsOptions) ([]*models.Commit, error) {
commits := []*models.Commit{}
var rebasingCommits []*models.Commit
rebaseMode, err := self.getRebaseMode()
if err != nil {
return nil, err
}
if opts.IncludeRebaseCommits && opts.FilterPath == "" {
var err error
rebasingCommits, err = self.MergeRebasingCommits(commits)
if err != nil {
return nil, err
}
commits = append(commits, rebasingCommits...)
}
passedFirstPushedCommit := false
firstPushedCommit, err := self.getFirstPushedCommit(opts.RefName)
if err != nil {
// must have no upstream branch so we'll consider everything as pushed
passedFirstPushedCommit = true
}
err = self.getLogCmd(opts).RunAndProcessLines(func(line string) (bool, error) {
if canExtractCommit(line) {
commit := self.extractCommitFromLine(line)
if commit.Sha == firstPushedCommit {
passedFirstPushedCommit = true
}
commit.Status = map[bool]string{true: "unpushed", false: "pushed"}[!passedFirstPushedCommit]
commits = append(commits, commit)
}
return false, nil
})
if err != nil {
return nil, err
}
if len(commits) == 0 {
return commits, nil
}
if rebaseMode != enums.REBASE_MODE_NONE {
currentCommit := commits[len(rebasingCommits)]
youAreHere := style.FgYellow.Sprintf("<-- %s ---", self.Tr.YouAreHere)
currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name)
}
commits, err = self.setCommitMergedStatuses(opts.RefName, commits)
if err != nil {
return nil, err
}
return commits, nil
}
func (self *CommitLoader) MergeRebasingCommits(commits []*models.Commit) ([]*models.Commit, error) {
// chances are we have as many commits as last time so we'll set the capacity to be the old length
result := make([]*models.Commit, 0, len(commits))
for i, commit := range commits {
if commit.Status != "rebasing" { // removing the existing rebase commits so we can add the refreshed ones
result = append(result, commits[i:]...)
break
}
}
rebaseMode, err := self.getRebaseMode()
if err != nil {
return nil, err
}
if rebaseMode == enums.REBASE_MODE_NONE {
// not in rebase mode so return original commits
return result, nil
}
rebasingCommits, err := self.getHydratedRebasingCommits(rebaseMode)
if err != nil {
return nil, err
}
if len(rebasingCommits) > 0 {
result = append(rebasingCommits, result...)
}
return result, nil
}
// extractCommitFromLine takes a line from a git log and extracts the sha, message, date, and tag if present
// then puts them into a commit object
// example input:
// 8ad01fe32fcc20f07bc6693f87aa4977c327f1e1|10 hours ago|Jesse Duffield| (HEAD -> master, tag: v0.15.2)|refresh commits when adding a tag
func (c *CommitListBuilder) extractCommitFromLine(line string) *models.Commit {
func (self *CommitLoader) extractCommitFromLine(line string) *models.Commit {
split := strings.Split(line, SEPARATION_CHAR)
sha := split[0]
@@ -88,104 +194,8 @@ func (c *CommitListBuilder) extractCommitFromLine(line string) *models.Commit {
}
}
type GetCommitsOptions struct {
Limit bool
FilterPath string
IncludeRebaseCommits bool
RefName string // e.g. "HEAD" or "my_branch"
// determines if we show the whole git graph i.e. pass the '--all' flag
All bool
}
func (c *CommitListBuilder) MergeRebasingCommits(commits []*models.Commit) ([]*models.Commit, error) {
// chances are we have as many commits as last time so we'll set the capacity to be the old length
result := make([]*models.Commit, 0, len(commits))
for i, commit := range commits {
if commit.Status != "rebasing" { // removing the existing rebase commits so we can add the refreshed ones
result = append(result, commits[i:]...)
break
}
}
rebaseMode, err := c.GitCommand.RebaseMode()
if err != nil {
return nil, err
}
if rebaseMode == "" {
// not in rebase mode so return original commits
return result, nil
}
rebasingCommits, err := c.getHydratedRebasingCommits(rebaseMode)
if err != nil {
return nil, err
}
if len(rebasingCommits) > 0 {
result = append(rebasingCommits, result...)
}
return result, nil
}
// GetCommits obtains the commits of the current branch
func (c *CommitListBuilder) GetCommits(opts GetCommitsOptions) ([]*models.Commit, error) {
commits := []*models.Commit{}
var rebasingCommits []*models.Commit
rebaseMode, err := c.GitCommand.RebaseMode()
if err != nil {
return nil, err
}
if opts.IncludeRebaseCommits && opts.FilterPath == "" {
var err error
rebasingCommits, err = c.MergeRebasingCommits(commits)
if err != nil {
return nil, err
}
commits = append(commits, rebasingCommits...)
}
passedFirstPushedCommit := false
firstPushedCommit, err := c.getFirstPushedCommit(opts.RefName)
if err != nil {
// must have no upstream branch so we'll consider everything as pushed
passedFirstPushedCommit = true
}
cmd := c.getLogCmd(opts)
err = oscommands.RunLineOutputCmd(cmd, func(line string) (bool, error) {
if canExtractCommit(line) {
commit := c.extractCommitFromLine(line)
if commit.Sha == firstPushedCommit {
passedFirstPushedCommit = true
}
commit.Status = map[bool]string{true: "unpushed", false: "pushed"}[!passedFirstPushedCommit]
commits = append(commits, commit)
}
return false, nil
})
if err != nil {
return nil, err
}
if rebaseMode != "" {
currentCommit := commits[len(rebasingCommits)]
youAreHere := style.FgYellow.Sprintf("<-- %s ---", c.Tr.YouAreHere)
currentCommit.Name = fmt.Sprintf("%s %s", youAreHere, currentCommit.Name)
}
commits, err = c.setCommitMergedStatuses(opts.RefName, commits)
if err != nil {
return nil, err
}
return commits, nil
}
func (c *CommitListBuilder) getHydratedRebasingCommits(rebaseMode string) ([]*models.Commit, error) {
commits, err := c.getRebasingCommits(rebaseMode)
func (self *CommitLoader) getHydratedRebasingCommits(rebaseMode enums.RebaseMode) ([]*models.Commit, error) {
commits, err := self.getRebasingCommits(rebaseMode)
if err != nil {
return nil, err
}
@@ -201,7 +211,7 @@ func (c *CommitListBuilder) getHydratedRebasingCommits(rebaseMode string) ([]*mo
// note that we're not filtering these as we do non-rebasing commits just because
// I suspect that will cause some damage
cmd := c.OSCommand.ExecutableFromString(
cmdObj := self.cmd.New(
fmt.Sprintf(
"git show %s --no-patch --oneline %s --abbrev=%d",
strings.Join(commitShas, " "),
@@ -212,9 +222,9 @@ func (c *CommitListBuilder) getHydratedRebasingCommits(rebaseMode string) ([]*mo
hydratedCommits := make([]*models.Commit, 0, len(commits))
i := 0
err = oscommands.RunLineOutputCmd(cmd, func(line string) (bool, error) {
err = cmdObj.RunAndProcessLines(func(line string) (bool, error) {
if canExtractCommit(line) {
commit := c.extractCommitFromLine(line)
commit := self.extractCommitFromLine(line)
matchingCommit := commits[i]
commit.Action = matchingCommit.Action
commit.Status = matchingCommit.Status
@@ -230,20 +240,20 @@ func (c *CommitListBuilder) getHydratedRebasingCommits(rebaseMode string) ([]*mo
}
// getRebasingCommits obtains the commits that we're in the process of rebasing
func (c *CommitListBuilder) getRebasingCommits(rebaseMode string) ([]*models.Commit, error) {
func (self *CommitLoader) getRebasingCommits(rebaseMode enums.RebaseMode) ([]*models.Commit, error) {
switch rebaseMode {
case REBASE_MODE_MERGING:
return c.getNormalRebasingCommits()
case REBASE_MODE_INTERACTIVE:
return c.getInteractiveRebasingCommits()
case enums.REBASE_MODE_MERGING:
return self.getNormalRebasingCommits()
case enums.REBASE_MODE_INTERACTIVE:
return self.getInteractiveRebasingCommits()
default:
return nil, nil
}
}
func (c *CommitListBuilder) getNormalRebasingCommits() ([]*models.Commit, error) {
func (self *CommitLoader) getNormalRebasingCommits() ([]*models.Commit, error) {
rewrittenCount := 0
bytesContent, err := ioutil.ReadFile(filepath.Join(c.GitCommand.DotGitDir, "rebase-apply/rewritten"))
bytesContent, err := self.readFile(filepath.Join(self.dotGitDir, "rebase-apply/rewritten"))
if err == nil {
content := string(bytesContent)
rewrittenCount = len(strings.Split(content, "\n"))
@@ -251,7 +261,7 @@ func (c *CommitListBuilder) getNormalRebasingCommits() ([]*models.Commit, error)
// we know we're rebasing, so lets get all the files whose names have numbers
commits := []*models.Commit{}
err = filepath.Walk(filepath.Join(c.GitCommand.DotGitDir, "rebase-apply"), func(path string, f os.FileInfo, err error) error {
err = self.walkFiles(filepath.Join(self.dotGitDir, "rebase-apply"), func(path string, f os.FileInfo, err error) error {
if rewrittenCount > 0 {
rewrittenCount--
return nil
@@ -263,12 +273,12 @@ func (c *CommitListBuilder) getNormalRebasingCommits() ([]*models.Commit, error)
if !re.MatchString(f.Name()) {
return nil
}
bytesContent, err := ioutil.ReadFile(path)
bytesContent, err := self.readFile(path)
if err != nil {
return err
}
content := string(bytesContent)
commit, err := c.commitFromPatch(content)
commit, err := self.commitFromPatch(content)
if err != nil {
return err
}
@@ -294,10 +304,10 @@ func (c *CommitListBuilder) getNormalRebasingCommits() ([]*models.Commit, error)
// getInteractiveRebasingCommits takes our git-rebase-todo and our git-rebase-todo.backup files
// and extracts out the sha and names of commits that we still have to go
// in the rebase:
func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*models.Commit, error) {
bytesContent, err := ioutil.ReadFile(filepath.Join(c.GitCommand.DotGitDir, "rebase-merge/git-rebase-todo"))
func (self *CommitLoader) getInteractiveRebasingCommits() ([]*models.Commit, error) {
bytesContent, err := self.readFile(filepath.Join(self.dotGitDir, "rebase-merge/git-rebase-todo"))
if err != nil {
c.Log.Error(fmt.Sprintf("error occurred reading git-rebase-todo: %s", err.Error()))
self.Log.Error(fmt.Sprintf("error occurred reading git-rebase-todo: %s", err.Error()))
// we assume an error means the file doesn't exist so we just return
return nil, nil
}
@@ -328,7 +338,7 @@ func (c *CommitListBuilder) getInteractiveRebasingCommits() ([]*models.Commit, e
// From: Lazygit Tester <test@example.com>
// Date: Wed, 5 Dec 2018 21:03:23 +1100
// Subject: second commit on master
func (c *CommitListBuilder) commitFromPatch(content string) (*models.Commit, error) {
func (self *CommitLoader) commitFromPatch(content string) (*models.Commit, error) {
lines := strings.Split(content, "\n")
sha := strings.Split(lines[0], " ")[1]
name := strings.TrimPrefix(lines[3], "Subject: ")
@@ -339,8 +349,8 @@ func (c *CommitListBuilder) commitFromPatch(content string) (*models.Commit, err
}, nil
}
func (c *CommitListBuilder) setCommitMergedStatuses(refName string, commits []*models.Commit) ([]*models.Commit, error) {
ancestor, err := c.getMergeBase(refName)
func (self *CommitLoader) setCommitMergedStatuses(refName string, commits []*models.Commit) ([]*models.Commit, error) {
ancestor, err := self.getMergeBase(refName)
if err != nil {
return nil, err
}
@@ -362,8 +372,8 @@ func (c *CommitListBuilder) setCommitMergedStatuses(refName string, commits []*m
return commits, nil
}
func (c *CommitListBuilder) getMergeBase(refName string) (string, error) {
currentBranch, _, err := c.GitCommand.CurrentBranchName()
func (self *CommitLoader) getMergeBase(refName string) (string, error) {
currentBranch, _, err := self.getCurrentBranchName()
if err != nil {
return "", err
}
@@ -374,7 +384,7 @@ func (c *CommitListBuilder) getMergeBase(refName string) (string, error) {
}
// swallowing error because it's not a big deal; probably because there are no commits yet
output, _ := c.OSCommand.RunCommandWithOutput("git merge-base %s %s", c.OSCommand.Quote(refName), c.OSCommand.Quote(baseBranch))
output, _ := self.cmd.New(fmt.Sprintf("git merge-base %s %s", self.cmd.Quote(refName), self.cmd.Quote(baseBranch))).RunWithOutput()
return ignoringWarnings(output), nil
}
@@ -390,8 +400,12 @@ func ignoringWarnings(commandOutput string) string {
// getFirstPushedCommit returns the first commit SHA which has been pushed to the ref's upstream.
// all commits above this are deemed unpushed and marked as such.
func (c *CommitListBuilder) getFirstPushedCommit(refName string) (string, error) {
output, err := c.OSCommand.RunCommandWithOutput("git merge-base %s %s@{u}", c.OSCommand.Quote(refName), c.OSCommand.Quote(refName))
func (self *CommitLoader) getFirstPushedCommit(refName string) (string, error) {
output, err := self.cmd.
New(
fmt.Sprintf("git merge-base %s %s@{u}", self.cmd.Quote(refName), self.cmd.Quote(refName)),
).
RunWithOutput()
if err != nil {
return "", err
}
@@ -400,18 +414,18 @@ func (c *CommitListBuilder) getFirstPushedCommit(refName string) (string, error)
}
// getLog gets the git log.
func (c *CommitListBuilder) getLogCmd(opts GetCommitsOptions) *exec.Cmd {
func (self *CommitLoader) getLogCmd(opts GetCommitsOptions) oscommands.ICmdObj {
limitFlag := ""
if opts.Limit {
limitFlag = "-300"
limitFlag = " -300"
}
filterFlag := ""
if opts.FilterPath != "" {
filterFlag = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(opts.FilterPath))
filterFlag = fmt.Sprintf(" --follow -- %s", self.cmd.Quote(opts.FilterPath))
}
config := c.GitCommand.Config.GetUserConfig().Git.Log
config := self.UserConfig.Git.Log
orderFlag := "--" + config.Order
allFlag := ""
@@ -419,10 +433,10 @@ func (c *CommitListBuilder) getLogCmd(opts GetCommitsOptions) *exec.Cmd {
allFlag = " --all"
}
return c.OSCommand.ExecutableFromString(
return self.cmd.New(
fmt.Sprintf(
"git log %s %s %s --oneline %s %s --abbrev=%d %s",
c.OSCommand.Quote(opts.RefName),
"git log %s %s %s --oneline %s%s --abbrev=%d%s",
self.cmd.Quote(opts.RefName),
orderFlag,
allFlag,
prettyFormat,
@@ -443,5 +457,5 @@ var prettyFormat = fmt.Sprintf(
)
func canExtractCommit(line string) bool {
return strings.Split(line, " ")[0] != "gpg:"
return line != "" && strings.Split(line, " ")[0] != "gpg:"
}

View File

@@ -0,0 +1,212 @@
package loaders
import (
"path/filepath"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
func NewDummyCommitLoader() *CommitLoader {
cmn := utils.NewDummyCommon()
return &CommitLoader{
Common: cmn,
cmd: nil,
getCurrentBranchName: func() (string, string, error) { return "master", "master", nil },
getRebaseMode: func() (enums.RebaseMode, error) { return enums.REBASE_MODE_NONE, nil },
dotGitDir: ".git",
readFile: func(filename string) ([]byte, error) {
return []byte(""), nil
},
walkFiles: func(root string, fn filepath.WalkFunc) error {
return nil
},
}
}
const commitsOutput = `0eea75e8c631fba6b58135697835d58ba4c18dbc|1640826609|Jesse Duffield| (HEAD -> better-tests)|b21997d6b4cbdf84b149|better typing for rebase mode
b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164|1640824515|Jesse Duffield| (origin/better-tests)|e94e8fc5b6fab4cb755f|fix logging
e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c|1640823749|Jesse Duffield||d8084cd558925eb7c9c3|refactor
d8084cd558925eb7c9c38afeed5725c21653ab90|1640821426|Jesse Duffield||65f910ebd85283b5cce9|WIP
65f910ebd85283b5cce9bf67d03d3f1a9ea3813a|1640821275|Jesse Duffield||26c07b1ab33860a1a759|WIP
26c07b1ab33860a1a7591a0638f9925ccf497ffa|1640750752|Jesse Duffield||3d4470a6c072208722e5|WIP
3d4470a6c072208722e5ae9a54bcb9634959a1c5|1640748818|Jesse Duffield||053a66a7be3da43aacdc|WIP
053a66a7be3da43aacdc7aa78e1fe757b82c4dd2|1640739815|Jesse Duffield||985fe482e806b172aea4|refactoring the config struct`
func TestGetCommits(t *testing.T) {
type scenario struct {
testName string
runner oscommands.ICmdObjRunner
expectedCommits []*models.Commit
expectedError error
rebaseMode enums.RebaseMode
currentBranchName string
opts GetCommitsOptions
}
scenarios := []scenario{
{
testName: "should return no commits if there are none",
rebaseMode: enums.REBASE_MODE_NONE,
currentBranchName: "master",
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
runner: oscommands.NewFakeRunner(t).
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
Expect(`git log "HEAD" --topo-order --oneline --pretty=format:"%H|%at|%aN|%d|%p|%s" --abbrev=20`, "", nil),
expectedCommits: []*models.Commit{},
expectedError: nil,
},
{
testName: "should return commits if they are present",
rebaseMode: enums.REBASE_MODE_NONE,
currentBranchName: "master",
opts: GetCommitsOptions{RefName: "HEAD", IncludeRebaseCommits: false},
runner: oscommands.NewFakeRunner(t).
// here it's seeing which commits are yet to be pushed
Expect(`git merge-base "HEAD" "HEAD"@{u}`, "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164", nil).
// here it's actually getting all the commits in a formatted form, one per line
Expect(`git log "HEAD" --topo-order --oneline --pretty=format:"%H|%at|%aN|%d|%p|%s" --abbrev=20`, commitsOutput, nil).
// here it's seeing where our branch diverged from the master branch so that we can mark that commit and parent commits as 'merged'
Expect(`git merge-base "HEAD" "master"`, "26c07b1ab33860a1a7591a0638f9925ccf497ffa", nil),
expectedCommits: []*models.Commit{
{
Sha: "0eea75e8c631fba6b58135697835d58ba4c18dbc",
Name: "better typing for rebase mode",
Status: "unpushed",
Action: "",
Tags: []string{},
ExtraInfo: "(HEAD -> better-tests)",
Author: "Jesse Duffield",
UnixTimestamp: 1640826609,
Parents: []string{
"b21997d6b4cbdf84b149",
},
},
{
Sha: "b21997d6b4cbdf84b149d8e6a2c4d06a8e9ec164",
Name: "fix logging",
Status: "pushed",
Action: "",
Tags: []string{},
ExtraInfo: "(origin/better-tests)",
Author: "Jesse Duffield",
UnixTimestamp: 1640824515,
Parents: []string{
"e94e8fc5b6fab4cb755f",
},
},
{
Sha: "e94e8fc5b6fab4cb755f29f1bdb3ee5e001df35c",
Name: "refactor",
Status: "pushed",
Action: "",
Tags: []string{},
ExtraInfo: "",
Author: "Jesse Duffield",
UnixTimestamp: 1640823749,
Parents: []string{
"d8084cd558925eb7c9c3",
},
},
{
Sha: "d8084cd558925eb7c9c38afeed5725c21653ab90",
Name: "WIP",
Status: "pushed",
Action: "",
Tags: []string{},
ExtraInfo: "",
Author: "Jesse Duffield",
UnixTimestamp: 1640821426,
Parents: []string{
"65f910ebd85283b5cce9",
},
},
{
Sha: "65f910ebd85283b5cce9bf67d03d3f1a9ea3813a",
Name: "WIP",
Status: "pushed",
Action: "",
Tags: []string{},
ExtraInfo: "",
Author: "Jesse Duffield",
UnixTimestamp: 1640821275,
Parents: []string{
"26c07b1ab33860a1a759",
},
},
{
Sha: "26c07b1ab33860a1a7591a0638f9925ccf497ffa",
Name: "WIP",
Status: "merged",
Action: "",
Tags: []string{},
ExtraInfo: "",
Author: "Jesse Duffield",
UnixTimestamp: 1640750752,
Parents: []string{
"3d4470a6c072208722e5",
},
},
{
Sha: "3d4470a6c072208722e5ae9a54bcb9634959a1c5",
Name: "WIP",
Status: "merged",
Action: "",
Tags: []string{},
ExtraInfo: "",
Author: "Jesse Duffield",
UnixTimestamp: 1640748818,
Parents: []string{
"053a66a7be3da43aacdc",
},
},
{
Sha: "053a66a7be3da43aacdc7aa78e1fe757b82c4dd2",
Name: "refactoring the config struct",
Status: "merged",
Action: "",
Tags: []string{},
ExtraInfo: "",
Author: "Jesse Duffield",
UnixTimestamp: 1640739815,
Parents: []string{
"985fe482e806b172aea4",
},
},
},
expectedError: nil,
},
}
for _, scenario := range scenarios {
t.Run(scenario.testName, func(t *testing.T) {
builder := &CommitLoader{
Common: utils.NewDummyCommon(),
cmd: oscommands.NewDummyCmdObjBuilder(scenario.runner),
getCurrentBranchName: func() (string, string, error) {
return scenario.currentBranchName, scenario.currentBranchName, nil
},
getRebaseMode: func() (enums.RebaseMode, error) { return scenario.rebaseMode, nil },
dotGitDir: ".git",
readFile: func(filename string) ([]byte, error) {
return []byte(""), nil
},
walkFiles: func(root string, fn filepath.WalkFunc) error {
return nil
},
}
commits, err := builder.GetCommits(scenario.opts)
assert.Equal(t, scenario.expectedCommits, commits)
assert.Equal(t, scenario.expectedError, err)
})
}
}

View File

@@ -1,36 +1,54 @@
package commands
package loaders
import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// GetStatusFiles git status files
type FileLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
gitConfig git_config.IGitConfig
getFileType func(string) string
}
func NewFileLoader(cmn *common.Common, cmd oscommands.ICmdObjBuilder, gitConfig git_config.IGitConfig) *FileLoader {
return &FileLoader{
Common: cmn,
cmd: cmd,
gitConfig: gitConfig,
getFileType: oscommands.FileType,
}
}
type GetStatusFileOptions struct {
NoRenames bool
}
func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
func (self *FileLoader) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
// check if config wants us ignoring untracked files
untrackedFilesSetting := c.GitConfig.Get("status.showUntrackedFiles")
untrackedFilesSetting := self.gitConfig.Get("status.showUntrackedFiles")
if untrackedFilesSetting == "" {
untrackedFilesSetting = "all"
}
untrackedFilesArg := fmt.Sprintf("--untracked-files=%s", untrackedFilesSetting)
statuses, err := c.GitStatus(GitStatusOptions{NoRenames: opts.NoRenames, UntrackedFilesArg: untrackedFilesArg})
statuses, err := self.GitStatus(GitStatusOptions{NoRenames: opts.NoRenames, UntrackedFilesArg: untrackedFilesArg})
if err != nil {
c.Log.Error(err)
self.Log.Error(err)
}
files := []*models.File{}
for _, status := range statuses {
if strings.HasPrefix(status.StatusString, "warning") {
c.Log.Warningf("warning when calling git status: %s", status.StatusString)
self.Log.Warningf("warning when calling git status: %s", status.StatusString)
continue
}
change := status.Change
@@ -52,7 +70,7 @@ func (c *GitCommand) GetStatusFiles(opts GetStatusFileOptions) []*models.File {
Added: unstagedChange == "A" || untracked,
HasMergeConflicts: hasMergeConflicts,
HasInlineMergeConflicts: hasInlineMergeConflicts,
Type: c.OSCommand.FileType(status.Name),
Type: self.getFileType(status.Name),
ShortStatus: change,
}
files = append(files, file)
@@ -74,13 +92,13 @@ type FileStatus struct {
PreviousName string
}
func (c *GitCommand) GitStatus(opts GitStatusOptions) ([]FileStatus, error) {
func (c *FileLoader) GitStatus(opts GitStatusOptions) ([]FileStatus, error) {
noRenamesFlag := ""
if opts.NoRenames {
noRenamesFlag = "--no-renames"
noRenamesFlag = " --no-renames"
}
statusLines, err := c.RunCommandWithOutput("git status %s --porcelain -z %s", opts.UntrackedFilesArg, noRenamesFlag)
statusLines, err := c.cmd.New(fmt.Sprintf("git status %s --porcelain -z%s", opts.UntrackedFilesArg, noRenamesFlag)).RunWithOutput()
if err != nil {
return []FileStatus{}, err
}

View File

@@ -0,0 +1,203 @@
package loaders
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
func TestGitCommandGetStatusFiles(t *testing.T) {
type scenario struct {
testName string
runner oscommands.ICmdObjRunner
expectedFiles []*models.File
}
scenarios := []scenario{
{
"No files found",
oscommands.NewFakeRunner(t).
Expect(`git status --untracked-files=yes --porcelain -z`, "", nil),
[]*models.File{},
},
{
"Several files found",
oscommands.NewFakeRunner(t).
Expect(
`git status --untracked-files=yes --porcelain -z`,
"MM file1.txt\x00A file3.txt\x00AM file2.txt\x00?? file4.txt\x00UU file5.txt",
nil,
),
[]*models.File{
{
Name: "file1.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "MM file1.txt",
Type: "file",
ShortStatus: "MM",
},
{
Name: "file3.txt",
HasStagedChanges: true,
HasUnstagedChanges: false,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "A file3.txt",
Type: "file",
ShortStatus: "A ",
},
{
Name: "file2.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "AM file2.txt",
Type: "file",
ShortStatus: "AM",
},
{
Name: "file4.txt",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "?? file4.txt",
Type: "file",
ShortStatus: "??",
},
{
Name: "file5.txt",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: true,
HasInlineMergeConflicts: true,
DisplayString: "UU file5.txt",
Type: "file",
ShortStatus: "UU",
},
},
},
{
"File with new line char",
oscommands.NewFakeRunner(t).
Expect(`git status --untracked-files=yes --porcelain -z`, "MM a\nb.txt", nil),
[]*models.File{
{
Name: "a\nb.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "MM a\nb.txt",
Type: "file",
ShortStatus: "MM",
},
},
},
{
"Renamed files",
oscommands.NewFakeRunner(t).
Expect(
`git status --untracked-files=yes --porcelain -z`,
"R after1.txt\x00before1.txt\x00RM after2.txt\x00before2.txt",
nil,
),
[]*models.File{
{
Name: "after1.txt",
PreviousName: "before1.txt",
HasStagedChanges: true,
HasUnstagedChanges: false,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "R before1.txt -> after1.txt",
Type: "file",
ShortStatus: "R ",
},
{
Name: "after2.txt",
PreviousName: "before2.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "RM before2.txt -> after2.txt",
Type: "file",
ShortStatus: "RM",
},
},
},
{
"File with arrow in name",
oscommands.NewFakeRunner(t).
Expect(
`git status --untracked-files=yes --porcelain -z`,
`?? a -> b.txt`,
nil,
),
[]*models.File{
{
Name: "a -> b.txt",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "?? a -> b.txt",
Type: "file",
ShortStatus: "??",
},
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
cmd := oscommands.NewDummyCmdObjBuilder(s.runner)
gitConfig := git_config.NewFakeGitConfig(map[string]string{"status.showUntrackedFiles": "yes"})
loader := &FileLoader{
Common: utils.NewDummyCommon(),
cmd: cmd,
gitConfig: gitConfig,
getFileType: func(string) string { return "file" },
}
assert.EqualValues(t, s.expectedFiles, loader.GetStatusFiles(GetStatusFileOptions{}))
})
}
}

View File

@@ -1,4 +1,4 @@
package commands
package loaders
import (
"fmt"
@@ -7,21 +7,34 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
)
type ReflogCommitLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
}
func NewReflogCommitLoader(common *common.Common, cmd oscommands.ICmdObjBuilder) *ReflogCommitLoader {
return &ReflogCommitLoader{
Common: common,
cmd: cmd,
}
}
// GetReflogCommits only returns the new reflog commits since the given lastReflogCommit
// if none is passed (i.e. it's value is nil) then we get all the reflog commits
func (c *GitCommand) GetReflogCommits(lastReflogCommit *models.Commit, filterPath string) ([]*models.Commit, bool, error) {
func (self *ReflogCommitLoader) GetReflogCommits(lastReflogCommit *models.Commit, filterPath string) ([]*models.Commit, bool, error) {
commits := make([]*models.Commit, 0)
filterPathArg := ""
if filterPath != "" {
filterPathArg = fmt.Sprintf(" --follow -- %s", c.OSCommand.Quote(filterPath))
filterPathArg = fmt.Sprintf(" --follow -- %s", self.cmd.Quote(filterPath))
}
cmd := c.OSCommand.ExecutableFromString(fmt.Sprintf(`git log -g --abbrev=20 --format="%%h %%ct %%gs" %s`, filterPathArg))
cmdObj := self.cmd.New(fmt.Sprintf(`git log -g --abbrev=20 --format="%%h %%ct %%gs" %s`, filterPathArg))
onlyObtainedNewReflogCommits := false
err := oscommands.RunLineOutputCmd(cmd, func(line string) (bool, error) {
err := cmdObj.RunAndProcessLines(func(line string) (bool, error) {
fields := strings.SplitN(line, " ", 3)
if len(fields) <= 2 {
return false, nil

View File

@@ -1,4 +1,4 @@
package commands
package loaders
import (
"fmt"
@@ -6,18 +6,37 @@ import (
"sort"
"strings"
gogit "github.com/jesseduffield/go-git/v5"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
)
func (c *GitCommand) GetRemotes() ([]*models.Remote, error) {
// get remote branches
unescaped := "git branch -r"
remoteBranchesStr, err := c.OSCommand.RunCommandWithOutput(unescaped)
type RemoteLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
getGoGitRemotes func() ([]*gogit.Remote, error)
}
func NewRemoteLoader(
common *common.Common,
cmd oscommands.ICmdObjBuilder,
getGoGitRemotes func() ([]*gogit.Remote, error),
) *RemoteLoader {
return &RemoteLoader{
Common: common,
cmd: cmd,
getGoGitRemotes: getGoGitRemotes,
}
}
func (self *RemoteLoader) GetRemotes() ([]*models.Remote, error) {
remoteBranchesStr, err := self.cmd.New("git branch -r").RunWithOutput()
if err != nil {
return nil, err
}
goGitRemotes, err := c.Repo.Remotes()
goGitRemotes, err := self.getGoGitRemotes()
if err != nil {
return nil, err
}

View File

@@ -0,0 +1,80 @@
package loaders
import (
"regexp"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
)
type StashLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
}
func NewStashLoader(
common *common.Common,
cmd oscommands.ICmdObjBuilder,
) *StashLoader {
return &StashLoader{
Common: common,
cmd: cmd,
}
}
func (self *StashLoader) GetStashEntries(filterPath string) []*models.StashEntry {
if filterPath == "" {
return self.getUnfilteredStashEntries()
}
rawString, err := self.cmd.New("git stash list --name-only").RunWithOutput()
if err != nil {
return self.getUnfilteredStashEntries()
}
stashEntries := []*models.StashEntry{}
var currentStashEntry *models.StashEntry
lines := utils.SplitLines(rawString)
isAStash := func(line string) bool { return strings.HasPrefix(line, "stash@{") }
re := regexp.MustCompile(`stash@\{(\d+)\}`)
outer:
for i := 0; i < len(lines); i++ {
if !isAStash(lines[i]) {
continue
}
match := re.FindStringSubmatch(lines[i])
idx, err := strconv.Atoi(match[1])
if err != nil {
return self.getUnfilteredStashEntries()
}
currentStashEntry = self.stashEntryFromLine(lines[i], idx)
for i+1 < len(lines) && !isAStash(lines[i+1]) {
i++
if lines[i] == filterPath {
stashEntries = append(stashEntries, currentStashEntry)
continue outer
}
}
}
return stashEntries
}
func (self *StashLoader) getUnfilteredStashEntries() []*models.StashEntry {
rawString, _ := self.cmd.New("git stash list --pretty='%gs'").RunWithOutput()
stashEntries := []*models.StashEntry{}
for i, line := range utils.SplitLines(rawString) {
stashEntries = append(stashEntries, self.stashEntryFromLine(line, i))
}
return stashEntries
}
func (c *StashLoader) stashEntryFromLine(line string, index int) *models.StashEntry {
return &models.StashEntry{
Name: line,
Index: index,
}
}

View File

@@ -0,0 +1,59 @@
package loaders
import (
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
func TestGitCommandGetStashEntries(t *testing.T) {
type scenario struct {
testName string
filterPath string
runner oscommands.ICmdObjRunner
expectedStashEntries []*models.StashEntry
}
scenarios := []scenario{
{
"No stash entries found",
"",
oscommands.NewFakeRunner(t).
Expect(`git stash list --pretty='%gs'`, "", nil),
[]*models.StashEntry{},
},
{
"Several stash entries found",
"",
oscommands.NewFakeRunner(t).
Expect(
`git stash list --pretty='%gs'`,
"WIP on add-pkg-commands-test: 55c6af2 increase parallel build\nWIP on master: bb86a3f update github template",
nil,
),
[]*models.StashEntry{
{
Index: 0,
Name: "WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
},
{
Index: 1,
Name: "WIP on master: bb86a3f update github template",
},
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
cmd := oscommands.NewDummyCmdObjBuilder(s.runner)
loader := NewStashLoader(utils.NewDummyCommon(), cmd)
assert.EqualValues(t, s.expectedStashEntries, loader.GetStashEntries(""))
})
}
}

View File

@@ -1,16 +1,33 @@
package commands
package loaders
import (
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (c *GitCommand) GetTags() ([]*models.Tag, error) {
type TagLoader struct {
*common.Common
cmd oscommands.ICmdObjBuilder
}
func NewTagLoader(
common *common.Common,
cmd oscommands.ICmdObjBuilder,
) *TagLoader {
return &TagLoader{
Common: common,
cmd: cmd,
}
}
func (self *TagLoader) GetTags() ([]*models.Tag, error) {
// get remote branches, sorted by creation date (descending)
// see: https://git-scm.com/docs/git-tag#Documentation/git-tag.txt---sortltkeygt
remoteBranchesStr, err := c.OSCommand.RunCommandWithOutput(`git tag --list --sort=-creatordate`)
remoteBranchesStr, err := self.cmd.New(`git tag --list --sort=-creatordate`).RunWithOutput()
if err != nil {
return nil, err
}

View File

@@ -1,42 +0,0 @@
package commands
import (
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
)
// GetFilesInDiff get the specified commit files
func (c *GitCommand) GetFilesInDiff(from string, to string, reverse bool) ([]*models.CommitFile, error) {
reverseFlag := ""
if reverse {
reverseFlag = " -R "
}
filenames, err := c.RunCommandWithOutput("git diff --submodule --no-ext-diff --name-status -z --no-renames %s %s %s", reverseFlag, from, to)
if err != nil {
return nil, err
}
return c.getCommitFilesFromFilenames(filenames), nil
}
// filenames string is something like "file1\nfile2\nfile3"
func (c *GitCommand) getCommitFilesFromFilenames(filenames string) []*models.CommitFile {
commitFiles := make([]*models.CommitFile, 0)
lines := strings.Split(strings.TrimRight(filenames, "\x00"), "\x00")
n := len(lines)
for i := 0; i < n-1; i += 2 {
// typical result looks like 'A my_file' meaning my_file was added
changeStatus := lines[i]
name := lines[i+1]
commitFiles = append(commitFiles, &models.CommitFile{
Name: name,
ChangeStatus: changeStatus,
})
}
return commitFiles
}

View File

@@ -1,114 +0,0 @@
package commands
import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
// NewDummyCommitListBuilder creates a new dummy CommitListBuilder for testing
func NewDummyCommitListBuilder() *CommitListBuilder {
osCommand := oscommands.NewDummyOSCommand()
return &CommitListBuilder{
Log: utils.NewDummyLog(),
GitCommand: NewDummyGitCommandWithOSCommand(osCommand),
OSCommand: osCommand,
Tr: i18n.NewTranslationSet(utils.NewDummyLog(), "auto"),
}
}
// TestCommitListBuilderGetMergeBase is a function.
func TestCommitListBuilderGetMergeBase(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func(string, error)
}
scenarios := []scenario{
{
"swallows an error if the call to merge-base returns an error",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return secureexec.Command("echo", "master")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return secureexec.Command("test")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.EqualValues(t, "", output)
},
},
{
"returns the commit when master",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return secureexec.Command("echo", "master")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "master"}, args)
return secureexec.Command("echo", "blah")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.Equal(t, "blah", output)
},
},
{
"checks against develop when a feature branch",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
switch args[0] {
case "symbolic-ref":
assert.EqualValues(t, []string{"symbolic-ref", "--short", "HEAD"}, args)
return secureexec.Command("echo", "feature/test")
case "merge-base":
assert.EqualValues(t, []string{"merge-base", "HEAD", "develop"}, args)
return secureexec.Command("echo", "blah")
}
return nil
},
func(output string, err error) {
assert.NoError(t, err)
assert.Equal(t, "blah", output)
},
},
{
"bubbles up error if there is one",
func(cmd string, args ...string) *exec.Cmd {
return secureexec.Command("test")
},
func(output string, err error) {
assert.Error(t, err)
assert.Equal(t, "", output)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
c := NewDummyCommitListBuilder()
c.OSCommand.SetCommand(s.command)
s.test(c.getMergeBase("HEAD"))
})
}
}

View File

@@ -1,227 +0,0 @@
package commands
import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/stretchr/testify/assert"
)
// TestGitCommandGetStatusFiles is a function.
func TestGitCommandGetStatusFiles(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func([]*models.File)
}
scenarios := []scenario{
{
"No files found",
func(cmd string, args ...string) *exec.Cmd {
return secureexec.Command("echo")
},
func(files []*models.File) {
assert.Len(t, files, 0)
},
},
{
"Several files found",
func(cmd string, args ...string) *exec.Cmd {
return secureexec.Command(
"printf",
`MM file1.txt\0A file3.txt\0AM file2.txt\0?? file4.txt\0UU file5.txt`,
)
},
func(files []*models.File) {
assert.Len(t, files, 5)
expected := []*models.File{
{
Name: "file1.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "MM file1.txt",
Type: "other",
ShortStatus: "MM",
},
{
Name: "file3.txt",
HasStagedChanges: true,
HasUnstagedChanges: false,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "A file3.txt",
Type: "other",
ShortStatus: "A ",
},
{
Name: "file2.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "AM file2.txt",
Type: "other",
ShortStatus: "AM",
},
{
Name: "file4.txt",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "?? file4.txt",
Type: "other",
ShortStatus: "??",
},
{
Name: "file5.txt",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: true,
HasInlineMergeConflicts: true,
DisplayString: "UU file5.txt",
Type: "other",
ShortStatus: "UU",
},
}
assert.EqualValues(t, expected, files)
},
},
{
"File with new line char",
func(cmd string, args ...string) *exec.Cmd {
return secureexec.Command(
"printf",
`MM a\nb.txt`,
)
},
func(files []*models.File) {
assert.Len(t, files, 1)
expected := []*models.File{
{
Name: "a\nb.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "MM a\nb.txt",
Type: "other",
ShortStatus: "MM",
},
}
assert.EqualValues(t, expected, files)
},
},
{
"Renamed files",
func(cmd string, args ...string) *exec.Cmd {
return secureexec.Command(
"printf",
`R after1.txt\0before1.txt\0RM after2.txt\0before2.txt`,
)
},
func(files []*models.File) {
assert.Len(t, files, 2)
expected := []*models.File{
{
Name: "after1.txt",
PreviousName: "before1.txt",
HasStagedChanges: true,
HasUnstagedChanges: false,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "R before1.txt -> after1.txt",
Type: "other",
ShortStatus: "R ",
},
{
Name: "after2.txt",
PreviousName: "before2.txt",
HasStagedChanges: true,
HasUnstagedChanges: true,
Tracked: true,
Added: false,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "RM before2.txt -> after2.txt",
Type: "other",
ShortStatus: "RM",
},
}
assert.EqualValues(t, expected, files)
},
},
{
"File with arrow in name",
func(cmd string, args ...string) *exec.Cmd {
return secureexec.Command(
"printf",
`?? a -> b.txt`,
)
},
func(files []*models.File) {
assert.Len(t, files, 1)
expected := []*models.File{
{
Name: "a -> b.txt",
HasStagedChanges: false,
HasUnstagedChanges: true,
Tracked: false,
Added: true,
Deleted: false,
HasMergeConflicts: false,
HasInlineMergeConflicts: false,
DisplayString: "?? a -> b.txt",
Type: "other",
ShortStatus: "??",
},
}
assert.EqualValues(t, expected, files)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
s.test(gitCmd.GetStatusFiles(GetStatusFileOptions{}))
})
}
}

View File

@@ -1,65 +0,0 @@
package commands
import (
"regexp"
"strconv"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
)
func (c *GitCommand) getUnfilteredStashEntries() []*models.StashEntry {
unescaped := "git stash list --pretty='%gs'"
rawString, _ := c.OSCommand.RunCommandWithOutput(unescaped)
stashEntries := []*models.StashEntry{}
for i, line := range utils.SplitLines(rawString) {
stashEntries = append(stashEntries, stashEntryFromLine(line, i))
}
return stashEntries
}
// GetStashEntries stash entries
func (c *GitCommand) GetStashEntries(filterPath string) []*models.StashEntry {
if filterPath == "" {
return c.getUnfilteredStashEntries()
}
rawString, err := c.RunCommandWithOutput("git stash list --name-only")
if err != nil {
return c.getUnfilteredStashEntries()
}
stashEntries := []*models.StashEntry{}
var currentStashEntry *models.StashEntry
lines := utils.SplitLines(rawString)
isAStash := func(line string) bool { return strings.HasPrefix(line, "stash@{") }
re := regexp.MustCompile(`stash@\{(\d+)\}`)
outer:
for i := 0; i < len(lines); i++ {
if !isAStash(lines[i]) {
continue
}
match := re.FindStringSubmatch(lines[i])
idx, err := strconv.Atoi(match[1])
if err != nil {
return c.getUnfilteredStashEntries()
}
currentStashEntry = stashEntryFromLine(lines[i], idx)
for i+1 < len(lines) && !isAStash(lines[i+1]) {
i++
if lines[i] == filterPath {
stashEntries = append(stashEntries, currentStashEntry)
continue outer
}
}
}
return stashEntries
}
func stashEntryFromLine(line string, index int) *models.StashEntry {
return &models.StashEntry{
Name: line,
Index: index,
}
}

View File

@@ -1,61 +0,0 @@
package commands
import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/stretchr/testify/assert"
)
// TestGitCommandGetStashEntries is a function.
func TestGitCommandGetStashEntries(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
test func([]*models.StashEntry)
}
scenarios := []scenario{
{
"No stash entries found",
func(string, ...string) *exec.Cmd {
return secureexec.Command("echo")
},
func(entries []*models.StashEntry) {
assert.Len(t, entries, 0)
},
},
{
"Several stash entries found",
func(string, ...string) *exec.Cmd {
return secureexec.Command("echo", "WIP on add-pkg-commands-test: 55c6af2 increase parallel build\nWIP on master: bb86a3f update github template")
},
func(entries []*models.StashEntry) {
expected := []*models.StashEntry{
{
Index: 0,
Name: "WIP on add-pkg-commands-test: 55c6af2 increase parallel build",
},
{
Index: 1,
Name: "WIP on master: bb86a3f update github template",
},
}
assert.Len(t, entries, 2)
assert.EqualValues(t, expected, entries)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
s.test(gitCmd.GetStashEntries(""))
})
}
}

View File

@@ -5,17 +5,33 @@ import (
)
// A command object is a general way to represent a command to be run on the
// command line. If you want to log the command you'll use .ToString() and
// if you want to run it you'll use .GetCmd()
// command line.
type ICmdObj interface {
GetCmd() *exec.Cmd
// outputs string representation of command. Note that if the command was built
// using NewFromArgs, the output won't be quite the same as what you would type
// into a terminal e.g. 'sh -c git commit' as opposed to 'sh -c "git commit"'
ToString() string
AddEnvVars(...string) ICmdObj
GetEnvVars() []string
// runs the command and returns an error if any
Run() error
// runs the command and returns the output as a string, and an error if any
RunWithOutput() (string, error)
// runs the command and runs a callback function on each line of the output. If the callback returns true for the boolean value, we kill the process and return.
RunAndProcessLines(onLine func(line string) (bool, error)) error
// logs command
Log() ICmdObj
}
type CmdObj struct {
cmdStr string
cmd *exec.Cmd
runner ICmdObjRunner
logCommand func(ICmdObj)
}
func (self *CmdObj) GetCmd() *exec.Cmd {
@@ -31,3 +47,25 @@ func (self *CmdObj) AddEnvVars(vars ...string) ICmdObj {
return self
}
func (self *CmdObj) GetEnvVars() []string {
return self.cmd.Env
}
func (self *CmdObj) Log() ICmdObj {
self.logCommand(self)
return self
}
func (self *CmdObj) Run() error {
return self.runner.Run(self)
}
func (self *CmdObj) RunWithOutput() (string, error) {
return self.runner.RunWithOutput(self)
}
func (self *CmdObj) RunAndProcessLines(onLine func(line string) (bool, error)) error {
return self.runner.RunAndProcessLines(self, onLine)
}

View File

@@ -0,0 +1,68 @@
package oscommands
import (
"os"
"strings"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/mgutz/str"
)
type ICmdObjBuilder interface {
// New returns a new command object based on the string provided
New(cmdStr string) ICmdObj
// NewShell takes a string like `git commit` and returns an executable shell command for it e.g. `sh -c 'git commit'`
NewShell(commandStr string) ICmdObj
// NewFromArgs takes a slice of strings like []string{"git", "commit"} and returns a new command object. This can be useful when you don't want to worry about whitespace and quoting and stuff.
NewFromArgs(args []string) ICmdObj
// Quote wraps a string in quotes with any necessary escaping applied. The reason for bundling this up with the other methods in this interface is that we basically always need to make use of this when creating new command objects.
Quote(str string) string
}
type CmdObjBuilder struct {
runner ICmdObjRunner
logCmdObj func(ICmdObj)
platform *Platform
}
// poor man's version of explicitly saying that struct X implements interface Y
var _ ICmdObjBuilder = &CmdObjBuilder{}
func (self *CmdObjBuilder) New(cmdStr string) ICmdObj {
args := str.ToArgv(cmdStr)
cmd := secureexec.Command(args[0], args[1:]...)
cmd.Env = os.Environ()
return &CmdObj{
cmdStr: cmdStr,
cmd: cmd,
runner: self.runner,
logCommand: self.logCmdObj,
}
}
func (self *CmdObjBuilder) NewFromArgs(args []string) ICmdObj {
cmd := secureexec.Command(args[0], args[1:]...)
cmd.Env = os.Environ()
return &CmdObj{
cmdStr: strings.Join(args, " "),
cmd: cmd,
runner: self.runner,
logCommand: self.logCmdObj,
}
}
func (self *CmdObjBuilder) NewShell(commandStr string) ICmdObj {
return self.NewFromArgs([]string{self.platform.Shell, self.platform.ShellArg, commandStr})
}
func (self *CmdObjBuilder) CloneWithNewRunner(decorate func(ICmdObjRunner) ICmdObjRunner) *CmdObjBuilder {
decoratedRunner := decorate(self.runner)
return &CmdObjBuilder{
runner: decoratedRunner,
logCmdObj: self.logCmdObj,
platform: self.platform,
}
}

View File

@@ -0,0 +1,79 @@
package oscommands
import (
"bufio"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
type ICmdObjRunner interface {
Run(cmdObj ICmdObj) error
RunWithOutput(cmdObj ICmdObj) (string, error)
RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error
}
type cmdObjRunner struct {
log *logrus.Entry
logCmdObj func(ICmdObj)
}
var _ ICmdObjRunner = &cmdObjRunner{}
func (self *cmdObjRunner) Run(cmdObj ICmdObj) error {
_, err := self.RunWithOutput(cmdObj)
return err
}
func (self *cmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
self.logCmdObj(cmdObj)
output, err := sanitisedCommandOutput(cmdObj.GetCmd().CombinedOutput())
if err != nil {
self.log.WithField("command", cmdObj.ToString()).Error(output)
}
return output, err
}
func (self *cmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error {
cmd := cmdObj.GetCmd()
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
return err
}
scanner := bufio.NewScanner(stdoutPipe)
scanner.Split(bufio.ScanLines)
if err := cmd.Start(); err != nil {
return err
}
for scanner.Scan() {
line := scanner.Text()
stop, err := onLine(line)
if err != nil {
return err
}
if stop {
_ = cmd.Process.Kill()
break
}
}
_ = cmd.Wait()
return nil
}
func sanitisedCommandOutput(output []byte, err error) (string, error) {
outputString := string(output)
if err != nil {
// errors like 'exit status 1' are not very useful so we'll create an error
// from the combined output
if outputString == "" {
return "", utils.WrapError(err)
}
return outputString, errors.New(outputString)
}
return outputString, nil
}

View File

@@ -1,11 +1,31 @@
package oscommands
import (
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/utils"
)
// NewDummyOSCommand creates a new dummy OSCommand for testing
func NewDummyOSCommand() *OSCommand {
return NewOSCommand(utils.NewDummyLog(), config.NewDummyAppConfig())
return NewOSCommand(utils.NewDummyCommon())
}
func NewDummyCmdObjBuilder(runner ICmdObjRunner) *CmdObjBuilder {
return &CmdObjBuilder{
runner: runner,
logCmdObj: func(ICmdObj) {},
platform: &Platform{
OS: "darwin",
Shell: "bash",
ShellArg: "-c",
OpenCommand: "open {{filename}}",
OpenLinkCommand: "open {{link}}",
},
}
}
func NewDummyOSCommandWithRunner(runner *FakeCmdObjRunner) *OSCommand {
osCommand := NewOSCommand(utils.NewDummyCommon())
osCommand.Cmd = NewDummyCmdObjBuilder(runner)
return osCommand
}

View File

@@ -0,0 +1,102 @@
package oscommands
import (
"bufio"
"fmt"
"strings"
"testing"
"github.com/go-errors/errors"
"github.com/stretchr/testify/assert"
)
// for use in testing
type FakeCmdObjRunner struct {
t *testing.T
expectedCmds []func(ICmdObj) (string, error)
expectedCmdIndex int
}
var _ ICmdObjRunner = &FakeCmdObjRunner{}
func NewFakeRunner(t *testing.T) *FakeCmdObjRunner {
return &FakeCmdObjRunner{t: t}
}
func (self *FakeCmdObjRunner) Run(cmdObj ICmdObj) error {
_, err := self.RunWithOutput(cmdObj)
return err
}
func (self *FakeCmdObjRunner) RunWithOutput(cmdObj ICmdObj) (string, error) {
if self.expectedCmdIndex > len(self.expectedCmds)-1 {
self.t.Errorf("ran too many commands. Unexpected command: `%s`", cmdObj.ToString())
return "", errors.New("ran too many commands")
}
expectedCmd := self.expectedCmds[self.expectedCmdIndex]
output, err := expectedCmd(cmdObj)
self.expectedCmdIndex++
return output, err
}
func (self *FakeCmdObjRunner) RunAndProcessLines(cmdObj ICmdObj, onLine func(line string) (bool, error)) error {
output, err := self.RunWithOutput(cmdObj)
if err != nil {
return err
}
scanner := bufio.NewScanner(strings.NewReader(output))
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
line := scanner.Text()
stop, err := onLine(line)
if err != nil {
return err
}
if stop {
break
}
}
return nil
}
func (self *FakeCmdObjRunner) ExpectFunc(fn func(cmdObj ICmdObj) (string, error)) *FakeCmdObjRunner {
self.expectedCmds = append(self.expectedCmds, fn)
return self
}
func (self *FakeCmdObjRunner) Expect(expectedCmdStr string, output string, err error) *FakeCmdObjRunner {
self.ExpectFunc(func(cmdObj ICmdObj) (string, error) {
cmdStr := cmdObj.ToString()
assert.Equal(self.t, expectedCmdStr, cmdStr, fmt.Sprintf("expected command %d to be %s, but was %s", self.expectedCmdIndex+1, expectedCmdStr, cmdStr))
return output, err
})
return self
}
func (self *FakeCmdObjRunner) ExpectArgs(expectedArgs []string, output string, err error) *FakeCmdObjRunner {
self.ExpectFunc(func(cmdObj ICmdObj) (string, error) {
args := cmdObj.GetCmd().Args
assert.EqualValues(self.t, expectedArgs, args, fmt.Sprintf("command %d did not match expectation", self.expectedCmdIndex+1))
return output, err
})
return self
}
func (self *FakeCmdObjRunner) CheckForMissingCalls() {
if self.expectedCmdIndex < len(self.expectedCmds) {
self.t.Errorf("expected command %d to be called, but was not", self.expectedCmdIndex+1)
}
return
}

View File

@@ -1,7 +1,6 @@
package oscommands
import (
"bufio"
"fmt"
"io/ioutil"
"os"
@@ -13,30 +12,15 @@ import (
"github.com/go-errors/errors"
"github.com/atotto/clipboard"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/mgutz/str"
"github.com/sirupsen/logrus"
)
// Platform stores the os state
type Platform struct {
OS string
Shell string
ShellArg string
OpenCommand string
OpenLinkCommand string
}
// OSCommand holds all the os commands
type OSCommand struct {
Log *logrus.Entry
Platform *Platform
Config config.AppConfigurer
Command func(string, ...string) *exec.Cmd
BeforeExecuteCmd func(*exec.Cmd)
Getenv func(string) string
*common.Common
Platform *Platform
Getenv func(string) string
// callback to run before running a command, i.e. for the purposes of logging
onRunCommand func(CmdLogEntry)
@@ -45,6 +29,17 @@ type OSCommand struct {
CmdLogSpan string
removeFile func(string) error
Cmd *CmdObjBuilder
}
// Platform stores the os state
type Platform struct {
OS string
Shell string
ShellArg string
OpenCommand string
OpenLinkCommand string
}
// TODO: make these fields private
@@ -78,16 +73,20 @@ func NewCmdLogEntry(cmdStr string, span string, commandLine bool) CmdLogEntry {
}
// NewOSCommand os command runner
func NewOSCommand(log *logrus.Entry, config config.AppConfigurer) *OSCommand {
return &OSCommand{
Log: log,
Platform: getPlatform(),
Config: config,
Command: secureexec.Command,
BeforeExecuteCmd: func(*exec.Cmd) {},
Getenv: os.Getenv,
removeFile: os.RemoveAll,
func NewOSCommand(common *common.Common) *OSCommand {
platform := getPlatform()
c := &OSCommand{
Common: common,
Platform: platform,
Getenv: os.Getenv,
removeFile: os.RemoveAll,
}
runner := &cmdObjRunner{log: common.Log, logCmdObj: c.LogCmdObj}
c.Cmd = &CmdObjBuilder{runner: runner, logCmdObj: c.LogCmdObj, platform: platform}
return c
}
func (c *OSCommand) WithSpan(span string) *OSCommand {
@@ -101,11 +100,13 @@ func (c *OSCommand) WithSpan(span string) *OSCommand {
newOSCommand := &OSCommand{}
*newOSCommand = *c
newOSCommand.CmdLogSpan = span
newOSCommand.Cmd.logCmdObj = newOSCommand.LogCmdObj
newOSCommand.Cmd.runner = &cmdObjRunner{log: c.Log, logCmdObj: newOSCommand.LogCmdObj}
return newOSCommand
}
func (c *OSCommand) LogExecCmd(cmd *exec.Cmd) {
c.LogCommand(strings.Join(cmd.Args, " "), true)
func (c *OSCommand) LogCmdObj(cmdObj ICmdObj) {
c.LogCommand(cmdObj.ToString(), true)
}
func (c *OSCommand) LogCommand(cmdStr string, commandLine bool) {
@@ -120,121 +121,13 @@ func (c *OSCommand) SetOnRunCommand(f func(CmdLogEntry)) {
c.onRunCommand = f
}
// SetCommand sets the command function used by the struct.
// To be used for testing only
func (c *OSCommand) SetCommand(cmd func(string, ...string) *exec.Cmd) {
c.Command = cmd
}
// To be used for testing only
func (c *OSCommand) SetRemoveFile(f func(string) error) {
c.removeFile = f
}
func (c *OSCommand) SetBeforeExecuteCmd(cmd func(*exec.Cmd)) {
c.BeforeExecuteCmd = cmd
}
type RunCommandOptions struct {
EnvVars []string
}
func (c *OSCommand) RunCommandWithOutputWithOptions(command string, options RunCommandOptions) (string, error) {
c.LogCommand(command, true)
cmd := c.ExecutableFromString(command)
cmd.Env = append(cmd.Env, "GIT_TERMINAL_PROMPT=0") // prevents git from prompting us for input which would freeze the program
cmd.Env = append(cmd.Env, options.EnvVars...)
return sanitisedCommandOutput(cmd.CombinedOutput())
}
func (c *OSCommand) RunCommandWithOptions(command string, options RunCommandOptions) error {
_, err := c.RunCommandWithOutputWithOptions(command, options)
return err
}
// RunCommandWithOutput wrapper around commands returning their output and error
// NOTE: If you don't pass any formatArgs we'll just use the command directly,
// however there's a bizarre compiler error/warning when you pass in a formatString
// with a percent sign because it thinks it's supposed to be a formatString when
// in that case it's not. To get around that error you'll need to define the string
// in a variable and pass the variable into RunCommandWithOutput.
func (c *OSCommand) RunCommandWithOutput(formatString string, formatArgs ...interface{}) (string, error) {
command := formatString
if formatArgs != nil {
command = fmt.Sprintf(formatString, formatArgs...)
}
cmd := c.ExecutableFromString(command)
c.LogExecCmd(cmd)
output, err := sanitisedCommandOutput(cmd.CombinedOutput())
if err != nil {
c.Log.WithField("command", command).Error(output)
}
return output, err
}
// RunExecutableWithOutput runs an executable file and returns its output
func (c *OSCommand) RunExecutableWithOutput(cmd *exec.Cmd) (string, error) {
c.LogExecCmd(cmd)
c.BeforeExecuteCmd(cmd)
return sanitisedCommandOutput(cmd.CombinedOutput())
}
// RunExecutable runs an executable file and returns an error if there was one
func (c *OSCommand) RunExecutable(cmd *exec.Cmd) error {
_, err := c.RunExecutableWithOutput(cmd)
return err
}
// ExecutableFromString takes a string like `git status` and returns an executable command for it
func (c *OSCommand) ExecutableFromString(commandStr string) *exec.Cmd {
splitCmd := str.ToArgv(commandStr)
cmd := c.Command(splitCmd[0], splitCmd[1:]...)
cmd.Env = append(os.Environ(), "GIT_OPTIONAL_LOCKS=0")
return cmd
}
// ShellCommandFromString takes a string like `git commit` and returns an executable shell command for it
func (c *OSCommand) ShellCommandFromString(commandStr string) *exec.Cmd {
quotedCommand := ""
// Windows does not seem to like quotes around the command
if c.Platform.OS == "windows" {
quotedCommand = strings.NewReplacer(
"^", "^^",
"&", "^&",
"|", "^|",
"<", "^<",
">", "^>",
"%", "^%",
).Replace(commandStr)
} else {
quotedCommand = c.Quote(commandStr)
}
shellCommand := fmt.Sprintf("%s %s %s", c.Platform.Shell, c.Platform.ShellArg, quotedCommand)
return c.ExecutableFromString(shellCommand)
}
// RunCommand runs a command and just returns the error
func (c *OSCommand) RunCommand(formatString string, formatArgs ...interface{}) error {
_, err := c.RunCommandWithOutput(formatString, formatArgs...)
return err
}
// RunShellCommand runs shell commands i.e. 'sh -c <command>'. Good for when you
// need access to the shell
func (c *OSCommand) RunShellCommand(command string) error {
cmd := c.ShellCommandFromString(command)
c.LogExecCmd(cmd)
_, err := sanitisedCommandOutput(cmd.CombinedOutput())
return err
}
// FileType tells us if the file is a file, directory or other
func (c *OSCommand) FileType(path string) string {
func FileType(path string) string {
fileInfo, err := os.Stat(path)
if err != nil {
return "other"
@@ -245,63 +138,36 @@ func (c *OSCommand) FileType(path string) string {
return "file"
}
func sanitisedCommandOutput(output []byte, err error) (string, error) {
outputString := string(output)
if err != nil {
// errors like 'exit status 1' are not very useful so we'll create an error
// from the combined output
if outputString == "" {
return "", utils.WrapError(err)
}
return outputString, errors.New(outputString)
}
return outputString, nil
}
// OpenFile opens a file with the given
func (c *OSCommand) OpenFile(filename string) error {
commandTemplate := c.Config.GetUserConfig().OS.OpenCommand
commandTemplate := c.UserConfig.OS.OpenCommand
templateValues := map[string]string{
"filename": c.Quote(filename),
}
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
err := c.RunShellCommand(command)
return err
return c.Cmd.NewShell(command).Run()
}
// OpenLink opens a file with the given
func (c *OSCommand) OpenLink(link string) error {
c.LogCommand(fmt.Sprintf("Opening link '%s'", link), false)
commandTemplate := c.Config.GetUserConfig().OS.OpenLinkCommand
commandTemplate := c.UserConfig.OS.OpenLinkCommand
templateValues := map[string]string{
"link": c.Quote(link),
}
command := utils.ResolvePlaceholderString(commandTemplate, templateValues)
err := c.RunShellCommand(command)
return err
}
// PrepareSubProcess iniPrepareSubProcessrocess then tells the Gui to switch to it
// TODO: see if this needs to exist, given that ExecutableFromString does the same things
func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *exec.Cmd {
cmd := c.Command(cmdName, commandArgs...)
if cmd != nil {
cmd.Env = append(os.Environ(), "GIT_OPTIONAL_LOCKS=0")
}
c.LogExecCmd(cmd)
return cmd
}
// PrepareShellSubProcess returns the pointer to a custom command
func (c *OSCommand) PrepareShellSubProcess(command string) *exec.Cmd {
return c.PrepareSubProcess(c.Platform.Shell, c.Platform.ShellArg, command)
return c.Cmd.NewShell(command).Run()
}
// Quote wraps a message in platform-specific quotation marks
func (c *OSCommand) Quote(message string) string {
return c.Cmd.Quote(message)
}
func (self *CmdObjBuilder) Quote(message string) string {
var quote string
if c.Platform.OS == "windows" {
if self.platform.OS == "windows" {
quote = `\"`
message = strings.NewReplacer(
`"`, `"'"'"`,
@@ -390,24 +256,6 @@ func (c *OSCommand) FileExists(path string) (bool, error) {
return true, nil
}
// RunPreparedCommand takes a pointer to an exec.Cmd and runs it
// this is useful if you need to give your command some environment variables
// before running it
func (c *OSCommand) RunPreparedCommand(cmd *exec.Cmd) error {
c.BeforeExecuteCmd(cmd)
c.LogExecCmd(cmd)
out, err := cmd.CombinedOutput()
outString := string(out)
c.Log.Info(outString)
if err != nil {
if len(outString) == 0 {
return err
}
return errors.New(outString)
}
return nil
}
// GetLazygitPath returns the path of the currently executed file
func (c *OSCommand) GetLazygitPath() string {
ex, err := os.Executable() // get the executable path for git to use
@@ -426,7 +274,7 @@ func (c *OSCommand) PipeCommands(commandStrings ...string) error {
logCmdStr += " | "
}
logCmdStr += str
cmds[i] = c.ExecutableFromString(str)
cmds[i] = c.Cmd.New(str).GetCmd()
}
c.LogCommand(logCmdStr, true)
@@ -489,35 +337,6 @@ func Kill(cmd *exec.Cmd) error {
return cmd.Process.Kill()
}
func RunLineOutputCmd(cmd *exec.Cmd, onLine func(line string) (bool, error)) error {
stdoutPipe, err := cmd.StdoutPipe()
if err != nil {
return err
}
scanner := bufio.NewScanner(stdoutPipe)
scanner.Split(bufio.ScanLines)
if err := cmd.Start(); err != nil {
return err
}
for scanner.Scan() {
line := scanner.Text()
stop, err := onLine(line)
if err != nil {
return err
}
if stop {
_ = cmd.Process.Kill()
break
}
}
_ = cmd.Wait()
return nil
}
func (c *OSCommand) CopyToClipboard(str string) error {
escaped := strings.Replace(str, "\n", "\\n", -1)
truncated := utils.TruncateWithEllipsis(escaped, 40)
@@ -531,29 +350,6 @@ func (c *OSCommand) RemoveFile(path string) error {
return c.removeFile(path)
}
func (c *OSCommand) NewCmdObjFromStr(cmdStr string) ICmdObj {
args := str.ToArgv(cmdStr)
cmd := c.Command(args[0], args[1:]...)
cmd.Env = os.Environ()
return &CmdObj{
cmdStr: cmdStr,
cmd: cmd,
}
}
func (c *OSCommand) NewCmdObjFromArgs(args []string) ICmdObj {
cmd := c.Command(args[0], args[1:]...)
return &CmdObj{
cmdStr: strings.Join(args, " "),
cmd: cmd,
}
}
func (c *OSCommand) NewCmdObj(cmd *exec.Cmd) ICmdObj {
return &CmdObj{
cmdStr: strings.Join(cmd.Args, " "),
cmd: cmd,
}
func GetTempDir() string {
return filepath.Join(os.TempDir(), "lazygit")
}

View File

@@ -7,59 +7,50 @@ import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/go-errors/errors"
"github.com/stretchr/testify/assert"
)
// TestOSCommandOpenFileDarwin is a function.
func TestOSCommandOpenFileDarwin(t *testing.T) {
type scenario struct {
filename string
command func(string, ...string) *exec.Cmd
runner *FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
"test",
func(name string, arg ...string) *exec.Cmd {
return secureexec.Command("exit", "1")
},
func(err error) {
filename: "test",
runner: NewFakeRunner(t).
ExpectArgs([]string{"bash", "-c", `open "test"`}, "", errors.New("error")),
test: func(err error) {
assert.Error(t, err)
},
},
{
"test",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "bash", name)
assert.Equal(t, []string{"-c", `open "test"`}, arg)
return secureexec.Command("echo")
},
func(err error) {
filename: "test",
runner: NewFakeRunner(t).
ExpectArgs([]string{"bash", "-c", `open "test"`}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
"filename with spaces",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "bash", name)
assert.Equal(t, []string{"-c", `open "filename with spaces"`}, arg)
return secureexec.Command("echo")
},
func(err error) {
filename: "filename with spaces",
runner: NewFakeRunner(t).
ExpectArgs([]string{"bash", "-c", `open "filename with spaces"`}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
OSCmd := NewDummyOSCommand()
OSCmd.Platform.OS = "darwin"
OSCmd.Command = s.command
OSCmd.Config.GetUserConfig().OS.OpenCommand = "open {{filename}}"
oSCmd := NewDummyOSCommandWithRunner(s.runner)
oSCmd.Platform.OS = "darwin"
oSCmd.UserConfig.OS.OpenCommand = "open {{filename}}"
s.test(OSCmd.OpenFile(s.filename))
s.test(oSCmd.OpenFile(s.filename))
}
}
@@ -67,72 +58,125 @@ func TestOSCommandOpenFileDarwin(t *testing.T) {
func TestOSCommandOpenFileLinux(t *testing.T) {
type scenario struct {
filename string
runner *FakeCmdObjRunner
command func(string, ...string) *exec.Cmd
test func(error)
}
scenarios := []scenario{
{
"test",
func(name string, arg ...string) *exec.Cmd {
return secureexec.Command("exit", "1")
},
func(err error) {
filename: "test",
runner: NewFakeRunner(t).
ExpectArgs([]string{"bash", "-c", `xdg-open "test" > /dev/null`}, "", errors.New("error")),
test: func(err error) {
assert.Error(t, err)
},
},
{
"test",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "bash", name)
assert.Equal(t, []string{"-c", `xdg-open "test" > /dev/null`}, arg)
return secureexec.Command("echo")
},
func(err error) {
filename: "test",
runner: NewFakeRunner(t).
ExpectArgs([]string{"bash", "-c", `xdg-open "test" > /dev/null`}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
"filename with spaces",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "bash", name)
assert.Equal(t, []string{"-c", `xdg-open "filename with spaces" > /dev/null`}, arg)
return secureexec.Command("echo")
},
func(err error) {
filename: "filename with spaces",
runner: NewFakeRunner(t).
ExpectArgs([]string{"bash", "-c", `xdg-open "filename with spaces" > /dev/null`}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
"let's_test_with_single_quote",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "bash", name)
assert.Equal(t, []string{"-c", `xdg-open "let's_test_with_single_quote" > /dev/null`}, arg)
return secureexec.Command("echo")
},
func(err error) {
filename: "let's_test_with_single_quote",
runner: NewFakeRunner(t).
ExpectArgs([]string{"bash", "-c", `xdg-open "let's_test_with_single_quote" > /dev/null`}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
"$USER.txt",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "bash", name)
assert.Equal(t, []string{"-c", `xdg-open "\$USER.txt" > /dev/null`}, arg)
return secureexec.Command("echo")
},
func(err error) {
filename: "$USER.txt",
runner: NewFakeRunner(t).
ExpectArgs([]string{"bash", "-c", `xdg-open "\$USER.txt" > /dev/null`}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
OSCmd := NewDummyOSCommand()
OSCmd.Command = s.command
OSCmd.Platform.OS = "linux"
OSCmd.Config.GetUserConfig().OS.OpenCommand = `xdg-open {{filename}} > /dev/null`
oSCmd := NewDummyOSCommandWithRunner(s.runner)
oSCmd.Platform.OS = "linux"
oSCmd.UserConfig.OS.OpenCommand = `xdg-open {{filename}} > /dev/null`
s.test(OSCmd.OpenFile(s.filename))
s.test(oSCmd.OpenFile(s.filename))
}
}
func TestOSCommandOpenFileWindows(t *testing.T) {
type scenario struct {
filename string
runner *FakeCmdObjRunner
command func(string, ...string) *exec.Cmd
test func(error)
}
scenarios := []scenario{
{
filename: "test",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/c", "start", "", "test"}, "", errors.New("error")),
test: func(err error) {
assert.Error(t, err)
},
},
{
filename: "test",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/c", "start", "", "test"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
filename: "filename with spaces",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/c", "start", "", "filename with spaces"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
filename: "let's_test_with_single_quote",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/c", "start", "", "let's_test_with_single_quote"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
filename: "$USER.txt",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/c", "start", "", "$USER.txt"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
oSCmd := NewDummyOSCommandWithRunner(s.runner)
platform := &Platform{
OS: "windows",
Shell: "cmd",
ShellArg: "/c",
}
oSCmd.Platform = platform
oSCmd.Cmd.platform = platform
oSCmd.UserConfig.OS.OpenCommand = `start "" {{filename}}`
s.test(oSCmd.OpenFile(s.filename))
}
}

View File

@@ -8,8 +8,7 @@ import (
"github.com/stretchr/testify/assert"
)
// TestOSCommandRunCommandWithOutput is a function.
func TestOSCommandRunCommandWithOutput(t *testing.T) {
func TestOSCommandRunWithOutput(t *testing.T) {
type scenario struct {
command string
test func(string, error)
@@ -32,12 +31,12 @@ func TestOSCommandRunCommandWithOutput(t *testing.T) {
}
for _, s := range scenarios {
s.test(NewDummyOSCommand().RunCommandWithOutput(s.command))
c := NewDummyOSCommand()
s.test(c.Cmd.New(s.command).RunWithOutput())
}
}
// TestOSCommandRunCommand is a function.
func TestOSCommandRunCommand(t *testing.T) {
func TestOSCommandRun(t *testing.T) {
type scenario struct {
command string
test func(error)
@@ -53,11 +52,11 @@ func TestOSCommandRunCommand(t *testing.T) {
}
for _, s := range scenarios {
s.test(NewDummyOSCommand().RunCommand(s.command))
c := NewDummyOSCommand()
s.test(c.Cmd.New(s.command).Run())
}
}
// TestOSCommandQuote is a function.
func TestOSCommandQuote(t *testing.T) {
osCommand := NewDummyOSCommand()
@@ -109,7 +108,6 @@ func TestOSCommandQuoteWindows(t *testing.T) {
assert.EqualValues(t, expected, actual)
}
// TestOSCommandFileType is a function.
func TestOSCommandFileType(t *testing.T) {
type scenario struct {
path string
@@ -162,7 +160,7 @@ func TestOSCommandFileType(t *testing.T) {
for _, s := range scenarios {
s.setup()
s.test(NewDummyOSCommand().FileType(s.path))
s.test(FileType(s.path))
_ = os.RemoveAll(s.path)
}
}

View File

@@ -11,76 +11,68 @@ import (
"github.com/stretchr/testify/assert"
)
// TestOSCommandOpenFileWindows tests the OpenFile command on Linux
func TestOSCommandOpenFileWindows(t *testing.T) {
type scenario struct {
filename string
runner *FakeCmdObjRunner
command func(string, ...string) *exec.Cmd
test func(error)
}
scenarios := []scenario{
{
"test",
func(name string, arg ...string) *exec.Cmd {
return secureexec.Command("exit", "1")
},
func(err error) {
filename: "test",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/c", "start", "", "test"}, "", errors.New("error")),
test: func(err error) {
assert.Error(t, err)
},
},
{
"test",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "cmd", name)
assert.Equal(t, []string{"/c", "start", "", "test"}, arg)
return secureexec.Command("echo")
},
func(err error) {
filename: "test",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/c", "start", "", "test"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
"filename with spaces",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "cmd", name)
assert.Equal(t, []string{"/c", "start", "", "filename with spaces"}, arg)
return secureexec.Command("echo")
},
func(err error) {
filename: "filename with spaces",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/c", "start", "", "filename with spaces"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
"let's_test_with_single_quote",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "cmd", name)
assert.Equal(t, []string{"/c", "start", "", "let's_test_with_single_quote"}, arg)
return secureexec.Command("echo")
},
func(err error) {
filename: "let's_test_with_single_quote",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/c", "start", "", "let's_test_with_single_quote"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
"$USER.txt",
func(name string, arg ...string) *exec.Cmd {
assert.Equal(t, "cmd", name)
assert.Equal(t, []string{"/c", "start", "", "$USER.txt"}, arg)
return secureexec.Command("echo")
},
func(err error) {
filename: "$USER.txt",
runner: NewFakeRunner(t).
ExpectArgs([]string{"cmd", "/c", "start", "", "$USER.txt"}, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
OSCmd := NewDummyOSCommand()
OSCmd.Command = s.command
OSCmd.Platform.OS = "windows"
OSCmd.Config.GetUserConfig().OS.OpenCommand = `start "" {{filename}}`
oSCmd := NewDummyOSCommandWithRunner(s.runner)
platform := &Platform{
OS: "windows",
Shell: "cmd",
ShellArg: "/c",
}
oSCmd.Platform = platform
oSCmd.Cmd.platform = platform
oSCmd.UserConfig.OS.OpenCommand = `start "" {{filename}}`
s.test(OSCmd.OpenFile(s.filename))
s.test(oSCmd.OpenFile(s.filename))
}
}

View File

@@ -6,6 +6,7 @@ import (
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/patch"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
)
// DeletePatchesFromCommit applies a patch in reverse for a commit
@@ -85,12 +86,12 @@ func (c *GitCommand) MovePatchToSelectedCommit(commits []*models.Commit, sourceC
todo = a + " " + commit.Sha + " " + commit.Name + "\n" + todo
}
cmd, err := c.PrepareInteractiveRebaseCommand(commits[baseIndex].Sha, todo, true)
cmdObj, err := c.PrepareInteractiveRebaseCommand(commits[baseIndex].Sha, todo, true)
if err != nil {
return err
}
if err := c.OSCommand.RunPreparedCommand(cmd); err != nil {
if err := cmdObj.Run(); err != nil {
return err
}
@@ -149,7 +150,7 @@ func (c *GitCommand) MovePatchIntoIndex(commits []*models.Commit, commitIdx int,
}
if err := p.ApplyPatches(true); err != nil {
if c.WorkingTreeState() == REBASE_MODE_REBASING {
if c.WorkingTreeState() == enums.REBASE_MODE_REBASING {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
return err
}
@@ -169,7 +170,7 @@ func (c *GitCommand) MovePatchIntoIndex(commits []*models.Commit, commitIdx int,
c.onSuccessfulContinue = func() error {
// add patches to index
if err := p.ApplyPatches(false); err != nil {
if c.WorkingTreeState() == REBASE_MODE_REBASING {
if c.WorkingTreeState() == enums.REBASE_MODE_REBASING {
if err := c.GenericMergeOrRebaseAction("rebase", "abort"); err != nil {
return err
}
@@ -217,7 +218,7 @@ func (c *GitCommand) PullPatchIntoNewCommit(commits []*models.Commit, commitIdx
head_message, _ := c.GetHeadCommitMessage()
new_message := fmt.Sprintf("Split from \"%s\"", head_message)
err := c.OSCommand.RunCommand(c.CommitCmdStr(new_message, ""))
err := c.CommitCmdObj(new_message, "").Run()
if err != nil {
return err
}

View File

@@ -3,17 +3,15 @@ package commands
import (
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/mgutz/str"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
)
func (c *GitCommand) RewordCommit(commits []*models.Commit, index int) (*exec.Cmd, error) {
func (c *GitCommand) RewordCommit(commits []*models.Commit, index int) (oscommands.ICmdObj, error) {
todo, sha, err := c.GenerateGenericRebaseTodo(commits, index, "reword")
if err != nil {
return nil, err
@@ -35,12 +33,12 @@ func (c *GitCommand) MoveCommitDown(commits []*models.Commit, index int) error {
todo = "pick " + commit.Sha + " " + commit.Name + "\n" + todo
}
cmd, err := c.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todo, true)
cmdObj, err := c.PrepareInteractiveRebaseCommand(commits[index+2].Sha, todo, true)
if err != nil {
return err
}
return c.OSCommand.RunPreparedCommand(cmd)
return cmdObj.Run()
}
func (c *GitCommand) InteractiveRebase(commits []*models.Commit, index int, action string) error {
@@ -49,30 +47,29 @@ func (c *GitCommand) InteractiveRebase(commits []*models.Commit, index int, acti
return err
}
cmd, err := c.PrepareInteractiveRebaseCommand(sha, todo, true)
cmdObj, err := c.PrepareInteractiveRebaseCommand(sha, todo, true)
if err != nil {
return err
}
return c.OSCommand.RunPreparedCommand(cmd)
return cmdObj.Run()
}
// PrepareInteractiveRebaseCommand returns the cmd for an interactive rebase
// we tell git to run lazygit to edit the todo list, and we pass the client
// lazygit a todo string to write to the todo file
func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string, overrideEditor bool) (*exec.Cmd, error) {
func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string, overrideEditor bool) (oscommands.ICmdObj, error) {
ex := c.OSCommand.GetLazygitPath()
debug := "FALSE"
if c.OSCommand.Config.GetDebug() {
if c.Debug {
debug = "TRUE"
}
cmdStr := fmt.Sprintf("git rebase --interactive --autostash --keep-empty %s", baseSha)
c.Log.WithField("command", cmdStr).Info("RunCommand")
splitCmd := str.ToArgv(cmdStr)
cmd := c.OSCommand.Command(splitCmd[0], splitCmd[1:]...)
cmdObj := c.Cmd.New(cmdStr)
gitSequenceEditor := ex
if todo == "" {
@@ -81,9 +78,7 @@ func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string
c.OSCommand.LogCommand(fmt.Sprintf("Creating TODO file for interactive rebase: \n\n%s", todo), false)
}
cmd.Env = os.Environ()
cmd.Env = append(
cmd.Env,
cmdObj.AddEnvVars(
"LAZYGIT_CLIENT_COMMAND=INTERACTIVE_REBASE",
"LAZYGIT_REBASE_TODO="+todo,
"DEBUG="+debug,
@@ -93,10 +88,10 @@ func (c *GitCommand) PrepareInteractiveRebaseCommand(baseSha string, todo string
)
if overrideEditor {
cmd.Env = append(cmd.Env, "GIT_EDITOR="+ex)
cmdObj.AddEnvVars("GIT_EDITOR=" + ex)
}
return cmd, nil
return cmdObj, nil
}
func (c *GitCommand) GenerateGenericRebaseTodo(commits []*models.Commit, actionIndex int, action string) (string, string, error) {
@@ -222,26 +217,22 @@ func (c *GitCommand) BeginInteractiveRebaseForCommit(commits []*models.Commit, c
return err
}
cmd, err := c.PrepareInteractiveRebaseCommand(sha, todo, true)
cmdObj, err := c.PrepareInteractiveRebaseCommand(sha, todo, true)
if err != nil {
return err
}
if err := c.OSCommand.RunPreparedCommand(cmd); err != nil {
return err
}
return nil
return cmdObj.Run()
}
// RebaseBranch interactive rebases onto a branch
func (c *GitCommand) RebaseBranch(branchName string) error {
cmd, err := c.PrepareInteractiveRebaseCommand(branchName, "", false)
cmdObj, err := c.PrepareInteractiveRebaseCommand(branchName, "", false)
if err != nil {
return err
}
return c.OSCommand.RunPreparedCommand(cmd)
return cmdObj.Run()
}
// GenericMerge takes a commandType of "merge" or "rebase" and a command of "abort", "skip" or "continue"
@@ -276,14 +267,14 @@ func (c *GitCommand) GenericMergeOrRebaseAction(commandType string, command stri
}
func (c *GitCommand) runSkipEditorCommand(command string) error {
cmd := c.OSCommand.ExecutableFromString(command)
cmdObj := c.OSCommand.Cmd.New(command)
lazyGitPath := c.OSCommand.GetLazygitPath()
cmd.Env = append(
cmd.Env,
"LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY",
"GIT_EDITOR="+lazyGitPath,
"EDITOR="+lazyGitPath,
"VISUAL="+lazyGitPath,
)
return c.OSCommand.RunExecutable(cmd)
return cmdObj.
AddEnvVars(
"LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY",
"GIT_EDITOR="+lazyGitPath,
"EDITOR="+lazyGitPath,
"VISUAL="+lazyGitPath,
).
Run()
}

View File

@@ -1,57 +1,47 @@
package commands
import (
"os/exec"
"regexp"
"testing"
"github.com/jesseduffield/lazygit/pkg/test"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert"
)
// TestGitCommandRebaseBranch is a function.
func TestGitCommandRebaseBranch(t *testing.T) {
type scenario struct {
testName string
arg string
command func(string, ...string) *exec.Cmd
runner *oscommands.FakeCmdObjRunner
test func(error)
}
scenarios := []scenario{
{
"successful rebase",
"master",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: "git rebase --interactive --autostash --keep-empty master",
Replace: "echo",
},
}),
func(err error) {
testName: "successful rebase",
arg: "master",
runner: oscommands.NewFakeRunner(t).
Expect(`git rebase --interactive --autostash --keep-empty master`, "", nil),
test: func(err error) {
assert.NoError(t, err)
},
},
{
"unsuccessful rebase",
"master",
test.CreateMockCommand(t, []*test.CommandSwapper{
{
Expect: "git rebase --interactive --autostash --keep-empty master",
Replace: "test",
},
}),
func(err error) {
testName: "unsuccessful rebase",
arg: "master",
runner: oscommands.NewFakeRunner(t).
Expect(`git rebase --interactive --autostash --keep-empty master`, "", errors.New("error")),
test: func(err error) {
assert.Error(t, err)
},
},
}
gitCmd := NewDummyGitCommand()
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd.OSCommand.Command = s.command
gitCmd := NewDummyGitCommandWithRunner(s.runner)
s.test(gitCmd.RebaseBranch(s.arg))
})
}
@@ -60,37 +50,27 @@ func TestGitCommandRebaseBranch(t *testing.T) {
// TestGitCommandSkipEditorCommand confirms that SkipEditorCommand injects
// environment variables that suppress an interactive editor
func TestGitCommandSkipEditorCommand(t *testing.T) {
cmd := NewDummyGitCommand()
cmd.OSCommand.SetBeforeExecuteCmd(func(cmd *exec.Cmd) {
test.AssertContainsMatch(
t,
cmd.Env,
regexp.MustCompile("^VISUAL="),
"expected VISUAL to be set for a non-interactive external command",
)
test.AssertContainsMatch(
t,
cmd.Env,
regexp.MustCompile("^EDITOR="),
"expected EDITOR to be set for a non-interactive external command",
)
test.AssertContainsMatch(
t,
cmd.Env,
regexp.MustCompile("^GIT_EDITOR="),
"expected GIT_EDITOR to be set for a non-interactive external command",
)
test.AssertContainsMatch(
t,
cmd.Env,
regexp.MustCompile("^LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY$"),
"expected LAZYGIT_CLIENT_COMMAND to be set for a non-interactive external command",
)
commandStr := "git blah"
runner := oscommands.NewFakeRunner(t).ExpectFunc(func(cmdObj oscommands.ICmdObj) (string, error) {
assert.Equal(t, commandStr, cmdObj.ToString())
envVars := cmdObj.GetEnvVars()
for _, regexStr := range []string{
`^VISUAL=.*$`,
`^EDITOR=.*$`,
`^GIT_EDITOR=.*$`,
"^LAZYGIT_CLIENT_COMMAND=EXIT_IMMEDIATELY$",
} {
foundMatch := utils.IncludesStringFunc(envVars, func(envVar string) bool {
return regexp.MustCompile(regexStr).MatchString(envVar)
})
if !foundMatch {
t.Errorf("expected environment variable %s to be set", regexStr)
}
}
return "", nil
})
_ = cmd.runSkipEditorCommand("true")
gitCmd := NewDummyGitCommandWithRunner(runner)
err := gitCmd.runSkipEditorCommand(commandStr)
assert.NoError(t, err)
runner.CheckForMissingCalls()
}

View File

@@ -7,24 +7,33 @@ import (
)
func (c *GitCommand) AddRemote(name string, url string) error {
return c.RunCommand("git remote add %s %s", c.OSCommand.Quote(name), c.OSCommand.Quote(url))
return c.Cmd.
New(fmt.Sprintf("git remote add %s %s", c.Cmd.Quote(name), c.Cmd.Quote(url))).
Run()
}
func (c *GitCommand) RemoveRemote(name string) error {
return c.RunCommand("git remote remove %s", c.OSCommand.Quote(name))
return c.Cmd.
New(fmt.Sprintf("git remote remove %s", c.Cmd.Quote(name))).
Run()
}
func (c *GitCommand) RenameRemote(oldRemoteName string, newRemoteName string) error {
return c.RunCommand("git remote rename %s %s", c.OSCommand.Quote(oldRemoteName), c.OSCommand.Quote(newRemoteName))
return c.Cmd.
New(fmt.Sprintf("git remote rename %s %s", c.Cmd.Quote(oldRemoteName), c.Cmd.Quote(newRemoteName))).
Run()
}
func (c *GitCommand) UpdateRemoteUrl(remoteName string, updatedUrl string) error {
return c.RunCommand("git remote set-url %s %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(updatedUrl))
return c.Cmd.
New(fmt.Sprintf("git remote set-url %s %s", c.Cmd.Quote(remoteName), c.Cmd.Quote(updatedUrl))).
Run()
}
func (c *GitCommand) DeleteRemoteBranch(remoteName string, branchName string, promptUserForCredential func(string) string) error {
command := fmt.Sprintf("git push %s --delete %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(branchName))
cmdObj := c.NewCmdObjFromStr(command)
command := fmt.Sprintf("git push %s --delete %s", c.Cmd.Quote(remoteName), c.Cmd.Quote(branchName))
cmdObj := c.Cmd.
New(command)
return c.DetectUnamePass(cmdObj, promptUserForCredential)
}
@@ -34,10 +43,12 @@ func (c *GitCommand) DetectUnamePass(cmdObj oscommands.ICmdObj, promptUserForCre
// CheckRemoteBranchExists Returns remote branch
func (c *GitCommand) CheckRemoteBranchExists(branchName string) bool {
_, err := c.OSCommand.RunCommandWithOutput(
"git show-ref --verify -- refs/remotes/origin/%s",
c.OSCommand.Quote(branchName),
)
_, err := c.Cmd.
New(
fmt.Sprintf("git show-ref --verify -- refs/remotes/origin/%s",
c.Cmd.Quote(branchName),
)).
RunWithOutput()
return err == nil
}

View File

@@ -1,28 +1,32 @@
package commands
import "fmt"
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
)
// StashDo modify stash
func (c *GitCommand) StashDo(index int, method string) error {
return c.RunCommand("git stash %s stash@{%d}", method, index)
return c.Cmd.New(fmt.Sprintf("git stash %s stash@{%d}", method, index)).Run()
}
// StashSave save stash
// TODO: before calling this, check if there is anything to save
func (c *GitCommand) StashSave(message string) error {
return c.RunCommand("git stash save %s", c.OSCommand.Quote(message))
return c.Cmd.New("git stash save " + c.OSCommand.Quote(message)).Run()
}
// GetStashEntryDiff stash diff
func (c *GitCommand) ShowStashEntryCmdStr(index int) string {
return fmt.Sprintf("git stash show -p --stat --color=%s --unified=%d stash@{%d}", c.colorArg(), c.Config.GetUserConfig().Git.DiffContextSize, index)
return fmt.Sprintf("git stash show -p --stat --color=%s --unified=%d stash@{%d}", c.colorArg(), c.UserConfig.Git.DiffContextSize, index)
}
// StashSaveStagedChanges stashes only the currently staged changes. This takes a few steps
// shoutouts to Joe on https://stackoverflow.com/questions/14759748/stashing-only-staged-changes-in-git-is-it-possible
func (c *GitCommand) StashSaveStagedChanges(message string) error {
// wrap in 'writing', which uses a mutex
if err := c.RunCommand("git stash --keep-index"); err != nil {
if err := c.Cmd.New("git stash --keep-index").Run(); err != nil {
return err
}
@@ -30,7 +34,7 @@ func (c *GitCommand) StashSaveStagedChanges(message string) error {
return err
}
if err := c.RunCommand("git stash apply stash@{1}"); err != nil {
if err := c.Cmd.New("git stash apply stash@{1}").Run(); err != nil {
return err
}
@@ -38,14 +42,17 @@ func (c *GitCommand) StashSaveStagedChanges(message string) error {
return err
}
if err := c.RunCommand("git stash drop stash@{1}"); err != nil {
if err := c.Cmd.New("git stash drop stash@{1}").Run(); err != nil {
return err
}
// if you had staged an untracked file, that will now appear as 'AD' in git status
// meaning it's deleted in your working tree but added in your index. Given that it's
// now safely stashed, we need to remove it.
files := c.GetStatusFiles(GetStatusFileOptions{})
files := loaders.
NewFileLoader(c.Common, c.Cmd, c.GitConfig).
GetStatusFiles(loaders.GetStatusFileOptions{})
for _, file := range files {
if file.ShortStatus == "AD" {
if err := c.UnStageFile(file.Names(), false); err != nil {

View File

@@ -1,40 +1,30 @@
package commands
import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/stretchr/testify/assert"
)
// TestGitCommandStashDo is a function.
func TestGitCommandStashDo(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"stash", "drop", "stash@{1}"}, args)
return secureexec.Command("echo")
}
runner := oscommands.NewFakeRunner(t).
ExpectArgs([]string{"git", "stash", "drop", "stash@{1}"}, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner)
assert.NoError(t, gitCmd.StashDo(1, "drop"))
runner.CheckForMissingCalls()
}
// TestGitCommandStashSave is a function.
func TestGitCommandStashSave(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"stash", "save", "A stash message"}, args)
return secureexec.Command("echo")
}
runner := oscommands.NewFakeRunner(t).
ExpectArgs([]string{"git", "stash", "save", "A stash message"}, "", nil)
gitCmd := NewDummyGitCommandWithRunner(runner)
assert.NoError(t, gitCmd.StashSave("A stash message"))
runner.CheckForMissingCalls()
}
// TestGitCommandShowStashEntryCmdStr is a function.
func TestGitCommandShowStashEntryCmdStr(t *testing.T) {
type scenario struct {
testName string
@@ -61,7 +51,7 @@ func TestGitCommandShowStashEntryCmdStr(t *testing.T) {
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.Config.GetUserConfig().Git.DiffContextSize = s.contextSize
gitCmd.UserConfig.Git.DiffContextSize = s.contextSize
cmdStr := gitCmd.ShowStashEntryCmdStr(s.index)
assert.Equal(t, s.expected, cmdStr)
})

View File

@@ -4,43 +4,37 @@ import (
"path/filepath"
gogit "github.com/jesseduffield/go-git/v5"
)
const (
REBASE_MODE_NORMAL = "normal"
REBASE_MODE_INTERACTIVE = "interactive"
REBASE_MODE_REBASING = "rebasing"
REBASE_MODE_MERGING = "merging"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
)
// RebaseMode returns "" for non-rebase mode, "normal" for normal rebase
// and "interactive" for interactive rebase
func (c *GitCommand) RebaseMode() (string, error) {
func (c *GitCommand) RebaseMode() (enums.RebaseMode, error) {
exists, err := c.OSCommand.FileExists(filepath.Join(c.DotGitDir, "rebase-apply"))
if err != nil {
return "", err
return enums.REBASE_MODE_NONE, err
}
if exists {
return REBASE_MODE_NORMAL, nil
return enums.REBASE_MODE_NORMAL, nil
}
exists, err = c.OSCommand.FileExists(filepath.Join(c.DotGitDir, "rebase-merge"))
if exists {
return REBASE_MODE_INTERACTIVE, err
return enums.REBASE_MODE_INTERACTIVE, err
} else {
return "", err
return enums.REBASE_MODE_NONE, err
}
}
func (c *GitCommand) WorkingTreeState() string {
func (c *GitCommand) WorkingTreeState() enums.RebaseMode {
rebaseMode, _ := c.RebaseMode()
if rebaseMode != "" {
return REBASE_MODE_REBASING
if rebaseMode != enums.REBASE_MODE_NONE {
return enums.REBASE_MODE_REBASING
}
merging, _ := c.IsInMergeState()
if merging {
return REBASE_MODE_MERGING
return enums.REBASE_MODE_MERGING
}
return REBASE_MODE_NORMAL
return enums.REBASE_MODE_NONE
}
// IsInMergeState states whether we are still mid-merge

View File

@@ -2,12 +2,15 @@ package commands
import (
"bufio"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
)
// .gitmodules looks like this:
@@ -15,7 +18,22 @@ import (
// path = blah/mysubmodule
// url = git@github.com:subbo.git
func (c *GitCommand) GetSubmoduleConfigs() ([]*models.SubmoduleConfig, error) {
type SubmoduleCommands struct {
*common.Common
cmd oscommands.ICmdObjBuilder
dotGitDir string
}
func NewSubmoduleCommands(common *common.Common, cmd oscommands.ICmdObjBuilder, dotGitDir string) SubmoduleCommands {
return SubmoduleCommands{
Common: common,
cmd: cmd,
dotGitDir: dotGitDir,
}
}
func (self *SubmoduleCommands) GetConfigs() ([]*models.SubmoduleConfig, error) {
file, err := os.Open(".gitmodules")
if err != nil {
if os.IsNotExist(err) {
@@ -61,36 +79,36 @@ func (c *GitCommand) GetSubmoduleConfigs() ([]*models.SubmoduleConfig, error) {
return configs, nil
}
func (c *GitCommand) SubmoduleStash(submodule *models.SubmoduleConfig) error {
func (self *SubmoduleCommands) Stash(submodule *models.SubmoduleConfig) error {
// if the path does not exist then it hasn't yet been initialized so we'll swallow the error
// because the intention here is to have no dirty worktree state
if _, err := os.Stat(submodule.Path); os.IsNotExist(err) {
c.Log.Infof("submodule path %s does not exist, returning", submodule.Path)
self.Log.Infof("submodule path %s does not exist, returning", submodule.Path)
return nil
}
return c.RunCommand("git -C %s stash --include-untracked", c.OSCommand.Quote(submodule.Path))
return self.cmd.New("git -C " + self.cmd.Quote(submodule.Path) + " stash --include-untracked").Run()
}
func (c *GitCommand) SubmoduleReset(submodule *models.SubmoduleConfig) error {
return c.RunCommand("git submodule update --init --force -- %s", c.OSCommand.Quote(submodule.Path))
func (self *SubmoduleCommands) Reset(submodule *models.SubmoduleConfig) error {
return self.cmd.New("git submodule update --init --force -- " + self.cmd.Quote(submodule.Path)).Run()
}
func (c *GitCommand) SubmoduleUpdateAll() error {
func (self *SubmoduleCommands) UpdateAll() error {
// not doing an --init here because the user probably doesn't want that
return c.RunCommand("git submodule update --force")
return self.cmd.New("git submodule update --force").Run()
}
func (c *GitCommand) SubmoduleDelete(submodule *models.SubmoduleConfig) error {
func (self *SubmoduleCommands) Delete(submodule *models.SubmoduleConfig) error {
// based on https://gist.github.com/myusuf3/7f645819ded92bda6677
if err := c.RunCommand("git submodule deinit --force -- %s", c.OSCommand.Quote(submodule.Path)); err != nil {
if err := self.cmd.New("git submodule deinit --force -- " + self.cmd.Quote(submodule.Path)).Run(); err != nil {
if strings.Contains(err.Error(), "did not match any file(s) known to git") {
if err := c.RunCommand("git config --file .gitmodules --remove-section submodule.%s", c.OSCommand.Quote(submodule.Name)); err != nil {
if err := self.cmd.New("git config --file .gitmodules --remove-section submodule." + self.cmd.Quote(submodule.Name)).Run(); err != nil {
return err
}
if err := c.RunCommand("git config --remove-section submodule.%s", c.OSCommand.Quote(submodule.Name)); err != nil {
if err := self.cmd.New("git config --remove-section submodule." + self.cmd.Quote(submodule.Name)).Run(); err != nil {
return err
}
@@ -100,66 +118,69 @@ func (c *GitCommand) SubmoduleDelete(submodule *models.SubmoduleConfig) error {
}
}
if err := c.RunCommand("git rm --force -r %s", submodule.Path); err != nil {
if err := self.cmd.New("git rm --force -r " + submodule.Path).Run(); err != nil {
// if the directory isn't there then that's fine
c.Log.Error(err)
self.Log.Error(err)
}
return os.RemoveAll(filepath.Join(c.DotGitDir, "modules", submodule.Path))
return os.RemoveAll(filepath.Join(self.dotGitDir, "modules", submodule.Path))
}
func (c *GitCommand) SubmoduleAdd(name string, path string, url string) error {
return c.OSCommand.RunCommand(
"git submodule add --force --name %s -- %s %s ",
c.OSCommand.Quote(name),
c.OSCommand.Quote(url),
c.OSCommand.Quote(path),
)
func (self *SubmoduleCommands) Add(name string, path string, url string) error {
return self.cmd.
New(
fmt.Sprintf(
"git submodule add --force --name %s -- %s %s ",
self.cmd.Quote(name),
self.cmd.Quote(url),
self.cmd.Quote(path),
)).
Run()
}
func (c *GitCommand) SubmoduleUpdateUrl(name string, path string, newUrl string) error {
func (self *SubmoduleCommands) UpdateUrl(name string, path string, newUrl string) error {
// the set-url command is only for later git versions so we're doing it manually here
if err := c.RunCommand("git config --file .gitmodules submodule.%s.url %s", c.OSCommand.Quote(name), c.OSCommand.Quote(newUrl)); err != nil {
if err := self.cmd.New("git config --file .gitmodules submodule." + self.cmd.Quote(name) + ".url " + self.cmd.Quote(newUrl)).Run(); err != nil {
return err
}
if err := c.RunCommand("git submodule sync -- %s", c.OSCommand.Quote(path)); err != nil {
if err := self.cmd.New("git submodule sync -- " + self.cmd.Quote(path)).Run(); err != nil {
return err
}
return nil
}
func (c *GitCommand) SubmoduleInit(path string) error {
return c.RunCommand("git submodule init -- %s", c.OSCommand.Quote(path))
func (self *SubmoduleCommands) Init(path string) error {
return self.cmd.New("git submodule init -- " + self.cmd.Quote(path)).Run()
}
func (c *GitCommand) SubmoduleUpdate(path string) error {
return c.RunCommand("git submodule update --init -- %s", c.OSCommand.Quote(path))
func (self *SubmoduleCommands) Update(path string) error {
return self.cmd.New("git submodule update --init -- " + self.cmd.Quote(path)).Run()
}
func (c *GitCommand) SubmoduleBulkInitCmdStr() string {
return "git submodule init"
func (self *SubmoduleCommands) BulkInitCmdObj() oscommands.ICmdObj {
return self.cmd.New("git submodule init")
}
func (c *GitCommand) SubmoduleBulkUpdateCmdStr() string {
return "git submodule update"
func (self *SubmoduleCommands) BulkUpdateCmdObj() oscommands.ICmdObj {
return self.cmd.New("git submodule update")
}
func (c *GitCommand) SubmoduleForceBulkUpdateCmdStr() string {
return "git submodule update --force"
func (self *SubmoduleCommands) ForceBulkUpdateCmdObj() oscommands.ICmdObj {
return self.cmd.New("git submodule update --force")
}
func (c *GitCommand) SubmoduleBulkDeinitCmdStr() string {
return "git submodule deinit --all --force"
func (self *SubmoduleCommands) BulkDeinitCmdObj() oscommands.ICmdObj {
return self.cmd.New("git submodule deinit --all --force")
}
func (c *GitCommand) ResetSubmodules(submodules []*models.SubmoduleConfig) error {
func (self *SubmoduleCommands) ResetSubmodules(submodules []*models.SubmoduleConfig) error {
for _, submodule := range submodules {
if err := c.SubmoduleStash(submodule); err != nil {
if err := self.Stash(submodule); err != nil {
return err
}
}
return c.SubmoduleUpdateAll()
return self.UpdateAll()
}

View File

@@ -4,18 +4,18 @@ import (
"fmt"
"github.com/go-errors/errors"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
)
// Push pushes to a branch
type PushOpts struct {
Force bool
UpstreamRemote string
UpstreamBranch string
SetUpstream bool
PromptUserForCredential func(string) string
Force bool
UpstreamRemote string
UpstreamBranch string
SetUpstream bool
}
func (c *GitCommand) Push(opts PushOpts) error {
func (c *GitCommand) PushCmdObj(opts PushOpts) (oscommands.ICmdObj, error) {
cmdStr := "git push"
if opts.Force {
@@ -32,13 +32,22 @@ func (c *GitCommand) Push(opts PushOpts) error {
if opts.UpstreamBranch != "" {
if opts.UpstreamRemote == "" {
return errors.New(c.Tr.MustSpecifyOriginError)
return nil, errors.New(c.Tr.MustSpecifyOriginError)
}
cmdStr += " " + c.OSCommand.Quote(opts.UpstreamBranch)
}
cmdObj := c.NewCmdObjFromStr(cmdStr)
return c.DetectUnamePass(cmdObj, opts.PromptUserForCredential)
cmdObj := c.Cmd.New(cmdStr)
return cmdObj, nil
}
func (c *GitCommand) Push(opts PushOpts, promptUserForCredential func(string) string) error {
cmdObj, err := c.PushCmdObj(opts)
if err != nil {
return err
}
return c.DetectUnamePass(cmdObj, promptUserForCredential)
}
type FetchOptions struct {
@@ -58,7 +67,7 @@ func (c *GitCommand) Fetch(opts FetchOptions) error {
cmdStr = fmt.Sprintf("%s %s", cmdStr, c.OSCommand.Quote(opts.BranchName))
}
cmdObj := c.NewCmdObjFromStr(cmdStr)
cmdObj := c.Cmd.New(cmdStr)
return c.DetectUnamePass(cmdObj, func(question string) string {
if opts.PromptUserForCredential != nil {
return opts.PromptUserForCredential(question)
@@ -94,18 +103,18 @@ func (c *GitCommand) Pull(opts PullOptions) error {
// setting GIT_SEQUENCE_EDITOR to ':' as a way of skipping it, in case the user
// has 'pull.rebase = interactive' configured.
cmdObj := c.NewCmdObjFromStr(cmdStr).AddEnvVars("GIT_SEQUENCE_EDITOR=:")
cmdObj := c.Cmd.New(cmdStr).AddEnvVars("GIT_SEQUENCE_EDITOR=:")
return c.DetectUnamePass(cmdObj, opts.PromptUserForCredential)
}
func (c *GitCommand) FastForward(branchName string, remoteName string, remoteBranchName string, promptUserForCredential func(string) string) error {
cmdStr := fmt.Sprintf("git fetch %s %s:%s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(remoteBranchName), c.OSCommand.Quote(branchName))
cmdObj := c.NewCmdObjFromStr(cmdStr)
cmdObj := c.Cmd.New(cmdStr)
return c.DetectUnamePass(cmdObj, promptUserForCredential)
}
func (c *GitCommand) FetchRemote(remoteName string, promptUserForCredential func(string) string) error {
cmdStr := fmt.Sprintf("git fetch %s", c.OSCommand.Quote(remoteName))
cmdObj := c.NewCmdObjFromStr(cmdStr)
cmdObj := c.Cmd.New(cmdStr)
return c.DetectUnamePass(cmdObj, promptUserForCredential)
}

View File

@@ -1,164 +1,93 @@
package commands
import (
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/secureexec"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/stretchr/testify/assert"
)
// TestGitCommandPush is a function.
func TestGitCommandPush(t *testing.T) {
type scenario struct {
testName string
command func(string, ...string) *exec.Cmd
opts PushOpts
test func(error)
}
prompt := func(passOrUname string) string {
return "\n"
test func(oscommands.ICmdObj, error)
}
scenarios := []scenario{
{
"Push with force disabled",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push"}, args)
return secureexec.Command("echo")
},
PushOpts{Force: false, PromptUserForCredential: prompt},
func(err error) {
testName: "Push with force disabled",
opts: PushOpts{Force: false},
test: func(cmdObj oscommands.ICmdObj, err error) {
assert.Equal(t, cmdObj.ToString(), "git push")
assert.NoError(t, err)
},
},
{
"Push with force enabled",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push", "--force-with-lease"}, args)
return secureexec.Command("echo")
},
PushOpts{Force: true, PromptUserForCredential: prompt},
func(err error) {
testName: "Push with force enabled",
opts: PushOpts{Force: true},
test: func(cmdObj oscommands.ICmdObj, err error) {
assert.Equal(t, cmdObj.ToString(), "git push --force-with-lease")
assert.NoError(t, err)
},
},
{
"Push with an error occurring",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push"}, args)
return secureexec.Command("test")
testName: "Push with force disabled, upstream supplied",
opts: PushOpts{
Force: false,
UpstreamRemote: "origin",
UpstreamBranch: "master",
},
PushOpts{Force: false, PromptUserForCredential: prompt},
func(err error) {
assert.Error(t, err)
},
},
{
"Push with force disabled, upstream supplied",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push", "origin", "master"}, args)
return secureexec.Command("echo")
},
PushOpts{
Force: false,
UpstreamRemote: "origin",
UpstreamBranch: "master",
PromptUserForCredential: prompt,
},
func(err error) {
test: func(cmdObj oscommands.ICmdObj, err error) {
assert.Equal(t, cmdObj.ToString(), `git push "origin" "master"`)
assert.NoError(t, err)
},
},
{
"Push with force disabled, setting upstream",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push", "--set-upstream", "origin", "master"}, args)
return secureexec.Command("echo")
testName: "Push with force disabled, setting upstream",
opts: PushOpts{
Force: false,
UpstreamRemote: "origin",
UpstreamBranch: "master",
SetUpstream: true,
},
PushOpts{
Force: false,
UpstreamRemote: "origin",
UpstreamBranch: "master",
PromptUserForCredential: prompt,
SetUpstream: true,
},
func(err error) {
test: func(cmdObj oscommands.ICmdObj, err error) {
assert.Equal(t, cmdObj.ToString(), `git push --set-upstream "origin" "master"`)
assert.NoError(t, err)
},
},
{
"Push with force enabled, setting upstream",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push", "--force-with-lease", "--set-upstream", "origin", "master"}, args)
return secureexec.Command("echo")
testName: "Push with force enabled, setting upstream",
opts: PushOpts{
Force: true,
UpstreamRemote: "origin",
UpstreamBranch: "master",
SetUpstream: true,
},
PushOpts{
Force: true,
UpstreamRemote: "origin",
UpstreamBranch: "master",
PromptUserForCredential: prompt,
SetUpstream: true,
},
func(err error) {
test: func(cmdObj oscommands.ICmdObj, err error) {
assert.Equal(t, cmdObj.ToString(), `git push --force-with-lease --set-upstream "origin" "master"`)
assert.NoError(t, err)
},
},
{
"Push with remote branch but no origin",
func(cmd string, args ...string) *exec.Cmd {
return nil
testName: "Push with remote branch but no origin",
opts: PushOpts{
Force: true,
UpstreamRemote: "",
UpstreamBranch: "master",
SetUpstream: true,
},
PushOpts{
Force: true,
UpstreamRemote: "",
UpstreamBranch: "master",
PromptUserForCredential: prompt,
SetUpstream: true,
},
func(err error) {
test: func(cmdObj oscommands.ICmdObj, err error) {
assert.Error(t, err)
assert.EqualValues(t, "Must specify a remote if specifying a branch", err.Error())
},
},
{
"Push with force disabled, upstream supplied",
func(cmd string, args ...string) *exec.Cmd {
assert.EqualValues(t, "git", cmd)
assert.EqualValues(t, []string{"push", "origin", "master"}, args)
return secureexec.Command("echo")
},
PushOpts{
Force: false,
UpstreamRemote: "origin",
UpstreamBranch: "master",
PromptUserForCredential: prompt,
},
func(err error) {
assert.NoError(t, err)
},
},
}
for _, s := range scenarios {
t.Run(s.testName, func(t *testing.T) {
gitCmd := NewDummyGitCommand()
gitCmd.OSCommand.Command = s.command
err := gitCmd.Push(s.opts)
s.test(err)
gitCmd := NewDummyGitCommandWithRunner(oscommands.NewFakeRunner(t))
s.test(gitCmd.PushCmdObj(s.opts))
})
}
}

View File

@@ -5,19 +5,19 @@ import (
)
func (c *GitCommand) CreateLightweightTag(tagName string, commitSha string) error {
return c.RunCommand("git tag -- %s %s", c.OSCommand.Quote(tagName), commitSha)
return c.Cmd.New(fmt.Sprintf("git tag -- %s %s", c.OSCommand.Quote(tagName), commitSha)).Run()
}
func (c *GitCommand) CreateAnnotatedTag(tagName, commitSha, msg string) error {
return c.RunCommand("git tag %s %s -m %s", tagName, commitSha, c.OSCommand.Quote(msg))
return c.Cmd.New(fmt.Sprintf("git tag %s %s -m %s", tagName, commitSha, c.OSCommand.Quote(msg))).Run()
}
func (c *GitCommand) DeleteTag(tagName string) error {
return c.RunCommand("git tag -d %s", c.OSCommand.Quote(tagName))
return c.Cmd.New(fmt.Sprintf("git tag -d %s", c.OSCommand.Quote(tagName))).Run()
}
func (c *GitCommand) PushTag(remoteName string, tagName string, promptUserForCredential func(string) string) error {
cmdStr := fmt.Sprintf("git push %s %s", c.OSCommand.Quote(remoteName), c.OSCommand.Quote(tagName))
cmdObj := c.NewCmdObjFromStr(cmdStr)
cmdObj := c.Cmd.New(cmdStr)
return c.DetectUnamePass(cmdObj, promptUserForCredential)
}

View File

@@ -0,0 +1,14 @@
package enums
type RebaseMode int
const (
// this means we're neither rebasing nor merging
REBASE_MODE_NONE RebaseMode = iota
// this means normal rebase as opposed to interactive rebase
REBASE_MODE_NORMAL
REBASE_MODE_INTERACTIVE
// REBASE_MODE_REBASING is a general state that captures both REBASE_MODE_NORMAL and REBASE_MODE_INTERACTIVE
REBASE_MODE_REBASING
REBASE_MODE_MERGING
)

15
pkg/common/common.go Normal file
View File

@@ -0,0 +1,15 @@
package common
import (
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/sirupsen/logrus"
)
// Commonly used things wrapped into one struct for convenience when passing it around
type Common struct {
Log *logrus.Entry
Tr *i18n.TranslationSet
UserConfig *config.UserConfig
Debug bool
}

View File

@@ -31,21 +31,21 @@ type AppConfig struct {
// from AppConfig and still be used by lazygit.
type AppConfigurer interface {
GetDebug() bool
// build info
GetVersion() string
GetCommit() string
GetBuildDate() string
GetName() string
GetBuildSource() string
GetUserConfig() *UserConfig
GetUserConfigPaths() []string
GetUserConfigDir() string
GetTempDir() string
ReloadUserConfig() error
GetAppState() *AppState
SaveAppState() error
SetIsNewRepo(bool)
GetIsNewRepo() bool
ReloadUserConfig() error
ShowCommandLogOnStartup() bool
}
// NewAppConfig makes a new app config
@@ -167,37 +167,22 @@ func loadUserConfig(configFiles []string, base *UserConfig) (*UserConfig, error)
return base, nil
}
// GetIsNewRepo returns known repo boolean
func (c *AppConfig) GetIsNewRepo() bool {
return c.IsNewRepo
}
// SetIsNewRepo set if the current repo is known
func (c *AppConfig) SetIsNewRepo(toSet bool) {
c.IsNewRepo = toSet
}
// GetDebug returns debug flag
func (c *AppConfig) GetDebug() bool {
return c.Debug
}
// GetVersion returns debug flag
func (c *AppConfig) GetVersion() string {
return c.Version
}
// GetCommit returns debug flag
func (c *AppConfig) GetCommit() string {
return c.Commit
}
// GetBuildDate returns debug flag
func (c *AppConfig) GetBuildDate() string {
return c.BuildDate
}
// GetName returns debug flag
func (c *AppConfig) GetName() string {
return c.Name
}
@@ -226,10 +211,6 @@ func (c *AppConfig) GetUserConfigDir() string {
return c.UserConfigDir
}
func (c *AppConfig) GetTempDir() string {
return c.TempDir
}
func (c *AppConfig) ReloadUserConfig() error {
userConfig, err := loadUserConfigWithDefaults(c.UserConfigPaths)
if err != nil {
@@ -277,17 +258,6 @@ func (c *AppConfig) SaveAppState() error {
return err
}
// originally we could only hide the command log permanently via the config
// but now we do it via state. So we need to still support the config for the
// sake of backwards compatibility
func (c *AppConfig) ShowCommandLogOnStartup() bool {
if !c.UserConfig.Gui.ShowCommandLog {
return false
}
return !c.AppState.HideCommandLog
}
// loadAppState loads recorded AppState from file
func loadAppState() (*AppState, error) {
filepath, err := configFilePath("state.yml")

View File

@@ -44,7 +44,7 @@ func (gui *Gui) getMidSectionWeights() (int, int) {
currentWindow := gui.currentWindow()
// we originally specified this as a ratio i.e. .20 would correspond to a weight of 1 against 4
sidePanelWidthRatio := gui.Config.GetUserConfig().Gui.SidePanelWidth
sidePanelWidthRatio := gui.UserConfig.Gui.SidePanelWidth
// we could make this better by creating ratios like 2:3 rather than always 1:something
mainSectionWeight := int(1/sidePanelWidthRatio) - 1
sideSectionWeight := 1
@@ -115,7 +115,7 @@ func (gui *Gui) splitMainPanelSideBySide() bool {
return false
}
mainPanelSplitMode := gui.Config.GetUserConfig().Gui.MainPanelSplitMode
mainPanelSplitMode := gui.UserConfig.Gui.MainPanelSplitMode
width, height := gui.g.Size()
switch mainPanelSplitMode {
@@ -143,7 +143,7 @@ func (gui *Gui) getExtrasWindowSize(screenHeight int) int {
} else if screenHeight < 40 {
baseSize = 1
} else {
baseSize = gui.Config.GetUserConfig().Gui.CommandLogSize
baseSize = gui.UserConfig.Gui.CommandLogSize
}
frameSize := 2
@@ -259,7 +259,7 @@ func (gui *Gui) sidePanelChildren(width int, height int) []*boxlayout.Box {
fullHeightBox("stash"),
}
} else if height >= 28 {
accordionMode := gui.Config.GetUserConfig().Gui.ExpandFocusedSidePanel
accordionMode := gui.UserConfig.Gui.ExpandFocusedSidePanel
accordionBox := func(defaultBox *boxlayout.Box) *boxlayout.Box {
if accordionMode && defaultBox.Window == currentWindow {
return &boxlayout.Box{

View File

@@ -32,11 +32,9 @@ func (gui *Gui) branchesRenderToMain() error {
if branch == nil {
task = NewRenderStringTask(gui.Tr.NoBranchesThisRepo)
} else {
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.GetBranchGraphCmdStr(branch.Name),
)
cmdObj := gui.GitCommand.GetBranchGraphCmdObj(branch.Name)
task = NewRunPtyTask(cmd)
task = NewRunPtyTask(cmdObj.GetCmd())
}
return gui.refreshMainViews(refreshMainOpts{
@@ -57,17 +55,13 @@ func (gui *Gui) refreshBranches() {
// which allows us to order them correctly. So if we're filtering we'll just
// manually load all the reflog commits here
var err error
reflogCommits, _, err = gui.GitCommand.GetReflogCommits(nil, "")
reflogCommits, _, err = gui.GitCommand.Loaders.ReflogCommits.GetReflogCommits(nil, "")
if err != nil {
gui.Log.Error(err)
}
}
builder, err := commands.NewBranchListBuilder(gui.Log, gui.GitCommand, reflogCommits)
if err != nil {
_ = gui.surfaceError(err)
}
gui.State.Branches = builder.Build()
gui.State.Branches = gui.GitCommand.Loaders.Branches.Load(reflogCommits)
if err := gui.postRefreshUpdate(gui.State.Contexts.Branches); err != nil {
gui.Log.Error(err)

View File

@@ -41,11 +41,11 @@ func (gui *Gui) GetOnRunCommand() func(entry oscommands.CmdLogEntry) {
func (gui *Gui) printCommandLogHeader() {
introStr := fmt.Sprintf(
gui.Tr.CommandLogHeader,
gui.getKeyDisplay(gui.Config.GetUserConfig().Keybinding.Universal.ExtrasMenu),
gui.getKeyDisplay(gui.UserConfig.Keybinding.Universal.ExtrasMenu),
)
fmt.Fprintln(gui.Views.Extras, style.FgCyan.Sprint(introStr))
if gui.Config.GetUserConfig().Gui.ShowRandomTip {
if gui.UserConfig.Gui.ShowRandomTip {
fmt.Fprintf(
gui.Views.Extras,
"%s: %s",
@@ -56,7 +56,7 @@ func (gui *Gui) printCommandLogHeader() {
}
func (gui *Gui) getRandomTip() string {
config := gui.Config.GetUserConfig().Keybinding
config := gui.UserConfig.Keybinding
formattedKey := func(key string) string {
return gui.getKeyDisplay(key)

View File

@@ -45,10 +45,8 @@ func (gui *Gui) commitFilesRenderToMain() error {
to := gui.State.CommitFileManager.GetParent()
from, reverse := gui.getFromAndReverseArgsForDiff(to)
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowFileDiffCmdStr(from, to, reverse, node.GetPath(), false),
)
task := NewRunPtyTask(cmd)
cmdObj := gui.GitCommand.ShowFileDiffCmdObj(from, to, reverse, node.GetPath(), false)
task := NewRunPtyTask(cmdObj.GetCmd())
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
@@ -107,7 +105,7 @@ func (gui *Gui) refreshCommitFilesView() error {
to := gui.State.Panels.CommitFiles.refName
from, reverse := gui.getFromAndReverseArgsForDiff(to)
files, err := gui.GitCommand.GetFilesInDiff(from, to, reverse)
files, err := gui.GitCommand.Loaders.CommitFiles.GetFilesInDiff(from, to, reverse)
if err != nil {
return gui.surfaceError(err)
}

View File

@@ -15,19 +15,20 @@ func (gui *Gui) handleCommitConfirm() error {
return gui.createErrorPanel(gui.Tr.CommitWithoutMessageErr)
}
flags := []string{}
skipHookPrefix := gui.Config.GetUserConfig().Git.SkipHookPrefix
skipHookPrefix := gui.UserConfig.Git.SkipHookPrefix
if skipHookPrefix != "" && strings.HasPrefix(message, skipHookPrefix) {
flags = append(flags, "--no-verify")
}
if gui.Config.GetUserConfig().Git.Commit.SignOff {
if gui.UserConfig.Git.Commit.SignOff {
flags = append(flags, "--signoff")
}
cmdStr := gui.GitCommand.CommitCmdStr(message, strings.Join(flags, " "))
gui.OnRunCommand(oscommands.NewCmdLogEntry(cmdStr, gui.Tr.Spans.Commit, true))
cmdObj := gui.GitCommand.CommitCmdObj(message, strings.Join(flags, " "))
gui.OnRunCommand(oscommands.NewCmdLogEntry(cmdObj.ToString(), gui.Tr.Spans.Commit, true))
_ = gui.returnFromContext()
return gui.withGpgHandling(cmdStr, gui.Tr.CommittingStatus, func() error {
return gui.withGpgHandling(cmdObj, gui.Tr.CommittingStatus, func() error {
gui.Views.CommitMessage.ClearTextArea()
return nil
})
@@ -41,9 +42,9 @@ func (gui *Gui) handleCommitMessageFocused() error {
message := utils.ResolvePlaceholderString(
gui.Tr.CommitMessageConfirm,
map[string]string{
"keyBindClose": gui.getKeyDisplay(gui.Config.GetUserConfig().Keybinding.Universal.Return),
"keyBindConfirm": gui.getKeyDisplay(gui.Config.GetUserConfig().Keybinding.Universal.Confirm),
"keyBindNewLine": gui.getKeyDisplay(gui.Config.GetUserConfig().Keybinding.Universal.AppendNewline),
"keyBindClose": gui.getKeyDisplay(gui.UserConfig.Keybinding.Universal.Return),
"keyBindConfirm": gui.getKeyDisplay(gui.UserConfig.Keybinding.Universal.Confirm),
"keyBindNewLine": gui.getKeyDisplay(gui.UserConfig.Keybinding.Universal.AppendNewline),
},
)
@@ -57,7 +58,7 @@ func (gui *Gui) getBufferLength(view *gocui.View) string {
// RenderCommitLength is a function.
func (gui *Gui) RenderCommitLength() {
if !gui.Config.GetUserConfig().Gui.CommitLength.Show {
if !gui.UserConfig.Gui.CommitLength.Show {
return
}

View File

@@ -4,7 +4,7 @@ import (
"fmt"
"sync"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils"
@@ -46,10 +46,8 @@ func (gui *Gui) branchCommitsRenderToMain() error {
if commit == nil {
task = NewRenderStringTask(gui.Tr.NoCommitsThisBranch)
} else {
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.Modes.Filtering.GetPath()),
)
task = NewRunPtyTask(cmd)
cmdObj := gui.GitCommand.ShowCmdObj(commit.Sha, gui.State.Modes.Filtering.GetPath())
task = NewRunPtyTask(cmdObj.GetCmd())
}
return gui.refreshMainViews(refreshMainOpts{
@@ -121,10 +119,8 @@ func (gui *Gui) refreshCommitsWithLimit() error {
gui.Mutexes.BranchCommitsMutex.Lock()
defer gui.Mutexes.BranchCommitsMutex.Unlock()
builder := commands.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr)
commits, err := builder.GetCommits(
commands.GetCommitsOptions{
commits, err := gui.GitCommand.Loaders.Commits.GetCommits(
loaders.GetCommitsOptions{
Limit: gui.State.Panels.Commits.LimitCommits,
FilterPath: gui.State.Modes.Filtering.GetPath(),
IncludeRebaseCommits: true,
@@ -144,9 +140,7 @@ func (gui *Gui) refreshRebaseCommits() error {
gui.Mutexes.BranchCommitsMutex.Lock()
defer gui.Mutexes.BranchCommitsMutex.Unlock()
builder := commands.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr)
updatedCommits, err := builder.MergeRebasingCommits(gui.State.Commits)
updatedCommits, err := gui.GitCommand.Loaders.Commits.MergeRebasingCommits(gui.State.Commits)
if err != nil {
return err
}
@@ -735,7 +729,7 @@ func (gui *Gui) handleOpenLogMenu() error {
opensMenu: true,
onPress: func() error {
onSelect := func(value string) {
gui.Config.GetUserConfig().Git.Log.ShowGraph = value
gui.UserConfig.Git.Log.ShowGraph = value
gui.render()
}
return gui.createMenu(gui.Tr.LogMenuTitle, []*menuItem{
@@ -768,7 +762,7 @@ func (gui *Gui) handleOpenLogMenu() error {
opensMenu: true,
onPress: func() error {
onSelect := func(value string) error {
gui.Config.GetUserConfig().Git.Log.Order = value
gui.UserConfig.Git.Log.Order = value
return gui.WithWaitingStatus(gui.Tr.LcLoadingCommits, func() error {
return gui.refreshSidePanels(refreshOptions{mode: SYNC, scope: []RefreshableView{COMMITS}})
})

View File

@@ -173,7 +173,7 @@ func (gui *Gui) prepareConfirmationPanel(
suggestionsView.FgColor = theme.GocuiDefaultTextColor
gui.setSuggestions(findSuggestionsFunc(""))
suggestionsView.Visible = true
suggestionsView.Title = fmt.Sprintf(gui.Tr.SuggestionsTitle, gui.Config.GetUserConfig().Keybinding.Universal.TogglePanel)
suggestionsView.Title = fmt.Sprintf(gui.Tr.SuggestionsTitle, gui.UserConfig.Keybinding.Universal.TogglePanel)
}
gui.g.Update(func(g *gocui.Gui) error {
@@ -240,7 +240,7 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
handler func() error
}
keybindingConfig := gui.Config.GetUserConfig().Keybinding
keybindingConfig := gui.UserConfig.Keybinding
onSuggestionConfirm := gui.wrappedPromptConfirmationFunction(
opts.handlersManageFocus,
opts.handleConfirmPrompt,
@@ -305,7 +305,7 @@ func (gui *Gui) setKeyBindings(opts createPopupPanelOpts) error {
}
func (gui *Gui) clearConfirmationViewKeyBindings() {
keybindingConfig := gui.Config.GetUserConfig().Keybinding
keybindingConfig := gui.UserConfig.Keybinding
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Confirm), gocui.ModNone)
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.ConfirmAlt1), gocui.ModNone)
_ = gui.g.DeleteKeybinding("confirmation", gui.getKey(keybindingConfig.Universal.Return), gocui.ModNone)

View File

@@ -57,7 +57,7 @@ func (gui *Gui) handleCloseCredentialsView() error {
}
func (gui *Gui) handleCredentialsViewFocused() error {
keybindingConfig := gui.Config.GetUserConfig().Keybinding
keybindingConfig := gui.UserConfig.Keybinding
message := utils.ResolvePlaceholderString(
gui.Tr.CloseConfirm,

View File

@@ -203,7 +203,7 @@ func (gui *Gui) menuPromptFromCommand(prompt config.CustomCommandPrompt, promptR
}
// Run and save output
message, err := gui.GitCommand.RunCommandWithOutput(cmdStr)
message, err := gui.GitCommand.Cmd.New(cmdStr).RunWithOutput()
if err != nil {
return gui.surfaceError(err)
}
@@ -244,7 +244,7 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
}
if customCommand.Subprocess {
return gui.runSubprocessWithSuspenseAndRefresh(gui.OSCommand.PrepareShellSubProcess(cmdStr))
return gui.runSubprocessWithSuspenseAndRefresh(gui.OSCommand.Cmd.NewShell(cmdStr))
}
loadingText := customCommand.LoadingText
@@ -252,7 +252,8 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
loadingText = gui.Tr.LcRunningCustomCommandStatus
}
return gui.WithWaitingStatus(loadingText, func() error {
if err := gui.OSCommand.WithSpan(gui.Tr.Spans.CustomCommand).RunShellCommand(cmdStr); err != nil {
err := gui.OSCommand.WithSpan(gui.Tr.Spans.CustomCommand).Cmd.NewShell(cmdStr).Run()
if err != nil {
return gui.surfaceError(err)
}
return gui.refreshSidePanels(refreshOptions{})
@@ -295,7 +296,7 @@ func (gui *Gui) handleCustomCommandKeybinding(customCommand config.CustomCommand
func (gui *Gui) GetCustomCommandKeybindings() []*Binding {
bindings := []*Binding{}
customCommands := gui.Config.GetUserConfig().CustomCommands
customCommands := gui.UserConfig.CustomCommands
for _, customCommand := range customCommands {
var viewName string

View File

@@ -16,7 +16,7 @@ func (gui *Gui) IncreaseContextInDiffView() error {
return gui.surfaceError(err)
}
gui.Config.GetUserConfig().Git.DiffContextSize = gui.Config.GetUserConfig().Git.DiffContextSize + 1
gui.UserConfig.Git.DiffContextSize = gui.UserConfig.Git.DiffContextSize + 1
return gui.currentStaticContext().HandleRenderToMain()
}
@@ -24,14 +24,14 @@ func (gui *Gui) IncreaseContextInDiffView() error {
}
func (gui *Gui) DecreaseContextInDiffView() error {
old_size := gui.Config.GetUserConfig().Git.DiffContextSize
old_size := gui.UserConfig.Git.DiffContextSize
if isShowingDiff(gui) && old_size > 1 {
if err := gui.CheckCanChangeContext(); err != nil {
return gui.surfaceError(err)
}
gui.Config.GetUserConfig().Git.DiffContextSize = old_size - 1
gui.UserConfig.Git.DiffContextSize = old_size - 1
return gui.currentStaticContext().HandleRenderToMain()
}

View File

@@ -46,12 +46,12 @@ func TestIncreasesContextInDiffViewByOneInContextWithDiff(t *testing.T) {
gui := NewDummyGui()
context := c(gui)
setupGuiForTest(gui)
gui.Config.GetUserConfig().Git.DiffContextSize = 1
gui.UserConfig.Git.DiffContextSize = 1
_ = gui.pushContextDirect(context)
_ = gui.IncreaseContextInDiffView()
assert.Equal(t, 2, gui.Config.GetUserConfig().Git.DiffContextSize, string(context.GetKey()))
assert.Equal(t, 2, gui.UserConfig.Git.DiffContextSize, string(context.GetKey()))
}
}
@@ -72,12 +72,12 @@ func TestDoesntIncreaseContextInDiffViewInContextWithoutDiff(t *testing.T) {
gui := NewDummyGui()
context := c(gui)
setupGuiForTest(gui)
gui.Config.GetUserConfig().Git.DiffContextSize = 1
gui.UserConfig.Git.DiffContextSize = 1
_ = gui.pushContextDirect(context)
_ = gui.IncreaseContextInDiffView()
assert.Equal(t, 1, gui.Config.GetUserConfig().Git.DiffContextSize, string(context.GetKey()))
assert.Equal(t, 1, gui.UserConfig.Git.DiffContextSize, string(context.GetKey()))
}
}
@@ -96,12 +96,12 @@ func TestDecreasesContextInDiffViewByOneInContextWithDiff(t *testing.T) {
gui := NewDummyGui()
context := c(gui)
setupGuiForTest(gui)
gui.Config.GetUserConfig().Git.DiffContextSize = 2
gui.UserConfig.Git.DiffContextSize = 2
_ = gui.pushContextDirect(context)
_ = gui.DecreaseContextInDiffView()
assert.Equal(t, 1, gui.Config.GetUserConfig().Git.DiffContextSize, string(context.GetKey()))
assert.Equal(t, 1, gui.UserConfig.Git.DiffContextSize, string(context.GetKey()))
}
}
@@ -122,19 +122,19 @@ func TestDoesntDecreaseContextInDiffViewInContextWithoutDiff(t *testing.T) {
gui := NewDummyGui()
context := c(gui)
setupGuiForTest(gui)
gui.Config.GetUserConfig().Git.DiffContextSize = 2
gui.UserConfig.Git.DiffContextSize = 2
_ = gui.pushContextDirect(context)
_ = gui.DecreaseContextInDiffView()
assert.Equal(t, 2, gui.Config.GetUserConfig().Git.DiffContextSize, string(context.GetKey()))
assert.Equal(t, 2, gui.UserConfig.Git.DiffContextSize, string(context.GetKey()))
}
}
func TestDoesntIncreaseContextInDiffViewInContextWhenInPatchBuildingMode(t *testing.T) {
gui := NewDummyGui()
setupGuiForTest(gui)
gui.Config.GetUserConfig().Git.DiffContextSize = 2
gui.UserConfig.Git.DiffContextSize = 2
_ = gui.pushContextDirect(gui.State.Contexts.CommitFiles)
gui.GitCommand.PatchManager.Start("from", "to", false, false)
@@ -150,13 +150,13 @@ func TestDoesntIncreaseContextInDiffViewInContextWhenInPatchBuildingMode(t *test
_ = gui.IncreaseContextInDiffView()
assert.Equal(t, 1, errorCount)
assert.Equal(t, 2, gui.Config.GetUserConfig().Git.DiffContextSize)
assert.Equal(t, 2, gui.UserConfig.Git.DiffContextSize)
}
func TestDoesntDecreaseContextInDiffViewInContextWhenInPatchBuildingMode(t *testing.T) {
gui := NewDummyGui()
setupGuiForTest(gui)
gui.Config.GetUserConfig().Git.DiffContextSize = 2
gui.UserConfig.Git.DiffContextSize = 2
_ = gui.pushContextDirect(gui.State.Contexts.CommitFiles)
gui.GitCommand.PatchManager.Start("from", "to", false, false)
@@ -171,15 +171,15 @@ func TestDoesntDecreaseContextInDiffViewInContextWhenInPatchBuildingMode(t *test
_ = gui.DecreaseContextInDiffView()
assert.Equal(t, 2, gui.Config.GetUserConfig().Git.DiffContextSize)
assert.Equal(t, 2, gui.UserConfig.Git.DiffContextSize)
}
func TestDecreasesContextInDiffViewNoFurtherThanOne(t *testing.T) {
gui := NewDummyGui()
setupGuiForTest(gui)
gui.Config.GetUserConfig().Git.DiffContextSize = 1
gui.UserConfig.Git.DiffContextSize = 1
_ = gui.DecreaseContextInDiffView()
assert.Equal(t, 1, gui.Config.GetUserConfig().Git.DiffContextSize)
assert.Equal(t, 1, gui.UserConfig.Git.DiffContextSize)
}

View File

@@ -13,10 +13,10 @@ func (gui *Gui) exitDiffMode() error {
}
func (gui *Gui) renderDiff() error {
cmd := gui.OSCommand.ExecutableFromString(
cmdObj := gui.OSCommand.Cmd.New(
fmt.Sprintf("git diff --submodule --no-ext-diff --color %s", gui.diffStr()),
)
task := NewRunPtyTask(cmd)
task := NewRunPtyTask(cmdObj.GetCmd())
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{

View File

@@ -4,7 +4,6 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/updates"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -12,12 +11,12 @@ import (
// NewDummyGui creates a new dummy GUI for testing
func NewDummyUpdater() *updates.Updater {
newAppConfig := config.NewDummyAppConfig()
dummyUpdater, _ := updates.NewUpdater(utils.NewDummyLog(), newAppConfig, oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog(), newAppConfig.GetUserConfig().Gui.Language))
dummyUpdater, _ := updates.NewUpdater(utils.NewDummyCommon(), newAppConfig, oscommands.NewDummyOSCommand())
return dummyUpdater
}
func NewDummyGui() *Gui {
newAppConfig := config.NewDummyAppConfig()
dummyGui, _ := NewGui(utils.NewDummyLog(), commands.NewDummyGitCommand(), oscommands.NewDummyOSCommand(), i18n.NewTranslationSet(utils.NewDummyLog(), newAppConfig.GetUserConfig().Gui.Language), newAppConfig, NewDummyUpdater(), "", false)
dummyGui, _ := NewGui(utils.NewDummyCommon(), commands.NewDummyGitCommand(), oscommands.NewDummyOSCommand(), newAppConfig, NewDummyUpdater(), "", false)
return dummyGui
}

View File

@@ -7,7 +7,7 @@ import (
)
func (gui *Gui) handleEditorKeypress(textArea *gocui.TextArea, key gocui.Key, ch rune, mod gocui.Modifier, allowMultiline bool) bool {
newlineKey, ok := gui.getKey(gui.Config.GetUserConfig().Keybinding.Universal.AppendNewline).(gocui.Key)
newlineKey, ok := gui.getKey(gui.UserConfig.Keybinding.Universal.AppendNewline).(gocui.Key)
if !ok {
newlineKey = gocui.KeyAltEnter
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/config"
@@ -58,22 +59,20 @@ func (gui *Gui) filesRenderToMain() error {
return gui.refreshMergePanelWithLock()
}
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(node, false, !node.GetHasUnstagedChanges() && node.GetHasStagedChanges(), gui.State.IgnoreWhitespaceInDiffView)
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
cmdObj := gui.GitCommand.WorktreeFileDiffCmdObj(node, false, !node.GetHasUnstagedChanges() && node.GetHasStagedChanges(), gui.State.IgnoreWhitespaceInDiffView)
refreshOpts := refreshMainOpts{main: &viewUpdateOpts{
title: gui.Tr.UnstagedChanges,
task: NewRunPtyTask(cmd),
task: NewRunPtyTask(cmdObj.GetCmd()),
}}
if node.GetHasUnstagedChanges() {
if node.GetHasStagedChanges() {
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(node, false, true, gui.State.IgnoreWhitespaceInDiffView)
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
cmdObj := gui.GitCommand.WorktreeFileDiffCmdObj(node, false, true, gui.State.IgnoreWhitespaceInDiffView)
refreshOpts.secondary = &viewUpdateOpts{
title: gui.Tr.StagedChanges,
task: NewRunPtyTask(cmd),
task: NewRunPtyTask(cmdObj.GetCmd()),
}
}
} else {
@@ -332,7 +331,7 @@ func (gui *Gui) handleIgnoreFile() error {
}
func (gui *Gui) handleWIPCommitPress() error {
skipHookPrefix := gui.Config.GetUserConfig().Git.SkipHookPrefix
skipHookPrefix := gui.UserConfig.Git.SkipHookPrefix
if skipHookPrefix == "" {
return gui.createErrorPanel(gui.Tr.SkipHookPrefixNotConfigured)
}
@@ -346,7 +345,7 @@ func (gui *Gui) handleWIPCommitPress() error {
}
func (gui *Gui) commitPrefixConfigForRepo() *config.CommitPrefixConfig {
cfg, ok := gui.Config.GetUserConfig().Git.CommitPrefixes[utils.GetCurrentRepoName()]
cfg, ok := gui.UserConfig.Git.CommitPrefixes[utils.GetCurrentRepoName()]
if !ok {
return nil
}
@@ -356,7 +355,7 @@ func (gui *Gui) commitPrefixConfigForRepo() *config.CommitPrefixConfig {
func (gui *Gui) prepareFilesForCommit() error {
noStagedFiles := len(gui.stagedFiles()) == 0
if noStagedFiles && gui.Config.GetUserConfig().Gui.SkipNoStagedFilesWarning {
if noStagedFiles && gui.UserConfig.Gui.SkipNoStagedFilesWarning {
err := gui.GitCommand.WithSpan(gui.Tr.Spans.StageAllFiles).StageAll()
if err != nil {
return err
@@ -440,9 +439,9 @@ func (gui *Gui) handleAmendCommitPress() error {
title: strings.Title(gui.Tr.AmendLastCommit),
prompt: gui.Tr.SureToAmend,
handleConfirm: func() error {
cmdStr := gui.GitCommand.AmendHeadCmdStr()
gui.OnRunCommand(oscommands.NewCmdLogEntry(cmdStr, gui.Tr.Spans.AmendCommit, true))
return gui.withGpgHandling(cmdStr, gui.Tr.AmendingStatus, nil)
cmdObj := gui.GitCommand.AmendHeadCmdObj()
gui.OnRunCommand(oscommands.NewCmdLogEntry(cmdObj.ToString(), gui.Tr.Spans.AmendCommit, true))
return gui.withGpgHandling(cmdObj, gui.Tr.AmendingStatus, nil)
},
})
}
@@ -460,12 +459,14 @@ func (gui *Gui) handleCommitEditorPress() error {
args := []string{"commit"}
if gui.Config.GetUserConfig().Git.Commit.SignOff {
if gui.UserConfig.Git.Commit.SignOff {
args = append(args, "--signoff")
}
cmdStr := "git " + strings.Join(args, " ")
return gui.runSubprocessWithSuspenseAndRefresh(
gui.OSCommand.WithSpan(gui.Tr.Spans.Commit).PrepareSubProcess("git", args...),
gui.GitCommand.WithSpan(gui.Tr.Spans.Commit).Cmd.New(cmdStr).Log(),
)
}
@@ -511,7 +512,7 @@ func (gui *Gui) editFileAtLine(filename string, lineNumber int) error {
}
return gui.runSubprocessWithSuspenseAndRefresh(
gui.OSCommand.WithSpan(gui.Tr.Spans.EditFile).ShellCommandFromString(cmdStr),
gui.OSCommand.WithSpan(gui.Tr.Spans.EditFile).Cmd.NewShell(cmdStr),
)
}
@@ -553,7 +554,9 @@ func (gui *Gui) refreshStateFiles() error {
prevNodes := gui.State.FileManager.GetAllItems()
prevSelectedLineIdx := gui.State.Panels.Files.SelectedLineIdx
files := gui.GitCommand.GetStatusFiles(commands.GetStatusFileOptions{})
files := loaders.
NewFileLoader(gui.Common, gui.GitCommand.Cmd, gui.GitCommand.GitConfig).
GetStatusFiles(loaders.GetStatusFileOptions{})
// for when you stage the old file of a rename and the new file is in a collapsed dir
state.FileManager.RWMutex.Lock()
@@ -733,15 +736,14 @@ func (gui *Gui) push(opts pushOpts) error {
}
go utils.Safe(func() {
err := gui.GitCommand.WithSpan(gui.Tr.Spans.Push).Push(commands.PushOpts{
Force: opts.force,
UpstreamRemote: opts.upstreamRemote,
UpstreamBranch: opts.upstreamBranch,
SetUpstream: opts.setUpstream,
PromptUserForCredential: gui.promptUserForCredential,
})
Force: opts.force,
UpstreamRemote: opts.upstreamRemote,
UpstreamBranch: opts.upstreamBranch,
SetUpstream: opts.setUpstream,
}, gui.promptUserForCredential)
if err != nil && !opts.force && strings.Contains(err.Error(), "Updates were rejected") {
forcePushDisabled := gui.Config.GetUserConfig().Git.DisableForcePushing
forcePushDisabled := gui.UserConfig.Git.DisableForcePushing
if forcePushDisabled {
_ = gui.createErrorPanel(gui.Tr.UpdatesRejectedAndForcePushDisabled)
return
@@ -846,7 +848,7 @@ func getSuggestedRemote(remotes []*models.Remote) string {
}
func (gui *Gui) requestToForcePush() error {
forcePushDisabled := gui.Config.GetUserConfig().Git.DisableForcePushing
forcePushDisabled := gui.UserConfig.Git.DisableForcePushing
if forcePushDisabled {
return gui.createErrorPanel(gui.Tr.ForcePushDisabled)
}
@@ -923,7 +925,7 @@ func (gui *Gui) handleCustomCommand() error {
gui.OnRunCommand(oscommands.NewCmdLogEntry(command, gui.Tr.Spans.CustomCommand, true))
return gui.runSubprocessWithSuspenseAndRefresh(
gui.OSCommand.PrepareShellSubProcess(command),
gui.OSCommand.Cmd.NewShell(command),
)
},
})
@@ -1004,7 +1006,7 @@ func (gui *Gui) handleOpenMergeTool() error {
prompt: gui.Tr.MergeToolPrompt,
handleConfirm: func() error {
return gui.runSubprocessWithSuspenseAndRefresh(
gui.OSCommand.ExecutableFromString(gui.GitCommand.OpenMergeToolCmd()),
gui.GitCommand.OpenMergeToolCmdObj(),
)
},
})

View File

@@ -32,7 +32,7 @@ func (gui *Gui) gitFlowFinishBranch(gitFlowConfig string, branchName string) err
}
return gui.runSubprocessWithSuspenseAndRefresh(
gui.OSCommand.WithSpan(gui.Tr.Spans.GitFlowFinish).PrepareSubProcess("git", "flow", branchType, "finish", suffix),
gui.GitCommand.WithSpan(gui.Tr.Spans.GitFlowFinish).Cmd.New("git flow " + branchType + " finish " + suffix).Log(),
)
}
@@ -43,7 +43,7 @@ func (gui *Gui) handleCreateGitFlowMenu() error {
}
// get config
gitFlowConfig, err := gui.GitCommand.RunCommandWithOutput("git config --local --get-regexp gitflow")
gitFlowConfig, err := gui.GitCommand.Cmd.New("git config --local --get-regexp gitflow").RunWithOutput()
if err != nil {
return gui.createErrorPanel("You need to install git-flow and enable it in this repo to use git-flow features")
}
@@ -56,7 +56,7 @@ func (gui *Gui) handleCreateGitFlowMenu() error {
title: title,
handleConfirm: func(name string) error {
return gui.runSubprocessWithSuspenseAndRefresh(
gui.OSCommand.WithSpan(gui.Tr.Spans.GitFlowStart).PrepareSubProcess("git", "flow", branchType, "start", name),
gui.GitCommand.WithSpan(gui.Tr.Spans.GitFlowStart).Cmd.New("git flow " + branchType + " start " + name).Log(),
)
},
})

View File

@@ -64,7 +64,7 @@ func (gui *Gui) prevScreenMode() error {
func (gui *Gui) scrollUpView(view *gocui.View) error {
ox, oy := view.Origin()
newOy := int(math.Max(0, float64(oy-gui.Config.GetUserConfig().Gui.ScrollHeight)))
newOy := int(math.Max(0, float64(oy-gui.UserConfig.Gui.ScrollHeight)))
return view.SetOrigin(ox, newOy)
}
@@ -86,12 +86,12 @@ func (gui *Gui) scrollDownView(view *gocui.View) error {
func (gui *Gui) linesToScrollDown(view *gocui.View) int {
_, oy := view.Origin()
y := oy
canScrollPastBottom := gui.Config.GetUserConfig().Gui.ScrollPastBottom
canScrollPastBottom := gui.UserConfig.Gui.ScrollPastBottom
if !canScrollPastBottom {
_, sy := view.Size()
y += sy
}
scrollHeight := gui.Config.GetUserConfig().Gui.ScrollHeight
scrollHeight := gui.UserConfig.Gui.ScrollHeight
scrollableLines := view.ViewLinesHeight() - y
if scrollableLines < 0 {
return 0

View File

@@ -3,6 +3,7 @@ package gui
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/gui/style"
)
@@ -10,12 +11,11 @@ import (
// WithWaitingStatus we get stuck there and can't return to lazygit. We could
// fix this bug, or just stop running subprocesses from within there, given that
// we don't need to see a loading status if we're in a subprocess.
func (gui *Gui) withGpgHandling(cmdStr string, waitingStatus string, onSuccess func() error) error {
// TODO: work out if we actually need to use a shell command here
func (gui *Gui) withGpgHandling(cmdObj oscommands.ICmdObj, waitingStatus string, onSuccess func() error) error {
useSubprocess := gui.GitCommand.UsingGpg()
if useSubprocess {
// Need to remember why we use the shell for the subprocess but not in the other case
// Maybe there's no good reason
success, err := gui.runSubprocessWithSuspense(gui.OSCommand.ShellCommandFromString(cmdStr))
success, err := gui.runSubprocessWithSuspense(gui.OSCommand.Cmd.NewShell(cmdObj.ToString()))
if success && onSuccess != nil {
if err := onSuccess(); err != nil {
return err
@@ -27,15 +27,16 @@ func (gui *Gui) withGpgHandling(cmdStr string, waitingStatus string, onSuccess f
return err
} else {
return gui.RunAndStream(cmdStr, waitingStatus, onSuccess)
return gui.RunAndStream(cmdObj, waitingStatus, onSuccess)
}
}
func (gui *Gui) RunAndStream(cmdStr string, waitingStatus string, onSuccess func() error) error {
func (gui *Gui) RunAndStream(cmdObj oscommands.ICmdObj, waitingStatus string, onSuccess func() error) error {
return gui.WithWaitingStatus(waitingStatus, func() error {
cmd := gui.OSCommand.ShellCommandFromString(cmdStr)
cmd.Env = append(cmd.Env, "TERM=dumb")
cmdObj := gui.OSCommand.Cmd.NewShell(cmdObj.ToString())
cmdObj.AddEnvVars("TERM=dumb")
cmdWriter := gui.getCmdWriter()
cmd := cmdObj.GetCmd()
cmd.Stdout = cmdWriter
cmd.Stderr = cmdWriter
@@ -46,7 +47,7 @@ func (gui *Gui) RunAndStream(cmdStr string, waitingStatus string, onSuccess func
_ = gui.refreshSidePanels(refreshOptions{mode: ASYNC})
return gui.surfaceError(
fmt.Errorf(
gui.Tr.GitCommandFailed, gui.Config.GetUserConfig().Keybinding.Universal.ExtrasMenu,
gui.Tr.GitCommandFailed, gui.UserConfig.Keybinding.Universal.ExtrasMenu,
),
)
}

View File

@@ -7,7 +7,6 @@ import (
"os"
"sync"
"os/exec"
"strings"
"time"
@@ -15,6 +14,7 @@ import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gui/filetree"
"github.com/jesseduffield/lazygit/pkg/gui/lbl"
@@ -26,12 +26,10 @@ import (
"github.com/jesseduffield/lazygit/pkg/gui/presentation/graph"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/tasks"
"github.com/jesseduffield/lazygit/pkg/theme"
"github.com/jesseduffield/lazygit/pkg/updates"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
"gopkg.in/ozeidan/fuzzy-patricia.v3/patricia"
)
@@ -68,8 +66,8 @@ type Repo string
// Gui wraps the gocui Gui object which handles rendering and events
type Gui struct {
*common.Common
g *gocui.Gui
Log *logrus.Entry
GitCommand *commands.GitCommand
OSCommand *oscommands.OSCommand
@@ -80,7 +78,6 @@ type Gui struct {
// gui state when returning from a subrepo
RepoStateMap map[Repo]*guiState
Config config.AppConfigurer
Tr *i18n.TranslationSet
Updater *updates.Updater
statusManager *statusManager
credentials credentials
@@ -123,6 +120,8 @@ type Gui struct {
suggestionsAsyncHandler *tasks.AsyncHandler
PopupHandler PopupHandler
IsNewRepo bool
}
type listPanelState struct {
@@ -374,7 +373,7 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
}
}
showTree := gui.Config.GetUserConfig().Gui.ShowFileTree
showTree := gui.UserConfig.Gui.ShowFileTree
contexts := gui.contextTree()
@@ -432,13 +431,13 @@ func (gui *Gui) resetState(filterPath string, reuseState bool) {
// for now the split view will always be on
// NewGui builds a new gui handler
func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *oscommands.OSCommand, tr *i18n.TranslationSet, config config.AppConfigurer, updater *updates.Updater, filterPath string, showRecentRepos bool) (*Gui, error) {
func NewGui(cmn *common.Common, gitCommand *commands.GitCommand, oSCommand *oscommands.OSCommand, config config.AppConfigurer, updater *updates.Updater, filterPath string, showRecentRepos bool) (*Gui, error) {
gui := &Gui{
Log: log,
Common: cmn,
GitCommand: gitCommand,
OSCommand: oSCommand,
Config: config,
Tr: tr,
Updater: updater,
statusManager: &statusManager{},
viewBufferManagerMap: map[string]*tasks.ViewBufferManager{},
@@ -446,8 +445,12 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *oscom
RepoPathStack: []string{},
RepoStateMap: map[Repo]*guiState{},
CmdLog: []string{},
ShowExtrasWindow: config.ShowCommandLogOnStartup(),
suggestionsAsyncHandler: tasks.NewAsyncHandler(),
// originally we could only hide the command log permanently via the config
// but now we do it via state. So we need to still support the config for the
// sake of backwards compatibility. We're making use of short circuiting here
ShowExtrasWindow: cmn.UserConfig.Gui.ShowCommandLog && !config.GetAppState().HideCommandLog,
}
gui.resetState(filterPath, false)
@@ -459,7 +462,7 @@ func NewGui(log *logrus.Entry, gitCommand *commands.GitCommand, oSCommand *oscom
gui.OnRunCommand = onRunCommand
gui.PopupHandler = &RealPopupHandler{gui: gui}
authors.SetCustomAuthors(gui.Config.GetUserConfig().Gui.AuthorColors)
authors.SetCustomAuthors(gui.UserConfig.Gui.AuthorColors)
return gui, nil
}
@@ -509,7 +512,7 @@ func (gui *Gui) Run() error {
if err := gui.Config.ReloadUserConfig(); err != nil {
return nil
}
userConfig := gui.Config.GetUserConfig()
userConfig := gui.UserConfig
g.SearchEscapeKey = gui.getKey(userConfig.Keybinding.Universal.Return)
g.NextSearchMatchKey = gui.getKey(userConfig.Keybinding.Universal.NextMatch)
g.PrevSearchMatchKey = gui.getKey(userConfig.Keybinding.Universal.PrevMatch)
@@ -525,7 +528,7 @@ func (gui *Gui) Run() error {
}
gui.waitForIntro.Add(1)
if gui.Config.GetUserConfig().Git.AutoFetch {
if gui.UserConfig.Git.AutoFetch {
go utils.Safe(gui.startBackgroundFetch)
}
@@ -578,7 +581,7 @@ func (gui *Gui) RunAndHandleError() error {
}
// returns whether command exited without error or not
func (gui *Gui) runSubprocessWithSuspenseAndRefresh(subprocess *exec.Cmd) error {
func (gui *Gui) runSubprocessWithSuspenseAndRefresh(subprocess oscommands.ICmdObj) error {
_, err := gui.runSubprocessWithSuspense(subprocess)
if err != nil {
return err
@@ -592,7 +595,7 @@ func (gui *Gui) runSubprocessWithSuspenseAndRefresh(subprocess *exec.Cmd) error
}
// returns whether command exited without error or not
func (gui *Gui) runSubprocessWithSuspense(subprocess *exec.Cmd) (bool, error) {
func (gui *Gui) runSubprocessWithSuspense(subprocess oscommands.ICmdObj) (bool, error) {
gui.Mutexes.SubprocessMutex.Lock()
defer gui.Mutexes.SubprocessMutex.Unlock()
@@ -621,7 +624,8 @@ func (gui *Gui) runSubprocessWithSuspense(subprocess *exec.Cmd) (bool, error) {
return cmdErr == nil, gui.surfaceError(cmdErr)
}
func (gui *Gui) runSubprocess(subprocess *exec.Cmd) error {
func (gui *Gui) runSubprocess(cmdObj oscommands.ICmdObj) error {
subprocess := cmdObj.GetCmd()
subprocess.Stdout = os.Stdout
subprocess.Stderr = os.Stdout
subprocess.Stdin = os.Stdin
@@ -710,8 +714,8 @@ func (gui *Gui) goEvery(interval time.Duration, stop chan struct{}, function fun
func (gui *Gui) startBackgroundFetch() {
gui.waitForIntro.Wait()
isNew := gui.Config.GetIsNewRepo()
userConfig := gui.Config.GetUserConfig()
isNew := gui.IsNewRepo
userConfig := gui.UserConfig
if !isNew {
time.After(time.Duration(userConfig.Refresher.FetchInterval) * time.Second)
}
@@ -732,7 +736,7 @@ func (gui *Gui) startBackgroundFetch() {
// setColorScheme sets the color scheme for the app based on the user config
func (gui *Gui) setColorScheme() error {
userConfig := gui.Config.GetUserConfig()
userConfig := gui.UserConfig
theme.UpdateTheme(userConfig.Gui.Theme)
gui.g.FgColor = theme.InactiveBorderColor

View File

@@ -204,7 +204,7 @@ func (gui *Gui) getKey(key string) interface{} {
// GetInitialKeybindings is a function.
func (gui *Gui) GetInitialKeybindings() []*Binding {
config := gui.Config.GetUserConfig().Keybinding
config := gui.UserConfig.Keybinding
bindings := []*Binding{
{

View File

@@ -374,7 +374,7 @@ func (gui *Gui) onInitialViewsCreation() error {
return err
}
if !gui.Config.GetUserConfig().DisableStartupPopups {
if !gui.UserConfig.DisableStartupPopups {
popupTasks := []func(chan struct{}) error{}
storedPopupVersion := gui.Config.GetAppState().StartupPopupVersion
if storedPopupVersion < StartupPopupVersion {

View File

@@ -145,7 +145,7 @@ func (gui *Gui) tagsListContext() IListContext {
}
func (gui *Gui) branchCommitsListContext() IListContext {
parseEmoji := gui.Config.GetUserConfig().Git.ParseEmoji
parseEmoji := gui.UserConfig.Git.ParseEmoji
return &ListContext{
BasicContext: &BasicContext{
ViewName: "commits",
@@ -188,7 +188,7 @@ func (gui *Gui) branchCommitsListContext() IListContext {
}
func (gui *Gui) subCommitsListContext() IListContext {
parseEmoji := gui.Config.GetUserConfig().Git.ParseEmoji
parseEmoji := gui.UserConfig.Git.ParseEmoji
return &ListContext{
BasicContext: &BasicContext{
ViewName: "branches",
@@ -229,7 +229,7 @@ func (gui *Gui) subCommitsListContext() IListContext {
}
func (gui *Gui) shouldShowGraph() bool {
value := gui.Config.GetUserConfig().Git.Log.ShowGraph
value := gui.UserConfig.Git.Log.ShowGraph
switch value {
case "always":
return true
@@ -244,7 +244,7 @@ func (gui *Gui) shouldShowGraph() bool {
}
func (gui *Gui) reflogCommitsListContext() IListContext {
parseEmoji := gui.Config.GetUserConfig().Git.ParseEmoji
parseEmoji := gui.UserConfig.Git.ParseEmoji
return &ListContext{
BasicContext: &BasicContext{
ViewName: "commits",
@@ -387,7 +387,7 @@ func (gui *Gui) getListContexts() []IListContext {
func (gui *Gui) getListContextKeyBindings() []*Binding {
bindings := make([]*Binding, 0)
keybindingConfig := gui.Config.GetUserConfig().Keybinding
keybindingConfig := gui.UserConfig.Keybinding
for _, listContext := range gui.getListContexts() {
listContext := listContext

View File

@@ -29,7 +29,7 @@ func (i *menuItem) ID() string {
// specific functions
func (gui *Gui) getMenuOptions() map[string]string {
keybindingConfig := gui.Config.GetUserConfig().Keybinding
keybindingConfig := gui.UserConfig.Keybinding
return map[string]string{
gui.getKeyDisplay(keybindingConfig.Universal.Return): gui.Tr.LcClose,

View File

@@ -9,8 +9,8 @@ import (
"github.com/go-errors/errors"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/gui/mergeconflicts"
)
@@ -227,7 +227,7 @@ func (gui *Gui) centerYPos(view *gocui.View, y int) {
}
func (gui *Gui) getMergingOptions() map[string]string {
keybindingConfig := gui.Config.GetUserConfig().Keybinding
keybindingConfig := gui.UserConfig.Keybinding
return map[string]string{
fmt.Sprintf("%s %s", gui.getKeyDisplay(keybindingConfig.Universal.PrevItem), gui.getKeyDisplay(keybindingConfig.Universal.NextItem)): gui.Tr.LcSelectHunk,
@@ -262,7 +262,7 @@ func (gui *Gui) handleCompleteMerge() error {
}
// if we got conflicts after unstashing, we don't want to call any git
// commands to continue rebasing/merging here
if gui.GitCommand.WorkingTreeState() == commands.REBASE_MODE_NORMAL {
if gui.GitCommand.WorkingTreeState() == enums.REBASE_MODE_NONE {
return gui.handleEscapeMerge()
}
// if there are no more files with merge conflicts, we should ask whether the user wants to continue

View File

@@ -1,7 +1,7 @@
package gui
import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/gui/style"
)
@@ -61,7 +61,7 @@ func (gui *Gui) modeStatuses() []modeStatus {
},
{
isActive: func() bool {
return gui.GitCommand.WorkingTreeState() != commands.REBASE_MODE_NORMAL
return gui.GitCommand.WorkingTreeState() != enums.REBASE_MODE_NONE
},
description: func() string {
workingTreeState := gui.GitCommand.WorkingTreeState()

View File

@@ -3,7 +3,7 @@ package gui
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
)
func (gui *Gui) handleCreatePatchOptionsMenu() error {
@@ -26,7 +26,7 @@ func (gui *Gui) handleCreatePatchOptionsMenu() error {
},
}
if gui.GitCommand.PatchManager.CanRebase && gui.workingTreeState() == commands.REBASE_MODE_NORMAL {
if gui.GitCommand.PatchManager.CanRebase && gui.GitCommand.WorkingTreeState() == enums.REBASE_MODE_NONE {
menuItems = append(menuItems, []*menuItem{
{
displayString: fmt.Sprintf("remove patch from original commit (%s)", gui.GitCommand.PatchManager.To),
@@ -74,7 +74,7 @@ func (gui *Gui) getPatchCommitIndex() int {
}
func (gui *Gui) validateNormalWorkingTreeState() (bool, error) {
if gui.GitCommand.WorkingTreeState() != commands.REBASE_MODE_NORMAL {
if gui.GitCommand.WorkingTreeState() != enums.REBASE_MODE_NONE {
return false, gui.createErrorPanel(gui.Tr.CantPatchWhileRebasingError)
}
return true, nil

View File

@@ -73,6 +73,6 @@ func (gui *Gui) createPullRequest(from string, to string) error {
func (gui *Gui) getHostingServiceMgr() *hosting_service.HostingServiceMgr {
remoteUrl := gui.GitCommand.GetRemoteURL()
configServices := gui.Config.GetUserConfig().Services
configServices := gui.UserConfig.Services
return hosting_service.NewHostingServiceMgr(gui.Log, gui.Tr, remoteUrl, configServices)
}

View File

@@ -60,7 +60,7 @@ func (gui *Gui) handleTopLevelReturn() error {
return gui.dispatchSwitchToRepo(path, true)
}
if gui.Config.GetUserConfig().QuitOnTopLevelReturn {
if gui.UserConfig.QuitOnTopLevelReturn {
return gui.handleQuit()
}
@@ -72,7 +72,7 @@ func (gui *Gui) quit() error {
return gui.createUpdateQuitConfirmation()
}
if gui.Config.GetUserConfig().ConfirmOnQuit {
if gui.UserConfig.ConfirmOnQuit {
return gui.ask(askOpts{
title: "",
prompt: gui.Tr.ConfirmQuit,

View File

@@ -4,7 +4,7 @@ import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
)
type RebaseOption string
@@ -18,7 +18,7 @@ const (
func (gui *Gui) handleCreateRebaseOptionsMenu() error {
options := []string{REBASE_OPTION_CONTINUE, REBASE_OPTION_ABORT}
if gui.GitCommand.WorkingTreeState() == commands.REBASE_MODE_REBASING {
if gui.GitCommand.WorkingTreeState() == enums.REBASE_MODE_REBASING {
options = append(options, REBASE_OPTION_SKIP)
}
@@ -35,7 +35,7 @@ func (gui *Gui) handleCreateRebaseOptionsMenu() error {
}
var title string
if gui.GitCommand.WorkingTreeState() == commands.REBASE_MODE_MERGING {
if gui.GitCommand.WorkingTreeState() == enums.REBASE_MODE_MERGING {
title = gui.Tr.MergeOptionsTitle
} else {
title = gui.Tr.RebaseOptionsTitle
@@ -47,18 +47,25 @@ func (gui *Gui) handleCreateRebaseOptionsMenu() error {
func (gui *Gui) genericMergeCommand(command string) error {
status := gui.GitCommand.WorkingTreeState()
if status != commands.REBASE_MODE_MERGING && status != commands.REBASE_MODE_REBASING {
if status != enums.REBASE_MODE_MERGING && status != enums.REBASE_MODE_REBASING {
return gui.createErrorPanel(gui.Tr.NotMergingOrRebasing)
}
gitCommand := gui.GitCommand.WithSpan(fmt.Sprintf("Merge/Rebase: %s", command))
commandType := strings.Replace(status, "ing", "e", 1)
commandType := ""
switch status {
case enums.REBASE_MODE_MERGING:
commandType = "merge"
case enums.REBASE_MODE_REBASING:
commandType = "rebase"
}
// we should end up with a command like 'git merge --continue'
// it's impossible for a rebase to require a commit so we'll use a subprocess only if it's a merge
if status == commands.REBASE_MODE_MERGING && command != REBASE_OPTION_ABORT && gui.Config.GetUserConfig().Git.Merging.ManualCommit {
sub := gitCommand.OSCommand.PrepareSubProcess("git", commandType, fmt.Sprintf("--%s", command))
if status == enums.REBASE_MODE_MERGING && command != REBASE_OPTION_ABORT && gui.UserConfig.Git.Merging.ManualCommit {
sub := gitCommand.Cmd.New("git " + commandType + " --" + command)
if sub != nil {
return gui.runSubprocessWithSuspenseAndRefresh(sub)
}
@@ -137,9 +144,9 @@ func (gui *Gui) abortMergeOrRebaseWithConfirm() error {
func (gui *Gui) workingTreeStateNoun() string {
workingTreeState := gui.GitCommand.WorkingTreeState()
switch workingTreeState {
case commands.REBASE_MODE_NORMAL:
case enums.REBASE_MODE_NONE:
return ""
case commands.REBASE_MODE_MERGING:
case enums.REBASE_MODE_MERGING:
return "merge"
default:
return "rebase"

View File

@@ -38,10 +38,10 @@ func (gui *Gui) handleCreateRecentReposMenu() error {
}
func (gui *Gui) handleShowAllBranchLogs() error {
cmd := gui.OSCommand.ExecutableFromString(
gui.Config.GetUserConfig().Git.AllBranchesLogCmd,
cmdObj := gui.OSCommand.Cmd.New(
gui.UserConfig.Git.AllBranchesLogCmd,
)
task := NewRunPtyTask(cmd)
task := NewRunPtyTask(cmdObj.GetCmd())
return gui.refreshMainViews(refreshMainOpts{
main: &viewUpdateOpts{
@@ -73,7 +73,7 @@ func (gui *Gui) dispatchSwitchToRepo(path string, reuse bool) error {
return err
}
newGitCommand, err := commands.NewGitCommand(gui.Log, gui.OSCommand, gui.Tr, gui.Config, git_config.NewStdCachedGitConfig(gui.Log))
newGitCommand, err := commands.NewGitCommand(gui.Common, gui.OSCommand, git_config.NewStdCachedGitConfig(gui.Log))
if err != nil {
return err
}
@@ -113,7 +113,7 @@ func (gui *Gui) updateRecentRepoList() error {
return err
}
known, recentRepos := newRecentReposList(recentRepos, currentRepo)
gui.Config.SetIsNewRepo(known)
gui.IsNewRepo = known
gui.Config.GetAppState().RecentRepos = recentRepos
return gui.Config.SaveAppState()
}

View File

@@ -1,6 +1,7 @@
package gui
import (
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/models"
)
@@ -22,11 +23,9 @@ func (gui *Gui) reflogCommitsRenderToMain() error {
if commit == nil {
task = NewRenderStringTask("No reflog history")
} else {
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.Modes.Filtering.GetPath()),
)
cmdObj := gui.GitCommand.ShowCmdObj(commit.Sha, gui.State.Modes.Filtering.GetPath())
task = NewRunPtyTask(cmd)
task = NewRunPtyTask(cmdObj.GetCmd())
}
return gui.refreshMainViews(refreshMainOpts{
@@ -54,7 +53,9 @@ func (gui *Gui) refreshReflogCommits() error {
}
refresh := func(stateCommits *[]*models.Commit, filterPath string) error {
commits, onlyObtainedNewReflogCommits, err := gui.GitCommand.GetReflogCommits(lastReflogCommit, filterPath)
commits, onlyObtainedNewReflogCommits, err := loaders.
NewReflogCommitLoader(gui.Common, gui.GitCommand.Cmd).
GetReflogCommits(lastReflogCommit, filterPath)
if err != nil {
return gui.surfaceError(err)
}

View File

@@ -24,10 +24,8 @@ func (gui *Gui) remoteBranchesRenderToMain() error {
if remoteBranch == nil {
task = NewRenderStringTask("No branches for this remote")
} else {
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.GetBranchGraphCmdStr(remoteBranch.FullName()),
)
task = NewRunCommandTask(cmd)
cmdObj := gui.GitCommand.GetBranchGraphCmdObj(remoteBranch.FullName())
task = NewRunCommandTask(cmdObj.GetCmd())
}
return gui.refreshMainViews(refreshMainOpts{

View File

@@ -40,7 +40,7 @@ func (gui *Gui) remotesRenderToMain() error {
func (gui *Gui) refreshRemotes() error {
prevSelectedRemote := gui.getSelectedRemote()
remotes, err := gui.GitCommand.GetRemotes()
remotes, err := gui.GitCommand.Loaders.Remotes.GetRemotes()
if err != nil {
return gui.surfaceError(err)
}

View File

@@ -3,12 +3,11 @@ package gui
import (
"fmt"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/gui/style"
)
func (gui *Gui) resetToRef(ref string, strength string, span string, options oscommands.RunCommandOptions) error {
if err := gui.GitCommand.WithSpan(span).ResetToCommit(ref, strength, options); err != nil {
func (gui *Gui) resetToRef(ref string, strength string, span string, envVars []string) error {
if err := gui.GitCommand.WithSpan(span).ResetToCommit(ref, strength, envVars); err != nil {
return gui.surfaceError(err)
}
@@ -39,7 +38,7 @@ func (gui *Gui) createResetMenu(ref string) error {
style.FgRed.Sprintf("reset --%s %s", strength, ref),
},
onPress: func() error {
return gui.resetToRef(ref, strength, "Reset", oscommands.RunCommandOptions{})
return gui.resetToRef(ref, strength, "Reset", []string{})
},
}
}

View File

@@ -43,7 +43,7 @@ func (gui *Gui) handleSearch() error {
}
func (gui *Gui) onSelectItemWrapper(innerFunc func(int) error) func(int, int, int) error {
keybindingConfig := gui.Config.GetUserConfig().Keybinding
keybindingConfig := gui.UserConfig.Keybinding
return func(y int, index int, total int) error {
if total == 0 {

View File

@@ -100,7 +100,7 @@ func (gui *Gui) handleResetSelection() error {
return gui.applySelection(true, state)
}
if !gui.Config.GetUserConfig().Gui.SkipUnstageLineWarning {
if !gui.UserConfig.Gui.SkipUnstageLineWarning {
return gui.ask(askOpts{
title: gui.Tr.UnstageLinesTitle,
prompt: gui.Tr.UnstageLinesPrompt,

View File

@@ -1,6 +1,7 @@
package gui
import (
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -22,10 +23,10 @@ func (gui *Gui) stashRenderToMain() error {
if stashEntry == nil {
task = NewRenderStringTask(gui.Tr.NoStashEntries)
} else {
cmd := gui.OSCommand.ExecutableFromString(
cmdObj := gui.OSCommand.Cmd.New(
gui.GitCommand.ShowStashEntryCmdStr(stashEntry.Index),
)
task = NewRunPtyTask(cmd)
task = NewRunPtyTask(cmdObj.GetCmd())
}
return gui.refreshMainViews(refreshMainOpts{
@@ -37,7 +38,9 @@ func (gui *Gui) stashRenderToMain() error {
}
func (gui *Gui) refreshStashEntries() error {
gui.State.StashEntries = gui.GitCommand.GetStashEntries(gui.State.Modes.Filtering.GetPath())
gui.State.StashEntries = loaders.
NewStashLoader(gui.Common, gui.GitCommand.Cmd).
GetStashEntries(gui.State.Modes.Filtering.GetPath())
return gui.State.Contexts.Stash.HandleRender()
}
@@ -45,7 +48,7 @@ func (gui *Gui) refreshStashEntries() error {
// specific functions
func (gui *Gui) handleStashApply() error {
skipStashWarning := gui.Config.GetUserConfig().Gui.SkipStashWarning
skipStashWarning := gui.UserConfig.Gui.SkipStashWarning
apply := func() error {
return gui.stashDo("apply")
@@ -65,7 +68,7 @@ func (gui *Gui) handleStashApply() error {
}
func (gui *Gui) handleStashPop() error {
skipStashWarning := gui.Config.GetUserConfig().Gui.SkipStashWarning
skipStashWarning := gui.UserConfig.Gui.SkipStashWarning
pop := func() error {
return gui.stashDo("pop")

View File

@@ -5,7 +5,7 @@ import (
"fmt"
"strings"
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/constants"
"github.com/jesseduffield/lazygit/pkg/gui/presentation"
"github.com/jesseduffield/lazygit/pkg/gui/style"
@@ -28,7 +28,7 @@ func (gui *Gui) refreshStatus() {
status += presentation.ColoredBranchStatus(currentBranch) + " "
}
if gui.GitCommand.WorkingTreeState() != commands.REBASE_MODE_NORMAL {
if gui.GitCommand.WorkingTreeState() != enums.REBASE_MODE_NONE {
status += style.FgYellow.Sprintf("(%s) ", gui.GitCommand.WorkingTreeState())
}
@@ -72,7 +72,7 @@ func (gui *Gui) handleStatusClick() error {
upstreamStatus := presentation.BranchStatus(currentBranch)
repoName := utils.GetCurrentRepoName()
switch gui.GitCommand.WorkingTreeState() {
case commands.REBASE_MODE_REBASING, commands.REBASE_MODE_MERGING:
case enums.REBASE_MODE_REBASING, enums.REBASE_MODE_MERGING:
workingTreeStatus := fmt.Sprintf("(%s)", gui.GitCommand.WorkingTreeState())
if cursorInSubstring(cx, upstreamStatus+" ", workingTreeStatus) {
return gui.handleCreateRebaseOptionsMenu()
@@ -156,15 +156,3 @@ func lazygitTitle() string {
__/ | __/ |
|___/ |___/ `
}
func (gui *Gui) workingTreeState() string {
rebaseMode, _ := gui.GitCommand.RebaseMode()
if rebaseMode != "" {
return commands.REBASE_MODE_REBASING
}
merging, _ := gui.GitCommand.IsInMergeState()
if merging {
return commands.REBASE_MODE_MERGING
}
return commands.REBASE_MODE_NORMAL
}

View File

@@ -1,7 +1,7 @@
package gui
import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/loaders"
"github.com/jesseduffield/lazygit/pkg/commands/models"
)
@@ -23,11 +23,9 @@ func (gui *Gui) subCommitsRenderToMain() error {
if commit == nil {
task = NewRenderStringTask("No commits")
} else {
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.ShowCmdStr(commit.Sha, gui.State.Modes.Filtering.GetPath()),
)
cmdObj := gui.GitCommand.ShowCmdObj(commit.Sha, gui.State.Modes.Filtering.GetPath())
task = NewRunPtyTask(cmd)
task = NewRunPtyTask(cmdObj.GetCmd())
}
return gui.refreshMainViews(refreshMainOpts{
@@ -77,10 +75,8 @@ func (gui *Gui) handleViewSubCommitFiles() error {
func (gui *Gui) switchToSubCommitsContext(refName string) error {
// need to populate my sub commits
builder := commands.NewCommitListBuilder(gui.Log, gui.GitCommand, gui.OSCommand, gui.Tr)
commits, err := builder.GetCommits(
commands.GetCommitsOptions{
commits, err := gui.GitCommand.Loaders.Commits.GetCommits(
loaders.GetCommitsOptions{
Limit: gui.State.Panels.Commits.LimitCommits,
FilterPath: gui.State.Modes.Filtering.GetPath(),
IncludeRebaseCommits: false,

View File

@@ -36,9 +36,8 @@ func (gui *Gui) submodulesRenderToMain() error {
if file == nil {
task = NewRenderStringTask(prefix)
} else {
cmdStr := gui.GitCommand.WorktreeFileDiffCmdStr(file, false, !file.HasUnstagedChanges && file.HasStagedChanges, gui.State.IgnoreWhitespaceInDiffView)
cmd := gui.OSCommand.ExecutableFromString(cmdStr)
task = NewRunCommandTaskWithPrefix(cmd, prefix)
cmdObj := gui.GitCommand.WorktreeFileDiffCmdObj(file, false, !file.HasUnstagedChanges && file.HasStagedChanges, gui.State.IgnoreWhitespaceInDiffView)
task = NewRunCommandTaskWithPrefix(cmdObj.GetCmd(), prefix)
}
}
@@ -51,7 +50,7 @@ func (gui *Gui) submodulesRenderToMain() error {
}
func (gui *Gui) refreshStateSubmoduleConfigs() error {
configs, err := gui.GitCommand.GetSubmoduleConfigs()
configs, err := gui.GitCommand.Submodules.GetConfigs()
if err != nil {
return err
}
@@ -80,7 +79,7 @@ func (gui *Gui) removeSubmodule(submodule *models.SubmoduleConfig) error {
title: gui.Tr.RemoveSubmodule,
prompt: fmt.Sprintf(gui.Tr.RemoveSubmodulePrompt, submodule.Name),
handleConfirm: func() error {
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.RemoveSubmodule).SubmoduleDelete(submodule); err != nil {
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.RemoveSubmodule).Submodules.Delete(submodule); err != nil {
return gui.surfaceError(err)
}
@@ -115,10 +114,10 @@ func (gui *Gui) resetSubmodule(submodule *models.SubmoduleConfig) error {
}
}
if err := gitCommand.SubmoduleStash(submodule); err != nil {
if err := gitCommand.Submodules.Stash(submodule); err != nil {
return gui.surfaceError(err)
}
if err := gitCommand.SubmoduleReset(submodule); err != nil {
if err := gitCommand.Submodules.Reset(submodule); err != nil {
return gui.surfaceError(err)
}
@@ -141,7 +140,7 @@ func (gui *Gui) handleAddSubmodule() error {
initialContent: submoduleName,
handleConfirm: func(submodulePath string) error {
return gui.WithWaitingStatus(gui.Tr.LcAddingSubmoduleStatus, func() error {
err := gui.GitCommand.WithSpan(gui.Tr.Spans.AddSubmodule).SubmoduleAdd(submoduleName, submodulePath, submoduleUrl)
err := gui.GitCommand.WithSpan(gui.Tr.Spans.AddSubmodule).Submodules.Add(submoduleName, submodulePath, submoduleUrl)
gui.handleCredentialsPopup(err)
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}})
@@ -161,7 +160,7 @@ func (gui *Gui) handleEditSubmoduleUrl(submodule *models.SubmoduleConfig) error
initialContent: submodule.Url,
handleConfirm: func(newUrl string) error {
return gui.WithWaitingStatus(gui.Tr.LcUpdatingSubmoduleUrlStatus, func() error {
err := gui.GitCommand.WithSpan(gui.Tr.Spans.UpdateSubmoduleUrl).SubmoduleUpdateUrl(submodule.Name, submodule.Path, newUrl)
err := gui.GitCommand.WithSpan(gui.Tr.Spans.UpdateSubmoduleUrl).Submodules.UpdateUrl(submodule.Name, submodule.Path, newUrl)
gui.handleCredentialsPopup(err)
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}})
@@ -172,7 +171,7 @@ func (gui *Gui) handleEditSubmoduleUrl(submodule *models.SubmoduleConfig) error
func (gui *Gui) handleSubmoduleInit(submodule *models.SubmoduleConfig) error {
return gui.WithWaitingStatus(gui.Tr.LcInitializingSubmoduleStatus, func() error {
err := gui.GitCommand.WithSpan(gui.Tr.Spans.InitialiseSubmodule).SubmoduleInit(submodule.Path)
err := gui.GitCommand.WithSpan(gui.Tr.Spans.InitialiseSubmodule).Submodules.Init(submodule.Path)
gui.handleCredentialsPopup(err)
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}})
@@ -212,10 +211,11 @@ func (gui *Gui) handleResetRemoveSubmodule(submodule *models.SubmoduleConfig) er
func (gui *Gui) handleBulkSubmoduleActionsMenu() error {
menuItems := []*menuItem{
{
displayStrings: []string{gui.Tr.LcBulkInitSubmodules, style.FgGreen.Sprint(gui.GitCommand.SubmoduleBulkInitCmdStr())},
displayStrings: []string{gui.Tr.LcBulkInitSubmodules, style.FgGreen.Sprint(gui.GitCommand.Submodules.BulkInitCmdObj().ToString())},
onPress: func() error {
return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error {
if err := gui.OSCommand.WithSpan(gui.Tr.Spans.BulkInitialiseSubmodules).RunCommand(gui.GitCommand.SubmoduleBulkInitCmdStr()); err != nil {
err := gui.GitCommand.WithSpan(gui.Tr.Spans.BulkInitialiseSubmodules).Submodules.BulkInitCmdObj().Run()
if err != nil {
return gui.surfaceError(err)
}
@@ -224,10 +224,10 @@ func (gui *Gui) handleBulkSubmoduleActionsMenu() error {
},
},
{
displayStrings: []string{gui.Tr.LcBulkUpdateSubmodules, style.FgYellow.Sprint(gui.GitCommand.SubmoduleBulkUpdateCmdStr())},
displayStrings: []string{gui.Tr.LcBulkUpdateSubmodules, style.FgYellow.Sprint(gui.GitCommand.Submodules.BulkUpdateCmdObj().ToString())},
onPress: func() error {
return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error {
if err := gui.OSCommand.WithSpan(gui.Tr.Spans.BulkUpdateSubmodules).RunCommand(gui.GitCommand.SubmoduleBulkUpdateCmdStr()); err != nil {
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.BulkUpdateSubmodules).Submodules.BulkUpdateCmdObj().Run(); err != nil {
return gui.surfaceError(err)
}
@@ -236,10 +236,10 @@ func (gui *Gui) handleBulkSubmoduleActionsMenu() error {
},
},
{
displayStrings: []string{gui.Tr.LcSubmoduleStashAndReset, style.FgRed.Sprintf("git stash in each submodule && %s", gui.GitCommand.SubmoduleForceBulkUpdateCmdStr())},
displayStrings: []string{gui.Tr.LcSubmoduleStashAndReset, style.FgRed.Sprintf("git stash in each submodule && %s", gui.GitCommand.Submodules.ForceBulkUpdateCmdObj().ToString())},
onPress: func() error {
return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error {
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.BulkStashAndResetSubmodules).ResetSubmodules(gui.State.Submodules); err != nil {
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.BulkStashAndResetSubmodules).Submodules.ResetSubmodules(gui.State.Submodules); err != nil {
return gui.surfaceError(err)
}
@@ -248,10 +248,10 @@ func (gui *Gui) handleBulkSubmoduleActionsMenu() error {
},
},
{
displayStrings: []string{gui.Tr.LcBulkDeinitSubmodules, style.FgRed.Sprint(gui.GitCommand.SubmoduleBulkDeinitCmdStr())},
displayStrings: []string{gui.Tr.LcBulkDeinitSubmodules, style.FgRed.Sprint(gui.GitCommand.Submodules.BulkDeinitCmdObj().ToString())},
onPress: func() error {
return gui.WithWaitingStatus(gui.Tr.LcRunningCommand, func() error {
if err := gui.OSCommand.WithSpan(gui.Tr.Spans.BulkDeinitialiseSubmodules).RunCommand(gui.GitCommand.SubmoduleBulkDeinitCmdStr()); err != nil {
if err := gui.GitCommand.WithSpan(gui.Tr.Spans.BulkDeinitialiseSubmodules).Submodules.BulkDeinitCmdObj().Run(); err != nil {
return gui.surfaceError(err)
}
@@ -266,7 +266,7 @@ func (gui *Gui) handleBulkSubmoduleActionsMenu() error {
func (gui *Gui) handleUpdateSubmodule(submodule *models.SubmoduleConfig) error {
return gui.WithWaitingStatus(gui.Tr.LcUpdatingSubmoduleStatus, func() error {
err := gui.GitCommand.WithSpan(gui.Tr.Spans.UpdateSubmodule).SubmoduleUpdate(submodule.Path)
err := gui.GitCommand.WithSpan(gui.Tr.Spans.UpdateSubmodule).Submodules.Update(submodule.Path)
gui.handleCredentialsPopup(err)
return gui.refreshSidePanels(refreshOptions{scope: []RefreshableView{SUBMODULES}})

View File

@@ -25,10 +25,8 @@ func (gui *Gui) tagsRenderToMain() error {
if tag == nil {
task = NewRenderStringTask("No tags")
} else {
cmd := gui.OSCommand.ExecutableFromString(
gui.GitCommand.GetBranchGraphCmdStr(tag.Name),
)
task = NewRunCommandTask(cmd)
cmdObj := gui.GitCommand.GetBranchGraphCmdObj(tag.Name)
task = NewRunCommandTask(cmdObj.GetCmd())
}
return gui.refreshMainViews(refreshMainOpts{
@@ -41,7 +39,7 @@ func (gui *Gui) tagsRenderToMain() error {
// this is a controller: it can't access tags directly. Or can it? It should be able to get but not set. But that's exactly what I'm doing here, setting it. but through a mutator which encapsulates the event.
func (gui *Gui) refreshTags() error {
tags, err := gui.GitCommand.GetTags()
tags, err := gui.GitCommand.Loaders.Tags.GetTags()
if err != nil {
return gui.surfaceError(err)
}

View File

@@ -1,8 +1,7 @@
package gui
import (
"github.com/jesseduffield/lazygit/pkg/commands"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/commands/types/enums"
"github.com/jesseduffield/lazygit/pkg/utils"
)
@@ -89,7 +88,7 @@ func (gui *Gui) reflogUndo() error {
undoEnvVars := []string{"GIT_REFLOG_ACTION=[lazygit undo]"}
undoingStatus := gui.Tr.UndoingStatus
if gui.GitCommand.WorkingTreeState() == commands.REBASE_MODE_REBASING {
if gui.GitCommand.WorkingTreeState() == enums.REBASE_MODE_REBASING {
return gui.createErrorPanel(gui.Tr.LcCantUndoWhileRebasing)
}
@@ -124,7 +123,7 @@ func (gui *Gui) reflogRedo() error {
redoEnvVars := []string{"GIT_REFLOG_ACTION=[lazygit redo]"}
redoingStatus := gui.Tr.RedoingStatus
if gui.GitCommand.WorkingTreeState() == commands.REBASE_MODE_REBASING {
if gui.GitCommand.WorkingTreeState() == enums.REBASE_MODE_REBASING {
return gui.createErrorPanel(gui.Tr.LcCantRedoWhileRebasing)
}
@@ -169,7 +168,7 @@ func (gui *Gui) handleHardResetWithAutoStash(commitSha string, options handleHar
gitCommand := gui.GitCommand.WithSpan(options.span)
reset := func() error {
if err := gui.resetToRef(commitSha, "hard", options.span, oscommands.RunCommandOptions{EnvVars: options.EnvVars}); err != nil {
if err := gui.resetToRef(commitSha, "hard", options.span, options.EnvVars); err != nil {
return gui.surfaceError(err)
}
return nil

View File

@@ -36,7 +36,7 @@ func (gui *Gui) onBackgroundUpdateCheckFinish(newVersion string, err error) erro
if newVersion == "" {
return nil
}
if gui.Config.GetUserConfig().Update.Method == "background" {
if gui.UserConfig.Update.Method == "background" {
gui.startUpdating(newVersion)
return nil
}

View File

@@ -314,7 +314,7 @@ func (gui *Gui) renderDisplayStringsAtPos(v *gocui.View, y int, displayStrings [
}
func (gui *Gui) globalOptionsMap() map[string]string {
keybindingConfig := gui.Config.GetUserConfig().Keybinding
keybindingConfig := gui.UserConfig.Keybinding
return map[string]string{
fmt.Sprintf("%s/%s", gui.getKeyDisplay(keybindingConfig.Universal.ScrollUpMain), gui.getKeyDisplay(keybindingConfig.Universal.ScrollDownMain)): gui.Tr.LcScroll,

View File

@@ -45,7 +45,7 @@ func RunTests(
testDir := filepath.Join(rootDir, "test", "integration")
osCommand := oscommands.NewDummyOSCommand()
err = osCommand.RunCommand("go build -o %s", tempLazygitPath())
err = osCommand.Cmd.New("go build -o " + tempLazygitPath()).Run()
if err != nil {
return err
}
@@ -216,11 +216,10 @@ func GetRootDirectory() string {
}
func createFixture(testPath, actualDir string) error {
osCommand := oscommands.NewDummyOSCommand()
bashScriptPath := filepath.Join(testPath, "setup.sh")
cmd := secureexec.Command("bash", bashScriptPath, actualDir)
if err := osCommand.RunExecutable(cmd); err != nil {
if _, err := cmd.CombinedOutput(); err != nil {
return err
}
@@ -320,7 +319,7 @@ func generateSnapshot(dir string) (string, error) {
for _, cmdStr := range cmdStrs {
// ignoring error for now. If there's an error it could be that there are no results
output, _ := osCommand.RunCommandWithOutput(cmdStr)
output, _ := osCommand.Cmd.New(cmdStr).RunWithOutput()
snapshot += output + "\n"
}
@@ -429,22 +428,16 @@ func getLazygitCommand(testPath string, rootDir string, record bool, speed float
cmdStr := fmt.Sprintf("%s -debug --use-config-dir=%s --path=%s %s", tempLazygitPath(), configDir, actualDir, extraCmdArgs)
cmd := osCommand.ExecutableFromString(cmdStr)
cmd.Env = append(cmd.Env, fmt.Sprintf("SPEED=%f", speed))
cmdObj := osCommand.Cmd.New(cmdStr)
cmdObj.AddEnvVars(fmt.Sprintf("SPEED=%f", speed))
if record {
cmd.Env = append(
cmd.Env,
fmt.Sprintf("RECORD_EVENTS_TO=%s", replayPath),
)
cmdObj.AddEnvVars(fmt.Sprintf("RECORD_EVENTS_TO=%s", replayPath))
} else {
cmd.Env = append(
cmd.Env,
fmt.Sprintf("REPLAY_EVENTS_FROM=%s", replayPath),
)
cmdObj.AddEnvVars(fmt.Sprintf("REPLAY_EVENTS_FROM=%s", replayPath))
}
return cmd, nil
return cmdObj.GetCmd(), nil
}
func folderExists(path string) bool {

View File

@@ -16,19 +16,17 @@ import (
"github.com/kardianos/osext"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/constants"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/sirupsen/logrus"
)
// Updater checks for updates and does updates
type Updater struct {
Log *logrus.Entry
*common.Common
Config config.AppConfigurer
OSCommand *oscommands.OSCommand
Tr *i18n.TranslationSet
}
// Updaterer implements the check and update methods
@@ -38,14 +36,11 @@ type Updaterer interface {
}
// NewUpdater creates a new updater
func NewUpdater(log *logrus.Entry, config config.AppConfigurer, osCommand *oscommands.OSCommand, tr *i18n.TranslationSet) (*Updater, error) {
contextLogger := log.WithField("context", "updates")
func NewUpdater(cmn *common.Common, config config.AppConfigurer, osCommand *oscommands.OSCommand) (*Updater, error) {
return &Updater{
Log: contextLogger,
Common: cmn,
Config: config,
OSCommand: osCommand,
Tr: tr,
}, nil
}
@@ -177,7 +172,7 @@ func (u *Updater) skipUpdateCheck() bool {
return true
}
userConfig := u.Config.GetUserConfig()
userConfig := u.UserConfig
if userConfig.Update.Method == "never" {
u.Log.Info("Update method is set to never so we won't check for an update")
return true
@@ -300,7 +295,8 @@ func (u *Updater) downloadAndInstall(rawUrl string) error {
}
u.Log.Info("untarring tarball/unzipping zip file")
if err := u.OSCommand.RunCommand("tar -zxf %s %s", u.OSCommand.Quote(zipPath), "lazygit"); err != nil {
err = u.OSCommand.Cmd.New(fmt.Sprintf("tar -zxf %s %s", u.OSCommand.Quote(zipPath), "lazygit")).Run()
if err != nil {
return err
}

View File

@@ -3,6 +3,9 @@ package utils
import (
"io/ioutil"
"github.com/jesseduffield/lazygit/pkg/common"
"github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/sirupsen/logrus"
)
@@ -12,3 +15,12 @@ func NewDummyLog() *logrus.Entry {
log.Out = ioutil.Discard
return log.WithField("test", "test")
}
func NewDummyCommon() *common.Common {
tr := i18n.EnglishTranslationSet()
return &common.Common{
Log: NewDummyLog(),
Tr: &tr,
UserConfig: config.GetDefaultConfig(),
}
}

View File

@@ -2,8 +2,12 @@ package utils
// IncludesString if the list contains the string
func IncludesString(list []string, a string) bool {
return IncludesStringFunc(list, func(b string) bool { return b == a })
}
func IncludesStringFunc(list []string, fn func(string) bool) bool {
for _, b := range list {
if b == a {
if fn(b) {
return true
}
}