From b3d4a29b0e38c0c0599fb7424953486b94953ed2 Mon Sep 17 00:00:00 2001 From: Lee Howett Date: Mon, 6 Apr 2026 12:40:57 -0700 Subject: [PATCH] Split repo path rev-parse to survive Git submodule.c abort GetRepoPathsForDir used one rev-parse with --show-superproject-working-tree together with the path flags. In checkouts such as Android repo tool (symlinked .git under .repo/projects/...), Git can abort that entire invocation with BUG: submodule.c:2455, so lazygit never gets the worktree / git-dir lines. Run the superproject query in a second rev-parse. If it errors or returns empty output, treat the checkout as non-submodule for repoPath (parent of git common dir). Add a unit test for the superproject failure fallback. Made-with: Cursor --- pkg/commands/git_commands/repo_paths.go | 23 ++-- pkg/commands/git_commands/repo_paths_test.go | 104 ++++++++++++++++--- 2 files changed, 104 insertions(+), 23 deletions(-) diff --git a/pkg/commands/git_commands/repo_paths.go b/pkg/commands/git_commands/repo_paths.go index c64debfc5aa..607f8986550 100644 --- a/pkg/commands/git_commands/repo_paths.go +++ b/pkg/commands/git_commands/repo_paths.go @@ -84,24 +84,33 @@ 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") + // Do not pass --show-superproject-working-tree in the same rev-parse as the path flags: + // some Git versions abort the whole command (BUG: submodule.c) when they are combined, + // so we would get no worktree or git-dir output at all (e.g. repo tool, symlinked .git). + // + // We run a second rev-parse for the superproject path only. That call can still fail or + // abort on the same bug when run alone; we treat failure or empty output like "not a + // submodule" for repoPath (see below). + gitDirOutput, err := callGitRevParseWithDir(cmd, dir, "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--is-bare-repository") if err != nil { return nil, err } gitDirResults := strings.Split(utils.NormalizeLinefeeds(gitDirOutput), "\n") + if len(gitDirResults) < 4 { + return nil, errors.Errorf("unexpected rev-parse output (expected 4 lines): %q", gitDirOutput) + } worktreePath := gitDirResults[0] worktreeGitDirPath := gitDirResults[1] repoGitDirPath := gitDirResults[2] isBareRepo := gitDirResults[3] == "true" - // If we're in a submodule, --show-superproject-working-tree will return - // a value, meaning gitDirResults will be length 5. In that case - // return the worktree path as the repoPath. Otherwise we're in a - // normal repo or a worktree so return the parent of the git common - // dir (repoGitDirPath) - isSubmodule := len(gitDirResults) == 5 + superprojectOut, superErr := callGitRevParseWithDir(cmd, dir, "--show-superproject-working-tree") + isSubmodule := superErr == nil && strings.TrimSpace(superprojectOut) != "" + // If we're in a submodule, --show-superproject-working-tree returns a non-empty path; use + // the worktree path as repoPath. Otherwise we're in a normal repo or a worktree, so use + // the parent of the git common dir (repoGitDirPath). var repoPath string if isSubmodule { repoPath = worktreePath diff --git a/pkg/commands/git_commands/repo_paths_test.go b/pkg/commands/git_commands/repo_paths_test.go index 29c40acee90..00db7663ba1 100644 --- a/pkg/commands/git_commands/repo_paths_test.go +++ b/pkg/commands/git_commands/repo_paths_test.go @@ -25,6 +25,17 @@ type Scenario struct { Err errFn } +// primaryRevParseArgs matches the first GetRepoPathsForDir rev-parse (path flags only). +func primaryRevParseArgs(getRevParseArgs argFn) []string { + return append(getRevParseArgs(), + "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--is-bare-repository") +} + +// superprojectRevParseArgs matches the second rev-parse (--show-superproject-working-tree alone). +func superprojectRevParseArgs(getRevParseArgs argFn) []string { + return append(getRevParseArgs(), "--show-superproject-working-tree") +} + func TestGetRepoPaths(t *testing.T) { scenarios := []Scenario{ { @@ -40,7 +51,6 @@ func TestGetRepoPaths(t *testing.T) { `C:\path\to\repo\.git`, // --is-bare-repository "false", - // --show-superproject-working-tree }, []string{ // --show-toplevel "/path/to/repo", @@ -50,12 +60,16 @@ func TestGetRepoPaths(t *testing.T) { "/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"), + primaryRevParseArgs(getRevParseArgs), strings.Join(mockOutput, "\n"), nil) + // --show-superproject-working-tree (empty: not inside a submodule checkout) + runner.ExpectGitArgs( + superprojectRevParseArgs(getRevParseArgs), + "", + nil) }, Path: "/path/to/repo", Expected: lo.Ternary(runtime.GOOS == "windows", &RepoPaths{ @@ -78,7 +92,7 @@ func TestGetRepoPaths(t *testing.T) { { Name: "bare repo", BeforeFunc: func(runner *oscommands.FakeCmdObjRunner, getRevParseArgs argFn) { - // setup for main worktree + // setup for main worktree with a separate bare git dir mockOutput := lo.Ternary(runtime.GOOS == "windows", []string{ // --show-toplevel `C:\path\to\repo`, @@ -88,7 +102,6 @@ func TestGetRepoPaths(t *testing.T) { `C:\path\to\bare_repo\bare.git`, // --is-bare-repository `true`, - // --show-superproject-working-tree }, []string{ // --show-toplevel "/path/to/repo", @@ -98,12 +111,16 @@ func TestGetRepoPaths(t *testing.T) { "/path/to/bare_repo/bare.git", // --is-bare-repository "true", - // --show-superproject-working-tree }) runner.ExpectGitArgs( - append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--is-bare-repository", "--show-superproject-working-tree"), + primaryRevParseArgs(getRevParseArgs), strings.Join(mockOutput, "\n"), nil) + // --show-superproject-working-tree (empty) + runner.ExpectGitArgs( + superprojectRevParseArgs(getRevParseArgs), + "", + nil) }, Path: "/path/to/repo", Expected: lo.Ternary(runtime.GOOS == "windows", &RepoPaths{ @@ -126,7 +143,7 @@ func TestGetRepoPaths(t *testing.T) { { Name: "submodule", BeforeFunc: func(runner *oscommands.FakeCmdObjRunner, getRevParseArgs argFn) { - mockOutput := lo.Ternary(runtime.GOOS == "windows", []string{ + mockPrimary := lo.Ternary(runtime.GOOS == "windows", []string{ // --show-toplevel `C:\path\to\repo\submodule1`, // --git-dir @@ -135,8 +152,6 @@ func TestGetRepoPaths(t *testing.T) { `C:\path\to\repo\.git\modules\submodule1`, // --is-bare-repository `false`, - // --show-superproject-working-tree - `C:\path\to\repo`, }, []string{ // --show-toplevel "/path/to/repo/submodule1", @@ -146,12 +161,16 @@ func TestGetRepoPaths(t *testing.T) { "/path/to/repo/.git/modules/submodule1", // --is-bare-repository "false", - // --show-superproject-working-tree - "/path/to/repo", }) + // --show-superproject-working-tree (superproject worktree path) + superOut := lo.Ternary(runtime.GOOS == "windows", `C:\path\to\repo`, "/path/to/repo") runner.ExpectGitArgs( - append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--is-bare-repository", "--show-superproject-working-tree"), - strings.Join(mockOutput, "\n"), + primaryRevParseArgs(getRevParseArgs), + strings.Join(mockPrimary, "\n"), + nil) + runner.ExpectGitArgs( + superprojectRevParseArgs(getRevParseArgs), + superOut, nil) }, Path: "/path/to/repo/submodule1", @@ -172,11 +191,64 @@ func TestGetRepoPaths(t *testing.T) { }), Err: nil, }, + { + Name: "superproject rev-parse fails (fallback to non-submodule repoPath)", + BeforeFunc: func(runner *oscommands.FakeCmdObjRunner, getRevParseArgs argFn) { + // Primary rev-parse succeeds (e.g. repo-tool symlinked .git); secondary can still error + // (e.g. BUG: submodule.c) — we ignore superproject failure and use repoPath from common-dir. + mockOutput := lo.Ternary(runtime.GOOS == "windows", []string{ + // --show-toplevel + `C:\path\to\repo`, + // --absolute-git-dir + `C:\path\to\repo\.git`, + // --git-common-dir + `C:\path\to\repo\.git`, + // --is-bare-repository + "false", + }, []string{ + // --show-toplevel + "/path/to/repo", + // --absolute-git-dir + "/path/to/repo/.git", + // --git-common-dir + "/path/to/repo/.git", + // --is-bare-repository + "false", + }) + runner.ExpectGitArgs( + primaryRevParseArgs(getRevParseArgs), + strings.Join(mockOutput, "\n"), + nil) + // --show-superproject-working-tree (Git errors, e.g. submodule.c internal BUG) + runner.ExpectGitArgs( + superprojectRevParseArgs(getRevParseArgs), + "", + errors.New("BUG: submodule.c:2455: returned path string doesn't match cwd?")) + }, + 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, + }, { Name: "git rev-parse returns an error", BeforeFunc: func(runner *oscommands.FakeCmdObjRunner, getRevParseArgs argFn) { + // Primary rev-parse fails; superproject call is never run runner.ExpectGitArgs( - append(getRevParseArgs(), "--show-toplevel", "--absolute-git-dir", "--git-common-dir", "--is-bare-repository", "--show-superproject-working-tree"), + primaryRevParseArgs(getRevParseArgs), "", errors.New("fatal: invalid gitfile format: /path/to/repo/worktree2/.git")) }, @@ -184,7 +256,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 --is-bare-repository' failed: fatal: invalid gitfile format: /path/to/repo/worktree2/.git", args) }, }, }