ref: 20d82eb47c63e6cb8a8bf360a79e8ba53948a6d6
parent: fd7b64c36c265253ee1dd93ac6588e8982ad0e01
author: Runxi Yu <me@runxiyu.org>
date: Tue Nov 25 03:00:00 EST 2025
refs: Add ShowRefs
--- a/refs.go
+++ b/refs.go
@@ -5,6 +5,8 @@
"bytes"
"fmt"
"os"
+ "path"
+ "path/filepath"
"slices"
"strings"
)
@@ -142,6 +144,14 @@
Peeled Hash
}
+// ShowRef represents a reference entry as returned by ShowRefs.
+type ShowRef struct {+ // Name is the fully qualified ref name (e.g., refs/heads/main).
+ Name string
+ // Ref describes the reference target.
+ Ref Ref
+}
+
// ResolveRef reads the given fully qualified ref (such as "HEAD" or "refs/heads/main")
// and interprets its contents as either a symbolic ref ("ref: refs/..."), a detached// hash, or invalid.
@@ -223,4 +233,142 @@
default:
return Hash{}, ErrInvalidRef}
+}
+
+// ShowRefs lists refs similarly to git-show-ref.
+//
+// The pattern must be empty or begin with "refs/". An empty pattern is
+// treated as "refs/*".
+
+// Loose refs are resolved using filesystem globbing relative to the
+// repository root, then packed refs are read while skipping any names
+// that already appeared as loose refs. Packed refs are filtered
+// similarly.
+func (repo *Repository) ShowRefs(pattern string) ([]ShowRef, error) {+ if pattern == "" {+ pattern = "refs/*"
+ }
+ if !strings.HasPrefix(pattern, "refs/") {+ return nil, ErrInvalidRef
+ }
+ if filepath.IsAbs(pattern) {+ return nil, ErrInvalidRef
+ }
+
+ var out []ShowRef
+ seen := make(map[string]struct{})+
+ globPattern := filepath.Join(repo.rootPath, filepath.FromSlash(pattern))
+ matches, err := filepath.Glob(globPattern)
+ if err != nil {+ return nil, err
+ }
+ for _, match := range matches {+ info, statErr := os.Stat(match)
+ if statErr != nil {+ return nil, statErr
+ }
+ if info.IsDir() {+ continue
+ }
+
+ rel, relErr := filepath.Rel(repo.rootPath, match)
+ if relErr != nil {+ return nil, relErr
+ }
+ name := filepath.ToSlash(rel)
+ if !strings.HasPrefix(name, "refs/") {+ continue
+ }
+
+ ref, resolveErr := repo.resolveLooseRef(name)
+ if resolveErr != nil {+ if resolveErr == ErrNotFound || os.IsNotExist(resolveErr) {+ continue
+ }
+ return nil, resolveErr
+ }
+
+ seen[name] = struct{}{}+ out = append(out, ShowRef{+ Name: name,
+ Ref: ref,
+ })
+ }
+
+ packedPath := repo.repoPath("packed-refs")+ f, err := os.Open(packedPath)
+ if err != nil {+ if os.IsNotExist(err) {+ return out, nil
+ }
+ return nil, err
+ }
+ defer func() { _ = f.Close() }()+
+ scanner := bufio.NewScanner(f)
+ lastIdx := -1
+ for scanner.Scan() {+ line := scanner.Bytes()
+ if len(line) == 0 || line[0] == '#' {+ continue
+ }
+
+ if line[0] == '^' {+ if lastIdx < 0 {+ continue
+ }
+ peeledHex := strings.TrimPrefix(string(line), "^")
+ peeledHex = strings.TrimSpace(peeledHex)
+ peeled, parseErr := repo.ParseHash(peeledHex)
+ if parseErr != nil {+ return nil, parseErr
+ }
+ out[lastIdx].Ref.Peeled = peeled
+ continue
+ }
+
+ sp := bytes.IndexByte(line, ' ')
+ if sp != repo.hashSize*2 {+ lastIdx = -1
+ continue
+ }
+
+ name := string(line[sp+1:])
+ if !strings.HasPrefix(name, "refs/") {+ lastIdx = -1
+ continue
+ }
+ if _, ok := seen[name]; ok {+ lastIdx = -1
+ continue
+ }
+
+ match, matchErr := path.Match(pattern, name)
+ if matchErr != nil {+ return nil, matchErr
+ }
+ if !match {+ lastIdx = -1
+ continue
+ }
+
+ hash, parseErr := repo.ParseHash(string(line[:sp]))
+ if parseErr != nil {+ return nil, parseErr
+ }
+ out = append(out, ShowRef{+ Name: name,
+ Ref: Ref{+ Kind: RefKindDetached,
+ Hash: hash,
+ },
+ })
+ lastIdx = len(out) - 1
+ }
+ if scanErr := scanner.Err(); scanErr != nil {+ return nil, scanErr
+ }
+
+ return out, nil
}
--- a/refs_test.go
+++ b/refs_test.go
@@ -279,3 +279,208 @@
t.Fatalf("expected error for invalid hash input")}
}
+
+func TestShowRefsLooseOverridesPacked(t *testing.T) {+ repoPath, cleanup := setupTestRepo(t)
+ defer cleanup()
+
+ workDir, cleanupWork := setupWorkDir(t)
+ defer cleanupWork()
+
+ gitCmd(t, repoPath, "symbolic-ref", "HEAD", "refs/heads/main")
+
+ err := os.WriteFile(filepath.Join(workDir, "file.txt"), []byte("one"), 0o644)+ if err != nil {+ t.Fatalf("failed to write file.txt: %v", err)+ }
+ gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
+ gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "c1")
+ commit1 := gitCmd(t, repoPath, "rev-parse", "HEAD")
+
+ gitCmd(t, repoPath, "update-ref", "refs/heads/main", commit1)
+ gitCmd(t, repoPath, "update-ref", "refs/heads/feature", commit1)
+ gitCmd(t, repoPath, "pack-refs", "--all", "--prune")
+
+ err = os.WriteFile(filepath.Join(workDir, "file.txt"), []byte("two"), 0o644)+ if err != nil {+ t.Fatalf("failed to write file.txt: %v", err)+ }
+ gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
+ gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "c2")
+ commit2 := gitCmd(t, repoPath, "rev-parse", "HEAD")
+ gitCmd(t, repoPath, "update-ref", "refs/heads/main", commit2)
+
+ repo, err := OpenRepository(repoPath)
+ if err != nil {+ t.Fatalf("OpenRepository failed: %v", err)+ }
+ defer func() { _ = repo.Close() }()+
+ hash1, _ := repo.ParseHash(commit1)
+ hash2, _ := repo.ParseHash(commit2)
+
+ refs, err := repo.ShowRefs("refs/heads/*")+ if err != nil {+ t.Fatalf("ShowRefs failed: %v", err)+ }
+
+ if len(refs) != 2 {+ t.Fatalf("expected 2 refs, got %d", len(refs))+ }
+
+ got := make(map[string]Ref, len(refs))
+ for _, r := range refs {+ if _, exists := got[r.Name]; exists {+ t.Fatalf("duplicate ref %q in results", r.Name)+ }
+ got[r.Name] = r.Ref
+ }
+
+ mainRef, ok := got["refs/heads/main"]
+ if !ok {+ t.Fatalf("missing refs/heads/main in results")+ }
+ if mainRef.Kind != RefKindDetached || mainRef.Hash != hash2 {+ t.Fatalf("refs/heads/main hash: got %s (kind %v), want %s", mainRef.Hash, mainRef.Kind, hash2)+ }
+
+ featureRef, ok := got["refs/heads/feature"]
+ if !ok {+ t.Fatalf("missing refs/heads/feature in results")+ }
+ if featureRef.Kind != RefKindDetached || featureRef.Hash != hash1 {+ t.Fatalf("refs/heads/feature hash: got %s (kind %v), want %s", featureRef.Hash, featureRef.Kind, hash1)+ }
+}
+
+func TestShowRefsPatternFiltering(t *testing.T) {+ repoPath, cleanup := setupTestRepo(t)
+ defer cleanup()
+
+ workDir, cleanupWork := setupWorkDir(t)
+ defer cleanupWork()
+
+ gitCmd(t, repoPath, "symbolic-ref", "HEAD", "refs/heads/main")
+
+ err := os.WriteFile(filepath.Join(workDir, "file.txt"), []byte("one"), 0o644)+ if err != nil {+ t.Fatalf("failed to write file.txt: %v", err)+ }
+ gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
+ gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "c1")
+ commit1 := gitCmd(t, repoPath, "rev-parse", "HEAD")
+
+ gitCmd(t, repoPath, "update-ref", "refs/heads/main", commit1)
+ gitCmd(t, repoPath, "update-ref", "refs/heads/feature", commit1)
+ gitCmd(t, repoPath, "pack-refs", "--all", "--prune")
+
+ repo, err := OpenRepository(repoPath)
+ if err != nil {+ t.Fatalf("OpenRepository failed: %v", err)+ }
+ defer func() { _ = repo.Close() }()+
+ hash1, _ := repo.ParseHash(commit1)
+
+ refs, err := repo.ShowRefs("refs/heads/fea*")+ if err != nil {+ t.Fatalf("ShowRefs failed: %v", err)+ }
+ if len(refs) != 1 {+ t.Fatalf("expected 1 ref, got %d", len(refs))+ }
+ if refs[0].Name != "refs/heads/feature" {+ t.Fatalf("unexpected ref name: got %q, want %q", refs[0].Name, "refs/heads/feature")+ }
+ if refs[0].Ref.Kind != RefKindDetached || refs[0].Ref.Hash != hash1 {+ t.Fatalf("refs/heads/feature hash: got %s (kind %v), want %s", refs[0].Ref.Hash, refs[0].Ref.Kind, hash1)+ }
+}
+
+func TestShowRefsPackedPatterns(t *testing.T) {+ repoPath, cleanup := setupTestRepo(t)
+ defer cleanup()
+
+ workDir, cleanupWork := setupWorkDir(t)
+ defer cleanupWork()
+
+ gitCmd(t, repoPath, "symbolic-ref", "HEAD", "refs/heads/main")
+
+ err := os.WriteFile(filepath.Join(workDir, "file.txt"), []byte("one"), 0o644)+ if err != nil {+ t.Fatalf("failed to write file.txt: %v", err)+ }
+ gitCmd(t, repoPath, "--work-tree="+workDir, "add", ".")
+ gitCmd(t, repoPath, "--work-tree="+workDir, "commit", "-m", "c1")
+ commit := gitCmd(t, repoPath, "rev-parse", "HEAD")
+
+ gitCmd(t, repoPath, "update-ref", "refs/heads/main", commit)
+ gitCmd(t, repoPath, "update-ref", "refs/heads/feature/one", commit)
+ gitCmd(t, repoPath, "update-ref", "refs/notes/review", commit)
+ gitCmd(t, repoPath, "update-ref", "refs/tags/v1", commit)
+ gitCmd(t, repoPath, "pack-refs", "--all", "--prune")
+
+ repo, err := OpenRepository(repoPath)
+ if err != nil {+ t.Fatalf("OpenRepository failed: %v", err)+ }
+ defer func() { _ = repo.Close() }()+
+ tests := []struct {+ pattern string
+ want []string
+ }{+ {+ pattern: "refs/heads/*",
+ want: []string{"refs/heads/main"},+ },
+ {+ pattern: "refs/heads/*/*",
+ want: []string{"refs/heads/feature/one"},+ },
+ {+ pattern: "refs/*/feature/one",
+ want: []string{"refs/heads/feature/one"},+ },
+ {+ pattern: "refs/heads/feat?re/one",
+ want: []string{"refs/heads/feature/one"},+ },
+ {+ pattern: "refs/tags/v[0-9]",
+ want: []string{"refs/tags/v1"},+ },
+ {+ pattern: "refs/*/*",
+ want: []string{"refs/heads/main", "refs/notes/review", "refs/tags/v1"},+ },
+ }
+
+ for _, tt := range tests {+ t.Run(tt.pattern, func(t *testing.T) {+ refs, err := repo.ShowRefs(tt.pattern)
+ if err != nil {+ t.Fatalf("ShowRefs(%q) failed: %v", tt.pattern, err)+ }
+
+ got := make(map[string]struct{}, len(refs))+ for _, r := range refs {+ got[r.Name] = struct{}{}+ }
+
+ want := make(map[string]struct{}, len(tt.want))+ for _, w := range tt.want {+ want[w] = struct{}{}+ }
+
+ if len(got) != len(want) {+ t.Fatalf("ShowRefs(%q) returned %d refs, want %d", tt.pattern, len(got), len(want))+ }
+ for name := range got {+ if _, ok := want[name]; !ok {+ t.Fatalf("ShowRefs(%q) unexpected ref %q", tt.pattern, name)+ }
+ }
+ })
+ }
+}
--
⑨