diff --git a/.gitignore b/.gitignore index d6b15ef10..21ec121da 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ lazygit test/git_server/data test/integration_test/ +test/integration_test_config/ diff --git a/main.go b/main.go index c5cd2f222..131f1e6e3 100644 --- a/main.go +++ b/main.go @@ -49,7 +49,10 @@ func main() { flaggy.Bool(&configFlag, "c", "config", "Print the default config") configDirFlag := false - flaggy.Bool(&configDirFlag, "cd", "config-dir", "Print the config directory") + flaggy.Bool(&configDirFlag, "cd", "print-config-dir", "Print the config directory") + + useConfigDir := "" + flaggy.String(&useConfigDir, "ucd", "use-config-dir", "override default config directory with provided directory") workTree := "" flaggy.String(&workTree, "w", "work-tree", "equivalent of the --work-tree git argument") @@ -68,6 +71,10 @@ func main() { gitDir = filepath.Join(repoPath, ".git") } + if useConfigDir != "" { + os.Setenv("CONFIG_DIR", useConfigDir) + } + if workTree != "" { env.SetGitWorkTreeEnv(workTree) } diff --git a/pkg/commands/oscommands/copy.go b/pkg/commands/oscommands/copy.go new file mode 100644 index 000000000..2681441c5 --- /dev/null +++ b/pkg/commands/oscommands/copy.go @@ -0,0 +1,134 @@ +package oscommands + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" +) + +/* MIT License + * + * Copyright (c) 2017 Roland Singer [roland.singer@desertbit.com] + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +// CopyFile copies the contents of the file named src to the file named +// by dst. The file will be created if it does not already exist. If the +// destination file exists, all it's contents will be replaced by the contents +// of the source file. The file mode will be copied from the source and +// the copied data is synced/flushed to stable storage. +func CopyFile(src, dst string) (err error) { + in, err := os.Open(src) + if err != nil { + return + } + defer in.Close() + + out, err := os.Create(dst) + if err != nil { + return + } + defer func() { + if e := out.Close(); e != nil { + err = e + } + }() + + _, err = io.Copy(out, in) + if err != nil { + return + } + + err = out.Sync() + if err != nil { + return + } + + si, err := os.Stat(src) + if err != nil { + return + } + err = os.Chmod(dst, si.Mode()) + if err != nil { + return + } + + return +} + +// CopyDir recursively copies a directory tree, attempting to preserve permissions. +// Source directory must exist, destination directory must *not* exist. +// Symlinks are ignored and skipped. +func CopyDir(src string, dst string) (err error) { + src = filepath.Clean(src) + dst = filepath.Clean(dst) + + si, err := os.Stat(src) + if err != nil { + return err + } + if !si.IsDir() { + return fmt.Errorf("source is not a directory") + } + + _, err = os.Stat(dst) + if err != nil && !os.IsNotExist(err) { + return + } + if err == nil { + return fmt.Errorf("destination already exists") + } + + err = os.MkdirAll(dst, si.Mode()) + if err != nil { + return + } + + entries, err := ioutil.ReadDir(src) + if err != nil { + return + } + + for _, entry := range entries { + srcPath := filepath.Join(src, entry.Name()) + dstPath := filepath.Join(dst, entry.Name()) + + if entry.IsDir() { + err = CopyDir(srcPath, dstPath) + if err != nil { + return + } + } else { + // Skip symlinks. + if entry.Mode()&os.ModeSymlink != 0 { + continue + } + + err = CopyFile(srcPath, dstPath) + if err != nil { + return + } + } + } + + return +} diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index 95ac99b73..df4707a94 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -81,6 +81,11 @@ func NewAppConfig(name, version, commit, date string, buildSource string, debugg } func ConfigDir() string { + envConfigDir := os.Getenv("CONFIG_DIR") + if envConfigDir != "" { + return envConfigDir + } + // chucking my name there is not for vanity purposes, the xdg spec (and that // function) requires a vendor name. May as well line up with github configDirs := xdg.New("jesseduffield", "lazygit") @@ -226,7 +231,7 @@ func loadAppState() (*AppState, error) { } appStateBytes, err := ioutil.ReadFile(filepath) - if err != nil { + if err != nil && !os.IsNotExist(err) { return nil, err } diff --git a/pkg/gui/gui_test.go b/pkg/gui/gui_test.go index 371cf1397..234ec2afb 100644 --- a/pkg/gui/gui_test.go +++ b/pkg/gui/gui_test.go @@ -2,12 +2,14 @@ package gui import ( "fmt" + "io" "io/ioutil" "os" "os/exec" "path/filepath" "testing" + "github.com/creack/pty" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/stretchr/testify/assert" ) @@ -34,6 +36,23 @@ type integrationTest struct { fixture string } +func tests() []integrationTest { + return []integrationTest{ + { + name: "commit", + fixture: "newFile", + }, + { + name: "squash", + fixture: "manyCommits", + }, + { + name: "patchBuilding", + fixture: "updatedFile", + }, + } +} + func generateSnapshot(t *testing.T) string { osCommand := oscommands.NewDummyOSCommand() cmd := `sh -c "git status; cat ./*; git log --pretty=%B -p"` @@ -59,20 +78,7 @@ func findOrCreateDir(path string) { } func Test(t *testing.T) { - tests := []integrationTest{ - { - name: "commit", - fixture: "newFile", - }, - { - name: "squash", - fixture: "manyCommits", - }, - { - name: "patchBuilding", - fixture: "updatedFile", - }, - } + tests := tests() gotoRootDirectory() @@ -87,7 +93,6 @@ func Test(t *testing.T) { testPath := filepath.Join(rootDir, "test", "integration", test.name) findOrCreateDir(testPath) - replayPath := filepath.Join(testPath, "recording.json") snapshotPath := filepath.Join(testPath, "snapshot.txt") err := os.Chdir(rootDir) @@ -99,7 +104,7 @@ func Test(t *testing.T) { assert.NoError(t, err) record := os.Getenv("RECORD_EVENTS") != "" - runLazygit(t, replayPath, record) + runLazygit(t, testPath, rootDir, record) updateSnapshot := record || os.Getenv("UPDATE_SNAPSHOT") != "" @@ -148,25 +153,55 @@ func gotoRootDirectory() { } } -func runLazygit(t *testing.T, replayPath string, record bool) { +func runLazygit(t *testing.T, testPath string, rootDir string, record bool) { osCommand := oscommands.NewDummyOSCommand() - var cmd *exec.Cmd + replayPath := filepath.Join(testPath, "recording.json") + cmdStr := fmt.Sprintf("go run %s", filepath.Join(rootDir, "main.go")) + templateConfigDir := filepath.Join(rootDir, "test", "default_test_config") + + exists, err := osCommand.FileExists(filepath.Join(testPath, "config")) + assert.NoError(t, err) + + if exists { + templateConfigDir = filepath.Join(testPath, "config") + } + + configDir := filepath.Join(rootDir, "test", "integration_test_config") + + err = os.RemoveAll(configDir) + assert.NoError(t, err) + err = oscommands.CopyDir(templateConfigDir, configDir) + assert.NoError(t, err) + + cmdStr = fmt.Sprintf("%s --use-config-dir=%s", cmdStr, configDir) + + cmd := osCommand.ExecutableFromString(cmdStr) if record { - cmd = osCommand.ExecutableFromString("lazygit") cmd.Env = append( cmd.Env, fmt.Sprintf("RECORD_EVENTS_TO=%s", replayPath), ) } else { - cmd = osCommand.ExecutableFromString("lazygit") cmd.Env = append( cmd.Env, fmt.Sprintf("REPLAY_EVENTS_FROM=%s", replayPath), ) } - err := osCommand.RunExecutable(cmd) - assert.NoError(t, err) + + // if we're on CI we'll need to use a PTY. We can work that out by seeing if the 'TERM' env is defined. + if os.Getenv("TERM") == "" { + cmd.Env = append(cmd.Env, "TERM=xterm") + + f, err := pty.StartWithSize(cmd, &pty.Winsize{Rows: 100, Cols: 100}) + assert.NoError(t, err) + + _, err = io.Copy(os.Stdout, f) + assert.NoError(t, err) + } else { + err := osCommand.RunExecutable(cmd) + assert.NoError(t, err) + } } func prepareIntegrationTestDir() { diff --git a/test/default_test_config/config.yml b/test/default_test_config/config.yml new file mode 100644 index 000000000..dc8bd1faa --- /dev/null +++ b/test/default_test_config/config.yml @@ -0,0 +1 @@ +disableStartupPopups: true diff --git a/test/fixtures/manyCommits.sh b/test/fixtures/manyCommits.sh index 8eec5ae57..ae92f3f15 100644 --- a/test/fixtures/manyCommits.sh +++ b/test/fixtures/manyCommits.sh @@ -1,5 +1,10 @@ #!/bin/sh + git init + +git config user.email "CI@example.com" +git config user.name "CI" + echo test1 > myfile1 git add . git commit -am "myfile1" diff --git a/test/fixtures/newFile.sh b/test/fixtures/newFile.sh index f8592c33f..83470bba8 100644 --- a/test/fixtures/newFile.sh +++ b/test/fixtures/newFile.sh @@ -1,5 +1,10 @@ #!/bin/sh + git init + +git config user.email "CI@example.com" +git config user.name "CI" + echo test1 > myfile1 git add . git commit -am "myfile1" diff --git a/test/fixtures/updatedFile.sh b/test/fixtures/updatedFile.sh index 2c42d6282..64e62c8d4 100644 --- a/test/fixtures/updatedFile.sh +++ b/test/fixtures/updatedFile.sh @@ -1,5 +1,10 @@ #!/bin/sh + git init + +git config user.email "CI@example.com" +git config user.name "CI" + echo test1 > myfile1 git add . git commit -am "myfile1"