diff --git a/pkg/gui/controllers/helpers/repos_helper.go b/pkg/gui/controllers/helpers/repos_helper.go index d12238f33..53018a2e1 100644 --- a/pkg/gui/controllers/helpers/repos_helper.go +++ b/pkg/gui/controllers/helpers/repos_helper.go @@ -10,6 +10,7 @@ import ( appTypes "github.com/jesseduffield/lazygit/pkg/app/types" "github.com/jesseduffield/lazygit/pkg/commands" "github.com/jesseduffield/lazygit/pkg/commands/direnv" + "github.com/jesseduffield/lazygit/pkg/commands/git_commands" "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/env" "github.com/jesseduffield/lazygit/pkg/gui/context" @@ -100,6 +101,12 @@ func gitDirOfRepo(repoPath string) (string, bool) { return gitDir, true } +// The branch git names in the HEAD file of a repo that keeps its refs in a +// reftable. The refs live in a binary table there, and the name in HEAD +// resolves nowhere, so that a reader of the file gets an error instead of a +// stale answer. +const reftablePlaceholderBranch = ".invalid" + // readHeadInfo reads the HEAD file of the repo at repoPath to find out what it // has checked out, and reports whether that worked. func readHeadInfo(repoPath string) (headInfo, bool) { @@ -115,13 +122,50 @@ func readHeadInfo(repoPath string) (headInfo, bool) { head := strings.TrimSpace(string(content)) if branch, ok := strings.CutPrefix(head, "ref: refs/heads/"); ok { + if branch == reftablePlaceholderBranch { + return headInfo{}, false + } return headInfo{branch: branch}, true } return headInfo{hash: head}, true } +// askGitForHeadInfo asks git what the repo at repoPath has checked out. This +// costs a process, so only the repos that readHeadInfo can't answer for go +// through here. +func (self *ReposHelper) askGitForHeadInfo(repoPath string) (headInfo, bool) { + // symbolic-ref names the branch even when it has no commit yet, and + // rev-parse resolves HEAD when it is detached. Neither can do the other's + // job, so ask for the branch first and only then for the commit. + if branch, ok := self.askGit(repoPath, "symbolic-ref", "--short", "--quiet", "HEAD"); ok { + return headInfo{branch: branch}, true + } + if hash, ok := self.askGit(repoPath, "rev-parse", "HEAD"); ok { + return headInfo{hash: hash}, true + } + return headInfo{}, false +} + +// askGit runs a git command against the repo at repoPath and returns the one +// line it writes, or false if it fails or writes nothing. +func (self *ReposHelper) askGit(repoPath string, subcommand string, args ...string) (string, bool) { + cmdObj := self.c.OS().Cmd.New(git_commands.NewGitCmd(subcommand). + Dir(repoPath). + Arg(args...). + ToArgv()).DontLog() + stdout, _, err := git_commands.ForOtherRepo(cmdObj).RunWithOutputs() + if err != nil { + return "", false + } + output := strings.TrimSpace(stdout) + return output, output != "" +} + func (self *ReposHelper) getCurrentBranch(path string) string { head, ok := readHeadInfo(path) + if !ok { + head, ok = self.askGitForHeadInfo(path) + } if !ok { return self.c.Tr.BranchUnknown } diff --git a/pkg/gui/controllers/helpers/repos_helper_test.go b/pkg/gui/controllers/helpers/repos_helper_test.go index 1bbcd9b6c..5e6b2e8c7 100644 --- a/pkg/gui/controllers/helpers/repos_helper_test.go +++ b/pkg/gui/controllers/helpers/repos_helper_test.go @@ -71,13 +71,8 @@ func TestReadHeadInfo(t *testing.T) { files: map[string]string{ "repo/.git/HEAD": "ref: refs/heads/.invalid\n", }, - /* EXPECTED: repoPath: "repo", expectedOk: false, - ACTUAL: */ - repoPath: "repo", - expected: headInfo{branch: ".invalid"}, - expectedOk: true, }, { name: "directory without a .git entry", diff --git a/pkg/integration/tests/misc/recent_repos_reftable_repo.go b/pkg/integration/tests/misc/recent_repos_reftable_repo.go index 616681063..797976bae 100644 --- a/pkg/integration/tests/misc/recent_repos_reftable_repo.go +++ b/pkg/integration/tests/misc/recent_repos_reftable_repo.go @@ -19,20 +19,22 @@ var RecentReposReftableRepo = NewIntegrationTest(NewIntegrationTestArgs{ // the first entry is the repo we're in, so it isn't offered current, _ := filepath.Abs(".") reftable, _ := filepath.Abs("../reftable") - cfg.GetAppState().RecentRepos = []string{current, reftable} + unborn, _ := filepath.Abs("../reftable-unborn") + cfg.GetAppState().RecentRepos = []string{current, reftable, unborn} }, SetupRepo: func(shell *Shell) { shell.EmptyCommit("one") shell.RunCommand([]string{"git", "clone", "--ref-format=reftable", ".", "../reftable"}) + // A branch without a commit is the one thing git can't answer for with + // rev-parse + shell.RunCommand([]string{"git", "init", "--ref-format=reftable", "../reftable-unborn"}) }, Run: func(t *TestDriver, keys config.KeybindingConfig) { t.ExpectPopup().Menu(). Title(Equals("Recent repositories")). Lines( - /* EXPECTED: Contains("reftable").Contains("master").IsSelected(), - ACTUAL: */ - Contains("reftable").Contains(".invalid").IsSelected(), + Contains("reftable-unborn").Contains("master"), Contains("Cancel"), ). Cancel()