diff --git a/.circleci/config.yml b/.circleci/config.yml index 0eef1a409..d60eadaab 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,14 +7,33 @@ jobs: working_directory: /go/src/github.com/jesseduffield/lazygit steps: - checkout + - restore_cache: + keys: + - v1-pkg-cache + - run: + name: Run gofmt -s + command: | + if [ $(find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \;|wc -l) -gt 0 ]; then + find . ! -path "./vendor/*" -name "*.go" -exec gofmt -s -d {} \; + exit 1; + fi - run: name: Run tests command: | ./test.sh + - run: + name: Compile project on every platform + command: | + go get github.com/mitchellh/gox + gox -parallel 10 -os "linux freebsd netbsd windows" -osarch "darwin/i386 darwin/amd64" - run: name: Push on codecov result command: | bash <(curl -s https://codecov.io/bash) + - save_cache: + key: v1-pkg-cache + paths: + - "/go/pkg" release: docker: diff --git a/main.go b/main.go index 2e0ad8f77..9b3ad9edf 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ var ( date string buildSource = "unknown" + configFlag = flag.Bool("config", false, "Print the current default config") debuggingFlag = flag.Bool("debug", false, "a boolean") versionFlag = flag.Bool("v", false, "Print the current version") ) @@ -32,6 +33,11 @@ func main() { fmt.Printf("commit=%s, build date=%s, build source=%s, version=%s, os=%s, arch=%s\n", commit, date, buildSource, version, runtime.GOOS, runtime.GOARCH) os.Exit(0) } + + if *configFlag { + fmt.Printf("%s\n", config.GetDefaultConfig()) + os.Exit(0) + } appConfig, err := config.NewAppConfig("lazygit", version, commit, date, buildSource, debuggingFlag) if err != nil { panic(err) diff --git a/pkg/app/app.go b/pkg/app/app.go index a00eb8c06..6685f5f9e 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -48,10 +48,7 @@ func NewApp(config config.AppConfigurer) (*App, error) { } var err error app.Log = newLogger(config) - app.OSCommand, err = commands.NewOSCommand(app.Log) - if err != nil { - return app, err - } + app.OSCommand = commands.NewOSCommand(app.Log) app.Tr = i18n.NewLocalizer(app.Log) diff --git a/pkg/commands/git.go b/pkg/commands/git.go index 7ec63ee7e..e5ee5dbfa 100644 --- a/pkg/commands/git.go +++ b/pkg/commands/git.go @@ -265,7 +265,7 @@ func (c *GitCommand) UsingGpg() bool { func (c *GitCommand) Commit(g *gocui.Gui, message string) (*exec.Cmd, error) { command := "git commit -m " + c.OSCommand.Quote(message) if c.UsingGpg() { - return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command) + return c.OSCommand.PrepareSubProcess(c.OSCommand.Platform.shell, c.OSCommand.Platform.shellArg, command), nil } return nil, c.OSCommand.RunCommand(command) } @@ -391,12 +391,12 @@ func (c *GitCommand) Checkout(branch string, force bool) error { // AddPatch prepares a subprocess for adding a patch by patch // this will eventually be swapped out for a better solution inside the Gui -func (c *GitCommand) AddPatch(filename string) (*exec.Cmd, error) { +func (c *GitCommand) AddPatch(filename string) *exec.Cmd { return c.OSCommand.PrepareSubProcess("git", "add", "--patch", filename) } // PrepareCommitSubProcess prepares a subprocess for `git commit` -func (c *GitCommand) PrepareCommitSubProcess() (*exec.Cmd, error) { +func (c *GitCommand) PrepareCommitSubProcess() *exec.Cmd { return c.OSCommand.PrepareSubProcess("git", "commit") } diff --git a/pkg/commands/git_test.go b/pkg/commands/git_test.go index c930f76eb..372009641 100644 --- a/pkg/commands/git_test.go +++ b/pkg/commands/git_test.go @@ -5,32 +5,25 @@ import ( "strings" "testing" - "github.com/sirupsen/logrus" "github.com/jesseduffield/lazygit/pkg/test" + "github.com/sirupsen/logrus" ) -func getDummyLog() *logrus.Logger { +func newDummyLog() *logrus.Logger { log := logrus.New() log.Out = ioutil.Discard return log } -func getDummyOSCommand() *OSCommand { - return &OSCommand{ - Log: getDummyLog(), - Platform: getPlatform(), - } -} - -func getDummyGitCommand() *GitCommand { +func newDummyGitCommand() *GitCommand { return &GitCommand{ - Log: getDummyLog(), - OSCommand: getDummyOSCommand(), + Log: newDummyLog(), + OSCommand: newDummyOSCommand(), } } func TestDiff(t *testing.T) { - gitCommand := getDummyGitCommand() + gitCommand := newDummyGitCommand() if err := test.GenerateRepo("lots_of_diffs.sh"); err != nil { t.Error(err.Error()) } diff --git a/pkg/commands/os.go b/pkg/commands/os.go index 1eef36151..8b9ecf7ec 100644 --- a/pkg/commands/os.go +++ b/pkg/commands/os.go @@ -4,7 +4,6 @@ import ( "errors" "os" "os/exec" - "runtime" "strings" "github.com/davecgh/go-spew/spew" @@ -25,17 +24,22 @@ type Platform struct { // OSCommand holds all the os commands type OSCommand struct { - Log *logrus.Logger - Platform *Platform + Log *logrus.Logger + Platform *Platform + command func(string, ...string) *exec.Cmd + getGlobalGitConfig func(string) (string, error) + getenv func(string) string } // NewOSCommand os command runner -func NewOSCommand(log *logrus.Logger) (*OSCommand, error) { - osCommand := &OSCommand{ - Log: log, - Platform: getPlatform(), +func NewOSCommand(log *logrus.Logger) *OSCommand { + return &OSCommand{ + Log: log, + Platform: getPlatform(), + command: exec.Command, + getGlobalGitConfig: gitconfig.Global, + getenv: os.Getenv, } - return osCommand, nil } // RunCommandWithOutput wrapper around commands returning their output and error @@ -43,8 +47,10 @@ func (c *OSCommand) RunCommandWithOutput(command string) (string, error) { c.Log.WithField("command", command).Info("RunCommand") splitCmd := str.ToArgv(command) c.Log.Info(splitCmd) - cmdOut, err := exec.Command(splitCmd[0], splitCmd[1:]...).CombinedOutput() - return sanitisedCommandOutput(cmdOut, err) + + return sanitisedCommandOutput( + c.command(splitCmd[0], splitCmd[1:]...).CombinedOutput(), + ) } // RunCommand runs a command and just returns the error @@ -59,10 +65,11 @@ func (c *OSCommand) RunDirectCommand(command string) (string, error) { args := str.ToArgv(c.Platform.shellArg + " " + command) c.Log.Info(spew.Sdump(args)) - cmdOut, err := exec. - Command(c.Platform.shell, args...). - CombinedOutput() - return sanitisedCommandOutput(cmdOut, err) + return sanitisedCommandOutput( + exec. + Command(c.Platform.shell, args...). + CombinedOutput(), + ) } func sanitisedCommandOutput(output []byte, err error) (string, error) { @@ -75,33 +82,15 @@ func sanitisedCommandOutput(output []byte, err error) (string, error) { return outputString, nil } -func getPlatform() *Platform { - switch runtime.GOOS { - case "windows": - return &Platform{ - os: "windows", - shell: "cmd", - shellArg: "/c", - escapedQuote: "\\\"", - } - default: - return &Platform{ - os: runtime.GOOS, - shell: "bash", - shellArg: "-c", - escapedQuote: "\"", - } - } -} - -// GetOpenCommand get open command -func (c *OSCommand) GetOpenCommand() (string, string, error) { +// getOpenCommand get open command +func (c *OSCommand) getOpenCommand() (string, string, error) { //NextStep open equivalents: xdg-open (linux), cygstart (cygwin), open (OSX) trailMap := map[string]string{ "xdg-open": " &>/dev/null &", "cygstart": "", "open": "", } + for name, trail := range trailMap { if err := c.RunCommand("which " + name); err == nil { return name, trail, nil @@ -126,24 +115,25 @@ func (c *OSCommand) SublimeOpenFile(filename string) (*exec.Cmd, error) { } // OpenFile opens a file with the given -func (c *OSCommand) OpenFile(filename string) (*exec.Cmd, error) { - cmdName, cmdTrail, err := c.GetOpenCommand() +func (c *OSCommand) OpenFile(filename string) error { + cmdName, cmdTrail, err := c.getOpenCommand() if err != nil { - return nil, err + return err } - err = c.RunCommand(cmdName + " " + c.Quote(filename) + cmdTrail) // TODO: test on linux - return nil, err + + return c.RunCommand(cmdName + " " + c.Quote(filename) + cmdTrail) // TODO: test on linux } // EditFile opens a file in a subprocess using whatever editor is available, // falling back to core.editor, VISUAL, EDITOR, then vi func (c *OSCommand) EditFile(filename string) (*exec.Cmd, error) { - editor, _ := gitconfig.Global("core.editor") + editor, _ := c.getGlobalGitConfig("core.editor") + if editor == "" { - editor = os.Getenv("VISUAL") + editor = c.getenv("VISUAL") } if editor == "" { - editor = os.Getenv("EDITOR") + editor = c.getenv("EDITOR") } if editor == "" { if err := c.RunCommand("which vi"); err == nil { @@ -153,13 +143,13 @@ func (c *OSCommand) EditFile(filename string) (*exec.Cmd, error) { if editor == "" { return nil, errors.New("No editor defined in $VISUAL, $EDITOR, or git config") } - return c.PrepareSubProcess(editor, filename) + + return c.PrepareSubProcess(editor, filename), nil } // PrepareSubProcess iniPrepareSubProcessrocess then tells the Gui to switch to it -func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) (*exec.Cmd, error) { - subprocess := exec.Command(cmdName, commandArgs...) - return subprocess, nil +func (c *OSCommand) PrepareSubProcess(cmdName string, commandArgs ...string) *exec.Cmd { + return c.command(cmdName, commandArgs...) } // Quote wraps a message in platform-specific quotation marks @@ -171,8 +161,7 @@ func (c *OSCommand) Quote(message string) string { // Unquote removes wrapping quotations marks if they are present // this is needed for removing quotes from staged filenames with spaces func (c *OSCommand) Unquote(message string) string { - message = strings.Replace(message, `"`, "", -1) - return message + return strings.Replace(message, `"`, "", -1) } // AppendLineToFile adds a new line in file diff --git a/pkg/commands/os_default_platform.go b/pkg/commands/os_default_platform.go new file mode 100644 index 000000000..f6ac1b515 --- /dev/null +++ b/pkg/commands/os_default_platform.go @@ -0,0 +1,16 @@ +// +build !windows + +package commands + +import ( + "runtime" +) + +func getPlatform() *Platform { + return &Platform{ + os: runtime.GOOS, + shell: "bash", + shellArg: "-c", + escapedQuote: "\"", + } +} diff --git a/pkg/commands/os_test.go b/pkg/commands/os_test.go index 29540aff6..d78391d17 100644 --- a/pkg/commands/os_test.go +++ b/pkg/commands/os_test.go @@ -1,16 +1,299 @@ package commands -import "testing" +import ( + "os/exec" + "testing" -func TestQuote(t *testing.T) { - osCommand := &OSCommand{ - Log: nil, - Platform: getPlatform(), + "github.com/stretchr/testify/assert" +) + +func newDummyOSCommand() *OSCommand { + return NewOSCommand(newDummyLog()) +} + +func TestOSCommandRunCommandWithOutput(t *testing.T) { + type scenario struct { + command string + test func(string, error) } - test := "hello `test`" - expected := osCommand.Platform.escapedQuote + "hello \\`test\\`" + osCommand.Platform.escapedQuote - test = osCommand.Quote(test) - if test != expected { - t.Error("Expected " + expected + ", got " + test) + + scenarios := []scenario{ + { + "echo -n '123'", + func(output string, err error) { + assert.NoError(t, err) + assert.EqualValues(t, "123", output) + }, + }, + { + "rmdir unexisting-folder", + func(output string, err error) { + assert.Regexp(t, ".*No such file or directory.*", err.Error()) + }, + }, + } + + for _, s := range scenarios { + s.test(newDummyOSCommand().RunCommandWithOutput(s.command)) } } + +func TestOSCommandRunCommand(t *testing.T) { + type scenario struct { + command string + test func(error) + } + + scenarios := []scenario{ + { + "rmdir unexisting-folder", + func(err error) { + assert.Regexp(t, ".*No such file or directory.*", err.Error()) + }, + }, + } + + for _, s := range scenarios { + s.test(newDummyOSCommand().RunCommand(s.command)) + } +} + +func TestOSCommandGetOpenCommand(t *testing.T) { + type scenario struct { + command func(string, ...string) *exec.Cmd + test func(string, string, error) + } + + scenarios := []scenario{ + { + func(name string, arg ...string) *exec.Cmd { + return exec.Command("exit", "1") + }, + func(name string, trail string, err error) { + assert.EqualError(t, err, "Unsure what command to use to open this file") + }, + }, + { + func(name string, arg ...string) *exec.Cmd { + assert.Equal(t, "which", name) + assert.Len(t, arg, 1) + assert.Regexp(t, "xdg-open|cygstart|open", arg[0]) + return exec.Command("echo") + }, + func(name string, trail string, err error) { + assert.NoError(t, err) + assert.Regexp(t, "xdg-open|cygstart|open", name) + assert.Regexp(t, " \\&\\>/dev/null \\&|", trail) + }, + }, + } + + for _, s := range scenarios { + OSCmd := newDummyOSCommand() + OSCmd.command = s.command + + s.test(OSCmd.getOpenCommand()) + } +} + +func TestOSCommandOpenFile(t *testing.T) { + type scenario struct { + filename string + command func(string, ...string) *exec.Cmd + test func(error) + } + + scenarios := []scenario{ + { + "test", + func(name string, arg ...string) *exec.Cmd { + return exec.Command("exit", "1") + }, + func(err error) { + assert.EqualError(t, err, "Unsure what command to use to open this file") + }, + }, + { + "test", + func(name string, arg ...string) *exec.Cmd { + if name == "which" { + return exec.Command("echo") + } + + switch len(arg) { + case 1: + assert.Regexp(t, "open|cygstart", name) + assert.EqualValues(t, "test", arg[0]) + case 3: + assert.Equal(t, "xdg-open", name) + assert.EqualValues(t, "test", arg[0]) + assert.Regexp(t, " \\&\\>/dev/null \\&|", arg[1]) + assert.EqualValues(t, "&", arg[2]) + default: + assert.Fail(t, "Unexisting command given") + } + + return exec.Command("echo") + }, + func(err error) { + assert.NoError(t, err) + }, + }, + } + + for _, s := range scenarios { + OSCmd := newDummyOSCommand() + OSCmd.command = s.command + + s.test(OSCmd.OpenFile(s.filename)) + } +} + +func TestOSCommandEditFile(t *testing.T) { + type scenario struct { + filename string + command func(string, ...string) *exec.Cmd + getenv func(string) string + getGlobalGitConfig func(string) (string, error) + test func(*exec.Cmd, error) + } + + scenarios := []scenario{ + { + "test", + func(name string, arg ...string) *exec.Cmd { + return exec.Command("exit", "1") + }, + func(env string) string { + return "" + }, + func(cf string) (string, error) { + return "", nil + }, + func(cmd *exec.Cmd, err error) { + assert.EqualError(t, err, "No editor defined in $VISUAL, $EDITOR, or git config") + }, + }, + { + "test", + func(name string, arg ...string) *exec.Cmd { + if name == "which" { + return exec.Command("exit", "1") + } + + assert.EqualValues(t, "nano", name) + + return nil + }, + func(env string) string { + return "" + }, + func(cf string) (string, error) { + return "nano", nil + }, + func(cmd *exec.Cmd, err error) { + assert.NoError(t, err) + }, + }, + { + "test", + func(name string, arg ...string) *exec.Cmd { + if name == "which" { + return exec.Command("exit", "1") + } + + assert.EqualValues(t, "nano", name) + + return nil + }, + func(env string) string { + if env == "VISUAL" { + return "nano" + } + + return "" + }, + func(cf string) (string, error) { + return "", nil + }, + func(cmd *exec.Cmd, err error) { + assert.NoError(t, err) + }, + }, + { + "test", + func(name string, arg ...string) *exec.Cmd { + if name == "which" { + return exec.Command("exit", "1") + } + + assert.EqualValues(t, "emacs", name) + + return nil + }, + func(env string) string { + if env == "EDITOR" { + return "emacs" + } + + return "" + }, + func(cf string) (string, error) { + return "", nil + }, + func(cmd *exec.Cmd, err error) { + assert.NoError(t, err) + }, + }, + { + "test", + func(name string, arg ...string) *exec.Cmd { + if name == "which" { + return exec.Command("echo") + } + + assert.EqualValues(t, "vi", name) + + return nil + }, + func(env string) string { + return "" + }, + func(cf string) (string, error) { + return "", nil + }, + func(cmd *exec.Cmd, err error) { + assert.NoError(t, err) + }, + }, + } + + for _, s := range scenarios { + OSCmd := newDummyOSCommand() + OSCmd.command = s.command + OSCmd.getGlobalGitConfig = s.getGlobalGitConfig + OSCmd.getenv = s.getenv + + s.test(OSCmd.EditFile(s.filename)) + } +} + +func TestOSCommandQuote(t *testing.T) { + osCommand := newDummyOSCommand() + + actual := osCommand.Quote("hello `test`") + + expected := osCommand.Platform.escapedQuote + "hello \\`test\\`" + osCommand.Platform.escapedQuote + + assert.EqualValues(t, expected, actual) +} + +func TestOSCommandUnquote(t *testing.T) { + osCommand := newDummyOSCommand() + + actual := osCommand.Unquote(`hello "test"`) + + expected := "hello test" + + assert.EqualValues(t, expected, actual) +} diff --git a/pkg/commands/os_windows.go b/pkg/commands/os_windows.go new file mode 100644 index 000000000..28dd7a982 --- /dev/null +++ b/pkg/commands/os_windows.go @@ -0,0 +1,10 @@ +package commands + +func getPlatform() *Platform { + return &Platform{ + os: "windows", + shell: "cmd", + shellArg: "/c", + escapedQuote: "\\\"", + } +} diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index 36cfdb9f4..fd3acb212 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -40,7 +40,7 @@ type AppConfigurer interface { // NewAppConfig makes a new app config func NewAppConfig(name, version, commit, date string, buildSource string, debuggingFlag *bool) (*AppConfig, error) { - defaultConfig := getDefaultConfig() + defaultConfig := GetDefaultConfig() userConfig, err := LoadConfig("config", defaultConfig) if err != nil { return nil, err @@ -205,26 +205,26 @@ func (c *AppConfig) LoadAppState() error { return yaml.Unmarshal(appStateBytes, c.AppState) } -func getDefaultConfig() []byte { - return []byte(` - gui: - ## stuff relating to the UI - scrollHeight: 2 - theme: - activeBorderColor: - - white - - bold - inactiveBorderColor: - - white - optionsTextColor: - - blue - git: - # stuff relating to git - os: - # stuff relating to the OS - update: - method: prompt # can be: prompt | background | never - days: 14 # how often a update is checked for +func GetDefaultConfig() []byte { + return []byte( + `gui: + ## stuff relating to the UI + scrollHeight: 2 + theme: + activeBorderColor: + - white + - bold + inactiveBorderColor: + - white + optionsTextColor: + - blue +git: + # stuff relating to git +os: + # stuff relating to the OS +update: + method: prompt # can be: prompt | background | never + days: 14 # how often a update is checked for `) } diff --git a/pkg/gui/files_panel.go b/pkg/gui/files_panel.go index 5791a9d15..760dee934 100644 --- a/pkg/gui/files_panel.go +++ b/pkg/gui/files_panel.go @@ -85,11 +85,8 @@ func (gui *Gui) handleAddPatch(g *gocui.Gui, v *gocui.View) error { if !file.Tracked { return gui.createErrorPanel(g, gui.Tr.SLocalize("CannotGitAdd")) } - sub, err := gui.GitCommand.AddPatch(file.Name) - if err != nil { - return err - } - gui.SubProcess = sub + + gui.SubProcess = gui.GitCommand.AddPatch(file.Name) return gui.Errors.ErrSubProcess } @@ -218,16 +215,11 @@ func (gui *Gui) handleCommitEditorPress(g *gocui.Gui, filesView *gocui.View) err } // PrepareSubProcess - prepare a subprocess for execution and tell the gui to switch to it -func (gui *Gui) PrepareSubProcess(g *gocui.Gui, commands ...string) error { - sub, err := gui.GitCommand.PrepareCommitSubProcess() - if err != nil { - return err - } - gui.SubProcess = sub +func (gui *Gui) PrepareSubProcess(g *gocui.Gui, commands ...string) { + gui.SubProcess = gui.GitCommand.PrepareCommitSubProcess() g.Update(func(g *gocui.Gui) error { return gui.Errors.ErrSubProcess }) - return nil } func (gui *Gui) genericFileOpen(g *gocui.Gui, v *gocui.View, filename string, open func(string) (*exec.Cmd, error)) error { @@ -256,7 +248,7 @@ func (gui *Gui) handleFileOpen(g *gocui.Gui, v *gocui.View) error { if err != nil { return err } - return gui.genericFileOpen(g, v, file.Name, gui.OSCommand.OpenFile) + return gui.openFile(file.Name) } func (gui *Gui) handleSublimeFileOpen(g *gocui.Gui, v *gocui.View) error { @@ -425,3 +417,10 @@ func (gui *Gui) handleResetHard(g *gocui.Gui, v *gocui.View) error { return gui.refreshFiles(g) }, nil) } + +func (gui *Gui) openFile(filename string) error { + if err := gui.OSCommand.OpenFile(filename); err != nil { + return gui.createErrorPanel(gui.g, err.Error()) + } + return nil +} diff --git a/pkg/gui/status_panel.go b/pkg/gui/status_panel.go index 1f0eac1ad..52842246e 100644 --- a/pkg/gui/status_panel.go +++ b/pkg/gui/status_panel.go @@ -71,8 +71,7 @@ func (gui *Gui) handleStatusSelect(g *gocui.Gui, v *gocui.View) error { } func (gui *Gui) handleOpenConfig(g *gocui.Gui, v *gocui.View) error { - filename := gui.Config.GetUserConfig().ConfigFileUsed() - return gui.genericFileOpen(g, v, filename, gui.OSCommand.OpenFile) + return gui.openFile(gui.Config.GetUserConfig().ConfigFileUsed()) } func (gui *Gui) handleEditConfig(g *gocui.Gui, v *gocui.View) error { diff --git a/pkg/i18n/i18n.go b/pkg/i18n/i18n.go index 898a13906..37de57cc6 100644 --- a/pkg/i18n/i18n.go +++ b/pkg/i18n/i18n.go @@ -1,9 +1,9 @@ package i18n import ( - "github.com/sirupsen/logrus" "github.com/cloudfoundry/jibber_jabber" "github.com/nicksnyder/go-i18n/v2/i18n" + "github.com/sirupsen/logrus" "golang.org/x/text/language" )