diff --git a/pkg/cheatsheet/generate.go b/pkg/cheatsheet/generate.go index 5c5a94530..ad62f9e54 100644 --- a/pkg/cheatsheet/generate.go +++ b/pkg/cheatsheet/generate.go @@ -19,12 +19,12 @@ import ( "strings" "github.com/jesseduffield/generics/maps" - "github.com/jesseduffield/lazycore/pkg/utils" "github.com/jesseduffield/lazygit/pkg/app" "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/gocui" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/i18n" + "github.com/jesseduffield/lazygit/pkg/utils" "github.com/samber/lo" ) @@ -49,7 +49,7 @@ func CommandToRun() string { } func GetKeybindingsDir() string { - return utils.GetLazyRootDirectory() + "/docs-master/keybindings" + return utils.MustFindLazygitRootDirectory() + "/docs-master/keybindings" } func generateAtDir(cheatsheetDir string) { diff --git a/pkg/integration/clients/cli.go b/pkg/integration/clients/cli.go index 34a5f85bd..b8f4bc5cc 100644 --- a/pkg/integration/clients/cli.go +++ b/pkg/integration/clients/cli.go @@ -8,9 +8,9 @@ import ( "strconv" "strings" - "github.com/jesseduffield/lazycore/pkg/utils" "github.com/jesseduffield/lazygit/pkg/integration/components" "github.com/jesseduffield/lazygit/pkg/integration/tests" + "github.com/jesseduffield/lazygit/pkg/utils" "github.com/samber/lo" ) @@ -54,7 +54,7 @@ func runAndPrintFatalError(test *components.IntegrationTest, f func() error) { } func getTestsToRun(testNames []string) []*components.IntegrationTest { - allIntegrationTests := tests.GetTests(utils.GetLazyRootDirectory()) + allIntegrationTests := tests.GetTests(utils.MustFindLazygitRootDirectory()) var testsToRun []*components.IntegrationTest if len(testNames) == 0 { diff --git a/pkg/integration/clients/go_test.go b/pkg/integration/clients/go_test.go index 4c1faa557..30e08bcd8 100644 --- a/pkg/integration/clients/go_test.go +++ b/pkg/integration/clients/go_test.go @@ -16,9 +16,9 @@ import ( "time" "github.com/creack/pty" - "github.com/jesseduffield/lazycore/pkg/utils" "github.com/jesseduffield/lazygit/pkg/integration/components" "github.com/jesseduffield/lazygit/pkg/integration/tests" + "github.com/jesseduffield/lazygit/pkg/utils" "github.com/stretchr/testify/assert" ) @@ -37,8 +37,13 @@ func TestIntegration(t *testing.T) { codeCoverageDir := os.Getenv("LAZYGIT_GOCOVERDIR") testNumber := 0 - err := components.RunTests(components.RunTestArgs{ - Tests: tests.GetTests(utils.GetLazyRootDirectory()), + rootDir, err := utils.FindLazygitRootDirectory() + if err != nil { + t.Fatal(err) + } + + err = components.RunTests(components.RunTestArgs{ + Tests: tests.GetTests(rootDir), Logf: t.Logf, RunCmd: runCmdHeadless, TestWrapper: func(test *components.IntegrationTest, f func() error) { diff --git a/pkg/integration/clients/tui.go b/pkg/integration/clients/tui.go index 0f07b5b19..aa344cb27 100644 --- a/pkg/integration/clients/tui.go +++ b/pkg/integration/clients/tui.go @@ -9,12 +9,13 @@ import ( "path/filepath" "strings" - "github.com/jesseduffield/lazycore/pkg/utils" + lazycoreUtils "github.com/jesseduffield/lazycore/pkg/utils" "github.com/jesseduffield/lazygit/pkg/gocui" "github.com/jesseduffield/lazygit/pkg/gui" "github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/integration/components" "github.com/jesseduffield/lazygit/pkg/integration/tests" + "github.com/jesseduffield/lazygit/pkg/utils" "github.com/samber/lo" ) @@ -23,7 +24,7 @@ import ( var SLOW_INPUT_DELAY = 600 func RunTUI(raceDetector bool) { - rootDir := utils.GetLazyRootDirectory() + rootDir := utils.MustFindLazygitRootDirectory() testDir := filepath.Join(rootDir, "test", "integration") app := newApp(testDir) @@ -206,7 +207,7 @@ type app struct { } func newApp(testDir string) *app { - return &app{testDir: testDir, allTests: tests.GetTests(utils.GetLazyRootDirectory())} + return &app{testDir: testDir, allTests: tests.GetTests(utils.MustFindLazygitRootDirectory())} } func (self *app) getCurrentTest() *components.IntegrationTest { @@ -224,7 +225,7 @@ func (self *app) loadTests() { } func (self *app) adjustCursor() { - self.itemIdx = utils.Clamp(self.itemIdx, 0, len(self.filteredTests)-1) + self.itemIdx = lazycoreUtils.Clamp(self.itemIdx, 0, len(self.filteredTests)-1) } func (self *app) filterWithString(needle string) { diff --git a/pkg/integration/components/runner.go b/pkg/integration/components/runner.go index 098f3f2e9..6e686b17b 100644 --- a/pkg/integration/components/runner.go +++ b/pkg/integration/components/runner.go @@ -9,7 +9,6 @@ import ( "sync" "time" - lazycoreUtils "github.com/jesseduffield/lazycore/pkg/utils" "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/utils" @@ -40,12 +39,15 @@ type RunTestArgs struct { // showing what's actually happening during the test, but it's still good at running // tests in telling you about their results. func RunTests(args RunTestArgs) error { - projectRootDir := lazycoreUtils.GetLazyRootDirectory() - err := os.Chdir(projectRootDir) + projectRootDir, err := utils.FindLazygitRootDirectory() if err != nil { return err } + if err := os.Chdir(projectRootDir); err != nil { + return err + } + testDir := filepath.Join(projectRootDir, "test", "_results") if err := buildLazygit(args); err != nil { return err diff --git a/pkg/integration/components/test_test.go b/pkg/integration/components/test_test.go index cce96105a..3b608e698 100644 --- a/pkg/integration/components/test_test.go +++ b/pkg/integration/components/test_test.go @@ -5,13 +5,13 @@ import ( "path/filepath" "testing" - lazycoreUtils "github.com/jesseduffield/lazycore/pkg/utils" "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/gocui" "github.com/jesseduffield/lazygit/pkg/gui/types" integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types" + "github.com/jesseduffield/lazygit/pkg/utils" "github.com/stretchr/testify/assert" ) @@ -227,7 +227,12 @@ func TestFailingFixture(t *testing.T) { paths := NewPaths(t.TempDir()) assert.NoError(t, os.MkdirAll(paths.ActualRepo(), 0o777)) - workingDir, err := createFixture(test, paths, lazycoreUtils.GetLazyRootDirectory()) + rootDir, err := utils.FindLazygitRootDirectory() + if err != nil { + t.Fatal(err) + } + + workingDir, err := createFixture(test, paths, rootDir) assert.ErrorContains(t, err, "git checkout no-such-branch") assert.Empty(t, workingDir) diff --git a/pkg/jsonschema/generate.go b/pkg/jsonschema/generate.go index cf7596761..4f22b04bd 100644 --- a/pkg/jsonschema/generate.go +++ b/pkg/jsonschema/generate.go @@ -9,14 +9,14 @@ import ( "reflect" "strings" - "github.com/jesseduffield/lazycore/pkg/utils" "github.com/jesseduffield/lazygit/pkg/config" + "github.com/jesseduffield/lazygit/pkg/utils" "github.com/karimkhaleel/jsonschema" "github.com/samber/lo" ) func GetSchemaDir() string { - return utils.GetLazyRootDirectory() + "/schema-master" + return utils.MustFindLazygitRootDirectory() + "/schema-master" } func GenerateSchema() *jsonschema.Schema { diff --git a/pkg/jsonschema/generate_config_docs.go b/pkg/jsonschema/generate_config_docs.go index ba245e99a..0caa13db9 100644 --- a/pkg/jsonschema/generate_config_docs.go +++ b/pkg/jsonschema/generate_config_docs.go @@ -7,7 +7,7 @@ import ( "os" "strings" - "github.com/jesseduffield/lazycore/pkg/utils" + "github.com/jesseduffield/lazygit/pkg/utils" "github.com/karimkhaleel/jsonschema" "github.com/samber/lo" @@ -163,7 +163,7 @@ func (n *Node) MarshalYAML() (any, error) { } func writeToConfigDocs(config []byte) error { - configPath := utils.GetLazyRootDirectory() + "/docs-master/Config.md" + configPath := utils.MustFindLazygitRootDirectory() + "/docs-master/Config.md" markdown, err := os.ReadFile(configPath) if err != nil { return fmt.Errorf("Error reading Config.md file %w", err) diff --git a/pkg/utils/project_root.go b/pkg/utils/project_root.go new file mode 100644 index 000000000..54dd9f616 --- /dev/null +++ b/pkg/utils/project_root.go @@ -0,0 +1,69 @@ +package utils + +import ( + "fmt" + "log" + "os" + "path/filepath" + "slices" + "strings" + + "github.com/samber/lo" +) + +// FindLazygitRootDirectory returns the root directory of the lazygit source +// tree, by searching the working directory and its parents for the go.mod file +// that declares lazygit's module. Only development tools use it: the +// integration test runner, the cheatsheet generator, and the JSON schema +// generator. Not to be confused with finding the root directory of the +// repository that lazygit is being run in. +// +// We search upwards rather than expect to be called from the root directory, +// because `go test` runs each test binary in the source directory of its +// package, not in the directory that `go test` was invoked from. +func FindLazygitRootDirectory() (string, error) { + startDir, err := os.Getwd() + if err != nil { + return "", err + } + + dir := startDir + for { + if declaresLazygitModule(filepath.Join(dir, "go.mod")) { + return dir, nil + } + + parent := filepath.Dir(dir) + if parent == dir { + return "", fmt.Errorf( + "failed to find the lazygit root directory: there is no go.mod for a lazygit module in %s or any of its parent directories", + startDir) + } + dir = parent + } +} + +// MustFindLazygitRootDirectory is FindLazygitRootDirectory for tools that can't +// do anything useful if the directory isn't found. +func MustFindLazygitRootDirectory() string { + dir, err := FindLazygitRootDirectory() + if err != nil { + log.Fatal(err) + } + return dir +} + +// A fork is free to rename the module, so we accept any module path with a +// "lazygit" element in it, e.g. github.com/jesseduffield/lazygit. +func declaresLazygitModule(goModPath string) bool { + contents, err := os.ReadFile(goModPath) + if err != nil { + return false + } + + return lo.SomeBy(strings.Split(string(contents), "\n"), func(line string) bool { + fields := strings.Fields(line) + return len(fields) >= 2 && fields[0] == "module" && + slices.Contains(strings.Split(fields[1], "/"), "lazygit") + }) +} diff --git a/pkg/utils/project_root_test.go b/pkg/utils/project_root_test.go new file mode 100644 index 000000000..c5b1c8d28 --- /dev/null +++ b/pkg/utils/project_root_test.go @@ -0,0 +1,86 @@ +package utils + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFindLazygitRootDirectory(t *testing.T) { + // This test runs in the pkg/utils directory, so we expect the function to + // search two levels up for the project root. + expectedRootDir, err := filepath.Abs(filepath.Join("..", "..")) + assert.NoError(t, err) + + rootDir, err := FindLazygitRootDirectory() + + assert.NoError(t, err) + assert.Equal(t, expectedRootDir, rootDir) +} + +func TestFindLazygitRootDirectoryOutsideProject(t *testing.T) { + t.Chdir(t.TempDir()) + + _, err := FindLazygitRootDirectory() + + assert.ErrorContains(t, err, "there is no go.mod for a lazygit module") +} + +func TestDeclaresLazygitModule(t *testing.T) { + scenarios := []struct { + testName string + contents string + expected bool + }{ + { + testName: "lazygit's go.mod", + contents: "module github.com/jesseduffield/lazygit\n\ngo 1.25.0\n", + expected: true, + }, + { + testName: "a fork's go.mod", + contents: "module gitlab.com/somebody-else/lazygit\n\ngo 1.25.0\n", + expected: true, + }, + { + testName: "a fork's go.mod with a major version suffix", + contents: "module github.com/somebody-else/lazygit/v2\n\ngo 1.25.0\n", + expected: true, + }, + { + testName: "module declaration preceded by a comment", + contents: "// a comment\n\nmodule github.com/jesseduffield/lazygit\n", + expected: true, + }, + { + testName: "module declaration followed by a comment", + contents: "module github.com/jesseduffield/lazygit // comment\n\ngo 1.25.0\n", + expected: true, + }, + { + testName: "another project's go.mod", + contents: "module github.com/jesseduffield/lazydocker\n\ngo 1.25.0\n", + expected: false, + }, + { + testName: "no module declaration", + contents: "go 1.25.0\n", + expected: false, + }, + } + + for _, scenario := range scenarios { + t.Run(scenario.testName, func(t *testing.T) { + path := filepath.Join(t.TempDir(), "go.mod") + assert.NoError(t, os.WriteFile(path, []byte(scenario.contents), 0o644)) + + assert.Equal(t, scenario.expected, declaresLazygitModule(path)) + }) + } +} + +func TestDeclaresLazygitModuleWithoutAGoModFile(t *testing.T) { + assert.False(t, declaresLazygitModule(filepath.Join(t.TempDir(), "go.mod"))) +}