1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-07-30 03:23:08 +03:00

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.
This commit is contained in:
John Whitley
2023-12-24 08:46:02 -08:00
committed by Stefan Haller
parent 74d937881e
commit 3d9f1e02e5
25 changed files with 590 additions and 442 deletions

View File

@ -0,0 +1,65 @@
package components
import (
"fmt"
"os"
)
const (
// These values will be passed to lazygit
LAZYGIT_ROOT_DIR = "LAZYGIT_ROOT_DIR"
SANDBOX_ENV_VAR = "SANDBOX"
TEST_NAME_ENV_VAR = "TEST_NAME"
WAIT_FOR_DEBUGGER_ENV_VAR = "WAIT_FOR_DEBUGGER"
// These values will be passed to both lazygit and shell commands
GIT_CONFIG_GLOBAL_ENV_VAR = "GIT_CONFIG_GLOBAL"
// We pass PWD because if it's defined, Go will use it as the working directory
// rather than make a syscall to the OS, and that means symlinks won't be resolved,
// which is good to test for.
PWD = "PWD"
// We set $HOME and $GIT_CONFIG_NOGLOBAL during integrationt tests so
// that older versions of git that don't respect $GIT_CONFIG_GLOBAL
// will find the correct global config file for testing
HOME = "HOME"
GIT_CONFIG_NOGLOBAL = "GIT_CONFIG_NOGLOBAL"
// These values will be passed through to lazygit and shell commands, with their
// values inherited from the host environment
PATH = "PATH"
TERM = "TERM"
)
// Tests will inherit these environment variables from the host environment, rather
// than the test runner deciding the values itself.
// All other environment variables present in the host environment will be ignored.
// Having such a minimal list ensures that lazygit behaves the same across different test environments.
var hostEnvironmentAllowlist = [...]string{
PATH,
TERM,
}
// Returns a copy of the environment filtered by
// hostEnvironmentAllowlist
func allowedHostEnvironment() []string {
env := []string{}
for _, envVar := range hostEnvironmentAllowlist {
env = append(env, fmt.Sprintf("%s=%s", envVar, os.Getenv(envVar)))
}
return env
}
func NewTestEnvironment(rootDir string) []string {
env := allowedHostEnvironment()
// Set $HOME to control the global git config location for git
// versions <= 2.31.8
env = append(env, fmt.Sprintf("%s=%s", HOME, testPath(rootDir)))
// $GIT_CONFIG_GLOBAL controls global git config location for git
// versions >= 2.32.0
env = append(env, fmt.Sprintf("%s=%s", GIT_CONFIG_GLOBAL_ENV_VAR, globalGitConfigPath(rootDir)))
return env
}

View File

@ -13,14 +13,6 @@ import (
"github.com/samber/lo"
)
const (
LAZYGIT_ROOT_DIR = "LAZYGIT_ROOT_DIR"
TEST_NAME_ENV_VAR = "TEST_NAME"
SANDBOX_ENV_VAR = "SANDBOX"
WAIT_FOR_DEBUGGER_ENV_VAR = "WAIT_FOR_DEBUGGER"
GIT_CONFIG_GLOBAL_ENV_VAR = "GIT_CONFIG_GLOBAL"
)
type RunTestArgs struct {
Tests []*IntegrationTest
Logf func(format string, formatArgs ...interface{})
@ -161,18 +153,27 @@ func buildLazygit(testArgs RunTestArgs) error {
// Sets up the fixture for test and returns the working directory to invoke
// lazygit in.
func createFixture(test *IntegrationTest, paths Paths, rootDir string) string {
shell := NewShell(paths.ActualRepo(), func(errorMsg string) { panic(errorMsg) })
shell.Init()
env := NewTestEnvironment(rootDir)
os.Setenv(GIT_CONFIG_GLOBAL_ENV_VAR, globalGitConfigPath(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(rootDir, "test", "global_git_config")
return filepath.Join(testPath(rootDir), "global_git_config")
}
func getGitVersion() (*git_commands.GitVersion, error) {
@ -215,9 +216,16 @@ func getLazygitCommand(
})
cmdArgs = append(cmdArgs, resolvedExtraArgs...)
cmdObj := osCommand.Cmd.New(cmdArgs)
// 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))

View File

@ -19,6 +19,9 @@ import (
type Shell struct {
// working directory the shell is invoked in
dir string
// passed into each command
env []string
// when running the shell outside the gui we can directly panic on failure,
// but inside the gui we need to close the gui before panicking
fail func(string)
@ -26,14 +29,15 @@ type Shell struct {
randomFileContentIndex int
}
func NewShell(dir string, fail func(string)) *Shell {
return &Shell{dir: dir, fail: fail}
func NewShell(dir string, env []string, fail func(string)) *Shell {
return &Shell{dir: dir, env: env, fail: fail}
}
func (self *Shell) RunCommand(args []string) *Shell {
return self.RunCommandWithEnv(args, []string{})
}
// Run a command with additional environment variables set
func (self *Shell) RunCommandWithEnv(args []string, env []string) *Shell {
output, err := self.runCommandWithOutputAndEnv(args, env)
if err != nil {
@ -58,7 +62,7 @@ func (self *Shell) runCommandWithOutput(args []string) (string, error) {
func (self *Shell) runCommandWithOutputAndEnv(args []string, env []string) (string, error) {
cmd := exec.Command(args[0], args[1:]...)
cmd.Env = append(os.Environ(), env...)
cmd.Env = append(self.env, env...)
cmd.Dir = self.dir
output, err := cmd.CombinedOutput()
@ -461,8 +465,8 @@ func (self *Shell) CopyFile(source string, destination string) *Shell {
return self
}
// NOTE: this only takes effect before running the test;
// the test will still run in the original directory
// The final value passed to Chdir() during setup
// will be the directory the test is run from.
func (self *Shell) Chdir(path string) *Shell {
self.dir = filepath.Join(self.dir, path)

View File

@ -182,7 +182,13 @@ func (self *IntegrationTest) Run(gui integrationTypes.GuiDriver) {
panic(err)
}
shell := NewShell(pwd, func(errorMsg string) { gui.Fail(errorMsg) })
shell := NewShell(
pwd,
// passing the full environment because it's already been filtered down
// in the parent process.
os.Environ(),
func(errorMsg string) { gui.Fail(errorMsg) },
)
keys := gui.Keys()
testDriver := NewTestDriver(gui, shell, keys, InputDelay())

View File

@ -274,13 +274,16 @@ var tests = []*components.IntegrationTest{
worktree.AssociateBranchBisect,
worktree.AssociateBranchRebase,
worktree.BareRepo,
worktree.BareRepoWorktreeConfig,
worktree.Crud,
worktree.CustomCommand,
worktree.DetachWorktreeFromBranch,
worktree.DotfileBareRepo,
worktree.DoubleNestedLinkedSubmodule,
worktree.FastForwardWorktreeBranch,
worktree.ForceRemoveWorktree,
worktree.RemoveWorktreeFromBranch,
worktree.ResetWindowTabs,
worktree.SymlinkIntoRepoSubdir,
worktree.WorktreeInRepo,
}

View File

@ -0,0 +1,92 @@
package worktree
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
// This case is identical to dotfile_bare_repo.go, except
// that it invokes lazygit with $GIT_DIR set but not
// $GIT_WORK_TREE. Instead, the repo uses the core.worktree
// config to identify the main worktre.
var BareRepoWorktreeConfig = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Open lazygit in the worktree of a vcsh-style bare repo and add a file and commit",
ExtraCmdArgs: []string{"--git-dir={{.actualPath}}/.bare"},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
config.UserConfig.Gui.ShowFileTree = false
},
SetupRepo: func(shell *Shell) {
// we're going to have a directory structure like this:
// project
// - .bare
// - . (a worktree at the same path as .bare)
//
//
// 'repo' is the repository/directory that all lazygit tests start in
shell.CreateFileAndAdd("a/b/c/blah", "blah\n")
shell.Commit("initial commit")
shell.CreateFileAndAdd(".gitignore", ".bare/\n/repo\n")
shell.Commit("add .gitignore")
shell.Chdir("..")
// configure this "fake bare"" repo using the vcsh convention
// of core.bare=false and core.worktree set to the actual
// worktree path (a homedir root). This allows $GIT_DIR
// alone to make this repo "self worktree identifying"
shell.RunCommand([]string{"git", "--git-dir=./.bare", "init", "--shared=false"})
shell.RunCommand([]string{"git", "--git-dir=./.bare", "config", "core.bare", "false"})
shell.RunCommand([]string{"git", "--git-dir=./.bare", "config", "core.worktree", ".."})
shell.RunCommand([]string{"git", "--git-dir=./.bare", "remote", "add", "origin", "./repo"})
shell.RunCommand([]string{"git", "--git-dir=./.bare", "checkout", "-b", "main"})
shell.RunCommand([]string{"git", "--git-dir=./.bare", "config", "branch.main.remote", "origin"})
shell.RunCommand([]string{"git", "--git-dir=./.bare", "config", "branch.main.merge", "refs/heads/master"})
shell.RunCommand([]string{"git", "--git-dir=./.bare", "fetch", "origin", "master"})
shell.RunCommand([]string{"git", "--git-dir=./.bare", "-c", "merge.ff=true", "merge", "origin/master"})
// we no longer need the original repo so remove it
shell.DeleteFile("repo")
shell.UpdateFile("a/b/c/blah", "updated content\n")
shell.Chdir("a/b/c")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Lines(
Contains("main"),
)
t.Views().Commits().
Lines(
Contains("add .gitignore"),
Contains("initial commit"),
)
t.Views().Files().
IsFocused().
Lines(
Contains(" M a/b/c/blah"), // shows as modified
).
PressPrimaryAction().
Press(keys.Files.CommitChanges)
t.ExpectPopup().CommitMessagePanel().
Title(Equals("Commit summary")).
Type("Add blah").
Confirm()
t.Views().Files().
IsEmpty()
t.Views().Commits().
Lines(
Contains("Add blah"),
Contains("add .gitignore"),
Contains("initial commit"),
)
},
})

View File

@ -0,0 +1,93 @@
package worktree
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
// Even though this involves submodules, it's a worktree test since
// it's really exercising lazygit's ability to correctly do pathfinding
// in a complex use case.
var DoubleNestedLinkedSubmodule = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Open lazygit in a link to a repo's double nested submodules",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
config.UserConfig.Gui.ShowFileTree = false
},
SetupRepo: func(shell *Shell) {
// we're going to have a directory structure like this:
// project
// - repo/outerSubmodule/innerSubmodule/a/b/c
// - link (symlink to repo/outerSubmodule/innerSubmodule/a/b/c)
//
shell.CreateFileAndAdd("rootFile", "rootStuff")
shell.Commit("initial repo commit")
shell.Chdir("..")
shell.CreateDir("innerSubmodule")
shell.Chdir("innerSubmodule")
shell.Init()
shell.CreateFileAndAdd("a/b/c/blah", "blah\n")
shell.Commit("initial inner commit")
shell.Chdir("..")
shell.CreateDir("outerSubmodule")
shell.Chdir("outerSubmodule")
shell.Init()
shell.CreateFileAndAdd("foo", "foo")
shell.Commit("initial outer commit")
// the git config (-c) parameter below is required
// to let git create a file-protocol/path submodule
shell.RunCommand([]string{"git", "-c", "protocol.file.allow=always", "submodule", "add", "../innerSubmodule"})
shell.Commit("add dependency as innerSubmodule")
shell.Chdir("../repo")
shell.RunCommand([]string{"git", "-c", "protocol.file.allow=always", "submodule", "add", "../outerSubmodule"})
shell.Commit("add dependency as outerSubmodule")
shell.Chdir("outerSubmodule")
shell.RunCommand([]string{"git", "-c", "protocol.file.allow=always", "submodule", "update", "--init", "--recursive"})
shell.Chdir("innerSubmodule")
shell.UpdateFile("a/b/c/blah", "updated content\n")
shell.Chdir("../../..")
shell.RunCommand([]string{"ln", "-s", "repo/outerSubmodule/innerSubmodule/a/b/c", "link"})
shell.Chdir("link")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Lines(
Contains("HEAD detached"),
Contains("master"),
)
t.Views().Commits().
Lines(
Contains("initial inner commit"),
)
t.Views().Files().
IsFocused().
Lines(
Contains(" M a/b/c/blah"), // shows as modified
).
PressPrimaryAction().
Press(keys.Files.CommitChanges)
t.ExpectPopup().CommitMessagePanel().
Title(Equals("Commit summary")).
Type("Update blah").
Confirm()
t.Views().Files().
IsEmpty()
t.Views().Commits().
Lines(
Contains("Update blah"),
Contains("initial inner commit"),
)
},
})

View File

@ -0,0 +1,63 @@
package worktree
import (
"github.com/jesseduffield/lazygit/pkg/config"
. "github.com/jesseduffield/lazygit/pkg/integration/components"
)
var SymlinkIntoRepoSubdir = NewIntegrationTest(NewIntegrationTestArgs{
Description: "Open lazygit in a symlink into a repo's subdirectory",
ExtraCmdArgs: []string{},
Skip: false,
SetupConfig: func(config *config.AppConfig) {
config.UserConfig.Gui.ShowFileTree = false
},
SetupRepo: func(shell *Shell) {
// we're going to have a directory structure like this:
// project
// - repo/a/b/c (main worktree with subdirs)
// - link (symlink to repo/a/b/c)
//
shell.CreateFileAndAdd("a/b/c/blah", "blah\n")
shell.Commit("initial commit")
shell.UpdateFile("a/b/c/blah", "updated content\n")
shell.Chdir("..")
shell.RunCommand([]string{"ln", "-s", "repo/a/b/c", "link"})
shell.Chdir("link")
},
Run: func(t *TestDriver, keys config.KeybindingConfig) {
t.Views().Branches().
Lines(
Contains("master"),
)
t.Views().Commits().
Lines(
Contains("initial commit"),
)
t.Views().Files().
IsFocused().
Lines(
Contains(" M a/b/c/blah"), // shows as modified
).
PressPrimaryAction().
Press(keys.Files.CommitChanges)
t.ExpectPopup().CommitMessagePanel().
Title(Equals("Commit summary")).
Type("Add blah").
Confirm()
t.Views().Files().
IsEmpty()
t.Views().Commits().
Lines(
Contains("Add blah"),
Contains("initial commit"),
)
},
})