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/reftable: Add basic implementation

Runxi Yu 680d30bd bda11a7b

+1159
+22
refstore/reftable/TODO
··· 1 + * One-time load -> immutable snapshots and such; publish only fully loaded 2 + snapshots. 3 + * Reload behavior for tables.list; read tables.list, open all listed tables, 4 + and retry from the start if any listed table is missing during open. 5 + * Snapshot staleness detection 6 + * Resolve's block search should be restart aware. Then the linear scan is only 7 + within the chosen restart window. 8 + * Cache parsed index-block metadata per table and block offset. Reuse restart 9 + offsets and decoded key boundaries across lookups to try to reduce repeated 10 + parsing. 11 + * Pin one snapshot for each high-level read call. ResolveFully should resolve 12 + all symbolic hops against one snapshot and avoid repeated ensure/reload 13 + checks per hop. 14 + * Add lazy snapshot-derived caches for list-heavy operations. Build visible 15 + merged refs and sorted names once per snapshot, and reuse for List and 16 + Shorten. 17 + * Make format parser stricter to match Git-level behavior. Validate unaligned 18 + multi-ref-block tables require ref index, and enforce more precise 19 + first-block/restart invariants where applicable. 20 + * Improve parse and lookup diagnostics with contextual errors. Return 21 + fmt.Errorf values that include table name, block offset, and record offset, 22 + where possible.
+424
refstore/reftable/lookup.go
··· 1 + package reftable 2 + 3 + import ( 4 + "encoding/binary" 5 + "fmt" 6 + "strings" 7 + 8 + "codeberg.org/lindenii/furgit/objectid" 9 + ) 10 + 11 + // resolveRecord resolves one ref name inside a single table file. 12 + func (table *tableFile) resolveRecord(name string) (recordValue, bool, error) { 13 + if table.refIndexPos != 0 { 14 + pos, ok, err := table.resolveRefBlockPosFromIndex(name, int(table.refIndexPos)) 15 + if err != nil { 16 + return recordValue{}, false, err 17 + } 18 + if !ok { 19 + return recordValue{}, false, nil 20 + } 21 + return table.lookupInRefBlock(name, pos) 22 + } 23 + 24 + // Without a ref index, fall back to scanning ref blocks in order. 25 + pos := table.headerLen 26 + for pos < table.refEnd { 27 + for pos < table.refEnd && table.data[pos] == 0 { 28 + pos++ 29 + } 30 + if pos >= table.refEnd { 31 + break 32 + } 33 + if table.data[pos] != blockTypeRef { 34 + return recordValue{}, false, fmt.Errorf("refstore/reftable: table %q: unexpected block type %q in ref section", table.name, table.data[pos]) 35 + } 36 + block, blockEnd, err := table.readBlockAt(pos) 37 + if err != nil { 38 + return recordValue{}, false, err 39 + } 40 + found, done, rec, err := lookupRecordInRefBlock(table, block, name) 41 + if err != nil { 42 + return recordValue{}, false, err 43 + } 44 + if found { 45 + return rec, true, nil 46 + } 47 + if done { 48 + return recordValue{}, false, nil 49 + } 50 + pos = table.nextBlockPos(blockEnd) 51 + } 52 + return recordValue{}, false, nil 53 + } 54 + 55 + // resolveRefBlockPosFromIndex resolves a candidate ref block position via index blocks. 56 + func (table *tableFile) resolveRefBlockPosFromIndex(name string, indexPos int) (int, bool, error) { 57 + block, _, err := table.readBlockAt(indexPos) 58 + if err != nil { 59 + return 0, false, err 60 + } 61 + if block.blockType != blockTypeIndex { 62 + return 0, false, fmt.Errorf("refstore/reftable: table %q: ref index root is not index block", table.name) 63 + } 64 + childPos, ok, err := lookupChildPosInIndexBlock(block, name) 65 + if err != nil { 66 + return 0, false, err 67 + } 68 + if !ok { 69 + return 0, false, nil 70 + } 71 + if childPos < 0 || childPos >= len(table.data) { 72 + return 0, false, fmt.Errorf("refstore/reftable: table %q: index child position out of range", table.name) 73 + } 74 + 75 + childType := table.data[childPos] 76 + switch childType { 77 + case blockTypeRef: 78 + return childPos, true, nil 79 + case blockTypeIndex: 80 + return table.resolveRefBlockPosFromIndex(name, childPos) 81 + default: 82 + return 0, false, fmt.Errorf("refstore/reftable: table %q: unexpected child block type %q", table.name, childType) 83 + } 84 + } 85 + 86 + // lookupInRefBlock searches one ref block by full ref name. 87 + func (table *tableFile) lookupInRefBlock(name string, pos int) (recordValue, bool, error) { 88 + block, _, err := table.readBlockAt(pos) 89 + if err != nil { 90 + return recordValue{}, false, err 91 + } 92 + if block.blockType != blockTypeRef { 93 + return recordValue{}, false, fmt.Errorf("refstore/reftable: table %q: expected ref block at %d", table.name, pos) 94 + } 95 + found, _, rec, err := lookupRecordInRefBlock(table, block, name) 96 + if err != nil { 97 + return recordValue{}, false, err 98 + } 99 + return rec, found, nil 100 + } 101 + 102 + // forEachRecord iterates all ref records in this table in lexical order. 103 + func (table *tableFile) forEachRecord(fn func(name string, rec recordValue) error) error { 104 + pos := table.headerLen 105 + prevLast := "" 106 + for pos < table.refEnd { 107 + for pos < table.refEnd && table.data[pos] == 0 { 108 + pos++ 109 + } 110 + if pos >= table.refEnd { 111 + break 112 + } 113 + if table.data[pos] != blockTypeRef { 114 + return fmt.Errorf("refstore/reftable: table %q: unexpected block type %q in ref section", table.name, table.data[pos]) 115 + } 116 + 117 + block, blockEnd, err := table.readBlockAt(pos) 118 + if err != nil { 119 + return err 120 + } 121 + var first, last string 122 + err = forEachRecordInRefBlock(table, block, func(name string, rec recordValue) error { 123 + if first == "" { 124 + first = name 125 + } 126 + last = name 127 + return fn(name, rec) 128 + }) 129 + if err != nil { 130 + return err 131 + } 132 + if prevLast != "" && first != "" && strings.Compare(first, prevLast) <= 0 { 133 + return fmt.Errorf("refstore/reftable: table %q: ref blocks are not strictly ordered", table.name) 134 + } 135 + if last != "" { 136 + prevLast = last 137 + } 138 + pos = table.nextBlockPos(blockEnd) 139 + } 140 + return nil 141 + } 142 + 143 + // blockView is one decoded block boundary within the mapped table bytes. 144 + type blockView struct { 145 + blockType byte 146 + start int 147 + end int 148 + first bool 149 + payload []byte 150 + } 151 + 152 + // readBlockAt validates and returns a block view starting at pos. 153 + func (table *tableFile) readBlockAt(pos int) (blockView, int, error) { 154 + if pos < 0 || pos+4 > len(table.data) { 155 + return blockView{}, 0, fmt.Errorf("refstore/reftable: table %q: block header out of range", table.name) 156 + } 157 + blockLen := int(readUint24(table.data[pos+1 : pos+4])) 158 + effectiveLen := blockLen 159 + if pos == table.headerLen { 160 + if blockLen < table.headerLen { 161 + return blockView{}, 0, fmt.Errorf("refstore/reftable: table %q: invalid first block length", table.name) 162 + } 163 + effectiveLen = blockLen - table.headerLen 164 + } 165 + if effectiveLen < 4 { 166 + return blockView{}, 0, fmt.Errorf("refstore/reftable: table %q: invalid block length", table.name) 167 + } 168 + end := pos + effectiveLen 169 + if end > len(table.data) { 170 + return blockView{}, 0, fmt.Errorf("refstore/reftable: table %q: block out of range", table.name) 171 + } 172 + view := blockView{blockType: table.data[pos], start: pos, end: end, first: pos == table.headerLen, payload: table.data[pos:end]} 173 + return view, end, nil 174 + } 175 + 176 + // nextBlockPos computes the next block start from current block end. 177 + func (table *tableFile) nextBlockPos(blockEnd int) int { 178 + if table.blockSize > 0 { 179 + return alignUp(blockEnd, table.blockSize) 180 + } 181 + return blockEnd 182 + } 183 + 184 + // lookupChildPosInIndexBlock selects a child block position for key. 185 + func lookupChildPosInIndexBlock(block blockView, key string) (int, bool, error) { 186 + off, recordsEnd, restarts, err := parseBlockLayout(block) 187 + if err != nil { 188 + return 0, false, err 189 + } 190 + if err := validateRestarts(block, restarts, off, recordsEnd, true); err != nil { 191 + return 0, false, err 192 + } 193 + prev := "" 194 + for off < recordsEnd { 195 + name, v, nextOff, err := parseKeyedRecord(block.payload, off, recordsEnd, prev) 196 + if err != nil { 197 + return 0, false, err 198 + } 199 + if (v & 0x7) != 0 { 200 + return 0, false, fmt.Errorf("index value_type must be 0") 201 + } 202 + childPos, nextOff, err := readVarint(block.payload, nextOff, recordsEnd) 203 + if err != nil { 204 + return 0, false, err 205 + } 206 + if strings.Compare(key, name) <= 0 { 207 + if childPos > uint64(int(^uint(0)>>1)) { 208 + return 0, false, fmt.Errorf("index child position overflows int") 209 + } 210 + return int(childPos), true, nil 211 + } 212 + prev = name 213 + off = nextOff 214 + } 215 + if off != recordsEnd { 216 + return 0, false, fmt.Errorf("malformed index block") 217 + } 218 + return 0, false, nil 219 + } 220 + 221 + // lookupRecordInRefBlock searches one ref block and may short-circuit by sort order. 222 + func lookupRecordInRefBlock(table *tableFile, block blockView, key string) (found bool, done bool, rec recordValue, err error) { 223 + off, recordsEnd, restarts, err := parseBlockLayout(block) 224 + if err != nil { 225 + return false, false, recordValue{}, err 226 + } 227 + if err := validateRestarts(block, restarts, off, recordsEnd, true); err != nil { 228 + return false, false, recordValue{}, err 229 + } 230 + prev := "" 231 + for off < recordsEnd { 232 + name, v, nextOff, err := parseKeyedRecord(block.payload, off, recordsEnd, prev) 233 + if err != nil { 234 + return false, false, recordValue{}, err 235 + } 236 + typeBits := byte(v & 0x7) 237 + _, nextOff, err = readVarint(block.payload, nextOff, recordsEnd) 238 + if err != nil { 239 + return false, false, recordValue{}, err 240 + } 241 + recVal, nextOff, err := parseRefValue(block.payload, nextOff, recordsEnd, table.algo, typeBits) 242 + if err != nil { 243 + return false, false, recordValue{}, err 244 + } 245 + cmp := strings.Compare(name, key) 246 + if cmp == 0 { 247 + return true, true, recVal, nil 248 + } 249 + if cmp > 0 { 250 + return false, true, recordValue{}, nil 251 + } 252 + prev = name 253 + off = nextOff 254 + } 255 + if off != recordsEnd { 256 + return false, false, recordValue{}, fmt.Errorf("malformed ref block") 257 + } 258 + return false, false, recordValue{}, nil 259 + } 260 + 261 + // forEachRecordInRefBlock iterates all records in one ref block. 262 + func forEachRecordInRefBlock(table *tableFile, block blockView, fn func(name string, rec recordValue) error) error { 263 + off, recordsEnd, restarts, err := parseBlockLayout(block) 264 + if err != nil { 265 + return err 266 + } 267 + if err := validateRestarts(block, restarts, off, recordsEnd, true); err != nil { 268 + return err 269 + } 270 + prev := "" 271 + for off < recordsEnd { 272 + name, v, nextOff, err := parseKeyedRecord(block.payload, off, recordsEnd, prev) 273 + if err != nil { 274 + return err 275 + } 276 + typeBits := byte(v & 0x7) 277 + _, nextOff, err = readVarint(block.payload, nextOff, recordsEnd) 278 + if err != nil { 279 + return err 280 + } 281 + recVal, nextOff, err := parseRefValue(block.payload, nextOff, recordsEnd, table.algo, typeBits) 282 + if err != nil { 283 + return err 284 + } 285 + if err := fn(name, recVal); err != nil { 286 + return err 287 + } 288 + prev = name 289 + off = nextOff 290 + } 291 + if off != recordsEnd { 292 + return fmt.Errorf("malformed ref block") 293 + } 294 + return nil 295 + } 296 + 297 + // parseBlockLayout parses common record/restart regions for ref and index blocks. 298 + func parseBlockLayout(block blockView) (recordsStart int, recordsEnd int, restarts []int, err error) { 299 + if len(block.payload) < 6 { 300 + return 0, 0, nil, fmt.Errorf("short block") 301 + } 302 + restartCount := int(binary.BigEndian.Uint16(block.payload[len(block.payload)-2:])) 303 + if restartCount <= 0 { 304 + return 0, 0, nil, fmt.Errorf("invalid restart count") 305 + } 306 + restarts = make([]int, restartCount) 307 + restartBytes := restartCount * 3 308 + restartsStart := len(block.payload) - 2 - restartBytes 309 + if restartsStart < 4 { 310 + return 0, 0, nil, fmt.Errorf("invalid restart table") 311 + } 312 + for i := 0; i < restartCount; i++ { 313 + off := restartsStart + i*3 314 + rel := int(readUint24(block.payload[off : off+3])) 315 + base := block.start 316 + if block.first { 317 + // In the first block, restart offsets are relative to file start. 318 + base = 0 319 + } 320 + abs := base + rel 321 + restarts[i] = abs - block.start 322 + } 323 + return 4, restartsStart, restarts, nil 324 + } 325 + 326 + // validateRestarts validates restart monotonicity, bounds and record-prefix invariants. 327 + func validateRestarts(block blockView, restarts []int, recordsStart, recordsEnd int, requirePrefixZero bool) error { 328 + prev := -1 329 + for _, off := range restarts { 330 + if off < recordsStart || off >= recordsEnd { 331 + return fmt.Errorf("restart offset out of range") 332 + } 333 + if off <= prev { 334 + return fmt.Errorf("restart offsets not strictly increasing") 335 + } 336 + prev = off 337 + if requirePrefixZero { 338 + prefix, _, err := readVarint(block.payload, off, recordsEnd) 339 + if err != nil { 340 + return err 341 + } 342 + if prefix != 0 { 343 + return fmt.Errorf("restart record prefix length must be zero") 344 + } 345 + } 346 + } 347 + return nil 348 + } 349 + 350 + // parseKeyedRecord parses one prefix-compressed key record header. 351 + func parseKeyedRecord(buf []byte, off, end int, prev string) (name string, rawType uint64, next int, err error) { 352 + prefixLen, next, err := readVarint(buf, off, end) 353 + if err != nil { 354 + return "", 0, 0, err 355 + } 356 + suffixAndType, next, err := readVarint(buf, next, end) 357 + if err != nil { 358 + return "", 0, 0, err 359 + } 360 + suffixLen := int(suffixAndType >> 3) 361 + if suffixLen < 0 || next+suffixLen > end { 362 + return "", 0, 0, fmt.Errorf("invalid suffix length") 363 + } 364 + if int(prefixLen) > len(prev) { 365 + return "", 0, 0, fmt.Errorf("invalid prefix length") 366 + } 367 + name = prev[:prefixLen] + string(buf[next:next+suffixLen]) 368 + next += suffixLen 369 + if prev != "" && strings.Compare(name, prev) <= 0 { 370 + return "", 0, 0, fmt.Errorf("keys not strictly increasing") 371 + } 372 + return name, suffixAndType, next, nil 373 + } 374 + 375 + // parseRefValue parses one ref-record value payload according to value_type. 376 + func parseRefValue(buf []byte, off, end int, algo objectid.Algorithm, valueType byte) (recordValue, int, error) { 377 + switch valueType { 378 + case 0x0: 379 + return recordValue{deleted: true}, off, nil 380 + case 0x1: 381 + id, next, err := readObjectID(buf, off, end, algo) 382 + if err != nil { 383 + return recordValue{}, 0, err 384 + } 385 + return recordValue{detachedID: id, hasDetached: true}, next, nil 386 + case 0x2: 387 + id, next, err := readObjectID(buf, off, end, algo) 388 + if err != nil { 389 + return recordValue{}, 0, err 390 + } 391 + peeled, next, err := readObjectID(buf, next, end, algo) 392 + if err != nil { 393 + return recordValue{}, 0, err 394 + } 395 + peeledCopy := peeled 396 + return recordValue{detachedID: id, hasDetached: true, peeled: &peeledCopy}, next, nil 397 + case 0x3: 398 + targetLen, next, err := readVarint(buf, off, end) 399 + if err != nil { 400 + return recordValue{}, 0, err 401 + } 402 + if targetLen > uint64(end-next) { 403 + return recordValue{}, 0, fmt.Errorf("invalid symref target length") 404 + } 405 + target := string(buf[next : next+int(targetLen)]) 406 + next += int(targetLen) 407 + return recordValue{symbolicTarget: target}, next, nil 408 + default: 409 + return recordValue{}, 0, fmt.Errorf("unsupported ref value type %d", valueType) 410 + } 411 + } 412 + 413 + // readObjectID reads one object ID using the table algorithm width. 414 + func readObjectID(buf []byte, off, end int, algo objectid.Algorithm) (objectid.ObjectID, int, error) { 415 + sz := algo.Size() 416 + if off < 0 || sz < 0 || off+sz > end { 417 + return objectid.ObjectID{}, 0, fmt.Errorf("truncated object id") 418 + } 419 + id, err := objectid.FromBytes(algo, buf[off:off+sz]) 420 + if err != nil { 421 + return objectid.ObjectID{}, 0, err 422 + } 423 + return id, off + sz, nil 424 + }
+36
refstore/reftable/parse_helpers.go
··· 1 + package reftable 2 + 3 + import "fmt" 4 + 5 + // readUint24 reads a 24-bit big-endian unsigned integer. 6 + func readUint24(b []byte) uint32 { 7 + return uint32(b[0])<<16 | uint32(b[1])<<8 | uint32(b[2]) 8 + } 9 + 10 + // alignUp rounds pos up to the next multiple of blockSize. 11 + func alignUp(pos, blockSize int) int { 12 + rem := pos % blockSize 13 + if rem == 0 { 14 + return pos 15 + } 16 + return pos + (blockSize - rem) 17 + } 18 + 19 + // readVarint decodes one reftable/ofs-delta style varint. 20 + func readVarint(buf []byte, off, end int) (uint64, int, error) { 21 + if off >= end { 22 + return 0, 0, fmt.Errorf("unexpected EOF") 23 + } 24 + b := buf[off] 25 + val := uint64(b & 0x7f) 26 + off++ 27 + for b&0x80 != 0 { 28 + if off >= end { 29 + return 0, 0, fmt.Errorf("unexpected EOF") 30 + } 31 + b = buf[off] 32 + off++ 33 + val = ((val + 1) << 7) | uint64(b&0x7f) 34 + } 35 + return val, off, nil 36 + }
+8
refstore/reftable/path.go
··· 1 + package reftable 2 + 3 + import "path" 4 + 5 + // pathMatch applies path.Match to full ref names. 6 + func pathMatch(pattern, name string) (bool, error) { 7 + return path.Match(pattern, name) 8 + }
+172
refstore/reftable/reftable_test.go
··· 1 + package reftable_test 2 + 3 + import ( 4 + "errors" 5 + "os" 6 + "path/filepath" 7 + "slices" 8 + "testing" 9 + 10 + "codeberg.org/lindenii/furgit/internal/testgit" 11 + "codeberg.org/lindenii/furgit/objectid" 12 + "codeberg.org/lindenii/furgit/ref" 13 + "codeberg.org/lindenii/furgit/refstore" 14 + "codeberg.org/lindenii/furgit/refstore/reftable" 15 + ) 16 + 17 + // newBareReftableRepo creates a bare repository that uses reftable ref storage. 18 + func newBareReftableRepo(tb testing.TB, algo objectid.Algorithm) *testgit.TestRepo { 19 + tb.Helper() 20 + return testgit.NewRepo(tb, algo, testgit.RepoOptions{Bare: true, RefFormat: "reftable"}) 21 + } 22 + 23 + // openStore opens a reftable store against repoDir/reftable. 24 + func openStore(tb testing.TB, repoDir string, algo objectid.Algorithm) *reftable.Store { 25 + tb.Helper() 26 + root, err := os.OpenRoot(filepath.Join(repoDir, "reftable")) 27 + if err != nil { 28 + tb.Fatalf("OpenRoot(reftable): %v", err) 29 + } 30 + tb.Cleanup(func() { _ = root.Close() }) 31 + store, err := reftable.New(root, algo) 32 + if err != nil { 33 + tb.Fatalf("reftable.New: %v", err) 34 + } 35 + return store 36 + } 37 + 38 + func TestResolveAndResolveFully(t *testing.T) { 39 + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { 40 + repo := newBareReftableRepo(t, algo) 41 + _, _, id := repo.MakeCommit(t, "resolve") 42 + repo.UpdateRef(t, "refs/heads/main", id) 43 + repo.SymbolicRef(t, "HEAD", "refs/heads/main") 44 + 45 + store := openStore(t, repo.Dir(), algo) 46 + head, err := store.Resolve("HEAD") 47 + if err != nil { 48 + t.Fatalf("Resolve(HEAD): %v", err) 49 + } 50 + sym, ok := head.(ref.Symbolic) 51 + if !ok { 52 + t.Fatalf("Resolve(HEAD) type = %T, want ref.Symbolic", head) 53 + } 54 + if sym.Target != "refs/heads/main" { 55 + t.Fatalf("Resolve(HEAD) target = %q, want refs/heads/main", sym.Target) 56 + } 57 + 58 + main, err := store.ResolveFully("HEAD") 59 + if err != nil { 60 + t.Fatalf("ResolveFully(HEAD): %v", err) 61 + } 62 + if main.ID != id { 63 + t.Fatalf("ResolveFully(HEAD) id = %s, want %s", main.ID, id) 64 + } 65 + 66 + if _, err := store.Resolve("refs/heads/missing"); !errors.Is(err, refstore.ErrReferenceNotFound) { 67 + t.Fatalf("Resolve(missing) error = %v", err) 68 + } 69 + }) 70 + } 71 + 72 + func TestResolveFullyCycle(t *testing.T) { 73 + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { 74 + repo := newBareReftableRepo(t, algo) 75 + repo.SymbolicRef(t, "refs/heads/a", "refs/heads/b") 76 + repo.SymbolicRef(t, "refs/heads/b", "refs/heads/a") 77 + 78 + store := openStore(t, repo.Dir(), algo) 79 + if _, err := store.ResolveFully("refs/heads/a"); err == nil { 80 + t.Fatalf("ResolveFully(cycle) expected error") 81 + } 82 + }) 83 + } 84 + 85 + func TestListAndShorten(t *testing.T) { 86 + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { 87 + repo := newBareReftableRepo(t, algo) 88 + _, _, id := repo.MakeCommit(t, "list") 89 + repo.UpdateRef(t, "refs/heads/main", id) 90 + repo.UpdateRef(t, "refs/heads/feature", id) 91 + repo.UpdateRef(t, "refs/tags/main", id) 92 + repo.UpdateRef(t, "refs/remotes/origin/main", id) 93 + 94 + store := openStore(t, repo.Dir(), algo) 95 + all, err := store.List("") 96 + if err != nil { 97 + t.Fatalf("List(all): %v", err) 98 + } 99 + names := make([]string, 0, len(all)) 100 + for _, entry := range all { 101 + names = append(names, entry.Name()) 102 + } 103 + want := []string{"HEAD", "refs/heads/feature", "refs/heads/main", "refs/remotes/origin/main", "refs/tags/main"} 104 + if !slices.Equal(names, want) { 105 + t.Fatalf("List(all) = %v, want %v", names, want) 106 + } 107 + 108 + heads, err := store.List("refs/heads/*") 109 + if err != nil { 110 + t.Fatalf("List(heads): %v", err) 111 + } 112 + headNames := make([]string, 0, len(heads)) 113 + for _, entry := range heads { 114 + headNames = append(headNames, entry.Name()) 115 + } 116 + wantHeads := []string{"refs/heads/feature", "refs/heads/main"} 117 + if !slices.Equal(headNames, wantHeads) { 118 + t.Fatalf("List(heads) = %v, want %v", headNames, wantHeads) 119 + } 120 + 121 + short, err := store.Shorten("refs/remotes/origin/main") 122 + if err != nil { 123 + t.Fatalf("Shorten(remote): %v", err) 124 + } 125 + if short != "origin/main" { 126 + t.Fatalf("Shorten(remote) = %q, want origin/main", short) 127 + } 128 + }) 129 + } 130 + 131 + func TestTombstoneNewestWins(t *testing.T) { 132 + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { 133 + repo := newBareReftableRepo(t, algo) 134 + _, _, oldID := repo.MakeCommit(t, "old") 135 + repo.UpdateRef(t, "refs/heads/main", oldID) 136 + _, _, newID := repo.MakeCommit(t, "new") 137 + repo.UpdateRef(t, "refs/heads/main", newID) 138 + repo.DeleteRef(t, "refs/heads/main") 139 + 140 + store := openStore(t, repo.Dir(), algo) 141 + if _, err := store.Resolve("refs/heads/main"); !errors.Is(err, refstore.ErrReferenceNotFound) { 142 + t.Fatalf("Resolve(main) after delete error = %v", err) 143 + } 144 + }) 145 + } 146 + 147 + func TestAnnotatedTagPeeled(t *testing.T) { 148 + testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { 149 + repo := newBareReftableRepo(t, algo) 150 + _, _, commitID := repo.MakeCommit(t, "tagged") 151 + tagID := repo.TagAnnotated(t, "v1.0.0", commitID, "annotated") 152 + 153 + store := openStore(t, repo.Dir(), algo) 154 + resolved, err := store.Resolve("refs/tags/v1.0.0") 155 + if err != nil { 156 + t.Fatalf("Resolve(tag): %v", err) 157 + } 158 + detached, ok := resolved.(ref.Detached) 159 + if !ok { 160 + t.Fatalf("Resolve(tag) type = %T, want ref.Detached", resolved) 161 + } 162 + if detached.ID != tagID { 163 + t.Fatalf("Resolve(tag) id = %s, want %s", detached.ID, tagID) 164 + } 165 + if detached.Peeled == nil { 166 + t.Fatalf("Resolve(tag) peeled = nil") 167 + } 168 + if *detached.Peeled != commitID { 169 + t.Fatalf("Resolve(tag) peeled = %s, want %s", *detached.Peeled, commitID) 170 + } 171 + }) 172 + }
+266
refstore/reftable/store.go
··· 1 + // Package reftable provides read access to Git reftable reference storage. 2 + // This store is experimental, has many issues, and should not be used in any serious capacity for now. 3 + package reftable 4 + 5 + import ( 6 + "errors" 7 + "os" 8 + "sort" 9 + "strings" 10 + "sync" 11 + 12 + "codeberg.org/lindenii/furgit/objectid" 13 + "codeberg.org/lindenii/furgit/ref" 14 + "codeberg.org/lindenii/furgit/refstore" 15 + ) 16 + 17 + // Store reads references from a reftable stack rooted at $GIT_DIR/reftable. 18 + // 19 + // Store does not own root. Callers are responsible for closing root. 20 + type Store struct { 21 + // root is the reftable directory capability. 22 + root *os.Root 23 + // algo is the repository object ID algorithm. 24 + algo objectid.Algorithm 25 + 26 + // loadOnce ensures tables.list and table handles are loaded once. 27 + loadOnce sync.Once 28 + // loadErr stores loading failure from loadOnce. 29 + loadErr error 30 + 31 + // stateMu guards tables publication and close transitions. 32 + stateMu sync.RWMutex 33 + // tables are loaded in oldest-to-newest order from tables.list. 34 + tables []*tableFile 35 + // closed reports whether Close has been called. 36 + closed bool 37 + } 38 + 39 + var _ refstore.Store = (*Store)(nil) 40 + 41 + // New creates a read-only reftable store rooted at $GIT_DIR/reftable. 42 + func New(root *os.Root, algo objectid.Algorithm) (*Store, error) { 43 + if algo.Size() == 0 { 44 + return nil, objectid.ErrInvalidAlgorithm 45 + } 46 + return &Store{root: root, algo: algo}, nil 47 + } 48 + 49 + // Close releases mapped table resources associated with this store. 50 + func (store *Store) Close() error { 51 + store.stateMu.Lock() 52 + if store.closed { 53 + store.stateMu.Unlock() 54 + return nil 55 + } 56 + store.closed = true 57 + tables := store.tables 58 + store.tables = nil 59 + store.stateMu.Unlock() 60 + 61 + var closeErr error 62 + for _, table := range tables { 63 + if table == nil { 64 + continue 65 + } 66 + if err := table.close(); err != nil && closeErr == nil { 67 + closeErr = err 68 + } 69 + } 70 + return closeErr 71 + } 72 + 73 + // Resolve resolves a reference name to either a symbolic or detached ref. 74 + func (store *Store) Resolve(name string) (ref.Ref, error) { 75 + tables, err := store.ensureTables() 76 + if err != nil { 77 + return nil, err 78 + } 79 + for i := len(tables) - 1; i >= 0; i-- { 80 + rec, found, err := tables[i].resolveRecord(name) 81 + if err != nil { 82 + return nil, err 83 + } 84 + if !found { 85 + continue 86 + } 87 + if rec.deleted { 88 + return nil, refstore.ErrReferenceNotFound 89 + } 90 + resolved, err := rec.toRef(name) 91 + if err != nil { 92 + return nil, err 93 + } 94 + return resolved, nil 95 + } 96 + return nil, refstore.ErrReferenceNotFound 97 + } 98 + 99 + // ResolveFully resolves symbolic references until it reaches a detached value. 100 + // 101 + // ResolveFully resolves symbolic references only. It does not imply peeling 102 + // annotated tag objects. 103 + func (store *Store) ResolveFully(name string) (ref.Detached, error) { 104 + seen := map[string]struct{}{} 105 + cur := name 106 + for { 107 + if _, exists := seen[cur]; exists { 108 + return ref.Detached{}, errors.New("refstore/reftable: symbolic reference cycle") 109 + } 110 + seen[cur] = struct{}{} 111 + resolved, err := store.Resolve(cur) 112 + if err != nil { 113 + return ref.Detached{}, err 114 + } 115 + switch resolved := resolved.(type) { 116 + case ref.Detached: 117 + return resolved, nil 118 + case ref.Symbolic: 119 + if resolved.Target == "" { 120 + return ref.Detached{}, errors.New("refstore/reftable: symbolic reference has empty target") 121 + } 122 + cur = resolved.Target 123 + default: 124 + return ref.Detached{}, errors.New("refstore/reftable: unsupported reference type") 125 + } 126 + } 127 + } 128 + 129 + // List returns references matching pattern. 130 + // 131 + // Pattern uses path.Match syntax against full reference names. 132 + // Empty pattern matches all references. 133 + func (store *Store) List(pattern string) ([]ref.Ref, error) { 134 + tables, err := store.ensureTables() 135 + if err != nil { 136 + return nil, err 137 + } 138 + visible := make(map[string]ref.Ref) 139 + masked := make(map[string]struct{}) 140 + 141 + for i := len(tables) - 1; i >= 0; i-- { 142 + if err := tables[i].forEachRecord(func(name string, rec recordValue) error { 143 + if _, done := masked[name]; done { 144 + return nil 145 + } 146 + masked[name] = struct{}{} 147 + if rec.deleted { 148 + return nil 149 + } 150 + resolved, err := rec.toRef(name) 151 + if err != nil { 152 + return err 153 + } 154 + visible[name] = resolved 155 + return nil 156 + }); err != nil { 157 + return nil, err 158 + } 159 + } 160 + 161 + matchAll := pattern == "" 162 + if !matchAll { 163 + if _, err := pathMatch(pattern, "refs/heads/main"); err != nil { 164 + return nil, err 165 + } 166 + } 167 + 168 + names := make([]string, 0, len(visible)) 169 + for name := range visible { 170 + names = append(names, name) 171 + } 172 + sort.Strings(names) 173 + 174 + out := make([]ref.Ref, 0, len(names)) 175 + for _, name := range names { 176 + if !matchAll { 177 + ok, err := pathMatch(pattern, name) 178 + if err != nil { 179 + return nil, err 180 + } 181 + if !ok { 182 + continue 183 + } 184 + } 185 + out = append(out, visible[name]) 186 + } 187 + return out, nil 188 + } 189 + 190 + // Shorten returns the shortest unambiguous shorthand for a full reference name. 191 + func (store *Store) Shorten(name string) (string, error) { 192 + refs, err := store.List("") 193 + if err != nil { 194 + return "", err 195 + } 196 + names := make([]string, 0, len(refs)) 197 + found := false 198 + for _, entry := range refs { 199 + if entry == nil { 200 + continue 201 + } 202 + full := entry.Name() 203 + names = append(names, full) 204 + if full == name { 205 + found = true 206 + } 207 + } 208 + if !found { 209 + return "", refstore.ErrReferenceNotFound 210 + } 211 + return refstore.ShortenName(name, names), nil 212 + } 213 + 214 + // ensureTables loads and validates tables listed by tables.list once. 215 + func (store *Store) ensureTables() ([]*tableFile, error) { 216 + store.loadOnce.Do(func() { 217 + tables, err := store.loadTables() 218 + store.stateMu.Lock() 219 + store.tables = tables 220 + store.loadErr = err 221 + store.stateMu.Unlock() 222 + }) 223 + 224 + store.stateMu.RLock() 225 + defer store.stateMu.RUnlock() 226 + if store.closed { 227 + return nil, errors.New("refstore/reftable: store is closed") 228 + } 229 + return store.tables, store.loadErr 230 + } 231 + 232 + // loadTables reads tables.list and opens all listed tables. 233 + func (store *Store) loadTables() ([]*tableFile, error) { 234 + listRaw, err := store.root.ReadFile("tables.list") 235 + if err != nil { 236 + if errors.Is(err, os.ErrNotExist) { 237 + return nil, nil 238 + } 239 + return nil, err 240 + } 241 + lines := strings.Split(string(listRaw), "\n") 242 + names := make([]string, 0, len(lines)) 243 + for _, line := range lines { 244 + line = strings.TrimSuffix(line, "\r") 245 + if line == "" { 246 + continue 247 + } 248 + if strings.Contains(line, "/") { 249 + return nil, errors.New("refstore/reftable: invalid table name") 250 + } 251 + names = append(names, line) 252 + } 253 + 254 + out := make([]*tableFile, 0, len(names)) 255 + for _, name := range names { 256 + table, err := openTableFile(store.root, name, store.algo) 257 + if err != nil { 258 + for _, opened := range out { 259 + _ = opened.close() 260 + } 261 + return nil, err 262 + } 263 + out = append(out, table) 264 + } 265 + return out, nil 266 + }
+231
refstore/reftable/table.go
··· 1 + package reftable 2 + 3 + import ( 4 + "encoding/binary" 5 + "errors" 6 + "fmt" 7 + "hash/crc32" 8 + "os" 9 + "syscall" 10 + 11 + "codeberg.org/lindenii/furgit/objectid" 12 + "codeberg.org/lindenii/furgit/ref" 13 + ) 14 + 15 + const ( 16 + reftableMagic = "REFT" 17 + 18 + version1 = 1 19 + version2 = 2 20 + 21 + blockTypeRef = byte('r') 22 + blockTypeIndex = byte('i') 23 + ) 24 + 25 + var ( 26 + hashIDSHA1 = binary.BigEndian.Uint32([]byte("sha1")) 27 + hashIDSHA256 = binary.BigEndian.Uint32([]byte("s256")) 28 + ) 29 + 30 + // tableFile is one opened and mapped reftable file. 31 + type tableFile struct { 32 + // name is the table filename from tables.list. 33 + name string 34 + // algo is the expected object ID algorithm. 35 + algo objectid.Algorithm 36 + 37 + // file is the opened table file. 38 + file *os.File 39 + // data is the mapped table bytes. 40 + data []byte 41 + 42 + // headerLen is 24 for v1 or 28 for v2. 43 + headerLen int 44 + // blockSize is configured alignment; 0 means unaligned. 45 + blockSize int 46 + 47 + // refEnd is the exclusive end of ref blocks section. 48 + refEnd int 49 + // refIndexPos is the root ref-index block position, or 0 when absent. 50 + refIndexPos uint64 51 + } 52 + 53 + // recordValue is one decoded reference record value. 54 + type recordValue struct { 55 + // deleted marks a tombstone record. 56 + deleted bool 57 + // detachedID stores a direct object ID for detached refs. 58 + detachedID objectid.ObjectID 59 + // hasDetached reports whether detachedID is valid. 60 + hasDetached bool 61 + // peeled stores an optional peeled ID for annotated tags. 62 + peeled *objectid.ObjectID 63 + // symbolicTarget stores symref target for symbolic refs. 64 + symbolicTarget string 65 + } 66 + 67 + // openTableFile maps and validates one reftable file. 68 + func openTableFile(root *os.Root, name string, algo objectid.Algorithm) (*tableFile, error) { 69 + file, err := root.Open(name) 70 + if err != nil { 71 + return nil, err 72 + } 73 + info, err := file.Stat() 74 + if err != nil { 75 + _ = file.Close() 76 + return nil, err 77 + } 78 + size := info.Size() 79 + if size < 0 || size > int64(int(^uint(0)>>1)) { 80 + _ = file.Close() 81 + return nil, fmt.Errorf("refstore/reftable: table %q has unsupported size", name) 82 + } 83 + data, err := syscall.Mmap(int(file.Fd()), 0, int(size), syscall.PROT_READ, syscall.MAP_PRIVATE) 84 + if err != nil { 85 + _ = file.Close() 86 + return nil, err 87 + } 88 + out := &tableFile{name: name, algo: algo, file: file, data: data} 89 + if err := out.parseMeta(); err != nil { 90 + _ = out.close() 91 + return nil, err 92 + } 93 + return out, nil 94 + } 95 + 96 + // close unmaps and closes one table file. 97 + func (table *tableFile) close() error { 98 + var closeErr error 99 + if table.data != nil { 100 + if err := syscall.Munmap(table.data); err != nil && closeErr == nil { 101 + closeErr = err 102 + } 103 + table.data = nil 104 + } 105 + if table.file != nil { 106 + if err := table.file.Close(); err != nil && closeErr == nil { 107 + closeErr = err 108 + } 109 + table.file = nil 110 + } 111 + return closeErr 112 + } 113 + 114 + // parseMeta validates header/footer and section boundaries. 115 + func (table *tableFile) parseMeta() error { 116 + if len(table.data) < 24 { 117 + return fmt.Errorf("refstore/reftable: table %q: file too short", table.name) 118 + } 119 + if string(table.data[:4]) != reftableMagic { 120 + return fmt.Errorf("refstore/reftable: table %q: bad magic", table.name) 121 + } 122 + version := table.data[4] 123 + switch version { 124 + case version1: 125 + table.headerLen = 24 126 + if table.algo != objectid.AlgorithmSHA1 { 127 + return fmt.Errorf("refstore/reftable: table %q: version 1 requires sha1", table.name) 128 + } 129 + case version2: 130 + table.headerLen = 28 131 + if len(table.data) < table.headerLen { 132 + return fmt.Errorf("refstore/reftable: table %q: truncated header", table.name) 133 + } 134 + hashID := binary.BigEndian.Uint32(table.data[24:28]) 135 + if err := validateHashID(hashID, table.algo); err != nil { 136 + return fmt.Errorf("refstore/reftable: table %q: %w", table.name, err) 137 + } 138 + default: 139 + return fmt.Errorf("refstore/reftable: table %q: unsupported version %d", table.name, version) 140 + } 141 + table.blockSize = int(readUint24(table.data[5:8])) 142 + 143 + footerLen := 68 144 + if version == version2 { 145 + footerLen = 72 146 + } 147 + if len(table.data) < footerLen { 148 + return fmt.Errorf("refstore/reftable: table %q: missing footer", table.name) 149 + } 150 + footerStart := len(table.data) - footerLen 151 + footer := table.data[footerStart:] 152 + if string(footer[:4]) != reftableMagic || footer[4] != version { 153 + return fmt.Errorf("refstore/reftable: table %q: invalid footer header", table.name) 154 + } 155 + wantCRC := binary.BigEndian.Uint32(footer[footerLen-4:]) 156 + haveCRC := crc32.ChecksumIEEE(footer[:footerLen-4]) 157 + if wantCRC != haveCRC { 158 + return fmt.Errorf("refstore/reftable: table %q: footer crc mismatch", table.name) 159 + } 160 + if version == version2 { 161 + hashID := binary.BigEndian.Uint32(footer[24:28]) 162 + if err := validateHashID(hashID, table.algo); err != nil { 163 + return fmt.Errorf("refstore/reftable: table %q: %w", table.name, err) 164 + } 165 + } 166 + 167 + off := table.headerLen 168 + table.refIndexPos = binary.BigEndian.Uint64(footer[off : off+8]) 169 + off += 8 170 + objPosAndLen := binary.BigEndian.Uint64(footer[off : off+8]) 171 + off += 8 172 + objPos := objPosAndLen >> 5 173 + objIndexPos := binary.BigEndian.Uint64(footer[off : off+8]) 174 + off += 8 175 + logPos := binary.BigEndian.Uint64(footer[off : off+8]) 176 + off += 8 177 + logIndexPos := binary.BigEndian.Uint64(footer[off : off+8]) 178 + _ = objIndexPos 179 + _ = logIndexPos 180 + 181 + refEnd := uint64(footerStart) 182 + if table.refIndexPos != 0 && table.refIndexPos < refEnd { 183 + refEnd = table.refIndexPos 184 + } 185 + if objPos != 0 && objPos < refEnd { 186 + refEnd = objPos 187 + } 188 + if logPos != 0 && logPos < refEnd { 189 + refEnd = logPos 190 + } 191 + if refEnd < uint64(table.headerLen) || refEnd > uint64(len(table.data)) { 192 + return fmt.Errorf("refstore/reftable: table %q: invalid ref section", table.name) 193 + } 194 + if table.refIndexPos > uint64(len(table.data)) { 195 + return fmt.Errorf("refstore/reftable: table %q: invalid ref index position", table.name) 196 + } 197 + table.refEnd = int(refEnd) 198 + return nil 199 + } 200 + 201 + // validateHashID validates a reftable v2 hash identifier. 202 + func validateHashID(hashID uint32, algo objectid.Algorithm) error { 203 + switch hashID { 204 + case hashIDSHA1: 205 + if algo != objectid.AlgorithmSHA1 { 206 + return errors.New("hash id sha1 mismatch") 207 + } 208 + return nil 209 + case hashIDSHA256: 210 + if algo != objectid.AlgorithmSHA256 { 211 + return errors.New("hash id s256 mismatch") 212 + } 213 + return nil 214 + default: 215 + return fmt.Errorf("unknown hash id 0x%08x", hashID) 216 + } 217 + } 218 + 219 + // toRef converts a decoded record value into a public ref value. 220 + func (record recordValue) toRef(name string) (ref.Ref, error) { 221 + if record.deleted { 222 + return nil, errors.New("refstore/reftable: cannot materialize deleted record") 223 + } 224 + if record.symbolicTarget != "" { 225 + return ref.Symbolic{RefName: name, Target: record.symbolicTarget}, nil 226 + } 227 + if !record.hasDetached { 228 + return nil, errors.New("refstore/reftable: malformed detached record") 229 + } 230 + return ref.Detached{RefName: name, ID: record.detachedID, Peeled: record.peeled}, nil 231 + }