aboutsummaryrefslogtreecommitdiff
path: root/refs.go
diff options
context:
space:
mode:
authorGravatar Runxi Yu2025-11-25 08:00:00 +0800
committerGravatar Runxi Yu2025-11-25 08:00:00 +0800
commit20d82eb47c63e6cb8a8bf360a79e8ba53948a6d6 (patch)
treeba464aab9fabeda017a428c2e60963b94ef43e58 /refs.go
parentREADME: Add performance (diff)
signatureNo signature
refs: Add ShowRefs
Diffstat (limited to 'refs.go')
-rw-r--r--refs.go148
1 files changed, 148 insertions, 0 deletions
diff --git a/refs.go b/refs.go
index 59621b40..10a3f66b 100644
--- a/refs.go
+++ b/refs.go
@@ -5,6 +5,8 @@ import (
"bytes"
"fmt"
"os"
+ "path"
+ "path/filepath"
"slices"
"strings"
)
@@ -142,6 +144,14 @@ type Ref struct {
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.
@@ -224,3 +234,141 @@ func (repo *Repository) resolveRefFully(path string, seen map[string]struct{}) (
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
+}