diff --git a/pkg/commands/git_commands/repo_paths.go b/pkg/commands/git_commands/repo_paths.go index 4e2f8ab1a..61afc943f 100644 --- a/pkg/commands/git_commands/repo_paths.go +++ b/pkg/commands/git_commands/repo_paths.go @@ -56,9 +56,19 @@ func (self *RepoPaths) RepoName() string { return self.repoName } -// Whether the repo has no worktree, so that there is nothing for lazygit to -// show. Note that this isn't quite git's core.bare: a repo that calls itself -// non-bare but doesn't have a worktree either counts as bare for us. +// Whether we found no worktree, so that there is nothing for lazygit to show. +// Note that this isn't quite git's core.bare: a repo that calls itself non-bare +// but whose worktree we couldn't find counts as bare for us too. Concretely, +// this is true when we're in +// +// - a genuinely bare repo; +// - the git dir of a linked worktree (.git/worktrees/x), whose worktree is +// recorded but not somewhere we look; +// - a repo that keeps its worktree somewhere only GIT_WORK_TREE knows, such +// as a vcsh-style dotfiles repo that hasn't been given core.worktree. +// +// The .git dir of an ordinary repo is not one of them: GetRepoPathsForDir +// notices the worktree holding it and hands back that repo instead. func (self *RepoPaths) IsBareRepo() bool { return self.isBareRepo } @@ -99,6 +109,39 @@ func GetRepoPaths( func GetRepoPathsForDir( dir string, cmd oscommands.ICmdObjBuilder, +) (*RepoPaths, error) { + repoPaths, err := repoPathsForDir(dir, cmd) + if err != nil || !repoPaths.IsBareRepo() { + return repoPaths, err + } + + // We're in a git dir rather than in a working tree, which usually just means + // somebody ran lazygit in the .git of an ordinary repo. git's convention is + // that a git dir called .git belongs to the directory holding it, so look + // there: if that is a working tree, it is the repo we were asked about, and + // there's no reason to make the user go up a directory and try again. + // + // The git dirs that aren't called .git keep the paths we have. A linked + // worktree's (.git/worktrees/x) and a submodule's (.git/modules/x) do have a + // working tree, but only the directory holding a .git tells us where, so we + // would be guessing. A bare repo's has none to find. + if filepath.Base(repoPaths.WorktreeGitDirPath()) != ".git" { + return repoPaths, nil + } + + pathsFromWorkTree, err := repoPathsForDir(filepath.Dir(repoPaths.WorktreeGitDirPath()), cmd) + if err != nil || pathsFromWorkTree.IsBareRepo() { + return repoPaths, nil + } + return pathsFromWorkTree, nil +} + +// repoPathsForDir asks git about the repo at dir, and reports a bare repo when +// there is no working tree there. Unlike GetRepoPathsForDir it never looks +// anywhere but dir, which is what keeps that one from going round in circles. +func repoPathsForDir( + dir string, + cmd oscommands.ICmdObjBuilder, ) (*RepoPaths, error) { gitDirOutput, err := callGitRevParseWithDir(cmd, dir, "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--show-superproject-working-tree") if err != nil { diff --git a/pkg/commands/git_commands/repo_paths_test.go b/pkg/commands/git_commands/repo_paths_test.go index e2827f22b..c7b7705b4 100644 --- a/pkg/commands/git_commands/repo_paths_test.go +++ b/pkg/commands/git_commands/repo_paths_test.go @@ -115,6 +115,48 @@ func TestGetRepoPaths(t *testing.T) { }), Err: nil, }, + { + // Standing in the .git dir of an ordinary repo: git refuses to name a + // work tree, but the directory holding the .git is one, so we open the + // repo from there. + Name: "in a repo's .git dir", + BeforeFunc: func(runner *oscommands.FakeCmdObjRunner, getRevParseArgs argFn) { + gitDir := lo.Ternary(runtime.GOOS == "windows", `C:\path\to\repo\.git`, "/path/to/repo/.git") + worktree := lo.Ternary(runtime.GOOS == "windows", `C:\path\to\repo`, "/path/to/repo") + + runner.ExpectGitArgs( + append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--show-superproject-working-tree"), + "", + errors.New("fatal: this operation must be run in a work tree")) + runner.ExpectGitArgs( + append(getRevParseArgs(), "--absolute-git-dir", "--git-common-dir"), + strings.Join([]string{gitDir, gitDir}, "\n"), + nil) + + // asking again from the directory holding the .git + runner.ExpectGitArgs( + append(append([]string{"-C", worktree}, getRevParseArgs()...), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--show-superproject-working-tree"), + strings.Join([]string{worktree, gitDir, gitDir}, "\n"), + nil) + }, + Path: "/path/to/repo/.git", + Expected: lo.Ternary(runtime.GOOS == "windows", &RepoPaths{ + worktreePath: `C:\path\to\repo`, + worktreeGitDirPath: `C:\path\to\repo\.git`, + repoPath: `C:\path\to\repo`, + repoGitDirPath: `C:\path\to\repo\.git`, + repoName: `repo`, + isBareRepo: false, + }, &RepoPaths{ + worktreePath: "/path/to/repo", + worktreeGitDirPath: "/path/to/repo/.git", + repoPath: "/path/to/repo", + repoGitDirPath: "/path/to/repo/.git", + repoName: "repo", + isBareRepo: false, + }), + Err: nil, + }, { // A repo whose work tree lives somewhere else entirely, as set up by // core.worktree or by --work-tree. We're in the main worktree, but the diff --git a/pkg/integration/tests/misc/start_in_git_dir.go b/pkg/integration/tests/misc/start_in_git_dir.go new file mode 100644 index 000000000..5fffcc2a9 --- /dev/null +++ b/pkg/integration/tests/misc/start_in_git_dir.go @@ -0,0 +1,34 @@ +package misc + +import ( + "github.com/jesseduffield/lazygit/pkg/config" + . "github.com/jesseduffield/lazygit/pkg/integration/components" +) + +var StartInGitDir = NewIntegrationTest(NewIntegrationTestArgs{ + Description: "Start lazygit in a repo's .git dir, and have it open the repo", + ExtraCmdArgs: []string{}, + Skip: false, + SetupConfig: func(cfg *config.AppConfig) {}, + SetupRepo: func(shell *Shell) { + shell.CreateFileAndAdd("blah", "original content\n") + shell.Commit("initial commit") + shell.UpdateFile("blah", "updated content\n") + + // this is where lazygit will start + shell.Chdir(".git") + }, + Run: func(t *TestDriver, keys config.KeybindingConfig) { + t.Views().Commits(). + Lines( + Contains("initial commit"), + ) + + // we're in the work tree the .git belongs to, not in the .git itself + t.Views().Files(). + IsFocused(). + Lines( + Contains(" M blah"), + ) + }, +}) diff --git a/pkg/integration/tests/test_list.go b/pkg/integration/tests/test_list.go index 9598982d3..bc2c25e09 100644 --- a/pkg/integration/tests/test_list.go +++ b/pkg/integration/tests/test_list.go @@ -353,6 +353,7 @@ var tests = []*components.IntegrationTest{ misc.DirenvUnloadsOnBlockedEnvrc, misc.InitialOpen, misc.RecentReposOnLaunch, + misc.StartInGitDir, patch_building.Apply, patch_building.ApplyInReverse, patch_building.ApplyInReverseWithConflict,