mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-10 07:36:27 -04:00
Recognize a repo that has no work tree instead of bailing out
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>
This commit is contained in:
parent
e10a2f6a27
commit
0ce248d1bf
|
|
@ -16,7 +16,7 @@ type errorMapping struct {
|
|||
func knownError(tr *i18n.TranslationSet, err error) (string, bool) {
|
||||
errorMessage := err.Error()
|
||||
|
||||
knownErrorMessages := []string{minGitVersionErrorMessage(tr)}
|
||||
knownErrorMessages := []string{minGitVersionErrorMessage(tr), tr.BareRepoNotSupported}
|
||||
|
||||
if lo.Contains(knownErrorMessages, errorMessage) {
|
||||
return errorMessage, true
|
||||
|
|
|
|||
|
|
@ -67,6 +67,13 @@ func NewGitCommand(
|
|||
return nil, errors.Errorf("Error getting repo paths: %v", err)
|
||||
}
|
||||
|
||||
// A bare repo has no worktree for us to work in. Callers that can offer the
|
||||
// user something better (app.setupRepo) check for this first; getting here
|
||||
// means nobody could, e.g. because --git-dir was pointed at a bare repo.
|
||||
if repoPaths.IsBareRepo() {
|
||||
return nil, errors.New(cmn.Tr.BareRepoNotSupported)
|
||||
}
|
||||
|
||||
err = os.Chdir(repoPaths.WorktreePath())
|
||||
if err != nil {
|
||||
return nil, utils.WrapError(err)
|
||||
|
|
|
|||
|
|
@ -22,7 +22,8 @@ type RepoPaths struct {
|
|||
}
|
||||
|
||||
// Path to the current worktree. If we're in the main worktree, this will
|
||||
// be the same as RepoPath()
|
||||
// be the same as RepoPath(). It is empty for a bare repo, which has no
|
||||
// worktree at all.
|
||||
func (self *RepoPaths) WorktreePath() string {
|
||||
return self.worktreePath
|
||||
}
|
||||
|
|
@ -53,6 +54,9 @@ 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.
|
||||
func (self *RepoPaths) IsBareRepo() bool {
|
||||
return self.isBareRepo
|
||||
}
|
||||
|
|
@ -84,16 +88,18 @@ func GetRepoPathsForDir(
|
|||
dir string,
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
) (*RepoPaths, error) {
|
||||
gitDirOutput, err := callGitRevParseWithDir(cmd, dir, "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--is-bare-repository", "--show-superproject-working-tree")
|
||||
gitDirOutput, err := callGitRevParseWithDir(cmd, dir, "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--show-superproject-working-tree")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// --show-toplevel is the only one of these that needs a work tree, and
|
||||
// git makes it fatal when there isn't one. So this may just mean we're in
|
||||
// a repo that has no work tree.
|
||||
return getBareRepoPathsForDir(dir, cmd, err)
|
||||
}
|
||||
|
||||
gitDirResults := strings.Split(utils.NormalizeLinefeeds(gitDirOutput), "\n")
|
||||
worktreePath := gitDirResults[0]
|
||||
worktreeGitDirPath := gitDirResults[1]
|
||||
repoGitDirPath := gitDirResults[2]
|
||||
isBareRepo := gitDirResults[3] == "true"
|
||||
|
||||
// A worktree that has the repo's common git dir to itself is the repo's main
|
||||
// worktree, so it is the repoPath. That holds for a submodule as well: its
|
||||
|
|
@ -102,9 +108,9 @@ func GetRepoPathsForDir(
|
|||
isMainWorktree := worktreeGitDirPath == repoGitDirPath
|
||||
|
||||
// If we're in a submodule, --show-superproject-working-tree will return a
|
||||
// value, meaning gitDirResults will be length 5. That only tells us anything
|
||||
// value, meaning gitDirResults will be length 4. That only tells us anything
|
||||
// new for a linked worktree of a submodule, which isMainWorktree misses.
|
||||
isSubmodule := len(gitDirResults) == 5
|
||||
isSubmodule := len(gitDirResults) == 4
|
||||
|
||||
// Otherwise we're in a linked worktree, and the repoPath is the repo's main
|
||||
// worktree. git won't tell us where that is: `git worktree list` reports it
|
||||
|
|
@ -131,7 +137,42 @@ func GetRepoPathsForDir(
|
|||
repoPath: repoPath,
|
||||
repoGitDirPath: repoGitDirPath,
|
||||
repoName: repoName,
|
||||
isBareRepo: isBareRepo,
|
||||
isBareRepo: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getBareRepoPathsForDir is the fallback for when we couldn't ask git for the
|
||||
// work tree. Everything but --show-toplevel works fine without one, so if the
|
||||
// remaining queries succeed we are in a bare repo, and we return what we know
|
||||
// about it with an empty worktreePath. If they fail too we simply aren't in a
|
||||
// repo, and the caller's original error says so better than ours would.
|
||||
func getBareRepoPathsForDir(
|
||||
dir string,
|
||||
cmd oscommands.ICmdObjBuilder,
|
||||
errWithWorktree error,
|
||||
) (*RepoPaths, error) {
|
||||
output, err := callGitRevParseWithDir(cmd, dir, "--absolute-git-dir", "--git-common-dir")
|
||||
if err != nil {
|
||||
return nil, errWithWorktree
|
||||
}
|
||||
|
||||
results := strings.Split(utils.NormalizeLinefeeds(output), "\n")
|
||||
repoGitDirPath := results[1]
|
||||
// A bare repo has no worktree, and so no repo path in the sense the caller
|
||||
// with a worktree means. It doesn't matter much what we say here, because
|
||||
// nobody reads it: whoever is handed a bare repo either offers to open a
|
||||
// recent one instead (app.setupRepo) or is turned away by NewGitCommand. The
|
||||
// directory holding the git dir is the nearest thing there is to a repo
|
||||
// path.
|
||||
repoPath := filepath.Dir(repoGitDirPath)
|
||||
|
||||
return &RepoPaths{
|
||||
worktreePath: "",
|
||||
worktreeGitDirPath: results[0],
|
||||
repoPath: repoPath,
|
||||
repoGitDirPath: repoGitDirPath,
|
||||
repoName: filepath.Base(repoPath),
|
||||
isBareRepo: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,8 +38,6 @@ func TestGetRepoPaths(t *testing.T) {
|
|||
`C:\path\to\repo\.git`,
|
||||
// --git-common-dir
|
||||
`C:\path\to\repo\.git`,
|
||||
// --is-bare-repository
|
||||
"false",
|
||||
// --show-superproject-working-tree
|
||||
}, []string{
|
||||
// --show-toplevel
|
||||
|
|
@ -48,12 +46,10 @@ func TestGetRepoPaths(t *testing.T) {
|
|||
"/path/to/repo/.git",
|
||||
// --git-common-dir
|
||||
"/path/to/repo/.git",
|
||||
// --is-bare-repository
|
||||
"false",
|
||||
// --show-superproject-working-tree
|
||||
})
|
||||
runner.ExpectGitArgs(
|
||||
append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--is-bare-repository", "--show-superproject-working-tree"),
|
||||
append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--show-superproject-working-tree"),
|
||||
strings.Join(mockOutput, "\n"),
|
||||
nil)
|
||||
},
|
||||
|
|
@ -76,49 +72,45 @@ func TestGetRepoPaths(t *testing.T) {
|
|||
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) {
|
||||
// setup for main worktree
|
||||
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{
|
||||
// --show-toplevel
|
||||
`C:\path\to\repo`,
|
||||
// --git-dir
|
||||
`C:\path\to\bare_repo\bare.git`,
|
||||
`C:\path\to\project\bare.git`,
|
||||
// --git-common-dir
|
||||
`C:\path\to\bare_repo\bare.git`,
|
||||
// --is-bare-repository
|
||||
`true`,
|
||||
// --show-superproject-working-tree
|
||||
`C:\path\to\project\bare.git`,
|
||||
}, []string{
|
||||
// --show-toplevel
|
||||
"/path/to/repo",
|
||||
// --git-dir
|
||||
"/path/to/bare_repo/bare.git",
|
||||
"/path/to/project/bare.git",
|
||||
// --git-common-dir
|
||||
"/path/to/bare_repo/bare.git",
|
||||
// --is-bare-repository
|
||||
"true",
|
||||
// --show-superproject-working-tree
|
||||
"/path/to/project/bare.git",
|
||||
})
|
||||
runner.ExpectGitArgs(
|
||||
append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--is-bare-repository", "--show-superproject-working-tree"),
|
||||
append(getRevParseArgs(), "--absolute-git-dir", "--git-common-dir"),
|
||||
strings.Join(mockOutput, "\n"),
|
||||
nil)
|
||||
},
|
||||
Path: "/path/to/repo",
|
||||
Path: "/path/to/project",
|
||||
Expected: lo.Ternary(runtime.GOOS == "windows", &RepoPaths{
|
||||
worktreePath: `C:\path\to\repo`,
|
||||
worktreeGitDirPath: `C:\path\to\bare_repo\bare.git`,
|
||||
repoPath: `C:\path\to\repo`,
|
||||
repoGitDirPath: `C:\path\to\bare_repo\bare.git`,
|
||||
repoName: `repo`,
|
||||
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: "/path/to/repo",
|
||||
worktreeGitDirPath: "/path/to/bare_repo/bare.git",
|
||||
repoPath: "/path/to/repo",
|
||||
repoGitDirPath: "/path/to/bare_repo/bare.git",
|
||||
repoName: "repo",
|
||||
worktreePath: "",
|
||||
worktreeGitDirPath: "/path/to/project/bare.git",
|
||||
repoPath: "/path/to/project",
|
||||
repoGitDirPath: "/path/to/project/bare.git",
|
||||
repoName: "project",
|
||||
isBareRepo: true,
|
||||
}),
|
||||
Err: nil,
|
||||
|
|
@ -136,8 +128,6 @@ func TestGetRepoPaths(t *testing.T) {
|
|||
`C:\path\to\repo\.git`,
|
||||
// --git-common-dir
|
||||
`C:\path\to\repo\.git`,
|
||||
// --is-bare-repository
|
||||
"false",
|
||||
// --show-superproject-working-tree
|
||||
}, []string{
|
||||
// --show-toplevel
|
||||
|
|
@ -146,12 +136,10 @@ func TestGetRepoPaths(t *testing.T) {
|
|||
"/path/to/repo/.git",
|
||||
// --git-common-dir
|
||||
"/path/to/repo/.git",
|
||||
// --is-bare-repository
|
||||
"false",
|
||||
// --show-superproject-working-tree
|
||||
})
|
||||
runner.ExpectGitArgs(
|
||||
append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--is-bare-repository", "--show-superproject-working-tree"),
|
||||
append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--show-superproject-working-tree"),
|
||||
strings.Join(mockOutput, "\n"),
|
||||
nil)
|
||||
},
|
||||
|
|
@ -183,8 +171,6 @@ func TestGetRepoPaths(t *testing.T) {
|
|||
`C:\path\to\repo\.git\modules\submodule1`,
|
||||
// --git-common-dir
|
||||
`C:\path\to\repo\.git\modules\submodule1`,
|
||||
// --is-bare-repository
|
||||
`false`,
|
||||
// --show-superproject-working-tree
|
||||
`C:\path\to\repo`,
|
||||
}, []string{
|
||||
|
|
@ -194,13 +180,11 @@ func TestGetRepoPaths(t *testing.T) {
|
|||
"/path/to/repo/.git/modules/submodule1",
|
||||
// --git-common-dir
|
||||
"/path/to/repo/.git/modules/submodule1",
|
||||
// --is-bare-repository
|
||||
"false",
|
||||
// --show-superproject-working-tree
|
||||
"/path/to/repo",
|
||||
})
|
||||
runner.ExpectGitArgs(
|
||||
append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--is-bare-repository", "--show-superproject-working-tree"),
|
||||
append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--show-superproject-working-tree"),
|
||||
strings.Join(mockOutput, "\n"),
|
||||
nil)
|
||||
},
|
||||
|
|
@ -226,7 +210,12 @@ func TestGetRepoPaths(t *testing.T) {
|
|||
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", "--is-bare-repository", "--show-superproject-working-tree"),
|
||||
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"))
|
||||
},
|
||||
|
|
@ -234,7 +223,7 @@ func TestGetRepoPaths(t *testing.T) {
|
|||
Expected: nil,
|
||||
Err: func(getRevParseArgs argFn) error {
|
||||
args := strings.Join(getRevParseArgs(), " ")
|
||||
return fmt.Errorf("'git %v --show-toplevel --absolute-git-dir --git-common-dir --is-bare-repository --show-superproject-working-tree' failed: fatal: invalid gitfile format: /path/to/repo/worktree2/.git", args)
|
||||
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)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -461,6 +461,7 @@ type TranslationSet struct {
|
|||
DisabledForGPG string
|
||||
CreateRepo string
|
||||
BareRepo string
|
||||
BareRepoNotSupported string
|
||||
InitialBranch string
|
||||
NoRecentRepositories string
|
||||
IncorrectNotARepository string
|
||||
|
|
@ -1620,6 +1621,7 @@ func EnglishTranslationSet() *TranslationSet {
|
|||
DisabledForGPG: "Feature not available for users using GPG.\n\nIf you are using a passphrase agent (e.g. gpg-agent) so that you don't have to type your passphrase when signing, you can enable this feature by adding\n\ngit:\n overrideGpg: true\n\nto your lazygit config file.",
|
||||
CreateRepo: "Not in a git repository. Create a new git repository? (y/N): ",
|
||||
BareRepo: "You've attempted to open Lazygit in a bare repo but Lazygit does not support bare repos. Open most recent repo? (y/n) ",
|
||||
BareRepoNotSupported: "Lazygit does not support bare repos.",
|
||||
InitialBranch: "Branch name? (leave empty for git's default): ",
|
||||
NoRecentRepositories: "Must open lazygit in a git repository. No valid recent repositories. Exiting.",
|
||||
IncorrectNotARepository: "The value of 'notARepository' is incorrect. It should be one of 'prompt', 'create', 'skip', or 'quit'.",
|
||||
|
|
|
|||
Loading…
Reference in a new issue