mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-09-14 09:36:24 -04:00
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:
parent
8d0f81cc15
commit
a3e940e20e
|
|
@ -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
|
||||
|
|
|
|||
80
pkg/gui/controllers/helpers/repos_helper_test.go
Normal file
80
pkg/gui/controllers/helpers/repos_helper_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue