Pull the reading of a repo's HEAD out of getCurrentBranch

The next commits change what the recent repos menu shows for a repo
without a branch, and teach it about layouts whose HEAD it reads wrongly
today. Give the reading a function of its own first, one that reports
what it found rather than what to display. This keeps the later changes
apart from each other, and it lets the tests call the reading directly
instead of going through a ReposHelper.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller 2026-09-14 10:16:16 +02:00
parent 8d0f81cc15
commit a3e940e20e
2 changed files with 136 additions and 37 deletions

View file

@ -61,46 +61,65 @@ func (self *ReposHelper) EnterSubmodule(submodule *models.SubmoduleConfig) error
return self.switchTo(submodule.FullPath(), self.c.Tr.ErrRepositoryMovedOrDeleted, context.NO_CONTEXT)
}
// What a repo has checked out. Exactly one of the two fields is set.
type headInfo struct {
// The name of the checked-out branch.
branch string
// The commit HEAD is detached at.
hash string
}
// gitDirOfRepo returns the directory that holds the git data of the repo at
// repoPath, and whether it could be found. An ordinary repo keeps that data in
// a .git directory; a worktree and a submodule have a .git file naming the
// directory instead.
func gitDirOfRepo(repoPath string) (string, bool) {
gitDirPath := filepath.Join(repoPath, ".git")
stat, err := os.Stat(gitDirPath)
if err != nil {
return "", false
}
if stat.IsDir() {
return gitDirPath, true
}
content, err := os.ReadFile(gitDirPath)
if err != nil {
return "", false
}
return strings.CutPrefix(strings.TrimSpace(string(content)), "gitdir: ")
}
// 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) {
gitDir, ok := gitDirOfRepo(repoPath)
if !ok {
return headInfo{}, false
}
content, err := os.ReadFile(filepath.Join(gitDir, "HEAD"))
if err != nil {
return headInfo{}, false
}
head := strings.TrimSpace(string(content))
if branch, ok := strings.CutPrefix(head, "ref: refs/heads/"); ok {
return headInfo{branch: branch}, true
}
return headInfo{hash: head}, true
}
func (self *ReposHelper) getCurrentBranch(path string) string {
readHeadFile := func(path string) (string, error) {
headFile, err := os.ReadFile(filepath.Join(path, "HEAD"))
if err == nil {
content := strings.TrimSpace(string(headFile))
refsPrefix := "ref: refs/heads/"
var branchDisplay string
if bareName, ok := strings.CutPrefix(content, refsPrefix); ok {
// is a branch
branchDisplay = bareName
} else {
// detached HEAD state, displaying short hash
branchDisplay = utils.ShortHash(content)
}
return branchDisplay, nil
}
return "", err
head, ok := readHeadInfo(path)
if !ok {
return self.c.Tr.BranchUnknown
}
gitDirPath := filepath.Join(path, ".git")
if gitDir, err := os.Stat(gitDirPath); err == nil {
if gitDir.IsDir() {
// ordinary repo
if branch, err := readHeadFile(gitDirPath); err == nil {
return branch
}
} else {
// worktree
if worktreeGitDir, err := os.ReadFile(gitDirPath); err == nil {
content := strings.TrimSpace(string(worktreeGitDir))
worktreePath := strings.TrimPrefix(content, "gitdir: ")
if branch, err := readHeadFile(worktreePath); err == nil {
return branch
}
}
}
if head.branch != "" {
return head.branch
}
return self.c.Tr.BranchUnknown
return utils.ShortHash(head.hash)
}
// The most that the name and the branch column of the recent repos menu are

View file

@ -0,0 +1,80 @@
package helpers
import (
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/assert"
)
func TestReadHeadInfo(t *testing.T) {
scenarios := []struct {
name string
// The files to lay out below a temporary root directory, by their path
// relative to it. "$root" in a file's content is replaced with the
// root's path, so that a scenario can write an absolute path.
files map[string]string
// The repo to read, relative to the root. The directory is created
// whether or not the scenario puts any files in it.
repoPath string
expected headInfo
expectedOk bool
}{
{
name: "ordinary repo on a branch",
files: map[string]string{"repo/.git/HEAD": "ref: refs/heads/mybranch\n"},
repoPath: "repo",
expected: headInfo{branch: "mybranch"},
expectedOk: true,
},
{
name: "ordinary repo at a detached head",
files: map[string]string{"repo/.git/HEAD": "d85cc9d2f5d0dc0b8f0e4d8e5b2ba0d1e7c8a3f6\n"},
repoPath: "repo",
expected: headInfo{hash: "d85cc9d2f5d0dc0b8f0e4d8e5b2ba0d1e7c8a3f6"},
expectedOk: true,
},
{
name: "worktree whose .git file names the git dir absolutely",
files: map[string]string{
"repo/.git/worktrees/wt/HEAD": "ref: refs/heads/mybranch\n",
"wt/.git": "gitdir: $root/repo/.git/worktrees/wt\n",
},
repoPath: "wt",
expected: headInfo{branch: "mybranch"},
expectedOk: true,
},
{
name: "directory without a .git entry",
repoPath: "notarepo",
expectedOk: false,
},
{
name: ".git file that doesn't name a git dir",
files: map[string]string{"repo/.git": "not what git writes\n"},
repoPath: "repo",
expectedOk: false,
},
}
for _, s := range scenarios {
t.Run(s.name, func(t *testing.T) {
root := t.TempDir()
for path, content := range s.files {
fullPath := filepath.Join(root, filepath.FromSlash(path))
assert.NoError(t, os.MkdirAll(filepath.Dir(fullPath), 0o700))
content = strings.ReplaceAll(content, "$root", root)
assert.NoError(t, os.WriteFile(fullPath, []byte(content), 0o600))
}
repoPath := filepath.Join(root, filepath.FromSlash(s.repoPath))
assert.NoError(t, os.MkdirAll(repoPath, 0o700))
head, ok := readHeadInfo(repoPath)
assert.Equal(t, s.expectedOk, ok)
assert.Equal(t, s.expected, head)
})
}
}