1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-03-23 21:51:07 +02:00
John Whitley 3d9f1e02e5 Refactor repo_paths.go to use git rev-parse
This changes GetRepoPaths() to pull information from `git rev-parse`
instead of effectively reimplementing git's logic for pathfinding. This
change fixes issues with bare repos, esp. versioned homedir use cases,
by aligning lazygit's path handling to what git itself does.

This change also enables lazygit to run from arbitrary subdirectories of
a repository, including correct handling of symlinks, including "deep"
symlinks into a repo, worktree, a repo's submodules, etc.

Integration tests are now resilient against unintended side effects from
the host's environment variables. Of necessity, $PATH and $TERM are the
only env vars allowed through now.
2024-01-24 08:40:01 +01:00

303 lines
7.8 KiB
Go

package components
import (
"fmt"
"os"
"os/exec"
"path/filepath"
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"
"github.com/samber/lo"
)
type RunTestArgs struct {
Tests []*IntegrationTest
Logf func(format string, formatArgs ...interface{})
RunCmd func(cmd *exec.Cmd) (int, error)
TestWrapper func(test *IntegrationTest, f func() error)
Sandbox bool
WaitForDebugger bool
RaceDetector bool
CodeCoverageDir string
InputDelay int
MaxAttempts int
}
// This function lets you run tests either from within `go test` or from a regular binary.
// The reason for having two separate ways of testing is that `go test` isn't great at
// 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)
if err != nil {
return err
}
testDir := filepath.Join(projectRootDir, "test", "_results")
if err := buildLazygit(args); err != nil {
return err
}
gitVersion, err := getGitVersion()
if err != nil {
return err
}
for _, test := range args.Tests {
test := test
args.TestWrapper(test, func() error { //nolint: thelper
paths := NewPaths(
filepath.Join(testDir, test.Name()),
)
for i := 0; i < args.MaxAttempts; i++ {
err := runTest(test, args, paths, projectRootDir, gitVersion)
if err != nil {
if i == args.MaxAttempts-1 {
return err
}
args.Logf("retrying test %s", test.Name())
} else {
break
}
}
return nil
})
}
return nil
}
func runTest(
test *IntegrationTest,
args RunTestArgs,
paths Paths,
projectRootDir string,
gitVersion *git_commands.GitVersion,
) error {
if test.Skip() {
args.Logf("Skipping test %s", test.Name())
return nil
}
if !test.ShouldRunForGitVersion(gitVersion) {
args.Logf("Skipping test %s for git version %d.%d.%d", test.Name(), gitVersion.Major, gitVersion.Minor, gitVersion.Patch)
return nil
}
workingDir, err := prepareTestDir(test, paths, projectRootDir)
if err != nil {
return err
}
cmd, err := getLazygitCommand(test, args, paths, projectRootDir, workingDir)
if err != nil {
return err
}
pid, err := args.RunCmd(cmd)
// Print race detector log regardless of the command's exit status
if args.RaceDetector {
logPath := fmt.Sprintf("%s.%d", raceDetectorLogsPath(), pid)
if bytes, err := os.ReadFile(logPath); err == nil {
args.Logf("Race detector log:\n" + string(bytes))
}
}
return err
}
func prepareTestDir(
test *IntegrationTest,
paths Paths,
rootDir string,
) (string, error) {
findOrCreateDir(paths.Root())
deleteAndRecreateEmptyDir(paths.Actual())
err := os.Mkdir(paths.ActualRepo(), 0o777)
if err != nil {
return "", err
}
workingDir := createFixture(test, paths, rootDir)
return workingDir, nil
}
func buildLazygit(testArgs RunTestArgs) error {
args := []string{"go", "build"}
if testArgs.WaitForDebugger {
// Disable compiler optimizations (-N) and inlining (-l) because this
// makes debugging work better
args = append(args, "-gcflags=all=-N -l")
}
if testArgs.RaceDetector {
args = append(args, "-race")
}
if testArgs.CodeCoverageDir != "" {
args = append(args, "-cover")
}
args = append(args, "-o", tempLazygitPath(), filepath.FromSlash("pkg/integration/clients/injector/main.go"))
osCommand := oscommands.NewDummyOSCommand()
return osCommand.Cmd.New(args).Run()
}
// Sets up the fixture for test and returns the working directory to invoke
// lazygit in.
func createFixture(test *IntegrationTest, paths Paths, rootDir string) string {
env := NewTestEnvironment(rootDir)
env = append(env, fmt.Sprintf("%s=%s", PWD, paths.ActualRepo()))
shell := NewShell(
paths.ActualRepo(),
env,
func(errorMsg string) { panic(errorMsg) },
)
shell.Init()
test.SetupRepo(shell)
return shell.dir
}
func testPath(rootdir string) string {
return filepath.Join(rootdir, "test")
}
func globalGitConfigPath(rootDir string) string {
return filepath.Join(testPath(rootDir), "global_git_config")
}
func getGitVersion() (*git_commands.GitVersion, error) {
osCommand := oscommands.NewDummyOSCommand()
cmdObj := osCommand.Cmd.New([]string{"git", "--version"})
versionStr, err := cmdObj.RunWithOutput()
if err != nil {
return nil, err
}
return git_commands.ParseGitVersion(versionStr)
}
func getLazygitCommand(
test *IntegrationTest,
args RunTestArgs,
paths Paths,
rootDir string,
workingDir string,
) (*exec.Cmd, error) {
osCommand := oscommands.NewDummyOSCommand()
err := os.RemoveAll(paths.Config())
if err != nil {
return nil, err
}
templateConfigDir := filepath.Join(rootDir, "test", "default_test_config")
err = oscommands.CopyDir(templateConfigDir, paths.Config())
if err != nil {
return nil, err
}
cmdArgs := []string{tempLazygitPath(), "-debug", "--use-config-dir=" + paths.Config()}
resolvedExtraArgs := lo.Map(test.ExtraCmdArgs(), func(arg string, _ int) string {
return utils.ResolvePlaceholderString(arg, map[string]string{
"actualPath": paths.Actual(),
"actualRepoPath": paths.ActualRepo(),
})
})
cmdArgs = append(cmdArgs, resolvedExtraArgs...)
// Use a limited environment for test isolation, including pass through
// of just allowed host environment variables
cmdObj := osCommand.Cmd.NewWithEnviron(cmdArgs, NewTestEnvironment(rootDir))
// Integration tests related to symlink behavior need a PWD that
// preserves symlinks. By default, SetWd will set a symlink-resolved
// value for PWD. Here, we override that with the path (that may)
// contain a symlink to simulate behavior in a user's shell correctly.
cmdObj.SetWd(workingDir)
cmdObj.AddEnvVars(fmt.Sprintf("%s=%s", PWD, workingDir))
cmdObj.AddEnvVars(fmt.Sprintf("%s=%s", LAZYGIT_ROOT_DIR, rootDir))
if args.CodeCoverageDir != "" {
// We set this explicitly here rather than inherit it from the test runner's
// environment because the test runner has its own coverage directory that
// it writes to and so if we pass GOCOVERDIR to that, it will be overwritten.
cmdObj.AddEnvVars("GOCOVERDIR=" + args.CodeCoverageDir)
}
cmdObj.AddEnvVars(fmt.Sprintf("%s=%s", TEST_NAME_ENV_VAR, test.Name()))
if args.Sandbox {
cmdObj.AddEnvVars(fmt.Sprintf("%s=%s", SANDBOX_ENV_VAR, "true"))
}
if args.WaitForDebugger {
cmdObj.AddEnvVars(fmt.Sprintf("%s=true", WAIT_FOR_DEBUGGER_ENV_VAR))
}
// Set a race detector log path only to avoid spamming the terminal with the
// logs. We are not showing this anywhere yet.
cmdObj.AddEnvVars(fmt.Sprintf("GORACE=log_path=%s", raceDetectorLogsPath()))
if test.ExtraEnvVars() != nil {
for key, value := range test.ExtraEnvVars() {
cmdObj.AddEnvVars(fmt.Sprintf("%s=%s", key, value))
}
}
if args.InputDelay > 0 {
cmdObj.AddEnvVars(fmt.Sprintf("INPUT_DELAY=%d", args.InputDelay))
}
cmdObj.AddEnvVars(fmt.Sprintf("%s=%s", GIT_CONFIG_GLOBAL_ENV_VAR, globalGitConfigPath(rootDir)))
return cmdObj.GetCmd(), nil
}
func tempLazygitPath() string {
return filepath.Join("/tmp", "lazygit", "test_lazygit")
}
func raceDetectorLogsPath() string {
return filepath.Join("/tmp", "lazygit", "race_log")
}
func findOrCreateDir(path string) {
_, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
err = os.MkdirAll(path, 0o777)
if err != nil {
panic(err)
}
} else {
panic(err)
}
}
}
func deleteAndRecreateEmptyDir(path string) {
// remove contents of integration test directory
dir, err := os.ReadDir(path)
if err != nil {
if os.IsNotExist(err) {
err = os.Mkdir(path, 0o777)
if err != nil {
panic(err)
}
} else {
panic(err)
}
}
for _, d := range dir {
os.RemoveAll(filepath.Join(path, d.Name()))
}
}