Fast implementation of Git in pure Go codeberg.org/lindenii/furgit
git go
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

refstore/packed: Add packed refs backend

Runxi Yu c2b2f7f5 607bcd7a

+381
+1
refstore/packed/TODO
··· 1 + Make ref name and parse-line validations stricter.
+176
refstore/packed/packed_test.go
··· 1 + package packed_test 2 + 3 + import ( 4 + "bytes" 5 + "errors" 6 + "os" 7 + "path/filepath" 8 + "slices" 9 + "testing" 10 + 11 + "codeberg.org/lindenii/furgit/internal/testgit" 12 + "codeberg.org/lindenii/furgit/objectid" 13 + "codeberg.org/lindenii/furgit/ref" 14 + "codeberg.org/lindenii/furgit/refstore" 15 + "codeberg.org/lindenii/furgit/refstore/packed" 16 + ) 17 + 18 + func openPackedRefStoreFromRepo(t *testing.T, repoPath string, algo objectid.Algorithm) *packed.Store { 19 + t.Helper() 20 + file, err := os.Open(filepath.Join(repoPath, "packed-refs")) 21 + if err != nil { 22 + t.Fatalf("open packed-refs: %v", err) 23 + } 24 + defer func() { _ = file.Close() }() 25 + 26 + store, err := packed.New(file, algo) 27 + if err != nil { 28 + t.Fatalf("packed.New: %v", err) 29 + } 30 + return store 31 + } 32 + 33 + func TestPackedResolveAndPeeled(t *testing.T) { 34 + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { 35 + testRepo := testgit.NewBareRepo(t, algo) 36 + _, _, commitID := testRepo.MakeCommit(t, "packed refs commit") 37 + testRepo.UpdateRef(t, "refs/heads/main", commitID) 38 + tagID := testRepo.TagAnnotated(t, "v1.0.0", commitID, "annotated tag") 39 + testRepo.PackRefs(t, "--all", "--prune") 40 + 41 + store := openPackedRefStoreFromRepo(t, testRepo.Dir(), algo) 42 + 43 + resolvedMain, err := store.Resolve("refs/heads/main") 44 + if err != nil { 45 + t.Fatalf("Resolve(main): %v", err) 46 + } 47 + mainDet, ok := resolvedMain.(ref.Detached) 48 + if !ok { 49 + t.Fatalf("Resolve(main) type = %T, want ref.Detached", resolvedMain) 50 + } 51 + if mainDet.ID != commitID { 52 + t.Fatalf("Resolve(main) id = %s, want %s", mainDet.ID, commitID) 53 + } 54 + 55 + resolvedTag, err := store.Resolve("refs/tags/v1.0.0") 56 + if err != nil { 57 + t.Fatalf("Resolve(tag): %v", err) 58 + } 59 + tagDet, ok := resolvedTag.(ref.Detached) 60 + if !ok { 61 + t.Fatalf("Resolve(tag) type = %T, want ref.Detached", resolvedTag) 62 + } 63 + if tagDet.ID != tagID { 64 + t.Fatalf("Resolve(tag) id = %s, want %s", tagDet.ID, tagID) 65 + } 66 + if tagDet.Peeled == nil { 67 + t.Fatalf("Resolve(tag) peeled = nil, want commit") 68 + } 69 + if *tagDet.Peeled != commitID { 70 + t.Fatalf("Resolve(tag) peeled = %s, want %s", *tagDet.Peeled, commitID) 71 + } 72 + 73 + fullTag, err := store.ResolveFully("refs/tags/v1.0.0") 74 + if err != nil { 75 + t.Fatalf("ResolveFully(tag): %v", err) 76 + } 77 + if fullTag.ID != tagDet.ID { 78 + t.Fatalf("ResolveFully(tag) id = %s, want %s", fullTag.ID, tagDet.ID) 79 + } 80 + 81 + if _, err := store.Resolve("refs/heads/does-not-exist"); !errors.Is(err, refstore.ErrReferenceNotFound) { 82 + t.Fatalf("Resolve(not-found) error = %v", err) 83 + } 84 + }) 85 + } 86 + 87 + func TestPackedListAndShorten(t *testing.T) { 88 + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { 89 + testRepo := testgit.NewBareRepo(t, algo) 90 + _, _, commitID := testRepo.MakeCommit(t, "packed refs list commit") 91 + testRepo.UpdateRef(t, "refs/heads/main", commitID) 92 + testRepo.UpdateRef(t, "refs/tags/main", commitID) 93 + testRepo.UpdateRef(t, "refs/remotes/origin/main", commitID) 94 + testRepo.PackRefs(t, "--all", "--prune") 95 + 96 + store := openPackedRefStoreFromRepo(t, testRepo.Dir(), algo) 97 + 98 + all, err := store.List("") 99 + if err != nil { 100 + t.Fatalf("List(all): %v", err) 101 + } 102 + allNames := make([]string, 0, len(all)) 103 + for _, entry := range all { 104 + allNames = append(allNames, entry.Name()) 105 + } 106 + slices.Sort(allNames) 107 + wantAll := []string{"refs/heads/main", "refs/remotes/origin/main", "refs/tags/main"} 108 + if !slices.Equal(allNames, wantAll) { 109 + t.Fatalf("List(all) names = %v, want %v", allNames, wantAll) 110 + } 111 + 112 + filtered, err := store.List("refs/heads/*") 113 + if err != nil { 114 + t.Fatalf("List(pattern): %v", err) 115 + } 116 + if len(filtered) != 1 || filtered[0].Name() != "refs/heads/main" { 117 + t.Fatalf("List(refs/heads/*) = %v, want refs/heads/main only", filtered) 118 + } 119 + 120 + short, err := store.Shorten("refs/heads/main") 121 + if err != nil { 122 + t.Fatalf("Shorten(main): %v", err) 123 + } 124 + if short != "heads/main" { 125 + t.Fatalf("Shorten(main) = %q, want %q", short, "heads/main") 126 + } 127 + 128 + if _, err := store.Shorten("refs/heads/does-not-exist"); !errors.Is(err, refstore.ErrReferenceNotFound) { 129 + t.Fatalf("Shorten(not-found) error = %v", err) 130 + } 131 + }) 132 + } 133 + 134 + func TestPackedParseErrors(t *testing.T) { 135 + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { 136 + cases := []struct { 137 + name string 138 + data string 139 + }{ 140 + { 141 + name: "peeled without ref", 142 + data: "^" + stringsOfLen("0", algo.HexLen()) + "\n", 143 + }, 144 + { 145 + name: "invalid entry", 146 + data: "not-a-valid-line\n", 147 + }, 148 + { 149 + name: "duplicate ref", 150 + data: stringsOfLen("0", algo.HexLen()) + " refs/heads/main\n" + 151 + stringsOfLen("1", algo.HexLen()) + " refs/heads/main\n", 152 + }, 153 + } 154 + 155 + for _, tt := range cases { 156 + t.Run(tt.name, func(t *testing.T) { 157 + if _, err := packed.New(bytes.NewBufferString(tt.data), algo); err == nil { 158 + t.Fatalf("packed.New expected parse error") 159 + } 160 + }) 161 + } 162 + }) 163 + } 164 + 165 + func TestPackedNewValidation(t *testing.T) { 166 + if _, err := packed.New(bytes.NewReader(nil), objectid.AlgorithmUnknown); !errors.Is(err, objectid.ErrInvalidAlgorithm) { 167 + t.Fatalf("packed.New invalid algorithm error = %v", err) 168 + } 169 + if _, err := packed.New(nil, objectid.AlgorithmSHA1); err == nil { 170 + t.Fatalf("packed.New nil reader expected error") 171 + } 172 + } 173 + 174 + func stringsOfLen(ch string, n int) string { 175 + return string(bytes.Repeat([]byte(ch), n)) 176 + }
+100
refstore/packed/parse.go
··· 1 + package packed 2 + 3 + import ( 4 + "bufio" 5 + "fmt" 6 + "io" 7 + "strings" 8 + 9 + "codeberg.org/lindenii/furgit/objectid" 10 + "codeberg.org/lindenii/furgit/ref" 11 + ) 12 + 13 + // parsePackedRefs parses packed-refs content into detached refs. 14 + func parsePackedRefs(r io.Reader, algo objectid.Algorithm) (map[string]ref.Detached, []ref.Detached, error) { 15 + byName := make(map[string]ref.Detached) 16 + ordered := make([]ref.Detached, 0, 32) 17 + 18 + br := bufio.NewReader(r) 19 + prev := -1 20 + lineNum := 0 21 + 22 + for { 23 + line, err := br.ReadString('\n') 24 + if err != nil && err != io.EOF { 25 + return nil, nil, err 26 + } 27 + if line == "" && err == io.EOF { 28 + break 29 + } 30 + lineNum++ 31 + 32 + line = strings.TrimSuffix(line, "\n") 33 + line = strings.TrimSuffix(line, "\r") 34 + line = strings.TrimSpace(line) 35 + if line == "" { 36 + if err == io.EOF { 37 + break 38 + } 39 + continue 40 + } 41 + if strings.HasPrefix(line, "#") { 42 + if err == io.EOF { 43 + break 44 + } 45 + continue 46 + } 47 + 48 + if strings.HasPrefix(line, "^") { 49 + if prev < 0 { 50 + return nil, nil, fmt.Errorf("refstore/packed: line %d: peeled line without preceding ref", lineNum) 51 + } 52 + peeledHex := strings.TrimSpace(strings.TrimPrefix(line, "^")) 53 + peeled, parseErr := objectid.ParseHex(algo, peeledHex) 54 + if parseErr != nil { 55 + return nil, nil, fmt.Errorf("refstore/packed: line %d: invalid peeled oid: %w", lineNum, parseErr) 56 + } 57 + peeledCopy := peeled 58 + cur := ordered[prev] 59 + cur.Peeled = &peeledCopy 60 + ordered[prev] = cur 61 + byName[cur.Name()] = cur 62 + if err == io.EOF { 63 + break 64 + } 65 + continue 66 + } 67 + 68 + fields := strings.Fields(line) 69 + if len(fields) != 2 { 70 + return nil, nil, fmt.Errorf("refstore/packed: line %d: malformed entry", lineNum) 71 + } 72 + 73 + id, parseErr := objectid.ParseHex(algo, fields[0]) 74 + if parseErr != nil { 75 + return nil, nil, fmt.Errorf("refstore/packed: line %d: invalid oid: %w", lineNum, parseErr) 76 + } 77 + 78 + name := fields[1] 79 + if name == "" { 80 + return nil, nil, fmt.Errorf("refstore/packed: line %d: empty ref name", lineNum) 81 + } 82 + if _, exists := byName[name]; exists { 83 + return nil, nil, fmt.Errorf("refstore/packed: line %d: duplicate ref %q", lineNum, name) 84 + } 85 + 86 + detached := ref.Detached{ 87 + RefName: name, 88 + ID: id, 89 + } 90 + ordered = append(ordered, detached) 91 + prev = len(ordered) - 1 92 + byName[name] = detached 93 + 94 + if err == io.EOF { 95 + break 96 + } 97 + } 98 + 99 + return byName, ordered, nil 100 + }
+104
refstore/packed/store.go
··· 1 + // Package packed provides read access to packed Git references. 2 + package packed 3 + 4 + import ( 5 + "io" 6 + "path" 7 + 8 + "codeberg.org/lindenii/furgit/objectid" 9 + "codeberg.org/lindenii/furgit/ref" 10 + "codeberg.org/lindenii/furgit/refstore" 11 + ) 12 + 13 + // Store reads references from a parsed packed-refs snapshot. 14 + type Store struct { 15 + byName map[string]ref.Detached 16 + ordered []ref.Detached 17 + } 18 + 19 + var _ refstore.Store = (*Store)(nil) 20 + 21 + // New parses packed-refs content from r using the given object ID algorithm. 22 + func New(r io.Reader, algo objectid.Algorithm) (*Store, error) { 23 + if algo.Size() == 0 { 24 + return nil, objectid.ErrInvalidAlgorithm 25 + } 26 + if r == nil { 27 + return nil, io.ErrUnexpectedEOF 28 + } 29 + byName, ordered, err := parsePackedRefs(r, algo) 30 + if err != nil { 31 + return nil, err 32 + } 33 + return &Store{ 34 + byName: byName, 35 + ordered: ordered, 36 + }, nil 37 + } 38 + 39 + // Resolve resolves a packed reference name to a detached ref. 40 + func (store *Store) Resolve(name string) (ref.Ref, error) { 41 + detached, ok := store.byName[name] 42 + if !ok { 43 + return nil, refstore.ErrReferenceNotFound 44 + } 45 + return detached, nil 46 + } 47 + 48 + // ResolveFully resolves a packed reference name to a detached ref. 49 + // 50 + // Packed refs are detached-only, so ResolveFully is equivalent to Resolve. 51 + func (store *Store) ResolveFully(name string) (ref.Detached, error) { 52 + detached, ok := store.byName[name] 53 + if !ok { 54 + return ref.Detached{}, refstore.ErrReferenceNotFound 55 + } 56 + return detached, nil 57 + } 58 + 59 + // List lists packed references matching pattern. 60 + // 61 + // Pattern uses path.Match syntax against full reference names. 62 + // Empty pattern matches all references. 63 + func (store *Store) List(pattern string) ([]ref.Ref, error) { 64 + matchAll := pattern == "" 65 + if !matchAll { 66 + if _, err := path.Match(pattern, "refs/heads/main"); err != nil { 67 + return nil, err 68 + } 69 + } 70 + 71 + refs := make([]ref.Ref, 0, len(store.ordered)) 72 + for _, entry := range store.ordered { 73 + if !matchAll { 74 + matched, err := path.Match(pattern, entry.Name()) 75 + if err != nil { 76 + return nil, err 77 + } 78 + if !matched { 79 + continue 80 + } 81 + } 82 + refs = append(refs, entry) 83 + } 84 + return refs, nil 85 + } 86 + 87 + // Shorten returns the shortest unambiguous shorthand for a packed ref name. 88 + func (store *Store) Shorten(name string) (string, error) { 89 + _, ok := store.byName[name] 90 + if !ok { 91 + return "", refstore.ErrReferenceNotFound 92 + } 93 + 94 + names := make([]string, 0, len(store.ordered)) 95 + for _, entry := range store.ordered { 96 + names = append(names, entry.Name()) 97 + } 98 + return refstore.ShortenName(name, names), nil 99 + } 100 + 101 + // Close releases resources associated with the backend. 102 + func (store *Store) Close() error { 103 + return nil 104 + }