aboutsummaryrefslogtreecommitdiff
path: root/refstore
diff options
context:
space:
mode:
Diffstat (limited to 'refstore')
-rw-r--r--refstore/chain/chain.go25
-rw-r--r--refstore/loose/list.go26
-rw-r--r--refstore/loose/loose_test.go43
-rw-r--r--refstore/loose/resolve.go11
-rw-r--r--refstore/loose/shorten.go6
-rw-r--r--refstore/loose/store.go1
-rw-r--r--refstore/packed/packed_test.go45
-rw-r--r--refstore/packed/parse.go12
-rw-r--r--refstore/packed/store.go12
-rw-r--r--refstore/reftable/lookup.go108
-rw-r--r--refstore/reftable/parse_helpers.go5
-rw-r--r--refstore/reftable/reftable_test.go31
-rw-r--r--refstore/reftable/store.go64
-rw-r--r--refstore/reftable/table.go59
-rw-r--r--refstore/shorten.go12
-rw-r--r--refstore/shorten_test.go4
16 files changed, 427 insertions, 37 deletions
diff --git a/refstore/chain/chain.go b/refstore/chain/chain.go
index 633bac25..9e04aeec 100644
--- a/refstore/chain/chain.go
+++ b/refstore/chain/chain.go
@@ -28,15 +28,19 @@ func (chain *Chain) Resolve(name string) (ref.Ref, error) {
if backend == nil {
continue
}
+
resolved, err := backend.Resolve(name)
if err == nil {
return resolved, nil
}
+
if errors.Is(err, refstore.ErrReferenceNotFound) {
continue
}
+
return nil, fmt.Errorf("refstore: backend %d resolve: %w", i, err)
}
+
return nil, refstore.ErrReferenceNotFound
}
@@ -46,11 +50,13 @@ func (chain *Chain) Resolve(name string) (ref.Ref, error) {
// references to cross backends in the chain.
func (chain *Chain) ResolveFully(name string) (ref.Detached, error) {
cur := name
+
seen := map[string]struct{}{}
for {
if _, ok := seen[cur]; ok {
return ref.Detached{}, fmt.Errorf("refstore: symbolic reference cycle at %q", cur)
}
+
seen[cur] = struct{}{}
resolved, err := chain.Resolve(cur)
@@ -65,6 +71,7 @@ func (chain *Chain) ResolveFully(name string) (ref.Detached, error) {
if resolved.Target == "" {
return ref.Detached{}, fmt.Errorf("refstore: symbolic reference %q has empty target", resolved.Name())
}
+
cur = resolved.Target
default:
return ref.Detached{}, fmt.Errorf("refstore: unsupported reference type %T", resolved)
@@ -77,25 +84,31 @@ func (chain *Chain) ResolveFully(name string) (ref.Detached, error) {
// First-seen wins, so earlier backends have precedence.
func (chain *Chain) List(pattern string) ([]ref.Ref, error) {
var refs []ref.Ref
+
seen := map[string]struct{}{}
for i, backend := range chain.backends {
if backend == nil {
continue
}
+
listed, err := backend.List(pattern)
if err != nil {
return nil, fmt.Errorf("refstore: backend %d list: %w", i, err)
}
+
for _, entry := range listed {
if entry == nil {
continue
}
+
name := entry.Name()
if _, ok := seen[name]; ok {
continue
}
+
seen[name] = struct{}{}
+
refs = append(refs, entry)
}
}
@@ -109,34 +122,44 @@ func (chain *Chain) Shorten(name string) (string, error) {
if err != nil {
return "", err
}
+
names := make([]string, 0, len(refs))
found := false
+
for _, entry := range refs {
if entry == nil {
continue
}
+
full := entry.Name()
+
names = append(names, full)
if full == name {
found = true
}
}
+
if !found {
return "", refstore.ErrReferenceNotFound
}
+
return refstore.ShortenName(name, names), nil
}
// Close closes all backends and joins close errors.
func (chain *Chain) Close() error {
var errs []error
+
for _, backend := range chain.backends {
if backend == nil {
continue
}
- if err := backend.Close(); err != nil {
+
+ err := backend.Close()
+ if err != nil {
errs = append(errs, err)
}
}
+
return errors.Join(errs...)
}
diff --git a/refstore/loose/list.go b/refstore/loose/list.go
index d28016da..1fa0adee 100644
--- a/refstore/loose/list.go
+++ b/refstore/loose/list.go
@@ -17,7 +17,8 @@ import (
func (store *Store) List(pattern string) ([]ref.Ref, error) {
matchAll := pattern == ""
if !matchAll {
- if _, err := path.Match(pattern, "HEAD"); err != nil {
+ _, err := path.Match(pattern, "HEAD")
+ if err != nil {
return nil, err
}
}
@@ -26,6 +27,7 @@ func (store *Store) List(pattern string) ([]ref.Ref, error) {
if err != nil {
return nil, err
}
+
slices.Sort(names)
refs := make([]ref.Ref, 0, len(names))
@@ -35,19 +37,24 @@ func (store *Store) List(pattern string) ([]ref.Ref, error) {
if err != nil {
return nil, err
}
+
if !matched {
continue
}
}
+
resolved, err := store.resolveOne(name)
if err != nil {
if errors.Is(err, refstore.ErrReferenceNotFound) {
continue
}
+
return nil, err
}
+
refs = append(refs, resolved)
}
+
return refs, nil
}
@@ -55,42 +62,53 @@ func (store *Store) List(pattern string) ([]ref.Ref, error) {
func (store *Store) collectLooseRefNames() ([]string, error) {
names := make([]string, 0, 16)
- if _, err := store.root.Stat("HEAD"); err == nil {
+ _, err := store.root.Stat("HEAD")
+ if err == nil {
names = append(names, "HEAD")
} else if !errors.Is(err, os.ErrNotExist) {
return nil, err
}
var walk func(string) error
+
walk = func(dir string) error {
file, err := store.root.Open(dir)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil
}
+
return err
}
+
defer func() { _ = file.Close() }()
entries, err := file.ReadDir(-1)
if err != nil {
return err
}
+
for _, entry := range entries {
name := path.Join(dir, entry.Name())
if entry.IsDir() {
- if err := walk(name); err != nil {
+ err := walk(name)
+ if err != nil {
return err
}
+
continue
}
+
names = append(names, name)
}
+
return nil
}
- if err := walk("refs"); err != nil {
+ err = walk("refs")
+ if err != nil {
return nil, err
}
+
return names, nil
}
diff --git a/refstore/loose/loose_test.go b/refstore/loose/loose_test.go
index 8c9d6f98..7b295bbb 100644
--- a/refstore/loose/loose_test.go
+++ b/refstore/loose/loose_test.go
@@ -16,16 +16,19 @@ import (
func openLooseStore(t *testing.T, repoPath string, algo objectid.Algorithm) *loose.Store {
t.Helper()
+
root, err := os.OpenRoot(repoPath)
if err != nil {
t.Fatalf("OpenRoot(%q): %v", repoPath, err)
}
+
t.Cleanup(func() { _ = root.Close() })
store, err := loose.New(root, algo)
if err != nil {
t.Fatalf("loose.New: %v", err)
}
+
return store
}
@@ -43,10 +46,12 @@ func TestLooseResolveAndResolveFully(t *testing.T) {
if err != nil {
t.Fatalf("Resolve(HEAD): %v", err)
}
+
headSym, ok := resolvedHead.(ref.Symbolic)
if !ok {
t.Fatalf("Resolve(HEAD) type = %T, want ref.Symbolic", resolvedHead)
}
+
if headSym.Target != "refs/heads/main" {
t.Fatalf("Resolve(HEAD) target = %q, want %q", headSym.Target, "refs/heads/main")
}
@@ -55,10 +60,12 @@ func TestLooseResolveAndResolveFully(t *testing.T) {
if err != nil {
t.Fatalf("Resolve(refs/heads/main): %v", err)
}
+
mainDet, ok := resolvedMain.(ref.Detached)
if !ok {
t.Fatalf("Resolve(main) type = %T, want ref.Detached", resolvedMain)
}
+
if mainDet.ID != commitID {
t.Fatalf("Resolve(main) id = %s, want %s", mainDet.ID, commitID)
}
@@ -67,11 +74,13 @@ func TestLooseResolveAndResolveFully(t *testing.T) {
if err != nil {
t.Fatalf("ResolveFully(HEAD): %v", err)
}
+
if fullHead.ID != commitID {
t.Fatalf("ResolveFully(HEAD) id = %s, want %s", fullHead.ID, commitID)
}
- if _, err := store.Resolve("refs/heads/does-not-exist"); !errors.Is(err, refstore.ErrReferenceNotFound) {
+ _, err = store.Resolve("refs/heads/does-not-exist")
+ if !errors.Is(err, refstore.ErrReferenceNotFound) {
t.Fatalf("Resolve(not-found) error = %v", err)
}
})
@@ -85,7 +94,9 @@ func TestLooseResolveFullyCycle(t *testing.T) {
testRepo.SymbolicRef(t, "refs/heads/b", "refs/heads/a")
store := openLooseStore(t, testRepo.Dir(), algo)
- if _, err := store.ResolveFully("refs/heads/a"); err == nil {
+
+ _, err := store.ResolveFully("refs/heads/a")
+ if err == nil {
t.Fatalf("ResolveFully(cycle) expected error")
}
})
@@ -107,11 +118,14 @@ func TestLooseListPattern(t *testing.T) {
if err != nil {
t.Fatalf("List(\"\"): %v", err)
}
+
allNames := make([]string, 0, len(allRefs))
for _, entry := range allRefs {
allNames = append(allNames, entry.Name())
}
+
slices.Sort(allNames)
+
wantAll := []string{"HEAD", "refs/heads/feature", "refs/heads/main", "refs/tags/v1.0.0"}
if !slices.Equal(allNames, wantAll) {
t.Fatalf("List(\"\") names = %v, want %v", allNames, wantAll)
@@ -121,11 +135,14 @@ func TestLooseListPattern(t *testing.T) {
if err != nil {
t.Fatalf("List(refs/heads/*): %v", err)
}
+
headNames := make([]string, 0, len(headRefs))
for _, entry := range headRefs {
headNames = append(headNames, entry.Name())
}
+
slices.Sort(headNames)
+
wantHeads := []string{"refs/heads/feature", "refs/heads/main"}
if !slices.Equal(headNames, wantHeads) {
t.Fatalf("List(refs/heads/*) names = %v, want %v", headNames, wantHeads)
@@ -182,13 +199,17 @@ func TestLooseListPatternMatrix(t *testing.T) {
if err != nil {
t.Fatalf("List(%q): %v", tt.pattern, err)
}
+
gotNames := make([]string, 0, len(got))
for _, entry := range got {
gotNames = append(gotNames, entry.Name())
}
+
slices.Sort(gotNames)
+
wantNames := append([]string(nil), tt.want...)
slices.Sort(wantNames)
+
if !slices.Equal(gotNames, wantNames) {
t.Fatalf("List(%q) names = %v, want %v", tt.pattern, gotNames, wantNames)
}
@@ -201,16 +222,23 @@ func TestLooseMalformedDetachedRef(t *testing.T) {
t.Parallel()
testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper
testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
+
refPath := filepath.Join(testRepo.Dir(), "refs", "heads", "bad")
- if err := os.MkdirAll(filepath.Dir(refPath), 0o755); err != nil {
+
+ err := os.MkdirAll(filepath.Dir(refPath), 0o755)
+ if err != nil {
t.Fatalf("MkdirAll: %v", err)
}
- if err := os.WriteFile(refPath, []byte("not-a-hash\n"), 0o644); err != nil {
+
+ err = os.WriteFile(refPath, []byte("not-a-hash\n"), 0o644)
+ if err != nil {
t.Fatalf("WriteFile: %v", err)
}
store := openLooseStore(t, testRepo.Dir(), algo)
- if _, err := store.Resolve("refs/heads/bad"); err == nil {
+
+ _, err = store.Resolve("refs/heads/bad")
+ if err == nil {
t.Fatalf("Resolve(malformed) expected error")
}
})
@@ -231,6 +259,7 @@ func TestLooseShorten(t *testing.T) {
if err != nil {
t.Fatalf("Shorten(head): %v", err)
}
+
if shortHead != "heads/main" {
t.Fatalf("Shorten(refs/heads/main) = %q, want %q", shortHead, "heads/main")
}
@@ -239,11 +268,13 @@ func TestLooseShorten(t *testing.T) {
if err != nil {
t.Fatalf("Shorten(remote): %v", err)
}
+
if shortRemote != "origin/main" {
t.Fatalf("Shorten(remote) = %q, want %q", shortRemote, "origin/main")
}
- if _, err := store.Shorten("refs/heads/does-not-exist"); !errors.Is(err, refstore.ErrReferenceNotFound) {
+ _, err = store.Shorten("refs/heads/does-not-exist")
+ if !errors.Is(err, refstore.ErrReferenceNotFound) {
t.Fatalf("Shorten(not-found) error = %v", err)
}
})
diff --git a/refstore/loose/resolve.go b/refstore/loose/resolve.go
index f54ab5a4..076c4098 100644
--- a/refstore/loose/resolve.go
+++ b/refstore/loose/resolve.go
@@ -16,10 +16,12 @@ func (store *Store) Resolve(name string) (ref.Ref, error) {
if name == "" {
return nil, refstore.ErrReferenceNotFound
}
+
resolved, err := store.resolveOne(name)
if err != nil {
return nil, err
}
+
return resolved, nil
}
@@ -30,17 +32,20 @@ func (store *Store) ResolveFully(name string) (ref.Detached, error) {
}
cur := name
+
seen := make(map[string]struct{})
for {
if _, ok := seen[cur]; ok {
return ref.Detached{}, fmt.Errorf("refstore/loose: symbolic reference cycle at %q", cur)
}
+
seen[cur] = struct{}{}
resolved, err := store.resolveOne(cur)
if err != nil {
return ref.Detached{}, err
}
+
switch resolved := resolved.(type) {
case ref.Detached:
return resolved, nil
@@ -49,6 +54,7 @@ func (store *Store) ResolveFully(name string) (ref.Detached, error) {
if target == "" {
return ref.Detached{}, fmt.Errorf("refstore/loose: symbolic reference %q has empty target", resolved.Name())
}
+
cur = target
default:
return ref.Detached{}, fmt.Errorf("refstore/loose: unsupported reference type %T", resolved)
@@ -63,23 +69,28 @@ func (store *Store) resolveOne(name string) (ref.Ref, error) {
if errors.Is(err, os.ErrNotExist) {
return nil, refstore.ErrReferenceNotFound
}
+
return nil, err
}
+
line := strings.TrimSpace(string(data))
if strings.HasPrefix(line, "ref: ") {
target := strings.TrimSpace(line[len("ref: "):])
if target == "" {
return nil, fmt.Errorf("refstore/loose: symbolic reference %q has empty target", name)
}
+
return ref.Symbolic{
RefName: name,
Target: target,
}, nil
}
+
id, err := objectid.ParseHex(store.algo, line)
if err != nil {
return nil, fmt.Errorf("refstore/loose: invalid detached reference %q: %w", name, err)
}
+
return ref.Detached{
RefName: name,
ID: id,
diff --git a/refstore/loose/shorten.go b/refstore/loose/shorten.go
index 17a60def..e863d783 100644
--- a/refstore/loose/shorten.go
+++ b/refstore/loose/shorten.go
@@ -10,20 +10,26 @@ func (store *Store) Shorten(name string) (string, error) {
if err != nil {
return "", err
}
+
names := make([]string, 0, len(refs))
found := false
+
for _, entry := range refs {
if entry == nil {
continue
}
+
full := entry.Name()
+
names = append(names, full)
if full == name {
found = true
}
}
+
if !found {
return "", refstore.ErrReferenceNotFound
}
+
return refstore.ShortenName(name, names), nil
}
diff --git a/refstore/loose/store.go b/refstore/loose/store.go
index e4dc3a34..ec814188 100644
--- a/refstore/loose/store.go
+++ b/refstore/loose/store.go
@@ -25,6 +25,7 @@ func New(root *os.Root, algo objectid.Algorithm) (*Store, error) {
if algo.Size() == 0 {
return nil, objectid.ErrInvalidAlgorithm
}
+
return &Store{
root: root,
algo: algo,
diff --git a/refstore/packed/packed_test.go b/refstore/packed/packed_test.go
index dffed2a8..0ddceabf 100644
--- a/refstore/packed/packed_test.go
+++ b/refstore/packed/packed_test.go
@@ -16,30 +16,39 @@ import (
func openPackedRefStoreFromRepo(t *testing.T, repoPath string, algo objectid.Algorithm) *packed.Store {
t.Helper()
+
root, err := os.OpenRoot(repoPath)
if err != nil {
t.Fatalf("OpenRoot(repo): %v", err)
}
+
defer func() { _ = root.Close() }()
store, err := packed.New(root, algo)
if err != nil {
t.Fatalf("packed.New: %v", err)
}
+
return store
}
func openPackedRefStoreFromContent(t *testing.T, content string, algo objectid.Algorithm) (*packed.Store, error) {
t.Helper()
+
dir := t.TempDir()
- if err := os.WriteFile(dir+"/packed-refs", []byte(content), 0o644); err != nil {
+
+ err := os.WriteFile(dir+"/packed-refs", []byte(content), 0o644)
+ if err != nil {
t.Fatalf("WriteFile(packed-refs): %v", err)
}
+
root, err := os.OpenRoot(dir)
if err != nil {
t.Fatalf("OpenRoot(temp): %v", err)
}
+
defer func() { _ = root.Close() }()
+
return packed.New(root, algo)
}
@@ -58,10 +67,12 @@ func TestPackedResolveAndPeeled(t *testing.T) {
if err != nil {
t.Fatalf("Resolve(main): %v", err)
}
+
mainDet, ok := resolvedMain.(ref.Detached)
if !ok {
t.Fatalf("Resolve(main) type = %T, want ref.Detached", resolvedMain)
}
+
if mainDet.ID != commitID {
t.Fatalf("Resolve(main) id = %s, want %s", mainDet.ID, commitID)
}
@@ -70,16 +81,20 @@ func TestPackedResolveAndPeeled(t *testing.T) {
if err != nil {
t.Fatalf("Resolve(tag): %v", err)
}
+
tagDet, ok := resolvedTag.(ref.Detached)
if !ok {
t.Fatalf("Resolve(tag) type = %T, want ref.Detached", resolvedTag)
}
+
if tagDet.ID != tagID {
t.Fatalf("Resolve(tag) id = %s, want %s", tagDet.ID, tagID)
}
+
if tagDet.Peeled == nil {
t.Fatalf("Resolve(tag) peeled = nil, want commit")
}
+
if *tagDet.Peeled != commitID {
t.Fatalf("Resolve(tag) peeled = %s, want %s", *tagDet.Peeled, commitID)
}
@@ -88,11 +103,13 @@ func TestPackedResolveAndPeeled(t *testing.T) {
if err != nil {
t.Fatalf("ResolveFully(tag): %v", err)
}
+
if fullTag.ID != tagDet.ID {
t.Fatalf("ResolveFully(tag) id = %s, want %s", fullTag.ID, tagDet.ID)
}
- if _, err := store.Resolve("refs/heads/does-not-exist"); !errors.Is(err, refstore.ErrReferenceNotFound) {
+ _, err = store.Resolve("refs/heads/does-not-exist")
+ if !errors.Is(err, refstore.ErrReferenceNotFound) {
t.Fatalf("Resolve(not-found) error = %v", err)
}
})
@@ -114,11 +131,14 @@ func TestPackedListAndShorten(t *testing.T) {
if err != nil {
t.Fatalf("List(all): %v", err)
}
+
allNames := make([]string, 0, len(all))
for _, entry := range all {
allNames = append(allNames, entry.Name())
}
+
slices.Sort(allNames)
+
wantAll := []string{"refs/heads/main", "refs/remotes/origin/main", "refs/tags/main"}
if !slices.Equal(allNames, wantAll) {
t.Fatalf("List(all) names = %v, want %v", allNames, wantAll)
@@ -128,6 +148,7 @@ func TestPackedListAndShorten(t *testing.T) {
if err != nil {
t.Fatalf("List(pattern): %v", err)
}
+
if len(filtered) != 1 || filtered[0].Name() != "refs/heads/main" {
t.Fatalf("List(refs/heads/*) = %v, want refs/heads/main only", filtered)
}
@@ -136,11 +157,13 @@ func TestPackedListAndShorten(t *testing.T) {
if err != nil {
t.Fatalf("Shorten(main): %v", err)
}
+
if short != "heads/main" {
t.Fatalf("Shorten(main) = %q, want %q", short, "heads/main")
}
- if _, err := store.Shorten("refs/heads/does-not-exist"); !errors.Is(err, refstore.ErrReferenceNotFound) {
+ _, err = store.Shorten("refs/heads/does-not-exist")
+ if !errors.Is(err, refstore.ErrReferenceNotFound) {
t.Fatalf("Shorten(not-found) error = %v", err)
}
})
@@ -195,10 +218,13 @@ func TestPackedListPatternMatrix(t *testing.T) {
if err != nil {
t.Fatalf("List(%q): %v", tt.pattern, err)
}
+
gotNames := refNames(got)
slices.Sort(gotNames)
+
wantNames := append([]string(nil), tt.want...)
slices.Sort(wantNames)
+
if !slices.Equal(gotNames, wantNames) {
t.Fatalf("List(%q) names = %v, want %v", tt.pattern, gotNames, wantNames)
}
@@ -231,7 +257,8 @@ func TestPackedParseErrors(t *testing.T) {
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
- if _, err := openPackedRefStoreFromContent(t, tt.data, algo); err == nil {
+ _, err := openPackedRefStoreFromContent(t, tt.data, algo)
+ if err == nil {
t.Fatalf("packed.New expected parse error")
}
})
@@ -242,16 +269,21 @@ func TestPackedParseErrors(t *testing.T) {
func TestPackedNewValidation(t *testing.T) {
t.Parallel()
dir := t.TempDir()
+
root, err := os.OpenRoot(dir)
if err != nil {
t.Fatalf("OpenRoot(temp): %v", err)
}
+
defer func() { _ = root.Close() }()
- if _, err := packed.New(root, objectid.AlgorithmUnknown); !errors.Is(err, objectid.ErrInvalidAlgorithm) {
+ _, err = packed.New(root, objectid.AlgorithmUnknown)
+ if !errors.Is(err, objectid.ErrInvalidAlgorithm) {
t.Fatalf("packed.New invalid algorithm error = %v", err)
}
- if _, err := packed.New(root, objectid.AlgorithmSHA256); !errors.Is(err, os.ErrNotExist) {
+
+ _, err = packed.New(root, objectid.AlgorithmSHA256)
+ if !errors.Is(err, os.ErrNotExist) {
t.Fatalf("packed.New missing packed-refs error = %v", err)
}
}
@@ -261,6 +293,7 @@ func refNames(refs []ref.Ref) []string {
for _, entry := range refs {
names = append(names, entry.Name())
}
+
return names
}
diff --git a/refstore/packed/parse.go b/refstore/packed/parse.go
index 6fe88061..4846d258 100644
--- a/refstore/packed/parse.go
+++ b/refstore/packed/parse.go
@@ -24,24 +24,30 @@ func parsePackedRefs(r io.Reader, algo objectid.Algorithm) (map[string]ref.Detac
if err != nil && err != io.EOF {
return nil, nil, err
}
+
if line == "" && err == io.EOF {
break
}
+
lineNum++
line = strings.TrimSuffix(line, "\n")
line = strings.TrimSuffix(line, "\r")
+
line = strings.TrimSpace(line)
if line == "" {
if err == io.EOF {
break
}
+
continue
}
+
if strings.HasPrefix(line, "#") {
if err == io.EOF {
break
}
+
continue
}
@@ -49,19 +55,24 @@ func parsePackedRefs(r io.Reader, algo objectid.Algorithm) (map[string]ref.Detac
if prev < 0 {
return nil, nil, fmt.Errorf("refstore/packed: line %d: peeled line without preceding ref", lineNum)
}
+
peeledHex := strings.TrimSpace(strings.TrimPrefix(line, "^"))
+
peeled, parseErr := objectid.ParseHex(algo, peeledHex)
if parseErr != nil {
return nil, nil, fmt.Errorf("refstore/packed: line %d: invalid peeled oid: %w", lineNum, parseErr)
}
+
peeledCopy := peeled
cur := ordered[prev]
cur.Peeled = &peeledCopy
ordered[prev] = cur
byName[cur.Name()] = cur
+
if err == io.EOF {
break
}
+
continue
}
@@ -79,6 +90,7 @@ func parsePackedRefs(r io.Reader, algo objectid.Algorithm) (map[string]ref.Detac
if name == "" {
return nil, nil, fmt.Errorf("refstore/packed: line %d: empty ref name", lineNum)
}
+
if _, exists := byName[name]; exists {
return nil, nil, fmt.Errorf("refstore/packed: line %d: duplicate ref %q", lineNum, name)
}
diff --git a/refstore/packed/store.go b/refstore/packed/store.go
index 7705dacb..5ab9d602 100644
--- a/refstore/packed/store.go
+++ b/refstore/packed/store.go
@@ -25,16 +25,19 @@ func New(root *os.Root, algo objectid.Algorithm) (*Store, error) {
if algo.Size() == 0 {
return nil, objectid.ErrInvalidAlgorithm
}
+
packedRefs, err := root.Open("packed-refs")
if err != nil {
return nil, fmt.Errorf("refstore/packed: open packed-refs: %w", err)
}
+
defer func() { _ = packedRefs.Close() }()
byName, ordered, err := parsePackedRefs(packedRefs, algo)
if err != nil {
return nil, err
}
+
return &Store{
byName: byName,
ordered: ordered,
@@ -47,6 +50,7 @@ func (store *Store) Resolve(name string) (ref.Ref, error) {
if !ok {
return nil, refstore.ErrReferenceNotFound
}
+
return detached, nil
}
@@ -58,6 +62,7 @@ func (store *Store) ResolveFully(name string) (ref.Detached, error) {
if !ok {
return ref.Detached{}, refstore.ErrReferenceNotFound
}
+
return detached, nil
}
@@ -68,7 +73,8 @@ func (store *Store) ResolveFully(name string) (ref.Detached, error) {
func (store *Store) List(pattern string) ([]ref.Ref, error) {
matchAll := pattern == ""
if !matchAll {
- if _, err := path.Match(pattern, "refs/heads/main"); err != nil {
+ _, err := path.Match(pattern, "refs/heads/main")
+ if err != nil {
return nil, err
}
}
@@ -80,12 +86,15 @@ func (store *Store) List(pattern string) ([]ref.Ref, error) {
if err != nil {
return nil, err
}
+
if !matched {
continue
}
}
+
refs = append(refs, entry)
}
+
return refs, nil
}
@@ -100,6 +109,7 @@ func (store *Store) Shorten(name string) (string, error) {
for _, entry := range store.ordered {
names = append(names, entry.Name())
}
+
return refstore.ShortenName(name, names), nil
}
diff --git a/refstore/reftable/lookup.go b/refstore/reftable/lookup.go
index 8862f7e6..53483bbf 100644
--- a/refstore/reftable/lookup.go
+++ b/refstore/reftable/lookup.go
@@ -16,13 +16,16 @@ func (table *tableFile) resolveRecord(name string) (recordValue, bool, error) {
if err != nil {
return recordValue{}, false, err
}
+
pos, ok, err := table.resolveRefBlockPosFromIndex(name, indexPos)
if err != nil {
return recordValue{}, false, err
}
+
if !ok {
return recordValue{}, false, nil
}
+
return table.lookupInRefBlock(name, pos)
}
@@ -32,28 +35,36 @@ func (table *tableFile) resolveRecord(name string) (recordValue, bool, error) {
for pos < table.refEnd && table.data[pos] == 0 {
pos++
}
+
if pos >= table.refEnd {
break
}
+
if table.data[pos] != blockTypeRef {
return recordValue{}, false, fmt.Errorf("refstore/reftable: table %q: unexpected block type %q in ref section", table.name, table.data[pos])
}
+
block, blockEnd, err := table.readBlockAt(pos)
if err != nil {
return recordValue{}, false, err
}
+
found, done, rec, err := lookupRecordInRefBlock(table, block, name)
if err != nil {
return recordValue{}, false, err
}
+
if found {
return rec, true, nil
}
+
if done {
return recordValue{}, false, nil
}
+
pos = table.nextBlockPos(blockEnd)
}
+
return recordValue{}, false, nil
}
@@ -63,16 +74,20 @@ func (table *tableFile) resolveRefBlockPosFromIndex(name string, indexPos int) (
if err != nil {
return 0, false, err
}
+
if block.blockType != blockTypeIndex {
return 0, false, fmt.Errorf("refstore/reftable: table %q: ref index root is not index block", table.name)
}
+
childPos, ok, err := lookupChildPosInIndexBlock(block, name)
if err != nil {
return 0, false, err
}
+
if !ok {
return 0, false, nil
}
+
if childPos < 0 || childPos >= len(table.data) {
return 0, false, fmt.Errorf("refstore/reftable: table %q: index child position out of range", table.name)
}
@@ -94,13 +109,16 @@ func (table *tableFile) lookupInRefBlock(name string, pos int) (recordValue, boo
if err != nil {
return recordValue{}, false, err
}
+
if block.blockType != blockTypeRef {
return recordValue{}, false, fmt.Errorf("refstore/reftable: table %q: expected ref block at %d", table.name, pos)
}
+
found, _, rec, err := lookupRecordInRefBlock(table, block, name)
if err != nil {
return recordValue{}, false, err
}
+
return rec, found, nil
}
@@ -108,13 +126,16 @@ func (table *tableFile) lookupInRefBlock(name string, pos int) (recordValue, boo
func (table *tableFile) forEachRecord(fn func(name string, rec recordValue) error) error {
pos := table.headerLen
prevLast := ""
+
for pos < table.refEnd {
for pos < table.refEnd && table.data[pos] == 0 {
pos++
}
+
if pos >= table.refEnd {
break
}
+
if table.data[pos] != blockTypeRef {
return fmt.Errorf("refstore/reftable: table %q: unexpected block type %q in ref section", table.name, table.data[pos])
}
@@ -123,25 +144,33 @@ func (table *tableFile) forEachRecord(fn func(name string, rec recordValue) erro
if err != nil {
return err
}
+
var first, last string
+
err = forEachRecordInRefBlock(table, block, func(name string, rec recordValue) error {
if first == "" {
first = name
}
+
last = name
+
return fn(name, rec)
})
if err != nil {
return err
}
+
if prevLast != "" && first != "" && strings.Compare(first, prevLast) <= 0 {
return fmt.Errorf("refstore/reftable: table %q: ref blocks are not strictly ordered", table.name)
}
+
if last != "" {
prevLast = last
}
+
pos = table.nextBlockPos(blockEnd)
}
+
return nil
}
@@ -159,22 +188,29 @@ func (table *tableFile) readBlockAt(pos int) (blockView, int, error) {
if pos < 0 || pos+4 > len(table.data) {
return blockView{}, 0, fmt.Errorf("refstore/reftable: table %q: block header out of range", table.name)
}
+
blockLen := int(readUint24(table.data[pos+1 : pos+4]))
+
effectiveLen := blockLen
if pos == table.headerLen {
if blockLen < table.headerLen {
return blockView{}, 0, fmt.Errorf("refstore/reftable: table %q: invalid first block length", table.name)
}
+
effectiveLen = blockLen - table.headerLen
}
+
if effectiveLen < 4 {
return blockView{}, 0, fmt.Errorf("refstore/reftable: table %q: invalid block length", table.name)
}
+
end := pos + effectiveLen
if end > len(table.data) {
return blockView{}, 0, fmt.Errorf("refstore/reftable: table %q: block out of range", table.name)
}
+
view := blockView{blockType: table.data[pos], start: pos, end: end, first: pos == table.headerLen, payload: table.data[pos:end]}
+
return view, end, nil
}
@@ -183,6 +219,7 @@ func (table *tableFile) nextBlockPos(blockEnd int) int {
if table.blockSize > 0 {
return alignUp(blockEnd, table.blockSize)
}
+
return blockEnd
}
@@ -192,35 +229,45 @@ func lookupChildPosInIndexBlock(block blockView, key string) (int, bool, error)
if err != nil {
return 0, false, err
}
- if err := validateRestarts(block, restarts, off, recordsEnd, true); err != nil {
+
+ err = validateRestarts(block, restarts, off, recordsEnd, true)
+ if err != nil {
return 0, false, err
}
+
prev := ""
for off < recordsEnd {
name, v, nextOff, err := parseKeyedRecord(block.payload, off, recordsEnd, prev)
if err != nil {
return 0, false, err
}
+
if (v & 0x7) != 0 {
return 0, false, fmt.Errorf("index value_type must be 0")
}
+
childPos, nextOff, err := readVarint(block.payload, nextOff, recordsEnd)
if err != nil {
return 0, false, err
}
+
if strings.Compare(key, name) <= 0 {
childPosInt, err := intconv.Uint64ToInt(childPos)
if err != nil {
return 0, false, fmt.Errorf("index child position conversion: %w", err)
}
+
return childPosInt, true, nil
}
+
prev = name
off = nextOff
}
+
if off != recordsEnd {
return 0, false, fmt.Errorf("malformed index block")
}
+
return 0, false, nil
}
@@ -230,37 +277,48 @@ func lookupRecordInRefBlock(table *tableFile, block blockView, key string) (foun
if err != nil {
return false, false, recordValue{}, err
}
- if err := validateRestarts(block, restarts, off, recordsEnd, true); err != nil {
+
+ err = validateRestarts(block, restarts, off, recordsEnd, true)
+ if err != nil {
return false, false, recordValue{}, err
}
+
prev := ""
for off < recordsEnd {
name, v, nextOff, err := parseKeyedRecord(block.payload, off, recordsEnd, prev)
if err != nil {
return false, false, recordValue{}, err
}
+
typeBits := byte(v & 0x7)
+
_, nextOff, err = readVarint(block.payload, nextOff, recordsEnd)
if err != nil {
return false, false, recordValue{}, err
}
+
recVal, nextOff, err := parseRefValue(block.payload, nextOff, recordsEnd, table.algo, typeBits)
if err != nil {
return false, false, recordValue{}, err
}
+
cmp := strings.Compare(name, key)
if cmp == 0 {
return true, true, recVal, nil
}
+
if cmp > 0 {
return false, true, recordValue{}, nil
}
+
prev = name
off = nextOff
}
+
if off != recordsEnd {
return false, false, recordValue{}, fmt.Errorf("malformed ref block")
}
+
return false, false, recordValue{}, nil
}
@@ -270,33 +328,44 @@ func forEachRecordInRefBlock(table *tableFile, block blockView, fn func(name str
if err != nil {
return err
}
- if err := validateRestarts(block, restarts, off, recordsEnd, true); err != nil {
+
+ err = validateRestarts(block, restarts, off, recordsEnd, true)
+ if err != nil {
return err
}
+
prev := ""
for off < recordsEnd {
name, v, nextOff, err := parseKeyedRecord(block.payload, off, recordsEnd, prev)
if err != nil {
return err
}
+
typeBits := byte(v & 0x7)
+
_, nextOff, err = readVarint(block.payload, nextOff, recordsEnd)
if err != nil {
return err
}
+
recVal, nextOff, err := parseRefValue(block.payload, nextOff, recordsEnd, table.algo, typeBits)
if err != nil {
return err
}
- if err := fn(name, recVal); err != nil {
+
+ err = fn(name, recVal)
+ if err != nil {
return err
}
+
prev = name
off = nextOff
}
+
if off != recordsEnd {
return fmt.Errorf("malformed ref block")
}
+
return nil
}
@@ -305,51 +374,63 @@ func parseBlockLayout(block blockView) (recordsStart, recordsEnd int, restarts [
if len(block.payload) < 6 {
return 0, 0, nil, fmt.Errorf("short block")
}
+
restartCount := int(binary.BigEndian.Uint16(block.payload[len(block.payload)-2:]))
if restartCount <= 0 {
return 0, 0, nil, fmt.Errorf("invalid restart count")
}
+
restarts = make([]int, restartCount)
restartBytes := restartCount * 3
+
restartsStart := len(block.payload) - 2 - restartBytes
if restartsStart < 4 {
return 0, 0, nil, fmt.Errorf("invalid restart table")
}
+
for i := range restartCount {
off := restartsStart + i*3
rel := int(readUint24(block.payload[off : off+3]))
+
base := block.start
if block.first {
// In the first block, restart offsets are relative to file start.
base = 0
}
+
abs := base + rel
restarts[i] = abs - block.start
}
+
return 4, restartsStart, restarts, nil
}
// validateRestarts validates restart monotonicity, bounds and record-prefix invariants.
func validateRestarts(block blockView, restarts []int, recordsStart, recordsEnd int, requirePrefixZero bool) error {
prev := -1
+
for _, off := range restarts {
if off < recordsStart || off >= recordsEnd {
return fmt.Errorf("restart offset out of range")
}
+
if off <= prev {
return fmt.Errorf("restart offsets not strictly increasing")
}
+
prev = off
if requirePrefixZero {
prefix, _, err := readVarint(block.payload, off, recordsEnd)
if err != nil {
return err
}
+
if prefix != 0 {
return fmt.Errorf("restart record prefix length must be zero")
}
}
}
+
return nil
}
@@ -359,26 +440,33 @@ func parseKeyedRecord(buf []byte, off, end int, prev string) (name string, rawTy
if err != nil {
return "", 0, 0, err
}
+
suffixAndType, next, err := readVarint(buf, next, end)
if err != nil {
return "", 0, 0, err
}
+
suffixLen, err := intconv.Uint64ToInt(suffixAndType >> 3)
if err != nil || suffixLen < 0 || next+suffixLen > end {
return "", 0, 0, fmt.Errorf("invalid suffix length")
}
+
prefixLenInt, err := intconv.Uint64ToInt(prefixLen)
if err != nil {
return "", 0, 0, fmt.Errorf("invalid prefix length")
}
+
if prefixLenInt > len(prev) {
return "", 0, 0, fmt.Errorf("invalid prefix length")
}
+
name = prev[:prefixLenInt] + string(buf[next:next+suffixLen])
next += suffixLen
+
if prev != "" && strings.Compare(name, prev) <= 0 {
return "", 0, 0, fmt.Errorf("keys not strictly increasing")
}
+
return name, suffixAndType, next, nil
}
@@ -392,40 +480,50 @@ func parseRefValue(buf []byte, off, end int, algo objectid.Algorithm, valueType
if err != nil {
return recordValue{}, 0, err
}
+
return recordValue{detachedID: id, hasDetached: true}, next, nil
case 0x2:
id, next, err := readObjectID(buf, off, end, algo)
if err != nil {
return recordValue{}, 0, err
}
+
peeled, next, err := readObjectID(buf, next, end, algo)
if err != nil {
return recordValue{}, 0, err
}
+
peeledCopy := peeled
+
return recordValue{detachedID: id, hasDetached: true, peeled: &peeledCopy}, next, nil
case 0x3:
targetLen, next, err := readVarint(buf, off, end)
if err != nil {
return recordValue{}, 0, err
}
+
remaining := end - next
if remaining < 0 {
return recordValue{}, 0, fmt.Errorf("invalid symref target length")
}
+
remainingU64, err := intconv.IntToUint64(remaining)
if err != nil {
return recordValue{}, 0, fmt.Errorf("invalid symref target length")
}
+
if targetLen > remainingU64 {
return recordValue{}, 0, fmt.Errorf("invalid symref target length")
}
+
targetLenInt, err := intconv.Uint64ToInt(targetLen)
if err != nil {
return recordValue{}, 0, fmt.Errorf("invalid symref target length")
}
+
target := string(buf[next : next+targetLenInt])
next += targetLenInt
+
return recordValue{symbolicTarget: target}, next, nil
default:
return recordValue{}, 0, fmt.Errorf("unsupported ref value type %d", valueType)
@@ -438,9 +536,11 @@ func readObjectID(buf []byte, off, end int, algo objectid.Algorithm) (objectid.O
if off < 0 || sz < 0 || off+sz > end {
return objectid.ObjectID{}, 0, fmt.Errorf("truncated object id")
}
+
id, err := objectid.FromBytes(algo, buf[off:off+sz])
if err != nil {
return objectid.ObjectID{}, 0, err
}
+
return id, off + sz, nil
}
diff --git a/refstore/reftable/parse_helpers.go b/refstore/reftable/parse_helpers.go
index b5da555e..5b5fae24 100644
--- a/refstore/reftable/parse_helpers.go
+++ b/refstore/reftable/parse_helpers.go
@@ -13,6 +13,7 @@ func alignUp(pos, blockSize int) int {
if rem == 0 {
return pos
}
+
return pos + (blockSize - rem)
}
@@ -21,16 +22,20 @@ func readVarint(buf []byte, off, end int) (uint64, int, error) {
if off >= end {
return 0, 0, fmt.Errorf("unexpected EOF")
}
+
b := buf[off]
val := uint64(b & 0x7f)
+
off++
for b&0x80 != 0 {
if off >= end {
return 0, 0, fmt.Errorf("unexpected EOF")
}
+
b = buf[off]
off++
val = ((val + 1) << 7) | uint64(b&0x7f)
}
+
return val, off, nil
}
diff --git a/refstore/reftable/reftable_test.go b/refstore/reftable/reftable_test.go
index 2a6e0738..26aa7584 100644
--- a/refstore/reftable/reftable_test.go
+++ b/refstore/reftable/reftable_test.go
@@ -17,6 +17,7 @@ import (
// newBareReftableRepo creates a bare repository that uses reftable ref storage.
func newBareReftableRepo(tb testing.TB, algo objectid.Algorithm) *testgit.TestRepo {
tb.Helper()
+
return testgit.NewRepo(tb, testgit.RepoOptions{
ObjectFormat: algo,
Bare: true,
@@ -27,15 +28,19 @@ func newBareReftableRepo(tb testing.TB, algo objectid.Algorithm) *testgit.TestRe
// openStore opens a reftable store against repoDir/reftable.
func openStore(tb testing.TB, repoDir string, algo objectid.Algorithm) *reftable.Store {
tb.Helper()
+
root, err := os.OpenRoot(filepath.Join(repoDir, "reftable"))
if err != nil {
tb.Fatalf("OpenRoot(reftable): %v", err)
}
+
tb.Cleanup(func() { _ = root.Close() })
+
store, err := reftable.New(root, algo)
if err != nil {
tb.Fatalf("reftable.New: %v", err)
}
+
return store
}
@@ -48,14 +53,17 @@ func TestResolveAndResolveFully(t *testing.T) {
repo.SymbolicRef(t, "HEAD", "refs/heads/main")
store := openStore(t, repo.Dir(), algo)
+
head, err := store.Resolve("HEAD")
if err != nil {
t.Fatalf("Resolve(HEAD): %v", err)
}
+
sym, ok := head.(ref.Symbolic)
if !ok {
t.Fatalf("Resolve(HEAD) type = %T, want ref.Symbolic", head)
}
+
if sym.Target != "refs/heads/main" {
t.Fatalf("Resolve(HEAD) target = %q, want refs/heads/main", sym.Target)
}
@@ -64,11 +72,13 @@ func TestResolveAndResolveFully(t *testing.T) {
if err != nil {
t.Fatalf("ResolveFully(HEAD): %v", err)
}
+
if main.ID != id {
t.Fatalf("ResolveFully(HEAD) id = %s, want %s", main.ID, id)
}
- if _, err := store.Resolve("refs/heads/missing"); !errors.Is(err, refstore.ErrReferenceNotFound) {
+ _, err = store.Resolve("refs/heads/missing")
+ if !errors.Is(err, refstore.ErrReferenceNotFound) {
t.Fatalf("Resolve(missing) error = %v", err)
}
})
@@ -82,7 +92,9 @@ func TestResolveFullyCycle(t *testing.T) {
repo.SymbolicRef(t, "refs/heads/b", "refs/heads/a")
store := openStore(t, repo.Dir(), algo)
- if _, err := store.ResolveFully("refs/heads/a"); err == nil {
+
+ _, err := store.ResolveFully("refs/heads/a")
+ if err == nil {
t.Fatalf("ResolveFully(cycle) expected error")
}
})
@@ -99,14 +111,17 @@ func TestListAndShorten(t *testing.T) {
repo.UpdateRef(t, "refs/remotes/origin/main", id)
store := openStore(t, repo.Dir(), algo)
+
all, err := store.List("")
if err != nil {
t.Fatalf("List(all): %v", err)
}
+
names := make([]string, 0, len(all))
for _, entry := range all {
names = append(names, entry.Name())
}
+
want := []string{"HEAD", "refs/heads/feature", "refs/heads/main", "refs/remotes/origin/main", "refs/tags/main"}
if !slices.Equal(names, want) {
t.Fatalf("List(all) = %v, want %v", names, want)
@@ -116,10 +131,12 @@ func TestListAndShorten(t *testing.T) {
if err != nil {
t.Fatalf("List(heads): %v", err)
}
+
headNames := make([]string, 0, len(heads))
for _, entry := range heads {
headNames = append(headNames, entry.Name())
}
+
wantHeads := []string{"refs/heads/feature", "refs/heads/main"}
if !slices.Equal(headNames, wantHeads) {
t.Fatalf("List(heads) = %v, want %v", headNames, wantHeads)
@@ -129,6 +146,7 @@ func TestListAndShorten(t *testing.T) {
if err != nil {
t.Fatalf("Shorten(remote): %v", err)
}
+
if short != "origin/main" {
t.Fatalf("Shorten(remote) = %q, want origin/main", short)
}
@@ -146,7 +164,9 @@ func TestTombstoneNewestWins(t *testing.T) {
repo.DeleteRef(t, "refs/heads/main")
store := openStore(t, repo.Dir(), algo)
- if _, err := store.Resolve("refs/heads/main"); !errors.Is(err, refstore.ErrReferenceNotFound) {
+
+ _, err := store.Resolve("refs/heads/main")
+ if !errors.Is(err, refstore.ErrReferenceNotFound) {
t.Fatalf("Resolve(main) after delete error = %v", err)
}
})
@@ -160,20 +180,25 @@ func TestAnnotatedTagPeeled(t *testing.T) {
tagID := repo.TagAnnotated(t, "v1.0.0", commitID, "annotated")
store := openStore(t, repo.Dir(), algo)
+
resolved, err := store.Resolve("refs/tags/v1.0.0")
if err != nil {
t.Fatalf("Resolve(tag): %v", err)
}
+
detached, ok := resolved.(ref.Detached)
if !ok {
t.Fatalf("Resolve(tag) type = %T, want ref.Detached", resolved)
}
+
if detached.ID != tagID {
t.Fatalf("Resolve(tag) id = %s, want %s", detached.ID, tagID)
}
+
if detached.Peeled == nil {
t.Fatalf("Resolve(tag) peeled = nil")
}
+
if *detached.Peeled != commitID {
t.Fatalf("Resolve(tag) peeled = %s, want %s", *detached.Peeled, commitID)
}
diff --git a/refstore/reftable/store.go b/refstore/reftable/store.go
index 7c02c157..d0d906fc 100644
--- a/refstore/reftable/store.go
+++ b/refstore/reftable/store.go
@@ -42,6 +42,7 @@ func New(root *os.Root, algo objectid.Algorithm) (*Store, error) {
if algo.Size() == 0 {
return nil, objectid.ErrInvalidAlgorithm
}
+
return &Store{root: root, algo: algo}, nil
}
@@ -50,25 +51,33 @@ func (store *Store) Close() error {
store.stateMu.Lock()
if store.closed {
store.stateMu.Unlock()
+
return nil
}
+
store.closed = true
root := store.root
tables := store.tables
store.stateMu.Unlock()
var closeErr error
+
for _, table := range tables {
if table == nil {
continue
}
- if err := table.close(); err != nil && closeErr == nil {
+
+ err := table.close()
+ if err != nil && closeErr == nil {
closeErr = err
}
}
- if err := root.Close(); err != nil && closeErr == nil {
+
+ err := root.Close()
+ if err != nil && closeErr == nil {
closeErr = err
}
+
return closeErr
}
@@ -78,23 +87,29 @@ func (store *Store) Resolve(name string) (ref.Ref, error) {
if err != nil {
return nil, err
}
+
for i := len(tables) - 1; i >= 0; i-- {
rec, found, err := tables[i].resolveRecord(name)
if err != nil {
return nil, err
}
+
if !found {
continue
}
+
if rec.deleted {
return nil, refstore.ErrReferenceNotFound
}
+
resolved, err := rec.toRef(name)
if err != nil {
return nil, err
}
+
return resolved, nil
}
+
return nil, refstore.ErrReferenceNotFound
}
@@ -104,16 +119,21 @@ func (store *Store) Resolve(name string) (ref.Ref, error) {
// annotated tag objects.
func (store *Store) ResolveFully(name string) (ref.Detached, error) {
seen := map[string]struct{}{}
+
cur := name
for {
- if _, exists := seen[cur]; exists {
+ _, exists := seen[cur]
+ if exists {
return ref.Detached{}, errors.New("refstore/reftable: symbolic reference cycle")
}
+
seen[cur] = struct{}{}
+
resolved, err := store.Resolve(cur)
if err != nil {
return ref.Detached{}, err
}
+
switch resolved := resolved.(type) {
case ref.Detached:
return resolved, nil
@@ -121,6 +141,7 @@ func (store *Store) ResolveFully(name string) (ref.Detached, error) {
if resolved.Target == "" {
return ref.Detached{}, errors.New("refstore/reftable: symbolic reference has empty target")
}
+
cur = resolved.Target
default:
return ref.Detached{}, errors.New("refstore/reftable: unsupported reference type")
@@ -137,32 +158,41 @@ func (store *Store) List(pattern string) ([]ref.Ref, error) {
if err != nil {
return nil, err
}
+
visible := make(map[string]ref.Ref)
masked := make(map[string]struct{})
for i := len(tables) - 1; i >= 0; i-- {
- if err := tables[i].forEachRecord(func(name string, rec recordValue) error {
- if _, done := masked[name]; done {
+ err := tables[i].forEachRecord(func(name string, rec recordValue) error {
+ _, done := masked[name]
+ if done {
return nil
}
+
masked[name] = struct{}{}
+
if rec.deleted {
return nil
}
+
resolved, err := rec.toRef(name)
if err != nil {
return err
}
+
visible[name] = resolved
+
return nil
- }); err != nil {
+ })
+ if err != nil {
return nil, err
}
}
matchAll := pattern == ""
if !matchAll {
- if _, err := pathMatch(pattern, "refs/heads/main"); err != nil {
+ _, err := pathMatch(pattern, "refs/heads/main")
+ if err != nil {
return nil, err
}
}
@@ -171,6 +201,7 @@ func (store *Store) List(pattern string) ([]ref.Ref, error) {
for name := range visible {
names = append(names, name)
}
+
sort.Strings(names)
out := make([]ref.Ref, 0, len(names))
@@ -180,12 +211,15 @@ func (store *Store) List(pattern string) ([]ref.Ref, error) {
if err != nil {
return nil, err
}
+
if !ok {
continue
}
}
+
out = append(out, visible[name])
}
+
return out, nil
}
@@ -195,21 +229,27 @@ func (store *Store) Shorten(name string) (string, error) {
if err != nil {
return "", err
}
+
names := make([]string, 0, len(refs))
found := false
+
for _, entry := range refs {
if entry == nil {
continue
}
+
full := entry.Name()
+
names = append(names, full)
if full == name {
found = true
}
}
+
if !found {
return "", refstore.ErrReferenceNotFound
}
+
return refstore.ShortenName(name, names), nil
}
@@ -225,9 +265,11 @@ func (store *Store) ensureTables() ([]*tableFile, error) {
store.stateMu.RLock()
defer store.stateMu.RUnlock()
+
if store.closed {
return nil, errors.New("refstore/reftable: store is closed")
}
+
return store.tables, store.loadErr
}
@@ -238,18 +280,23 @@ func (store *Store) loadTables() ([]*tableFile, error) {
if errors.Is(err, os.ErrNotExist) {
return nil, nil
}
+
return nil, err
}
+
lines := strings.Split(string(listRaw), "\n")
+
names := make([]string, 0, len(lines))
for _, line := range lines {
line = strings.TrimSuffix(line, "\r")
if line == "" {
continue
}
+
if strings.Contains(line, "/") {
return nil, errors.New("refstore/reftable: invalid table name")
}
+
names = append(names, line)
}
@@ -260,9 +307,12 @@ func (store *Store) loadTables() ([]*tableFile, error) {
for _, opened := range out {
_ = opened.close()
}
+
return nil, err
}
+
out = append(out, table)
}
+
return out, nil
}
diff --git a/refstore/reftable/table.go b/refstore/reftable/table.go
index 35982bf9..5c05a633 100644
--- a/refstore/reftable/table.go
+++ b/refstore/reftable/table.go
@@ -71,49 +71,69 @@ func openTableFile(root *os.Root, name string, algo objectid.Algorithm) (*tableF
if err != nil {
return nil, err
}
+
info, err := file.Stat()
if err != nil {
_ = file.Close()
+
return nil, err
}
+
size := info.Size()
if size < 0 || size > int64(int(^uint(0)>>1)) {
_ = file.Close()
+
return nil, fmt.Errorf("refstore/reftable: table %q has unsupported size", name)
}
+
fd, err := intconv.UintptrToInt(file.Fd())
if err != nil {
_ = file.Close()
+
return nil, err
}
+
data, err := syscall.Mmap(fd, 0, int(size), syscall.PROT_READ, syscall.MAP_PRIVATE)
if err != nil {
_ = file.Close()
+
return nil, err
}
+
out := &tableFile{name: name, algo: algo, file: file, data: data}
- if err := out.parseMeta(); err != nil {
+
+ err = out.parseMeta()
+ if err != nil {
_ = out.close()
+
return nil, err
}
+
return out, nil
}
// close unmaps and closes one table file.
func (table *tableFile) close() error {
var closeErr error
+
if table.data != nil {
- if err := syscall.Munmap(table.data); err != nil && closeErr == nil {
+ err := syscall.Munmap(table.data)
+ if err != nil && closeErr == nil {
closeErr = err
}
+
table.data = nil
}
+
if table.file != nil {
- if err := table.file.Close(); err != nil && closeErr == nil {
+ err := table.file.Close()
+ if err != nil && closeErr == nil {
closeErr = err
}
+
table.file = nil
}
+
return closeErr
}
@@ -122,9 +142,11 @@ func (table *tableFile) parseMeta() error {
if len(table.data) < 24 {
return fmt.Errorf("refstore/reftable: table %q: file too short", table.name)
}
+
if string(table.data[:4]) != reftableMagic {
return fmt.Errorf("refstore/reftable: table %q: bad magic", table.name)
}
+
version := table.data[4]
switch version {
case version1:
@@ -137,35 +159,47 @@ func (table *tableFile) parseMeta() error {
if len(table.data) < table.headerLen {
return fmt.Errorf("refstore/reftable: table %q: truncated header", table.name)
}
+
hashID := binary.BigEndian.Uint32(table.data[24:28])
- if err := validateHashID(hashID, table.algo); err != nil {
+
+ err := validateHashID(hashID, table.algo)
+ if err != nil {
return fmt.Errorf("refstore/reftable: table %q: %w", table.name, err)
}
default:
return fmt.Errorf("refstore/reftable: table %q: unsupported version %d", table.name, version)
}
+
table.blockSize = int(readUint24(table.data[5:8]))
footerLen := 68
if version == version2 {
footerLen = 72
}
+
if len(table.data) < footerLen {
return fmt.Errorf("refstore/reftable: table %q: missing footer", table.name)
}
+
footerStart := len(table.data) - footerLen
+
footer := table.data[footerStart:]
if string(footer[:4]) != reftableMagic || footer[4] != version {
return fmt.Errorf("refstore/reftable: table %q: invalid footer header", table.name)
}
+
wantCRC := binary.BigEndian.Uint32(footer[footerLen-4:])
+
haveCRC := crc32.ChecksumIEEE(footer[:footerLen-4])
if wantCRC != haveCRC {
return fmt.Errorf("refstore/reftable: table %q: footer crc mismatch", table.name)
}
+
if version == version2 {
hashID := binary.BigEndian.Uint32(footer[24:28])
- if err := validateHashID(hashID, table.algo); err != nil {
+
+ err := validateHashID(hashID, table.algo)
+ if err != nil {
return fmt.Errorf("refstore/reftable: table %q: %w", table.name, err)
}
}
@@ -188,34 +222,44 @@ func (table *tableFile) parseMeta() error {
if err != nil {
return fmt.Errorf("refstore/reftable: table %q: invalid footer offset: %w", table.name, err)
}
+
if table.refIndexPos != 0 && table.refIndexPos < refEnd {
refEnd = table.refIndexPos
}
+
if objPos != 0 && objPos < refEnd {
refEnd = objPos
}
+
if logPos != 0 && logPos < refEnd {
refEnd = logPos
}
+
headerLenU64, err := intconv.IntToUint64(table.headerLen)
if err != nil {
return fmt.Errorf("refstore/reftable: table %q: invalid header length: %w", table.name, err)
}
+
dataLenU64, err := intconv.IntToUint64(len(table.data))
if err != nil {
return fmt.Errorf("refstore/reftable: table %q: invalid data length: %w", table.name, err)
}
+
if refEnd < headerLenU64 || refEnd > dataLenU64 {
return fmt.Errorf("refstore/reftable: table %q: invalid ref section", table.name)
}
+
if table.refIndexPos > dataLenU64 {
return fmt.Errorf("refstore/reftable: table %q: invalid ref index position", table.name)
}
+
refEndInt, err := intconv.Uint64ToInt(refEnd)
if err != nil {
return fmt.Errorf("refstore/reftable: table %q: invalid ref section end: %w", table.name, err)
}
+
table.refEnd = refEndInt
+
return nil
}
@@ -226,11 +270,13 @@ func validateHashID(hashID uint32, algo objectid.Algorithm) error {
if algo != objectid.AlgorithmSHA1 {
return errors.New("hash id sha1 mismatch")
}
+
return nil
case hashIDSHA256:
if algo != objectid.AlgorithmSHA256 {
return errors.New("hash id s256 mismatch")
}
+
return nil
default:
return fmt.Errorf("unknown hash id 0x%08x", hashID)
@@ -242,11 +288,14 @@ func (record recordValue) toRef(name string) (ref.Ref, error) {
if record.deleted {
return nil, errors.New("refstore/reftable: cannot materialize deleted record")
}
+
if record.symbolicTarget != "" {
return ref.Symbolic{RefName: name, Target: record.symbolicTarget}, nil
}
+
if !record.hasDetached {
return nil, errors.New("refstore/reftable: malformed detached record")
}
+
return ref.Detached{RefName: name, ID: record.detachedID, Peeled: record.peeled}, nil
}
diff --git a/refstore/shorten.go b/refstore/shorten.go
index 26fa82c0..250ab01f 100644
--- a/refstore/shorten.go
+++ b/refstore/shorten.go
@@ -20,17 +20,22 @@ func (rule shortenRule) match(name string) (string, bool) {
if !strings.HasPrefix(name, rule.prefix) {
return "", false
}
+
if !strings.HasSuffix(name, rule.suffix) {
return "", false
}
+
short := strings.TrimPrefix(name, rule.prefix)
+
short = strings.TrimSuffix(short, rule.suffix)
if short == "" {
return "", false
}
+
if rule.prefix+short+rule.suffix != name {
return "", false
}
+
return short, true
}
@@ -47,6 +52,7 @@ func ShortenName(name string, all []string) string {
if full == "" {
continue
}
+
names[full] = struct{}{}
}
@@ -55,20 +61,26 @@ func ShortenName(name string, all []string) string {
if !ok {
continue
}
+
ambiguous := false
+
for j := range shortenRules {
if j == i {
continue
}
+
full := shortenRules[j].render(short)
if _, found := names[full]; found {
ambiguous = true
+
break
}
}
+
if !ambiguous {
return short
}
}
+
return name
}
diff --git a/refstore/shorten_test.go b/refstore/shorten_test.go
index 53e7e003..a4d91453 100644
--- a/refstore/shorten_test.go
+++ b/refstore/shorten_test.go
@@ -11,6 +11,7 @@ func TestShortenName(t *testing.T) {
t.Run("simple", func(t *testing.T) {
t.Parallel()
+
got := refstore.ShortenName("refs/heads/main", []string{"refs/heads/main"})
if got != "main" {
t.Fatalf("ShortenName simple = %q, want %q", got, "main")
@@ -19,6 +20,7 @@ func TestShortenName(t *testing.T) {
t.Run("ambiguous with tags", func(t *testing.T) {
t.Parallel()
+
got := refstore.ShortenName(
"refs/heads/main",
[]string{
@@ -64,7 +66,9 @@ func TestShortenName(t *testing.T) {
t.Run("refs-prefix fallback", func(t *testing.T) {
t.Parallel()
+
name := "refs/notes/review/topic"
+
got := refstore.ShortenName(name, []string{name})
if got != "notes/review/topic" {
t.Fatalf("ShortenName refs-prefix fallback = %q, want %q", got, "notes/review/topic")