Compare commits

..

3 commits

Author SHA1 Message Date
Stefan Haller d0ede21e9c
Allow running tests from a tarball (#6003)
Some checks failed
Continuous Integration / ci - ${{matrix.os}} (~/.cache/go-build, ubuntu-latest) (push) Has been cancelled
Continuous Integration / ci - ${{matrix.os}} (~\AppData\Local\go-build, windows-latest) (push) Has been cancelled
Continuous Integration / Integration Tests - git ${{matrix.git-version}}${{ matrix.race && ' (race)' || '' }} (2.32.0, false) (push) Has been cancelled
Continuous Integration / Integration Tests - git ${{matrix.git-version}}${{ matrix.race && ' (race)' || '' }} (2.38.2, false) (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been cancelled
Continuous Integration / Integration Tests - git ${{matrix.git-version}}${{ matrix.race && ' (race)' || '' }} (2.44.0, false) (push) Has been cancelled
Continuous Integration / Integration Tests - git ${{matrix.git-version}}${{ matrix.race && ' (race)' || '' }} (latest, false) (push) Has been cancelled
Continuous Integration / Integration Tests - git ${{matrix.git-version}}${{ matrix.race && ' (race)' || '' }} (latest, true) (push) Has been cancelled
Continuous Integration / build (push) Has been cancelled
Continuous Integration / check-codebase (push) Has been cancelled
Continuous Integration / lint (push) Has been cancelled
Continuous Integration / check-for-fixups (push) Has been cancelled
Codespell / Check for spelling errors (push) Has been cancelled
Continuous Integration / upload-coverage (push) Has been cancelled
Running integration tests from a tarball was never possible, but running
unit tests (`go test ./... -short`) was; this broke with v0.64.1
(specifically, with 34da956f5d). Make that possible again, and for all
tests now, including integration tests.
2026-09-09 09:30:10 +02:00
Stefan Haller 8b049be31d Find the lazygit root directory by go.mod instead of .git
Running the tests in an exported source tarball fails with "must run in
lazy project folder or child folder". GetLazyRootDirectory searches the
working directory and its parents for a .git directory, and a tarball
doesn't have one. This has always affected the integration tests; since
34da956f5d a unit test calls the function too, so now even
`go test ./... -short` fails.

Search for the go.mod file that declares lazygit's module instead. It
ships in tarballs, and there is exactly one of it per source tree.

Put the function in our own pkg/utils rather than change lazycore's; the
criterion is specific to lazygit, and I don't feel like making a change
to lazycore.

Return an error rather than call log.Fatal, and report it from the two
callers that run under `go test`. In a test binary, log.Fatal exits
without attributing the failure to any test. That is the failure mode
34da956f5d set out to remove. The remaining callers are development
tools that have nothing useful to do without the root directory; they
keep exiting, now through MustFindLazygitRootDirectory.

Also stop the search at the root of the file system rather than at "/".
On Windows the old loop walks up to "C:\" and then spins there forever.
2026-09-09 09:27:44 +02:00
Stefan Haller a8dc4aaf1b AGENTS.md addition 2026-09-07 09:51:49 +02:00
11 changed files with 192 additions and 20 deletions

View file

@ -120,6 +120,10 @@ separate, reviewable commit that the user decides when to fold in. A bare
`--amend` rewrites the commit on the spot and skips that checkpoint. Don't `--amend` rewrites the commit on the spot and skips that checkpoint. Don't
treat "I'm only touching the tip commit" as an exception. treat "I'm only touching the tip commit" as an exception.
Always use `fixup!` or `amend!` commits, never amend changes directly, even if
you naturally would because "the branch isn't pushed yet". The user always wants
to review what you changed, so make this transparent; no exceptions.
**When the tip is the wrong place for a fixup, insert it mid-branch.** **When the tip is the wrong place for a fixup, insert it mid-branch.**
Committing a fixup at the tip of the branch only works while the code it Committing a fixup at the tip of the branch only works while the code it
touches still looks the same there; once later commits have rewritten that touches still looks the same there; once later commits have rewritten that

View file

@ -19,12 +19,12 @@ import (
"strings" "strings"
"github.com/jesseduffield/generics/maps" "github.com/jesseduffield/generics/maps"
"github.com/jesseduffield/lazycore/pkg/utils"
"github.com/jesseduffield/lazygit/pkg/app" "github.com/jesseduffield/lazygit/pkg/app"
"github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gocui" "github.com/jesseduffield/lazygit/pkg/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
"github.com/jesseduffield/lazygit/pkg/i18n" "github.com/jesseduffield/lazygit/pkg/i18n"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo" "github.com/samber/lo"
) )
@ -49,7 +49,7 @@ func CommandToRun() string {
} }
func GetKeybindingsDir() string { func GetKeybindingsDir() string {
return utils.GetLazyRootDirectory() + "/docs-master/keybindings" return utils.MustFindLazygitRootDirectory() + "/docs-master/keybindings"
} }
func generateAtDir(cheatsheetDir string) { func generateAtDir(cheatsheetDir string) {

View file

@ -8,9 +8,9 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/jesseduffield/lazycore/pkg/utils"
"github.com/jesseduffield/lazygit/pkg/integration/components" "github.com/jesseduffield/lazygit/pkg/integration/components"
"github.com/jesseduffield/lazygit/pkg/integration/tests" "github.com/jesseduffield/lazygit/pkg/integration/tests"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo" "github.com/samber/lo"
) )
@ -54,7 +54,7 @@ func runAndPrintFatalError(test *components.IntegrationTest, f func() error) {
} }
func getTestsToRun(testNames []string) []*components.IntegrationTest { func getTestsToRun(testNames []string) []*components.IntegrationTest {
allIntegrationTests := tests.GetTests(utils.GetLazyRootDirectory()) allIntegrationTests := tests.GetTests(utils.MustFindLazygitRootDirectory())
var testsToRun []*components.IntegrationTest var testsToRun []*components.IntegrationTest
if len(testNames) == 0 { if len(testNames) == 0 {

View file

@ -16,9 +16,9 @@ import (
"time" "time"
"github.com/creack/pty" "github.com/creack/pty"
"github.com/jesseduffield/lazycore/pkg/utils"
"github.com/jesseduffield/lazygit/pkg/integration/components" "github.com/jesseduffield/lazygit/pkg/integration/components"
"github.com/jesseduffield/lazygit/pkg/integration/tests" "github.com/jesseduffield/lazygit/pkg/integration/tests"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -37,8 +37,13 @@ func TestIntegration(t *testing.T) {
codeCoverageDir := os.Getenv("LAZYGIT_GOCOVERDIR") codeCoverageDir := os.Getenv("LAZYGIT_GOCOVERDIR")
testNumber := 0 testNumber := 0
err := components.RunTests(components.RunTestArgs{ rootDir, err := utils.FindLazygitRootDirectory()
Tests: tests.GetTests(utils.GetLazyRootDirectory()), if err != nil {
t.Fatal(err)
}
err = components.RunTests(components.RunTestArgs{
Tests: tests.GetTests(rootDir),
Logf: t.Logf, Logf: t.Logf,
RunCmd: runCmdHeadless, RunCmd: runCmdHeadless,
TestWrapper: func(test *components.IntegrationTest, f func() error) { TestWrapper: func(test *components.IntegrationTest, f func() error) {

View file

@ -9,12 +9,13 @@ import (
"path/filepath" "path/filepath"
"strings" "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/gocui"
"github.com/jesseduffield/lazygit/pkg/gui" "github.com/jesseduffield/lazygit/pkg/gui"
"github.com/jesseduffield/lazygit/pkg/gui/style" "github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/integration/components" "github.com/jesseduffield/lazygit/pkg/integration/components"
"github.com/jesseduffield/lazygit/pkg/integration/tests" "github.com/jesseduffield/lazygit/pkg/integration/tests"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/samber/lo" "github.com/samber/lo"
) )
@ -23,7 +24,7 @@ import (
var SLOW_INPUT_DELAY = 600 var SLOW_INPUT_DELAY = 600
func RunTUI(raceDetector bool) { func RunTUI(raceDetector bool) {
rootDir := utils.GetLazyRootDirectory() rootDir := utils.MustFindLazygitRootDirectory()
testDir := filepath.Join(rootDir, "test", "integration") testDir := filepath.Join(rootDir, "test", "integration")
app := newApp(testDir) app := newApp(testDir)
@ -206,7 +207,7 @@ type app struct {
} }
func newApp(testDir string) *app { 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 { func (self *app) getCurrentTest() *components.IntegrationTest {
@ -224,7 +225,7 @@ func (self *app) loadTests() {
} }
func (self *app) adjustCursor() { 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) { func (self *app) filterWithString(needle string) {

View file

@ -9,7 +9,6 @@ import (
"sync" "sync"
"time" "time"
lazycoreUtils "github.com/jesseduffield/lazycore/pkg/utils"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands" "github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/utils" "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 // showing what's actually happening during the test, but it's still good at running
// tests in telling you about their results. // tests in telling you about their results.
func RunTests(args RunTestArgs) error { func RunTests(args RunTestArgs) error {
projectRootDir := lazycoreUtils.GetLazyRootDirectory() projectRootDir, err := utils.FindLazygitRootDirectory()
err := os.Chdir(projectRootDir)
if err != nil { if err != nil {
return err return err
} }
if err := os.Chdir(projectRootDir); err != nil {
return err
}
testDir := filepath.Join(projectRootDir, "test", "_results") testDir := filepath.Join(projectRootDir, "test", "_results")
if err := buildLazygit(args); err != nil { if err := buildLazygit(args); err != nil {
return err return err

View file

@ -5,13 +5,13 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
lazycoreUtils "github.com/jesseduffield/lazycore/pkg/utils"
"github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/git_commands"
"github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/commands/models"
"github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/gocui" "github.com/jesseduffield/lazygit/pkg/gocui"
"github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/gui/types"
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types" integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
@ -227,7 +227,12 @@ func TestFailingFixture(t *testing.T) {
paths := NewPaths(t.TempDir()) paths := NewPaths(t.TempDir())
assert.NoError(t, os.MkdirAll(paths.ActualRepo(), 0o777)) 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.ErrorContains(t, err, "git checkout no-such-branch")
assert.Empty(t, workingDir) assert.Empty(t, workingDir)

View file

@ -9,14 +9,14 @@ import (
"reflect" "reflect"
"strings" "strings"
"github.com/jesseduffield/lazycore/pkg/utils"
"github.com/jesseduffield/lazygit/pkg/config" "github.com/jesseduffield/lazygit/pkg/config"
"github.com/jesseduffield/lazygit/pkg/utils"
"github.com/karimkhaleel/jsonschema" "github.com/karimkhaleel/jsonschema"
"github.com/samber/lo" "github.com/samber/lo"
) )
func GetSchemaDir() string { func GetSchemaDir() string {
return utils.GetLazyRootDirectory() + "/schema-master" return utils.MustFindLazygitRootDirectory() + "/schema-master"
} }
func GenerateSchema() *jsonschema.Schema { func GenerateSchema() *jsonschema.Schema {

View file

@ -7,7 +7,7 @@ import (
"os" "os"
"strings" "strings"
"github.com/jesseduffield/lazycore/pkg/utils" "github.com/jesseduffield/lazygit/pkg/utils"
"github.com/karimkhaleel/jsonschema" "github.com/karimkhaleel/jsonschema"
"github.com/samber/lo" "github.com/samber/lo"
@ -163,7 +163,7 @@ func (n *Node) MarshalYAML() (any, error) {
} }
func writeToConfigDocs(config []byte) 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) markdown, err := os.ReadFile(configPath)
if err != nil { if err != nil {
return fmt.Errorf("Error reading Config.md file %w", err) return fmt.Errorf("Error reading Config.md file %w", err)

69
pkg/utils/project_root.go Normal file
View file

@ -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")
})
}

View file

@ -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")))
}