diff options
| author | 2026-03-04 08:26:56 +0800 | |
|---|---|---|
| committer | 2026-03-04 08:59:53 +0800 | |
| commit | ab7501be34032fb9e5c48726a68ae90a917af9eb (patch) | |
| tree | 20d005647569befea8133e953c3270e8fd2a2a5b /refstore | |
| parent | *: gofumpt (diff) | |
| signature | No signature | |
*: Lint
Diffstat (limited to 'refstore')
| -rw-r--r-- | refstore/chain/chain.go | 25 | ||||
| -rw-r--r-- | refstore/loose/list.go | 26 | ||||
| -rw-r--r-- | refstore/loose/loose_test.go | 43 | ||||
| -rw-r--r-- | refstore/loose/resolve.go | 11 | ||||
| -rw-r--r-- | refstore/loose/shorten.go | 6 | ||||
| -rw-r--r-- | refstore/loose/store.go | 1 | ||||
| -rw-r--r-- | refstore/packed/packed_test.go | 45 | ||||
| -rw-r--r-- | refstore/packed/parse.go | 12 | ||||
| -rw-r--r-- | refstore/packed/store.go | 12 | ||||
| -rw-r--r-- | refstore/reftable/lookup.go | 108 | ||||
| -rw-r--r-- | refstore/reftable/parse_helpers.go | 5 | ||||
| -rw-r--r-- | refstore/reftable/reftable_test.go | 31 | ||||
| -rw-r--r-- | refstore/reftable/store.go | 64 | ||||
| -rw-r--r-- | refstore/reftable/table.go | 59 | ||||
| -rw-r--r-- | refstore/shorten.go | 12 | ||||
| -rw-r--r-- | refstore/shorten_test.go | 4 |
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") |
