mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
git makes `rev-parse --show-toplevel` fatal when there's no work tree, so asking for it together with everything else meant we never got an answer at all for a bare repo: GetRepoPaths returned an error, nobody ever saw IsBareRepo() == true, and lazygit either died with a stack trace or decided we weren't in a repository. That's what you got for opening it in a directory holding a bare repo and a .git file pointing at it, which is a normal way to keep a repo and its worktrees together. Ask again without --show-toplevel when the first query fails: the other queries work fine without a work tree, so if they now succeed we know we're in a bare repo, and the existing prompt offering to open a recent repo does its job. If they fail too we're not in a repo at all, and the first error already says so. --is-bare-repository is gone from the query: a work tree implies core.bare is false, so it could only ever come back false there, and what matters to us is whether there is a work tree to show, which is what we now go by. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
256 lines
8.3 KiB
Go
256 lines
8.3 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,
|
|
},
|
|
{
|
|
// 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)
|
|
},
|
|
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,
|
|
}, &RepoPaths{
|
|
worktreePath: "/path/to/worktree",
|
|
worktreeGitDirPath: "/path/to/repo/.git",
|
|
repoPath: "/path/to/worktree",
|
|
repoGitDirPath: "/path/to/repo/.git",
|
|
repoName: "worktree",
|
|
isBareRepo: false,
|
|
}),
|
|
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)
|
|
},
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|