mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 15:46:26 -04:00
Running lazygit in a .git dir got you told you were in a bare repo, which you weren't: the worktree was sitting right there, one directory up. git's own convention is that a git dir called .git belongs to the directory holding it — that's how `git worktree list` names the main worktree — so ask that directory, and if it is a worktree, open the repo we were really being asked about. The git dirs that aren't called .git keep the answer they had. A linked worktree's and a submodule's do have a worktree, but nothing we look at says where, so we would be guessing; a bare repo's has none to find. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
317 lines
11 KiB
Go
317 lines
11 KiB
Go
package git_commands
|
|
|
|
import (
|
|
"fmt"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/go-errors/errors"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
|
"github.com/samber/lo"
|
|
"github.com/stretchr/testify/assert"
|
|
)
|
|
|
|
type (
|
|
argFn func() []string
|
|
errFn func(getRevParseArgs argFn) error
|
|
)
|
|
|
|
type Scenario struct {
|
|
Name string
|
|
BeforeFunc func(runner *oscommands.FakeCmdObjRunner, getRevParseArgs argFn)
|
|
Path string
|
|
Expected *RepoPaths
|
|
Err errFn
|
|
}
|
|
|
|
func TestGetRepoPaths(t *testing.T) {
|
|
scenarios := []Scenario{
|
|
{
|
|
Name: "typical case",
|
|
BeforeFunc: func(runner *oscommands.FakeCmdObjRunner, getRevParseArgs argFn) {
|
|
// setup for main worktree
|
|
mockOutput := lo.Ternary(runtime.GOOS == "windows", []string{
|
|
// --show-toplevel
|
|
`C:\path\to\repo`,
|
|
// --git-dir
|
|
`C:\path\to\repo\.git`,
|
|
// --git-common-dir
|
|
`C:\path\to\repo\.git`,
|
|
// --show-superproject-working-tree
|
|
}, []string{
|
|
// --show-toplevel
|
|
"/path/to/repo",
|
|
// --git-dir
|
|
"/path/to/repo/.git",
|
|
// --git-common-dir
|
|
"/path/to/repo/.git",
|
|
// --show-superproject-working-tree
|
|
})
|
|
runner.ExpectGitArgs(
|
|
append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--show-superproject-working-tree"),
|
|
strings.Join(mockOutput, "\n"),
|
|
nil)
|
|
},
|
|
Path: "/path/to/repo",
|
|
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,
|
|
},
|
|
{
|
|
// git refuses to answer --show-toplevel when there's no work tree, so
|
|
// we have to ask a second time without it.
|
|
Name: "bare repo",
|
|
BeforeFunc: func(runner *oscommands.FakeCmdObjRunner, getRevParseArgs argFn) {
|
|
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"))
|
|
|
|
mockOutput := lo.Ternary(runtime.GOOS == "windows", []string{
|
|
// --git-dir
|
|
`C:\path\to\project\bare.git`,
|
|
// --git-common-dir
|
|
`C:\path\to\project\bare.git`,
|
|
}, []string{
|
|
// --git-dir
|
|
"/path/to/project/bare.git",
|
|
// --git-common-dir
|
|
"/path/to/project/bare.git",
|
|
})
|
|
runner.ExpectGitArgs(
|
|
append(getRevParseArgs(), "--absolute-git-dir", "--git-common-dir"),
|
|
strings.Join(mockOutput, "\n"),
|
|
nil)
|
|
},
|
|
Path: "/path/to/project",
|
|
Expected: lo.Ternary(runtime.GOOS == "windows", &RepoPaths{
|
|
worktreePath: "",
|
|
worktreeGitDirPath: `C:\path\to\project\bare.git`,
|
|
repoPath: `C:\path\to\project`,
|
|
repoGitDirPath: `C:\path\to\project\bare.git`,
|
|
repoName: `project`,
|
|
isBareRepo: true,
|
|
}, &RepoPaths{
|
|
worktreePath: "",
|
|
worktreeGitDirPath: "/path/to/project/bare.git",
|
|
repoPath: "/path/to/project",
|
|
repoGitDirPath: "/path/to/project/bare.git",
|
|
repoName: "project",
|
|
isBareRepo: true,
|
|
}),
|
|
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
|
|
// git dir is not inside it.
|
|
Name: "repo with a separate work tree",
|
|
BeforeFunc: func(runner *oscommands.FakeCmdObjRunner, getRevParseArgs argFn) {
|
|
mockOutput := lo.Ternary(runtime.GOOS == "windows", []string{
|
|
// --show-toplevel
|
|
`C:\path\to\worktree`,
|
|
// --git-dir
|
|
`C:\path\to\repo\.git`,
|
|
// --git-common-dir
|
|
`C:\path\to\repo\.git`,
|
|
// --show-superproject-working-tree
|
|
}, []string{
|
|
// --show-toplevel
|
|
"/path/to/worktree",
|
|
// --git-dir
|
|
"/path/to/repo/.git",
|
|
// --git-common-dir
|
|
"/path/to/repo/.git",
|
|
// --show-superproject-working-tree
|
|
})
|
|
runner.ExpectGitArgs(
|
|
append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--show-superproject-working-tree"),
|
|
strings.Join(mockOutput, "\n"),
|
|
nil)
|
|
|
|
// asking git to find the repo from the work tree gets us nowhere,
|
|
// because there is no .git there
|
|
worktree := lo.Ternary(runtime.GOOS == "windows", `C:\path\to\worktree`, "/path/to/worktree")
|
|
runner.ExpectGitArgs(
|
|
append([]string{"-C", worktree}, append(getRevParseArgs(), "--absolute-git-dir")...),
|
|
"",
|
|
errors.New("fatal: not a git repository (or any of the parent directories): .git"))
|
|
},
|
|
Path: "/path/to/repo",
|
|
Expected: lo.Ternary(runtime.GOOS == "windows", &RepoPaths{
|
|
worktreePath: `C:\path\to\worktree`,
|
|
worktreeGitDirPath: `C:\path\to\repo\.git`,
|
|
repoPath: `C:\path\to\worktree`,
|
|
repoGitDirPath: `C:\path\to\repo\.git`,
|
|
repoName: `worktree`,
|
|
isBareRepo: false,
|
|
gitLocationEnvVars: []string{`GIT_DIR=C:\path\to\repo\.git`, `GIT_WORK_TREE=C:\path\to\worktree`},
|
|
}, &RepoPaths{
|
|
worktreePath: "/path/to/worktree",
|
|
worktreeGitDirPath: "/path/to/repo/.git",
|
|
repoPath: "/path/to/worktree",
|
|
repoGitDirPath: "/path/to/repo/.git",
|
|
repoName: "worktree",
|
|
isBareRepo: false,
|
|
gitLocationEnvVars: []string{"GIT_DIR=/path/to/repo/.git", "GIT_WORK_TREE=/path/to/worktree"},
|
|
}),
|
|
Err: nil,
|
|
},
|
|
{
|
|
Name: "submodule",
|
|
BeforeFunc: func(runner *oscommands.FakeCmdObjRunner, getRevParseArgs argFn) {
|
|
mockOutput := lo.Ternary(runtime.GOOS == "windows", []string{
|
|
// --show-toplevel
|
|
`C:\path\to\repo\submodule1`,
|
|
// --git-dir
|
|
`C:\path\to\repo\.git\modules\submodule1`,
|
|
// --git-common-dir
|
|
`C:\path\to\repo\.git\modules\submodule1`,
|
|
// --show-superproject-working-tree
|
|
`C:\path\to\repo`,
|
|
}, []string{
|
|
// --show-toplevel
|
|
"/path/to/repo/submodule1",
|
|
// --git-dir
|
|
"/path/to/repo/.git/modules/submodule1",
|
|
// --git-common-dir
|
|
"/path/to/repo/.git/modules/submodule1",
|
|
// --show-superproject-working-tree
|
|
"/path/to/repo",
|
|
})
|
|
runner.ExpectGitArgs(
|
|
append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--show-superproject-working-tree"),
|
|
strings.Join(mockOutput, "\n"),
|
|
nil)
|
|
|
|
// git finds the submodule's git dir from its work tree, via the
|
|
// .git file there
|
|
worktree := lo.Ternary(runtime.GOOS == "windows", `C:\path\to\repo\submodule1`, "/path/to/repo/submodule1")
|
|
gitDir := lo.Ternary(runtime.GOOS == "windows", `C:\path\to\repo\.git\modules\submodule1`, "/path/to/repo/.git/modules/submodule1")
|
|
runner.ExpectGitArgs(
|
|
append([]string{"-C", worktree}, append(getRevParseArgs(), "--absolute-git-dir")...),
|
|
gitDir,
|
|
nil)
|
|
},
|
|
Path: "/path/to/repo/submodule1",
|
|
Expected: lo.Ternary(runtime.GOOS == "windows", &RepoPaths{
|
|
worktreePath: `C:\path\to\repo\submodule1`,
|
|
worktreeGitDirPath: `C:\path\to\repo\.git\modules\submodule1`,
|
|
repoPath: `C:\path\to\repo\submodule1`,
|
|
repoGitDirPath: `C:\path\to\repo\.git\modules\submodule1`,
|
|
repoName: `submodule1`,
|
|
isBareRepo: false,
|
|
}, &RepoPaths{
|
|
worktreePath: "/path/to/repo/submodule1",
|
|
worktreeGitDirPath: "/path/to/repo/.git/modules/submodule1",
|
|
repoPath: "/path/to/repo/submodule1",
|
|
repoGitDirPath: "/path/to/repo/.git/modules/submodule1",
|
|
repoName: "submodule1",
|
|
isBareRepo: false,
|
|
}),
|
|
Err: nil,
|
|
},
|
|
{
|
|
Name: "git rev-parse returns an error",
|
|
BeforeFunc: func(runner *oscommands.FakeCmdObjRunner, getRevParseArgs argFn) {
|
|
runner.ExpectGitArgs(
|
|
append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--show-superproject-working-tree"),
|
|
"",
|
|
errors.New("fatal: invalid gitfile format: /path/to/repo/worktree2/.git"))
|
|
// we're not in a repo at all, so asking about a bare one fails too
|
|
runner.ExpectGitArgs(
|
|
append(getRevParseArgs(), "--absolute-git-dir", "--git-common-dir"),
|
|
"",
|
|
errors.New("fatal: invalid gitfile format: /path/to/repo/worktree2/.git"))
|
|
},
|
|
Path: "/path/to/repo/worktree2",
|
|
Expected: nil,
|
|
Err: func(getRevParseArgs argFn) error {
|
|
args := strings.Join(getRevParseArgs(), " ")
|
|
return fmt.Errorf("'git %v --show-toplevel --absolute-git-dir --git-common-dir --show-superproject-working-tree' failed: fatal: invalid gitfile format: /path/to/repo/worktree2/.git", args)
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
t.Run(s.Name, func(t *testing.T) {
|
|
runner := oscommands.NewFakeRunner(t)
|
|
cmd := oscommands.NewDummyCmdObjBuilder(runner)
|
|
|
|
getRevParseArgs := func() []string {
|
|
return []string{"rev-parse", "--path-format=absolute"}
|
|
}
|
|
// prepare the filesystem for the scenario
|
|
s.BeforeFunc(runner, getRevParseArgs)
|
|
|
|
repoPaths, err := GetRepoPathsForDir("", cmd)
|
|
|
|
// check the error and the paths
|
|
if s.Err != nil {
|
|
scenarioErr := s.Err(getRevParseArgs)
|
|
assert.Error(t, err)
|
|
assert.EqualError(t, err, scenarioErr.Error())
|
|
} else {
|
|
assert.Nil(t, err)
|
|
assert.Equal(t, s.Expected, repoPaths)
|
|
}
|
|
})
|
|
}
|
|
}
|