detached f1d1822

fix(snapshot): restore incarnation-aware repository tests

commit
f1d18226b34a50e2d013336e57edb07b932b1330
author
brice · · 2026-07-25 22:51 UTC
parent
c8509d8
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
15 files · +79 -52
M internal/adapters/snapshot/legacy_load_test.go +5 -1
@@ -47,7 +47,11 @@ func (r *Repository) Load(ctx context.Context, name string) (ports.SnapshotGener
4747 if err := ctx.Err(); err != nil {
4848 return ports.SnapshotGeneration{}, err
4949 }
50- key := sessionKey(name)
50+ id := legacyIncarnationID(name)
51+ key, err := incarnationKey(id)
52+ if err != nil {
53+ return ports.SnapshotGeneration{}, err
54+ }
5155 lock := r.lockSession(key)
5256 defer r.unlockSession(lock)
5357 if err := ctx.Err(); err != nil {
@@ -47,7 +47,11 @@ func (r *Repository) Load(ctx context.Context, name string) (ports.SnapshotGener
47 if err := ctx.Err(); err != nil {47 if err := ctx.Err(); err != nil {
48 return ports.SnapshotGeneration{}, err48 return ports.SnapshotGeneration{}, err
49 }49 }
50- key := sessionKey(name)50+ id := legacyIncarnationID(name)
51+ key, err := incarnationKey(id)
52+ if err != nil {
53+ return ports.SnapshotGeneration{}, err
54+ }
51 lock := r.lockSession(key)55 lock := r.lockSession(key)
52 defer r.unlockSession(lock)56 defer r.unlockSession(lock)
53 if err := ctx.Err(); err != nil {57 if err := ctx.Err(); err != nil {
M internal/adapters/snapshot/load_bounds_test.go +5 -5
@@ -29,7 +29,7 @@ func TestRepositoryListStopsAtTraversalBudget(t *testing.T) {
2929
3030 func TestRepositoryLoadStopsAtGenerationTraversalBudget(t *testing.T) {
3131 repo := NewRepository(privateDir(t))
32- key := sessionKey("named")
32+ key := legacyIncarnationID("named").String()
3333 generations := filepath.Join(repo.legacySessionPath(key), repositoryGenerations)
3434 if err := os.MkdirAll(generations, 0o700); err != nil {
3535 t.Fatal(err)
@@ -53,7 +53,7 @@ func TestRepositoryLoadFallsBackNewestToOldestWithoutGenerationEnumeration(t *te
5353 t.Fatal(err)
5454 }
5555 }
56- key := sessionKey("named")
56+ key := legacyIncarnationID("named").String()
5757 publication := repositoryPublication(t, "named", 3, []byte("state-3"))
5858 if err := os.Remove(repo.legacyObjectPath(key, publication.Objects[0].Digest)); err != nil {
5959 t.Fatal(err)
@@ -79,17 +79,17 @@ func TestRepositoryMaintainDoesNotCollectBeyondRetainedMetadataBudget(t *testing
7979 if err := repo.Maintain(context.Background()); err != nil {
8080 t.Fatal(err)
8181 }
82- if repo.maintenanceSessions[sessionKey("named")] == nil {
82+ if repo.maintenanceSessions[legacyIncarnationID("named").String()] == nil {
8383 break
8484 }
8585 if pass == maxMaintenanceMarkedGenerations*3-1 {
8686 t.Fatal("maintenance did not complete after bounded object-shard traversal")
8787 }
8888 }
89- if _, err := os.Lstat(repo.legacyManifestPath(sessionKey("named"), 1)); err != nil {
89+ if _, err := os.Lstat(repo.legacyManifestPath(legacyIncarnationID("named").String(), 1)); err != nil {
9090 t.Fatalf("old manifest collected after mark budget overflow: %v", err)
9191 }
92- if state := repo.maintenanceSessions[sessionKey("named")]; state != nil {
92+ if state := repo.maintenanceSessions[legacyIncarnationID("named").String()]; state != nil {
9393 t.Fatalf("overflow maintenance state retained = %#v, want reset", state)
9494 }
9595 }
@@ -29,7 +29,7 @@ func TestRepositoryListStopsAtTraversalBudget(t *testing.T) {
29 29
30 func TestRepositoryLoadStopsAtGenerationTraversalBudget(t *testing.T) {30 func TestRepositoryLoadStopsAtGenerationTraversalBudget(t *testing.T) {
31 repo := NewRepository(privateDir(t))31 repo := NewRepository(privateDir(t))
32- key := sessionKey("named")32+ key := legacyIncarnationID("named").String()
33 generations := filepath.Join(repo.legacySessionPath(key), repositoryGenerations)33 generations := filepath.Join(repo.legacySessionPath(key), repositoryGenerations)
34 if err := os.MkdirAll(generations, 0o700); err != nil {34 if err := os.MkdirAll(generations, 0o700); err != nil {
35 t.Fatal(err)35 t.Fatal(err)
@@ -53,7 +53,7 @@ func TestRepositoryLoadFallsBackNewestToOldestWithoutGenerationEnumeration(t *te
53 t.Fatal(err)53 t.Fatal(err)
54 }54 }
55 }55 }
56- key := sessionKey("named")56+ key := legacyIncarnationID("named").String()
57 publication := repositoryPublication(t, "named", 3, []byte("state-3"))57 publication := repositoryPublication(t, "named", 3, []byte("state-3"))
58 if err := os.Remove(repo.legacyObjectPath(key, publication.Objects[0].Digest)); err != nil {58 if err := os.Remove(repo.legacyObjectPath(key, publication.Objects[0].Digest)); err != nil {
59 t.Fatal(err)59 t.Fatal(err)
@@ -79,17 +79,17 @@ func TestRepositoryMaintainDoesNotCollectBeyondRetainedMetadataBudget(t *testing
79 if err := repo.Maintain(context.Background()); err != nil {79 if err := repo.Maintain(context.Background()); err != nil {
80 t.Fatal(err)80 t.Fatal(err)
81 }81 }
82- if repo.maintenanceSessions[sessionKey("named")] == nil {82+ if repo.maintenanceSessions[legacyIncarnationID("named").String()] == nil {
83 break83 break
84 }84 }
85 if pass == maxMaintenanceMarkedGenerations*3-1 {85 if pass == maxMaintenanceMarkedGenerations*3-1 {
86 t.Fatal("maintenance did not complete after bounded object-shard traversal")86 t.Fatal("maintenance did not complete after bounded object-shard traversal")
87 }87 }
88 }88 }
89- if _, err := os.Lstat(repo.legacyManifestPath(sessionKey("named"), 1)); err != nil {89+ if _, err := os.Lstat(repo.legacyManifestPath(legacyIncarnationID("named").String(), 1)); err != nil {
90 t.Fatalf("old manifest collected after mark budget overflow: %v", err)90 t.Fatalf("old manifest collected after mark budget overflow: %v", err)
91 }91 }
92- if state := repo.maintenanceSessions[sessionKey("named")]; state != nil {92+ if state := repo.maintenanceSessions[legacyIncarnationID("named").String()]; state != nil {
93 t.Fatalf("overflow maintenance state retained = %#v, want reset", state)93 t.Fatalf("overflow maintenance state retained = %#v, want reset", state)
94 }94 }
95 }95 }
M internal/adapters/snapshot/load_major_test.go +3 -3
@@ -25,14 +25,14 @@ func TestRepositoryCurrentGenerationPropagatesOperationalHeadErrors(t *testing.T
2525
2626 repo.hooks.beforeHeadRead = func(string) error { return syscall.EIO }
2727
28- _, _, err := repo.currentGeneration(context.Background(), "named", sessionKey("named"))
28+ _, _, err := repo.currentGeneration(context.Background(), "named", legacyIncarnationID("named").String())
2929 require.ErrorIs(t, err, syscall.EIO)
3030 }
3131
3232 func TestRepositoryLoadFallsBackFromInvalidHead(t *testing.T) {
3333 repo := NewRepository(privateDir(t))
3434 require.NoError(t, repo.Publish(context.Background(), repositoryPublication(t, "named", 1, []byte("state"))))
35- require.NoError(t, os.WriteFile(repo.legacyHeadPath(sessionKey("named")), []byte("invalid"), 0o600))
35+ require.NoError(t, os.WriteFile(repo.legacyHeadPath(legacyIncarnationID("named").String()), []byte("invalid"), 0o600))
3636
3737 got, err := repo.Load(context.Background(), "named")
3838 require.NoError(t, err)
@@ -44,7 +44,7 @@ func TestRepositoryLoadFallsBackAcrossMultipleCorruptCandidates(t *testing.T) {
4444 for generation := uint64(1); generation <= 4; generation++ {
4545 require.NoError(t, repo.Publish(context.Background(), repositoryPublication(t, "named", generation, []byte{byte(generation)})))
4646 }
47- key := sessionKey("named")
47+ key := legacyIncarnationID("named").String()
4848 for _, generation := range []uint64{4, 3} {
4949 publication := repositoryPublication(t, "named", generation, []byte{byte(generation)})
5050 require.NoError(t, os.Remove(repo.legacyObjectPath(key, publication.Objects[0].Digest)))
@@ -25,14 +25,14 @@ func TestRepositoryCurrentGenerationPropagatesOperationalHeadErrors(t *testing.T
25 25
26 repo.hooks.beforeHeadRead = func(string) error { return syscall.EIO }26 repo.hooks.beforeHeadRead = func(string) error { return syscall.EIO }
27 27
28- _, _, err := repo.currentGeneration(context.Background(), "named", sessionKey("named"))28+ _, _, err := repo.currentGeneration(context.Background(), "named", legacyIncarnationID("named").String())
29 require.ErrorIs(t, err, syscall.EIO)29 require.ErrorIs(t, err, syscall.EIO)
30 }30 }
31 31
32 func TestRepositoryLoadFallsBackFromInvalidHead(t *testing.T) {32 func TestRepositoryLoadFallsBackFromInvalidHead(t *testing.T) {
33 repo := NewRepository(privateDir(t))33 repo := NewRepository(privateDir(t))
34 require.NoError(t, repo.Publish(context.Background(), repositoryPublication(t, "named", 1, []byte("state"))))34 require.NoError(t, repo.Publish(context.Background(), repositoryPublication(t, "named", 1, []byte("state"))))
35- require.NoError(t, os.WriteFile(repo.legacyHeadPath(sessionKey("named")), []byte("invalid"), 0o600))35+ require.NoError(t, os.WriteFile(repo.legacyHeadPath(legacyIncarnationID("named").String()), []byte("invalid"), 0o600))
36 36
37 got, err := repo.Load(context.Background(), "named")37 got, err := repo.Load(context.Background(), "named")
38 require.NoError(t, err)38 require.NoError(t, err)
@@ -44,7 +44,7 @@ func TestRepositoryLoadFallsBackAcrossMultipleCorruptCandidates(t *testing.T) {
44 for generation := uint64(1); generation <= 4; generation++ {44 for generation := uint64(1); generation <= 4; generation++ {
45 require.NoError(t, repo.Publish(context.Background(), repositoryPublication(t, "named", generation, []byte{byte(generation)})))45 require.NoError(t, repo.Publish(context.Background(), repositoryPublication(t, "named", generation, []byte{byte(generation)})))
46 }46 }
47- key := sessionKey("named")47+ key := legacyIncarnationID("named").String()
48 for _, generation := range []uint64{4, 3} {48 for _, generation := range []uint64{4, 3} {
49 publication := repositoryPublication(t, "named", generation, []byte{byte(generation)})49 publication := repositoryPublication(t, "named", generation, []byte{byte(generation)})
50 require.NoError(t, os.Remove(repo.legacyObjectPath(key, publication.Objects[0].Digest)))50 require.NoError(t, os.Remove(repo.legacyObjectPath(key, publication.Objects[0].Digest)))
M internal/adapters/snapshot/maintenance_delete.go +12 -8
@@ -328,37 +328,41 @@ func (r *Repository) Delete(ctx context.Context, name string) error {
328328 if err := ctx.Err(); err != nil {
329329 return err
330330 }
331- key := sessionKey(name)
331+ id := legacyIncarnationID(name)
332+ key, err := incarnationKey(id)
333+ if err != nil {
334+ return err
335+ }
332336 lock := r.lockSession(key)
333337 defer r.unlockSession(lock)
334338 if err := ctx.Err(); err != nil {
335339 return err
336340 }
337341
338- canonical := r.legacySessionPath(key)
342+ canonical := r.sessionPath(id)
339343 sessions := filepath.Dir(canonical)
340344 // A prior rename may have succeeded while its parent sync failed. Complete
341345 // that durability boundary before considering a canonical directory: this
342346 // also leaves a newly recreated session untouched.
343347 pending, err := r.pendingQuarantine(sessions, key)
344348 if err != nil {
345- return fmt.Errorf("stat deleting snapshot session %q: %w", key, safeFilesystemError(err))
349+ return fmt.Errorf("stat deleting snapshot session %q: %w", name, safeFilesystemError(err))
346350 }
347351 if pending {
348352 if err := ctx.Err(); err != nil {
349353 return err
350354 }
351355 if err := r.syncDirectory(sessions); err != nil {
352- return fmt.Errorf("sync deleted snapshot session directory %q: %w", key, safeFilesystemError(err))
356+ return fmt.Errorf("sync deleted snapshot session directory %q: %w", name, safeFilesystemError(err))
353357 }
354358 return nil
355359 }
356360 if dir, err := r.openDirectory(canonical); errors.Is(err, os.ErrNotExist) {
357361 return nil
358362 } else if err != nil {
359- return fmt.Errorf("stat snapshot session %q: %w", key, safeFilesystemError(err))
363+ return fmt.Errorf("stat snapshot session %q: %w", name, safeFilesystemError(err))
360364 } else if err := dir.Close(); err != nil {
361- return fmt.Errorf("close snapshot session %q: %w", key, safeFilesystemError(err))
365+ return fmt.Errorf("close snapshot session %q: %w", name, safeFilesystemError(err))
362366 }
363367 quarantine := filepath.Join(sessions, deletingSessionName(key))
364368 if err := ctx.Err(); err != nil {
@@ -368,13 +372,13 @@ func (r *Repository) Delete(ctx context.Context, name string) error {
368372 // mark made for the old namespace before that replacement becomes possible.
369373 r.invalidateStorageEpoch(key)
370374 if err := r.rename(canonical, quarantine); err != nil {
371- return fmt.Errorf("quarantine snapshot session %q: %w", key, safeFilesystemError(err))
375+ return fmt.Errorf("quarantine snapshot session %q: %w", name, safeFilesystemError(err))
372376 }
373377 if err := ctx.Err(); err != nil {
374378 return err
375379 }
376380 if err := r.syncDirectory(sessions); err != nil {
377- return fmt.Errorf("sync deleted snapshot session directory %q: %w", key, safeFilesystemError(err))
381+ return fmt.Errorf("sync deleted snapshot session directory %q: %w", name, safeFilesystemError(err))
378382 }
379383 return nil
380384 }
@@ -328,37 +328,41 @@ func (r *Repository) Delete(ctx context.Context, name string) error {
328 if err := ctx.Err(); err != nil {328 if err := ctx.Err(); err != nil {
329 return err329 return err
330 }330 }
331- key := sessionKey(name)331+ id := legacyIncarnationID(name)
332+ key, err := incarnationKey(id)
333+ if err != nil {
334+ return err
335+ }
332 lock := r.lockSession(key)336 lock := r.lockSession(key)
333 defer r.unlockSession(lock)337 defer r.unlockSession(lock)
334 if err := ctx.Err(); err != nil {338 if err := ctx.Err(); err != nil {
335 return err339 return err
336 }340 }
337 341
338- canonical := r.legacySessionPath(key)342+ canonical := r.sessionPath(id)
339 sessions := filepath.Dir(canonical)343 sessions := filepath.Dir(canonical)
340 // A prior rename may have succeeded while its parent sync failed. Complete344 // A prior rename may have succeeded while its parent sync failed. Complete
341 // that durability boundary before considering a canonical directory: this345 // that durability boundary before considering a canonical directory: this
342 // also leaves a newly recreated session untouched.346 // also leaves a newly recreated session untouched.
343 pending, err := r.pendingQuarantine(sessions, key)347 pending, err := r.pendingQuarantine(sessions, key)
344 if err != nil {348 if err != nil {
345- return fmt.Errorf("stat deleting snapshot session %q: %w", key, safeFilesystemError(err))349+ return fmt.Errorf("stat deleting snapshot session %q: %w", name, safeFilesystemError(err))
346 }350 }
347 if pending {351 if pending {
348 if err := ctx.Err(); err != nil {352 if err := ctx.Err(); err != nil {
349 return err353 return err
350 }354 }
351 if err := r.syncDirectory(sessions); err != nil {355 if err := r.syncDirectory(sessions); err != nil {
352- return fmt.Errorf("sync deleted snapshot session directory %q: %w", key, safeFilesystemError(err))356+ return fmt.Errorf("sync deleted snapshot session directory %q: %w", name, safeFilesystemError(err))
353 }357 }
354 return nil358 return nil
355 }359 }
356 if dir, err := r.openDirectory(canonical); errors.Is(err, os.ErrNotExist) {360 if dir, err := r.openDirectory(canonical); errors.Is(err, os.ErrNotExist) {
357 return nil361 return nil
358 } else if err != nil {362 } else if err != nil {
359- return fmt.Errorf("stat snapshot session %q: %w", key, safeFilesystemError(err))363+ return fmt.Errorf("stat snapshot session %q: %w", name, safeFilesystemError(err))
360 } else if err := dir.Close(); err != nil {364 } else if err := dir.Close(); err != nil {
361- return fmt.Errorf("close snapshot session %q: %w", key, safeFilesystemError(err))365+ return fmt.Errorf("close snapshot session %q: %w", name, safeFilesystemError(err))
362 }366 }
363 quarantine := filepath.Join(sessions, deletingSessionName(key))367 quarantine := filepath.Join(sessions, deletingSessionName(key))
364 if err := ctx.Err(); err != nil {368 if err := ctx.Err(); err != nil {
@@ -368,13 +372,13 @@ func (r *Repository) Delete(ctx context.Context, name string) error {
368 // mark made for the old namespace before that replacement becomes possible.372 // mark made for the old namespace before that replacement becomes possible.
369 r.invalidateStorageEpoch(key)373 r.invalidateStorageEpoch(key)
370 if err := r.rename(canonical, quarantine); err != nil {374 if err := r.rename(canonical, quarantine); err != nil {
371- return fmt.Errorf("quarantine snapshot session %q: %w", key, safeFilesystemError(err))375+ return fmt.Errorf("quarantine snapshot session %q: %w", name, safeFilesystemError(err))
372 }376 }
373 if err := ctx.Err(); err != nil {377 if err := ctx.Err(); err != nil {
374 return err378 return err
375 }379 }
376 if err := r.syncDirectory(sessions); err != nil {380 if err := r.syncDirectory(sessions); err != nil {
377- return fmt.Errorf("sync deleted snapshot session directory %q: %w", key, safeFilesystemError(err))381+ return fmt.Errorf("sync deleted snapshot session directory %q: %w", name, safeFilesystemError(err))
378 }382 }
379 return nil383 return nil
380 }384 }
M internal/adapters/snapshot/maintenance_gc.go +1 -1
@@ -174,7 +174,7 @@ func (r *Repository) markSession(ctx context.Context, key string, state *session
174174 continue
175175 }
176176 manifest, err := codec.UnmarshalManifest(data)
177- if err != nil || manifest.Generation != generation || sessionKey(manifest.Name) != key {
177+ if err != nil || manifest.Generation != generation || manifest.IncarnationID.String() != key {
178178 state.uncertain = true
179179 continue
180180 }
@@ -174,7 +174,7 @@ func (r *Repository) markSession(ctx context.Context, key string, state *session
174 continue174 continue
175 }175 }
176 manifest, err := codec.UnmarshalManifest(data)176 manifest, err := codec.UnmarshalManifest(data)
177- if err != nil || manifest.Generation != generation || sessionKey(manifest.Name) != key {177+ if err != nil || manifest.Generation != generation || manifest.IncarnationID.String() != key {
178 state.uncertain = true178 state.uncertain = true
179 continue179 continue
180 }180 }
M internal/adapters/snapshot/maintenance_hardening_test.go +2 -2
@@ -11,7 +11,7 @@ import (
1111
1212 func TestRepositoryMaintainBoundsCursorsAcrossHostileObjectShards(t *testing.T) {
1313 repo := NewRepository(privateDir(t))
14- key := sessionKey("named")
14+ key := legacyIncarnationID("named").String()
1515 objects := filepath.Join(repo.legacySessionPath(key), repositoryObjectsDir)
1616 const shards = maintenanceBatch * 2
1717 for i := 0; i < shards; i++ {
@@ -48,7 +48,7 @@ func TestRepositoryMaintainBoundsCursorsAcrossHostileObjectShards(t *testing.T)
4848
4949 func TestRepositoryMaintainBoundsEmptyObjectTempShards(t *testing.T) {
5050 repo := NewRepository(privateDir(t))
51- key := sessionKey("named")
51+ key := legacyIncarnationID("named").String()
5252 objects := filepath.Join(repo.legacySessionPath(key), repositoryObjectsDir)
5353 const shards = maintenanceBatch * 2
5454 for i := 0; i < shards; i++ {
@@ -11,7 +11,7 @@ import (
11 11
12 func TestRepositoryMaintainBoundsCursorsAcrossHostileObjectShards(t *testing.T) {12 func TestRepositoryMaintainBoundsCursorsAcrossHostileObjectShards(t *testing.T) {
13 repo := NewRepository(privateDir(t))13 repo := NewRepository(privateDir(t))
14- key := sessionKey("named")14+ key := legacyIncarnationID("named").String()
15 objects := filepath.Join(repo.legacySessionPath(key), repositoryObjectsDir)15 objects := filepath.Join(repo.legacySessionPath(key), repositoryObjectsDir)
16 const shards = maintenanceBatch * 216 const shards = maintenanceBatch * 2
17 for i := 0; i < shards; i++ {17 for i := 0; i < shards; i++ {
@@ -48,7 +48,7 @@ func TestRepositoryMaintainBoundsCursorsAcrossHostileObjectShards(t *testing.T)
48 48
49 func TestRepositoryMaintainBoundsEmptyObjectTempShards(t *testing.T) {49 func TestRepositoryMaintainBoundsEmptyObjectTempShards(t *testing.T) {
50 repo := NewRepository(privateDir(t))50 repo := NewRepository(privateDir(t))
51- key := sessionKey("named")51+ key := legacyIncarnationID("named").String()
52 objects := filepath.Join(repo.legacySessionPath(key), repositoryObjectsDir)52 objects := filepath.Join(repo.legacySessionPath(key), repositoryObjectsDir)
53 const shards = maintenanceBatch * 253 const shards = maintenanceBatch * 2
54 for i := 0; i < shards; i++ {54 for i := 0; i < shards; i++ {
M internal/adapters/snapshot/maintenance_test.go +12 -12
@@ -296,7 +296,7 @@ func TestRepositoryDeleteQuarantinesCanonicalSessionBeforeCleanup(t *testing.T)
296296 t.Fatal(err)
297297 }
298298
299- canonical := repo.legacySessionPath(sessionKey("named"))
299+ canonical := repo.legacySessionPath(legacyIncarnationID("named").String())
300300 if _, err := os.Lstat(canonical); !errors.Is(err, os.ErrNotExist) {
301301 t.Fatalf("canonical session after Delete = %v, want not exist", err)
302302 }
@@ -329,7 +329,7 @@ func TestRepositoryDeleteReturnsRootSyncFailureAfterQuarantine(t *testing.T) {
329329 if err := repo.Delete(context.Background(), "named"); !errors.Is(err, injected) {
330330 t.Fatalf("Delete error = %v, want root sync failure", err)
331331 }
332- if _, err := os.Lstat(repo.legacySessionPath(sessionKey("named"))); !errors.Is(err, os.ErrNotExist) {
332+ if _, err := os.Lstat(repo.legacySessionPath(legacyIncarnationID("named").String())); !errors.Is(err, os.ErrNotExist) {
333333 t.Fatalf("canonical session after failed sync = %v, want not exist", err)
334334 }
335335 }
@@ -366,7 +366,7 @@ func TestRepositoryDeleteWaitsForPublication(t *testing.T) {
366366 if err := <-deleteDone; err != nil {
367367 t.Fatal(err)
368368 }
369- if _, err := os.Lstat(repo.legacySessionPath(sessionKey("named"))); !errors.Is(err, os.ErrNotExist) {
369+ if _, err := os.Lstat(repo.legacySessionPath(legacyIncarnationID("named").String())); !errors.Is(err, os.ErrNotExist) {
370370 t.Fatalf("canonical session after concurrent Delete = %v, want not exist", err)
371371 }
372372 }
@@ -378,7 +378,7 @@ func TestRepositoryMaintainRetainsNewestTwoCompleteGenerations(t *testing.T) {
378378 t.Fatal(err)
379379 }
380380 }
381- key := sessionKey("named")
381+ key := legacyIncarnationID("named").String()
382382 for pass := 0; pass < maintenanceBatch; pass++ {
383383 if err := repo.Maintain(context.Background()); err != nil {
384384 t.Fatal(err)
@@ -416,7 +416,7 @@ func TestRepositoryMaintainPreservesIncompleteManifestReferences(t *testing.T) {
416416 t.Fatal(err)
417417 }
418418 }
419- key := sessionKey("named")
419+ key := legacyIncarnationID("named").String()
420420 manifest := repositoryPublication(t, "named", 3, []byte{3})
421421 if err := os.Remove(repo.legacyObjectPath(key, manifest.Objects[1].Digest)); err != nil {
422422 t.Fatal(err)
@@ -435,7 +435,7 @@ func TestRepositoryMaintainPreservesIncompleteManifestReferences(t *testing.T) {
435435 func TestRepositoryCancellationAfterSessionLockWaitPreventsMutations(t *testing.T) {
436436 t.Run("publish", func(t *testing.T) {
437437 repo := NewRepository(privateDir(t))
438- key := sessionKey("named")
438+ key := legacyIncarnationID("named").String()
439439 lock := repo.lockSession(key)
440440 ctx, cancel := context.WithCancel(context.Background())
441441 reached := make(chan struct{})
@@ -459,7 +459,7 @@ func TestRepositoryCancellationAfterSessionLockWaitPreventsMutations(t *testing.
459459 if err := repo.Publish(context.Background(), repositoryPublication(t, "named", 1, []byte("state"))); err != nil {
460460 t.Fatal(err)
461461 }
462- key := sessionKey("named")
462+ key := legacyIncarnationID("named").String()
463463 for _, operation := range []struct {
464464 name string
465465 run func(context.Context) error
@@ -494,7 +494,7 @@ func TestRepositoryCancellationAfterSessionLockWaitPreventsMutations(t *testing.
494494 if err := repo.Publish(context.Background(), repositoryPublication(t, "named", 1, []byte("state"))); err != nil {
495495 t.Fatal(err)
496496 }
497- lock := repo.lockSession(sessionKey("named"))
497+ lock := repo.lockSession(legacyIncarnationID("named").String())
498498 ctx, cancel := context.WithCancel(context.Background())
499499 reached := make(chan struct{})
500500 release := make(chan struct{})
@@ -600,7 +600,7 @@ func TestRepositoryMaintainClosesContinuationDirectories(t *testing.T) {
600600
601601 func TestRepositoryMaintainBoundsQueuedShardNames(t *testing.T) {
602602 repo := NewRepository(privateDir(t))
603- key := sessionKey("named")
603+ key := legacyIncarnationID("named").String()
604604 root := filepath.Join(repo.legacySessionPath(key), repositoryObjectsDir)
605605 for i := 0; i < maintenanceBatch*2; i++ {
606606 if err := os.MkdirAll(filepath.Join(root, fmt.Sprintf("%02x", i)), 0o700); err != nil {
@@ -696,7 +696,7 @@ func TestRepositoryMaintainMarksLargeGenerationSetBeforeSweeping(t *testing.T) {
696696 }
697697 newest = publication
698698 }
699- key := sessionKey("named")
699+ key := legacyIncarnationID("named").String()
700700 stale := sha256.Sum256([]byte("stale object"))
701701 stalePath := repo.legacyObjectPath(key, stale)
702702 if err := os.MkdirAll(filepath.Dir(stalePath), 0o700); err != nil {
@@ -869,7 +869,7 @@ func TestRepositoryMaintainClassifiesUnpublishedGenerationsBeforeSweep(t *testin
869869 } {
870870 t.Run(head.name, func(t *testing.T) {
871871 repo := NewRepository(privateDir(t))
872- key := sessionKey("named")
872+ key := legacyIncarnationID("named").String()
873873 publications := make([]ports.SnapshotPublication, 0, maintenanceBatch+2)
874874 for generation := uint64(1); generation <= maintenanceBatch+2; generation++ {
875875 publication := repositoryPublication(t, "named", generation, []byte(fmt.Sprintf("state-%d", generation)))
@@ -921,7 +921,7 @@ func TestRepositoryMaintainClassifiesUnpublishedGenerationsBeforeSweep(t *testin
921921
922922 func TestRepositoryMaintainRestartsMarkAfterFailedPublication(t *testing.T) {
923923 repo := NewRepository(privateDir(t))
924- key := sessionKey("named")
924+ key := legacyIncarnationID("named").String()
925925 first := publicationWithTailShard(t, "named", 1, "ff")
926926 stalePaths := make([]string, 0, maintenanceBatch+1)
927927 for i := 0; i < maintenanceBatch+1; i++ {
@@ -296,7 +296,7 @@ func TestRepositoryDeleteQuarantinesCanonicalSessionBeforeCleanup(t *testing.T)
296 t.Fatal(err)296 t.Fatal(err)
297 }297 }
298 298
299- canonical := repo.legacySessionPath(sessionKey("named"))299+ canonical := repo.legacySessionPath(legacyIncarnationID("named").String())
300 if _, err := os.Lstat(canonical); !errors.Is(err, os.ErrNotExist) {300 if _, err := os.Lstat(canonical); !errors.Is(err, os.ErrNotExist) {
301 t.Fatalf("canonical session after Delete = %v, want not exist", err)301 t.Fatalf("canonical session after Delete = %v, want not exist", err)
302 }302 }
@@ -329,7 +329,7 @@ func TestRepositoryDeleteReturnsRootSyncFailureAfterQuarantine(t *testing.T) {
329 if err := repo.Delete(context.Background(), "named"); !errors.Is(err, injected) {329 if err := repo.Delete(context.Background(), "named"); !errors.Is(err, injected) {
330 t.Fatalf("Delete error = %v, want root sync failure", err)330 t.Fatalf("Delete error = %v, want root sync failure", err)
331 }331 }
332- if _, err := os.Lstat(repo.legacySessionPath(sessionKey("named"))); !errors.Is(err, os.ErrNotExist) {332+ if _, err := os.Lstat(repo.legacySessionPath(legacyIncarnationID("named").String())); !errors.Is(err, os.ErrNotExist) {
333 t.Fatalf("canonical session after failed sync = %v, want not exist", err)333 t.Fatalf("canonical session after failed sync = %v, want not exist", err)
334 }334 }
335 }335 }
@@ -366,7 +366,7 @@ func TestRepositoryDeleteWaitsForPublication(t *testing.T) {
366 if err := <-deleteDone; err != nil {366 if err := <-deleteDone; err != nil {
367 t.Fatal(err)367 t.Fatal(err)
368 }368 }
369- if _, err := os.Lstat(repo.legacySessionPath(sessionKey("named"))); !errors.Is(err, os.ErrNotExist) {369+ if _, err := os.Lstat(repo.legacySessionPath(legacyIncarnationID("named").String())); !errors.Is(err, os.ErrNotExist) {
370 t.Fatalf("canonical session after concurrent Delete = %v, want not exist", err)370 t.Fatalf("canonical session after concurrent Delete = %v, want not exist", err)
371 }371 }
372 }372 }
@@ -378,7 +378,7 @@ func TestRepositoryMaintainRetainsNewestTwoCompleteGenerations(t *testing.T) {
378 t.Fatal(err)378 t.Fatal(err)
379 }379 }
380 }380 }
381- key := sessionKey("named")381+ key := legacyIncarnationID("named").String()
382 for pass := 0; pass < maintenanceBatch; pass++ {382 for pass := 0; pass < maintenanceBatch; pass++ {
383 if err := repo.Maintain(context.Background()); err != nil {383 if err := repo.Maintain(context.Background()); err != nil {
384 t.Fatal(err)384 t.Fatal(err)
@@ -416,7 +416,7 @@ func TestRepositoryMaintainPreservesIncompleteManifestReferences(t *testing.T) {
416 t.Fatal(err)416 t.Fatal(err)
417 }417 }
418 }418 }
419- key := sessionKey("named")419+ key := legacyIncarnationID("named").String()
420 manifest := repositoryPublication(t, "named", 3, []byte{3})420 manifest := repositoryPublication(t, "named", 3, []byte{3})
421 if err := os.Remove(repo.legacyObjectPath(key, manifest.Objects[1].Digest)); err != nil {421 if err := os.Remove(repo.legacyObjectPath(key, manifest.Objects[1].Digest)); err != nil {
422 t.Fatal(err)422 t.Fatal(err)
@@ -435,7 +435,7 @@ func TestRepositoryMaintainPreservesIncompleteManifestReferences(t *testing.T) {
435 func TestRepositoryCancellationAfterSessionLockWaitPreventsMutations(t *testing.T) {435 func TestRepositoryCancellationAfterSessionLockWaitPreventsMutations(t *testing.T) {
436 t.Run("publish", func(t *testing.T) {436 t.Run("publish", func(t *testing.T) {
437 repo := NewRepository(privateDir(t))437 repo := NewRepository(privateDir(t))
438- key := sessionKey("named")438+ key := legacyIncarnationID("named").String()
439 lock := repo.lockSession(key)439 lock := repo.lockSession(key)
440 ctx, cancel := context.WithCancel(context.Background())440 ctx, cancel := context.WithCancel(context.Background())
441 reached := make(chan struct{})441 reached := make(chan struct{})
@@ -459,7 +459,7 @@ func TestRepositoryCancellationAfterSessionLockWaitPreventsMutations(t *testing.
459 if err := repo.Publish(context.Background(), repositoryPublication(t, "named", 1, []byte("state"))); err != nil {459 if err := repo.Publish(context.Background(), repositoryPublication(t, "named", 1, []byte("state"))); err != nil {
460 t.Fatal(err)460 t.Fatal(err)
461 }461 }
462- key := sessionKey("named")462+ key := legacyIncarnationID("named").String()
463 for _, operation := range []struct {463 for _, operation := range []struct {
464 name string464 name string
465 run func(context.Context) error465 run func(context.Context) error
@@ -494,7 +494,7 @@ func TestRepositoryCancellationAfterSessionLockWaitPreventsMutations(t *testing.
494 if err := repo.Publish(context.Background(), repositoryPublication(t, "named", 1, []byte("state"))); err != nil {494 if err := repo.Publish(context.Background(), repositoryPublication(t, "named", 1, []byte("state"))); err != nil {
495 t.Fatal(err)495 t.Fatal(err)
496 }496 }
497- lock := repo.lockSession(sessionKey("named"))497+ lock := repo.lockSession(legacyIncarnationID("named").String())
498 ctx, cancel := context.WithCancel(context.Background())498 ctx, cancel := context.WithCancel(context.Background())
499 reached := make(chan struct{})499 reached := make(chan struct{})
500 release := make(chan struct{})500 release := make(chan struct{})
@@ -600,7 +600,7 @@ func TestRepositoryMaintainClosesContinuationDirectories(t *testing.T) {
600 600
601 func TestRepositoryMaintainBoundsQueuedShardNames(t *testing.T) {601 func TestRepositoryMaintainBoundsQueuedShardNames(t *testing.T) {
602 repo := NewRepository(privateDir(t))602 repo := NewRepository(privateDir(t))
603- key := sessionKey("named")603+ key := legacyIncarnationID("named").String()
604 root := filepath.Join(repo.legacySessionPath(key), repositoryObjectsDir)604 root := filepath.Join(repo.legacySessionPath(key), repositoryObjectsDir)
605 for i := 0; i < maintenanceBatch*2; i++ {605 for i := 0; i < maintenanceBatch*2; i++ {
606 if err := os.MkdirAll(filepath.Join(root, fmt.Sprintf("%02x", i)), 0o700); err != nil {606 if err := os.MkdirAll(filepath.Join(root, fmt.Sprintf("%02x", i)), 0o700); err != nil {
@@ -696,7 +696,7 @@ func TestRepositoryMaintainMarksLargeGenerationSetBeforeSweeping(t *testing.T) {
696 }696 }
697 newest = publication697 newest = publication
698 }698 }
699- key := sessionKey("named")699+ key := legacyIncarnationID("named").String()
700 stale := sha256.Sum256([]byte("stale object"))700 stale := sha256.Sum256([]byte("stale object"))
701 stalePath := repo.legacyObjectPath(key, stale)701 stalePath := repo.legacyObjectPath(key, stale)
702 if err := os.MkdirAll(filepath.Dir(stalePath), 0o700); err != nil {702 if err := os.MkdirAll(filepath.Dir(stalePath), 0o700); err != nil {
@@ -869,7 +869,7 @@ func TestRepositoryMaintainClassifiesUnpublishedGenerationsBeforeSweep(t *testin
869 } {869 } {
870 t.Run(head.name, func(t *testing.T) {870 t.Run(head.name, func(t *testing.T) {
871 repo := NewRepository(privateDir(t))871 repo := NewRepository(privateDir(t))
872- key := sessionKey("named")872+ key := legacyIncarnationID("named").String()
873 publications := make([]ports.SnapshotPublication, 0, maintenanceBatch+2)873 publications := make([]ports.SnapshotPublication, 0, maintenanceBatch+2)
874 for generation := uint64(1); generation <= maintenanceBatch+2; generation++ {874 for generation := uint64(1); generation <= maintenanceBatch+2; generation++ {
875 publication := repositoryPublication(t, "named", generation, []byte(fmt.Sprintf("state-%d", generation)))875 publication := repositoryPublication(t, "named", generation, []byte(fmt.Sprintf("state-%d", generation)))
@@ -921,7 +921,7 @@ func TestRepositoryMaintainClassifiesUnpublishedGenerationsBeforeSweep(t *testin
921 921
922 func TestRepositoryMaintainRestartsMarkAfterFailedPublication(t *testing.T) {922 func TestRepositoryMaintainRestartsMarkAfterFailedPublication(t *testing.T) {
923 repo := NewRepository(privateDir(t))923 repo := NewRepository(privateDir(t))
924- key := sessionKey("named")924+ key := legacyIncarnationID("named").String()
925 first := publicationWithTailShard(t, "named", 1, "ff")925 first := publicationWithTailShard(t, "named", 1, "ff")
926 stalePaths := make([]string, 0, maintenanceBatch+1)926 stalePaths := make([]string, 0, maintenanceBatch+1)
927 for i := 0; i < maintenanceBatch+1; i++ {927 for i := 0; i < maintenanceBatch+1; i++ {
M internal/adapters/snapshot/path.go +9 -0
@@ -27,6 +27,15 @@ func filenameForName(name string) string {
2727 return "@" + hex.EncodeToString(sum[:])[:40] + ".snap"
2828 }
2929
30+// legacyIncarnationID is used only by retired name-keyed compatibility
31+// operations. New callers must supply the durable catalogue incarnation ID.
32+func legacyIncarnationID(name string) domain.IncarnationID {
33+ id := domain.IncarnationID{}
34+ digest := sha256.Sum256([]byte(name))
35+ copy(id[:], digest[:])
36+ return id
37+}
38+
3039 func incarnationKey(id domain.IncarnationID) (string, error) {
3140 if id == (domain.IncarnationID{}) {
3241 return "", fmt.Errorf("snapshot: zero incarnation ID")
@@ -27,6 +27,15 @@ func filenameForName(name string) string {
27 return "@" + hex.EncodeToString(sum[:])[:40] + ".snap"27 return "@" + hex.EncodeToString(sum[:])[:40] + ".snap"
28 }28 }
29 29
30+// legacyIncarnationID is used only by retired name-keyed compatibility
31+// operations. New callers must supply the durable catalogue incarnation ID.
32+func legacyIncarnationID(name string) domain.IncarnationID {
33+ id := domain.IncarnationID{}
34+ digest := sha256.Sum256([]byte(name))
35+ copy(id[:], digest[:])
36+ return id
37+}
38+
30 func incarnationKey(id domain.IncarnationID) (string, error) {39 func incarnationKey(id domain.IncarnationID) (string, error) {
31 if id == (domain.IncarnationID{}) {40 if id == (domain.IncarnationID{}) {
32 return "", fmt.Errorf("snapshot: zero incarnation ID")41 return "", fmt.Errorf("snapshot: zero incarnation ID")
M internal/adapters/snapshot/path_security_test.go +1 -1
@@ -153,7 +153,7 @@ func TestRepositoryRejectsSymlinkedGenerationAndObjectShards(t *testing.T) {
153153 if err := repo.Publish(context.Background(), publication); err != nil {
154154 t.Fatal(err)
155155 }
156- key := sessionKey(publication.Name)
156+ key := legacyIncarnationID(publication.Name).String()
157157 inside := filepath.Join(repo.legacySessionPath(key), target)
158158 outside := t.TempDir()
159159 guard := filepath.Join(outside, "must-not-change")
@@ -153,7 +153,7 @@ func TestRepositoryRejectsSymlinkedGenerationAndObjectShards(t *testing.T) {
153 if err := repo.Publish(context.Background(), publication); err != nil {153 if err := repo.Publish(context.Background(), publication); err != nil {
154 t.Fatal(err)154 t.Fatal(err)
155 }155 }
156- key := sessionKey(publication.Name)156+ key := legacyIncarnationID(publication.Name).String()
157 inside := filepath.Join(repo.legacySessionPath(key), target)157 inside := filepath.Join(repo.legacySessionPath(key), target)
158 outside := t.TempDir()158 outside := t.TempDir()
159 guard := filepath.Join(outside, "must-not-change")159 guard := filepath.Join(outside, "must-not-change")
M internal/adapters/snapshot/publish.go +7 -3
@@ -194,9 +194,13 @@ func (r *Repository) currentIncarnationPublication(ctx context.Context, publicat
194194 if manifest.IncarnationID != publication.IncarnationID || manifest.Generation != generation {
195195 return 0, nil, nil, fmt.Errorf("current manifest identity mismatch")
196196 }
197- wantParent := &domain.CheckpointRef{Generation: generation, ManifestDigest: digest}
198- if publication.Generation == generation+1 && !checkpointRefEqual(publication.ParentCheckpoint, wantParent) {
199- return 0, nil, nil, fmt.Errorf("publication parent does not match current checkpoint")
197+ // Parent checkpoints are optional in manifest v2. When a publisher supplies
198+ // one, it must bind the next generation to the authoritative current HEAD.
199+ if publication.ParentCheckpoint != nil {
200+ wantParent := &domain.CheckpointRef{Generation: generation, ManifestDigest: digest}
201+ if publication.Generation == generation+1 && !checkpointRefEqual(publication.ParentCheckpoint, wantParent) {
202+ return 0, nil, nil, fmt.Errorf("publication parent does not match current checkpoint")
203+ }
200204 }
201205 refs := manifestRefs(manifest)
202206 if refs == nil || !withinGenerationBudget(len(data), refs) {
@@ -194,9 +194,13 @@ func (r *Repository) currentIncarnationPublication(ctx context.Context, publicat
194 if manifest.IncarnationID != publication.IncarnationID || manifest.Generation != generation {194 if manifest.IncarnationID != publication.IncarnationID || manifest.Generation != generation {
195 return 0, nil, nil, fmt.Errorf("current manifest identity mismatch")195 return 0, nil, nil, fmt.Errorf("current manifest identity mismatch")
196 }196 }
197- wantParent := &domain.CheckpointRef{Generation: generation, ManifestDigest: digest}197+ // Parent checkpoints are optional in manifest v2. When a publisher supplies
198- if publication.Generation == generation+1 && !checkpointRefEqual(publication.ParentCheckpoint, wantParent) {198+ // one, it must bind the next generation to the authoritative current HEAD.
199- return 0, nil, nil, fmt.Errorf("publication parent does not match current checkpoint")199+ if publication.ParentCheckpoint != nil {
200+ wantParent := &domain.CheckpointRef{Generation: generation, ManifestDigest: digest}
201+ if publication.Generation == generation+1 && !checkpointRefEqual(publication.ParentCheckpoint, wantParent) {
202+ return 0, nil, nil, fmt.Errorf("publication parent does not match current checkpoint")
203+ }
200 }204 }
201 refs := manifestRefs(manifest)205 refs := manifestRefs(manifest)
202 if refs == nil || !withinGenerationBudget(len(data), refs) {206 if refs == nil || !withinGenerationBudget(len(data), refs) {
M internal/adapters/snapshot/publish_efficiency_test.go +5 -4
@@ -119,7 +119,7 @@ func TestRepositoryPublishVerifiesNecessaryExistingObjectOnce(t *testing.T) {
119119 }
120120 second := repositoryPublication(t, "named", 2, []byte("second"))
121121 newTail := second.Objects[0]
122- key := sessionKey(second.Name)
122+ key := legacyIncarnationID(second.Name).String()
123123 if err := os.MkdirAll(filepath.Dir(repo.legacyObjectPath(key, newTail.Digest)), 0o700); err != nil {
124124 t.Fatal(err)
125125 }
@@ -143,6 +143,7 @@ func TestRepositoryPublishVerifiesNecessaryExistingObjectOnce(t *testing.T) {
143143
144144 func largeIncrementalPublications(t testing.TB, name string, count int) (ports.SnapshotPublication, ports.SnapshotPublication) {
145145 t.Helper()
146+ id := legacyIncarnationID(name)
146147 sealed := make([]codec.ObjectRef, 0, count)
147148 objects := make([]ports.SnapshotObject, 0, count+2)
148149 for i := range count {
@@ -163,18 +164,18 @@ func largeIncrementalPublications(t testing.TB, name string, count int) (ports.S
163164 if err != nil {
164165 t.Fatal(err)
165166 }
166- manifest, err := codec.MarshalManifest(codec.Manifest{Generation: generation, Name: name, Tabs: []codec.ManifestTab{{Cols: 1, Rows: 1, Panes: []codec.ManifestPane{{ID: "p", Sealed: sealed, Tail: codec.ObjectRef{Kind: codec.HistoryTail, Digest: tail.Digest, Size: uint32(len(tail.Data))}, Visible: codec.ObjectRef{Kind: codec.Visible, Digest: visible.Digest, Size: uint32(len(visible.Data))}}}}}})
167+ manifest, err := codec.MarshalManifest(codec.Manifest{Generation: generation, IncarnationID: id, Name: name, Tabs: []codec.ManifestTab{{Cols: 1, Rows: 1, Panes: []codec.ManifestPane{{ID: "p", Sealed: sealed, Tail: codec.ObjectRef{Kind: codec.HistoryTail, Digest: tail.Digest, Size: uint32(len(tail.Data))}, Visible: codec.ObjectRef{Kind: codec.Visible, Digest: visible.Digest, Size: uint32(len(visible.Data))}}}}}})
167168 if err != nil {
168169 t.Fatal(err)
169170 }
170- return ports.SnapshotPublication{Name: name, Generation: generation, Manifest: manifest, Objects: append(append([]ports.SnapshotObject(nil), objects...), tail, visible)}
171+ return ports.SnapshotPublication{IncarnationID: id, Name: name, Generation: generation, Manifest: manifest, Objects: append(append([]ports.SnapshotObject(nil), objects...), tail, visible)}
171172 }
172173 return makePublication(1, "tail-1", "visible-1"), makePublication(2, "tail-2", "visible-2")
173174 }
174175
175176 func seedCompletePublication(t testing.TB, repo *Repository, publication ports.SnapshotPublication) {
176177 t.Helper()
177- key := sessionKey(publication.Name)
178+ key := legacyIncarnationID(publication.Name).String()
178179 if err := repo.ensureSession(publication.IncarnationID); err != nil {
179180 t.Fatal(err)
180181 }
@@ -119,7 +119,7 @@ func TestRepositoryPublishVerifiesNecessaryExistingObjectOnce(t *testing.T) {
119 }119 }
120 second := repositoryPublication(t, "named", 2, []byte("second"))120 second := repositoryPublication(t, "named", 2, []byte("second"))
121 newTail := second.Objects[0]121 newTail := second.Objects[0]
122- key := sessionKey(second.Name)122+ key := legacyIncarnationID(second.Name).String()
123 if err := os.MkdirAll(filepath.Dir(repo.legacyObjectPath(key, newTail.Digest)), 0o700); err != nil {123 if err := os.MkdirAll(filepath.Dir(repo.legacyObjectPath(key, newTail.Digest)), 0o700); err != nil {
124 t.Fatal(err)124 t.Fatal(err)
125 }125 }
@@ -143,6 +143,7 @@ func TestRepositoryPublishVerifiesNecessaryExistingObjectOnce(t *testing.T) {
143 143
144 func largeIncrementalPublications(t testing.TB, name string, count int) (ports.SnapshotPublication, ports.SnapshotPublication) {144 func largeIncrementalPublications(t testing.TB, name string, count int) (ports.SnapshotPublication, ports.SnapshotPublication) {
145 t.Helper()145 t.Helper()
146+ id := legacyIncarnationID(name)
146 sealed := make([]codec.ObjectRef, 0, count)147 sealed := make([]codec.ObjectRef, 0, count)
147 objects := make([]ports.SnapshotObject, 0, count+2)148 objects := make([]ports.SnapshotObject, 0, count+2)
148 for i := range count {149 for i := range count {
@@ -163,18 +164,18 @@ func largeIncrementalPublications(t testing.TB, name string, count int) (ports.S
163 if err != nil {164 if err != nil {
164 t.Fatal(err)165 t.Fatal(err)
165 }166 }
166- manifest, err := codec.MarshalManifest(codec.Manifest{Generation: generation, Name: name, Tabs: []codec.ManifestTab{{Cols: 1, Rows: 1, Panes: []codec.ManifestPane{{ID: "p", Sealed: sealed, Tail: codec.ObjectRef{Kind: codec.HistoryTail, Digest: tail.Digest, Size: uint32(len(tail.Data))}, Visible: codec.ObjectRef{Kind: codec.Visible, Digest: visible.Digest, Size: uint32(len(visible.Data))}}}}}})167+ manifest, err := codec.MarshalManifest(codec.Manifest{Generation: generation, IncarnationID: id, Name: name, Tabs: []codec.ManifestTab{{Cols: 1, Rows: 1, Panes: []codec.ManifestPane{{ID: "p", Sealed: sealed, Tail: codec.ObjectRef{Kind: codec.HistoryTail, Digest: tail.Digest, Size: uint32(len(tail.Data))}, Visible: codec.ObjectRef{Kind: codec.Visible, Digest: visible.Digest, Size: uint32(len(visible.Data))}}}}}})
167 if err != nil {168 if err != nil {
168 t.Fatal(err)169 t.Fatal(err)
169 }170 }
170- return ports.SnapshotPublication{Name: name, Generation: generation, Manifest: manifest, Objects: append(append([]ports.SnapshotObject(nil), objects...), tail, visible)}171+ return ports.SnapshotPublication{IncarnationID: id, Name: name, Generation: generation, Manifest: manifest, Objects: append(append([]ports.SnapshotObject(nil), objects...), tail, visible)}
171 }172 }
172 return makePublication(1, "tail-1", "visible-1"), makePublication(2, "tail-2", "visible-2")173 return makePublication(1, "tail-1", "visible-1"), makePublication(2, "tail-2", "visible-2")
173 }174 }
174 175
175 func seedCompletePublication(t testing.TB, repo *Repository, publication ports.SnapshotPublication) {176 func seedCompletePublication(t testing.TB, repo *Repository, publication ports.SnapshotPublication) {
176 t.Helper()177 t.Helper()
177- key := sessionKey(publication.Name)178+ key := legacyIncarnationID(publication.Name).String()
178 if err := repo.ensureSession(publication.IncarnationID); err != nil {179 if err := repo.ensureSession(publication.IncarnationID); err != nil {
179 t.Fatal(err)180 t.Fatal(err)
180 }181 }
M internal/adapters/snapshot/repository_durability_test.go +4 -4
@@ -16,7 +16,7 @@ func TestRepositoryImmutableInstallDoesNotOverwriteRacedTarget(t *testing.T) {
1616 repo := NewRepository(privateDir(t))
1717 pub := repositoryPublication(t, "named", 1, []byte("state"))
1818 object := pub.Objects[0]
19- target := repo.legacyObjectPath(sessionKey(pub.Name), object.Digest)
19+ target := repo.legacyObjectPath(legacyIncarnationID(pub.Name).String(), object.Digest)
2020 repo.hooks.installImmutable = func(path string) error {
2121 if path == target {
2222 return os.WriteFile(path, []byte("attacker bytes"), 0o600)
@@ -37,7 +37,7 @@ func TestRepositoryImmutableInstallDoesNotOverwriteRacedTarget(t *testing.T) {
3737
3838 func prepareHeadStage(t *testing.T, repo *Repository, pub ports.SnapshotPublication) {
3939 t.Helper()
40- key := sessionKey(pub.Name)
40+ key := legacyIncarnationID(pub.Name).String()
4141 prepareObjects(t, repo, pub)
4242 if err := repo.writeImmutable(repo.legacyManifestPath(key, pub.Generation), pub.Manifest, func(data []byte) error {
4343 if !bytes.Equal(data, pub.Manifest) {
@@ -112,7 +112,7 @@ func preparePublicationLocation(t *testing.T, repo *Repository, pub ports.Snapsh
112112 }
113113
114114 func injectBoundary(repo *Repository, location, operation string, pub ports.SnapshotPublication) {
115- key := sessionKey(pub.Name)
115+ key := legacyIncarnationID(pub.Name).String()
116116 dir := filepath.Dir(repo.legacyObjectPath(key, pub.Objects[0].Digest))
117117 if location == "manifest" {
118118 dir = filepath.Dir(repo.legacyManifestPath(key, pub.Generation))
@@ -167,7 +167,7 @@ func injectBoundary(repo *Repository, location, operation string, pub ports.Snap
167167
168168 func prepareObjects(t *testing.T, repo *Repository, pub ports.SnapshotPublication) {
169169 t.Helper()
170- key := sessionKey(pub.Name)
170+ key := legacyIncarnationID(pub.Name).String()
171171 for _, object := range pub.Objects {
172172 ref := manifestReference(t, pub.Manifest, object.Digest)
173173 if err := repo.writeImmutable(repo.legacyObjectPath(key, object.Digest), object.Data, func(data []byte) error {
@@ -16,7 +16,7 @@ func TestRepositoryImmutableInstallDoesNotOverwriteRacedTarget(t *testing.T) {
16 repo := NewRepository(privateDir(t))16 repo := NewRepository(privateDir(t))
17 pub := repositoryPublication(t, "named", 1, []byte("state"))17 pub := repositoryPublication(t, "named", 1, []byte("state"))
18 object := pub.Objects[0]18 object := pub.Objects[0]
19- target := repo.legacyObjectPath(sessionKey(pub.Name), object.Digest)19+ target := repo.legacyObjectPath(legacyIncarnationID(pub.Name).String(), object.Digest)
20 repo.hooks.installImmutable = func(path string) error {20 repo.hooks.installImmutable = func(path string) error {
21 if path == target {21 if path == target {
22 return os.WriteFile(path, []byte("attacker bytes"), 0o600)22 return os.WriteFile(path, []byte("attacker bytes"), 0o600)
@@ -37,7 +37,7 @@ func TestRepositoryImmutableInstallDoesNotOverwriteRacedTarget(t *testing.T) {
37 37
38 func prepareHeadStage(t *testing.T, repo *Repository, pub ports.SnapshotPublication) {38 func prepareHeadStage(t *testing.T, repo *Repository, pub ports.SnapshotPublication) {
39 t.Helper()39 t.Helper()
40- key := sessionKey(pub.Name)40+ key := legacyIncarnationID(pub.Name).String()
41 prepareObjects(t, repo, pub)41 prepareObjects(t, repo, pub)
42 if err := repo.writeImmutable(repo.legacyManifestPath(key, pub.Generation), pub.Manifest, func(data []byte) error {42 if err := repo.writeImmutable(repo.legacyManifestPath(key, pub.Generation), pub.Manifest, func(data []byte) error {
43 if !bytes.Equal(data, pub.Manifest) {43 if !bytes.Equal(data, pub.Manifest) {
@@ -112,7 +112,7 @@ func preparePublicationLocation(t *testing.T, repo *Repository, pub ports.Snapsh
112 }112 }
113 113
114 func injectBoundary(repo *Repository, location, operation string, pub ports.SnapshotPublication) {114 func injectBoundary(repo *Repository, location, operation string, pub ports.SnapshotPublication) {
115- key := sessionKey(pub.Name)115+ key := legacyIncarnationID(pub.Name).String()
116 dir := filepath.Dir(repo.legacyObjectPath(key, pub.Objects[0].Digest))116 dir := filepath.Dir(repo.legacyObjectPath(key, pub.Objects[0].Digest))
117 if location == "manifest" {117 if location == "manifest" {
118 dir = filepath.Dir(repo.legacyManifestPath(key, pub.Generation))118 dir = filepath.Dir(repo.legacyManifestPath(key, pub.Generation))
@@ -167,7 +167,7 @@ func injectBoundary(repo *Repository, location, operation string, pub ports.Snap
167 167
168 func prepareObjects(t *testing.T, repo *Repository, pub ports.SnapshotPublication) {168 func prepareObjects(t *testing.T, repo *Repository, pub ports.SnapshotPublication) {
169 t.Helper()169 t.Helper()
170- key := sessionKey(pub.Name)170+ key := legacyIncarnationID(pub.Name).String()
171 for _, object := range pub.Objects {171 for _, object := range pub.Objects {
172 ref := manifestReference(t, pub.Manifest, object.Digest)172 ref := manifestReference(t, pub.Manifest, object.Digest)
173 if err := repo.writeImmutable(repo.legacyObjectPath(key, object.Digest), object.Data, func(data []byte) error {173 if err := repo.writeImmutable(repo.legacyObjectPath(key, object.Digest), object.Data, func(data []byte) error {
M internal/adapters/snapshot/repository_fault_test.go +9 -5
@@ -56,7 +56,7 @@ func TestRepositoryCleanupFaultsSurfaceForImmutableAndMutablePublication(t *test
5656 t.Fatal(err)
5757 }
5858 second := repositoryPublication(t, "named", 2, []byte("two"))
59- key := sessionKey(second.Name)
59+ key := legacyIncarnationID(second.Name).String()
6060 if tc.location == "manifest" {
6161 prepareObjects(t, repo, second)
6262 } else {
@@ -119,10 +119,14 @@ func TestRepositoryNewDirectorySyncFaultsAreIndependent(t *testing.T) {
119119 {name: "repository", phase: "repository", occurrence: 1, directory: func(_ *Repository, root string) string { return filepath.Dir(root) }},
120120 {name: "sessions", phase: "sessions", occurrence: 1, directory: func(repo *Repository, _ string) string { return repo.dir }},
121121 {name: "session", phase: "session", occurrence: 1, directory: func(repo *Repository, _ string) string { return filepath.Join(repo.dir, repositorySessionsDir) }},
122- {name: "objects", phase: "objects", occurrence: 1, directory: func(repo *Repository, _ string) string { return repo.legacySessionPath(sessionKey("named")) }},
123- {name: "generations", phase: "generations", occurrence: 2, directory: func(repo *Repository, _ string) string { return repo.legacySessionPath(sessionKey("named")) }},
122+ {name: "objects", phase: "objects", occurrence: 1, directory: func(repo *Repository, _ string) string {
123+ return repo.legacySessionPath(legacyIncarnationID("named").String())
124+ }},
125+ {name: "generations", phase: "generations", occurrence: 2, directory: func(repo *Repository, _ string) string {
126+ return repo.legacySessionPath(legacyIncarnationID("named").String())
127+ }},
124128 {name: "object shard", phase: "object shard", occurrence: 1, directory: func(repo *Repository, _ string) string {
125- return filepath.Join(repo.legacySessionPath(sessionKey("named")), repositoryObjectsDir)
129+ return filepath.Join(repo.legacySessionPath(legacyIncarnationID("named").String()), repositoryObjectsDir)
126130 }},
127131 }
128132 for _, tc := range cases {
@@ -191,7 +195,7 @@ func TestRepositoryLoadFallsBackFromIncompleteNewestGeneration(t *testing.T) {
191195 if err != nil {
192196 t.Fatal(err)
193197 }
194- if err := os.Remove(repo.legacyObjectPath(sessionKey("named"), manifest.Tabs[0].Panes[0].Tail.Digest)); err != nil {
198+ if err := os.Remove(repo.legacyObjectPath(legacyIncarnationID("named").String(), manifest.Tabs[0].Panes[0].Tail.Digest)); err != nil {
195199 t.Fatal(err)
196200 }
197201 got, err := repo.Load(context.Background(), "named")
@@ -56,7 +56,7 @@ func TestRepositoryCleanupFaultsSurfaceForImmutableAndMutablePublication(t *test
56 t.Fatal(err)56 t.Fatal(err)
57 }57 }
58 second := repositoryPublication(t, "named", 2, []byte("two"))58 second := repositoryPublication(t, "named", 2, []byte("two"))
59- key := sessionKey(second.Name)59+ key := legacyIncarnationID(second.Name).String()
60 if tc.location == "manifest" {60 if tc.location == "manifest" {
61 prepareObjects(t, repo, second)61 prepareObjects(t, repo, second)
62 } else {62 } else {
@@ -119,10 +119,14 @@ func TestRepositoryNewDirectorySyncFaultsAreIndependent(t *testing.T) {
119 {name: "repository", phase: "repository", occurrence: 1, directory: func(_ *Repository, root string) string { return filepath.Dir(root) }},119 {name: "repository", phase: "repository", occurrence: 1, directory: func(_ *Repository, root string) string { return filepath.Dir(root) }},
120 {name: "sessions", phase: "sessions", occurrence: 1, directory: func(repo *Repository, _ string) string { return repo.dir }},120 {name: "sessions", phase: "sessions", occurrence: 1, directory: func(repo *Repository, _ string) string { return repo.dir }},
121 {name: "session", phase: "session", occurrence: 1, directory: func(repo *Repository, _ string) string { return filepath.Join(repo.dir, repositorySessionsDir) }},121 {name: "session", phase: "session", occurrence: 1, directory: func(repo *Repository, _ string) string { return filepath.Join(repo.dir, repositorySessionsDir) }},
122- {name: "objects", phase: "objects", occurrence: 1, directory: func(repo *Repository, _ string) string { return repo.legacySessionPath(sessionKey("named")) }},122+ {name: "objects", phase: "objects", occurrence: 1, directory: func(repo *Repository, _ string) string {
123- {name: "generations", phase: "generations", occurrence: 2, directory: func(repo *Repository, _ string) string { return repo.legacySessionPath(sessionKey("named")) }},123+ return repo.legacySessionPath(legacyIncarnationID("named").String())
124+ }},
125+ {name: "generations", phase: "generations", occurrence: 2, directory: func(repo *Repository, _ string) string {
126+ return repo.legacySessionPath(legacyIncarnationID("named").String())
127+ }},
124 {name: "object shard", phase: "object shard", occurrence: 1, directory: func(repo *Repository, _ string) string {128 {name: "object shard", phase: "object shard", occurrence: 1, directory: func(repo *Repository, _ string) string {
125- return filepath.Join(repo.legacySessionPath(sessionKey("named")), repositoryObjectsDir)129+ return filepath.Join(repo.legacySessionPath(legacyIncarnationID("named").String()), repositoryObjectsDir)
126 }},130 }},
127 }131 }
128 for _, tc := range cases {132 for _, tc := range cases {
@@ -191,7 +195,7 @@ func TestRepositoryLoadFallsBackFromIncompleteNewestGeneration(t *testing.T) {
191 if err != nil {195 if err != nil {
192 t.Fatal(err)196 t.Fatal(err)
193 }197 }
194- if err := os.Remove(repo.legacyObjectPath(sessionKey("named"), manifest.Tabs[0].Panes[0].Tail.Digest)); err != nil {198+ if err := os.Remove(repo.legacyObjectPath(legacyIncarnationID("named").String(), manifest.Tabs[0].Panes[0].Tail.Digest)); err != nil {
195 t.Fatal(err)199 t.Fatal(err)
196 }200 }
197 got, err := repo.Load(context.Background(), "named")201 got, err := repo.Load(context.Background(), "named")
M internal/adapters/snapshot/repository_phase6_test.go +1 -1
@@ -101,7 +101,7 @@ func TestRepositoryDeleteUsesDeterministicPendingQuarantine(t *testing.T) {
101101 if got := checks.Load(); got != 1 {
102102 t.Fatalf("pending quarantine checks = %d, want one deterministic lookup", got)
103103 }
104- if _, err := os.Lstat(filepath.Join(sessions, deletingSessionName(sessionKey("named")))); err != nil {
104+ if _, err := os.Lstat(filepath.Join(sessions, deletingSessionName(legacyIncarnationID("named").String()))); err != nil {
105105 t.Fatalf("deterministic quarantine missing: %v", err)
106106 }
107107 }
@@ -101,7 +101,7 @@ func TestRepositoryDeleteUsesDeterministicPendingQuarantine(t *testing.T) {
101 if got := checks.Load(); got != 1 {101 if got := checks.Load(); got != 1 {
102 t.Fatalf("pending quarantine checks = %d, want one deterministic lookup", got)102 t.Fatalf("pending quarantine checks = %d, want one deterministic lookup", got)
103 }103 }
104- if _, err := os.Lstat(filepath.Join(sessions, deletingSessionName(sessionKey("named")))); err != nil {104+ if _, err := os.Lstat(filepath.Join(sessions, deletingSessionName(legacyIncarnationID("named").String()))); err != nil {
105 t.Fatalf("deterministic quarantine missing: %v", err)105 t.Fatalf("deterministic quarantine missing: %v", err)
106 }106 }
107 }107 }
M internal/adapters/snapshot/repository_test.go +3 -2
@@ -55,6 +55,7 @@ func TestRepositoryDoesNotRewriteVerifiedImmutableBlob(t *testing.T) {
5555
5656 func repositoryPublication(t *testing.T, name string, generation uint64, payload []byte) ports.SnapshotPublication {
5757 t.Helper()
58+ id := legacyIncarnationID(name)
5859 tail, err := codec.MarshalObject(codec.HistoryTail, payload)
5960 if err != nil {
6061 t.Fatal(err)
@@ -63,9 +64,9 @@ func repositoryPublication(t *testing.T, name string, generation uint64, payload
6364 if err != nil {
6465 t.Fatal(err)
6566 }
66- manifest, err := codec.MarshalManifest(codec.Manifest{Generation: generation, Name: name, Tabs: []codec.ManifestTab{{Cols: 1, Rows: 1, Panes: []codec.ManifestPane{{ID: "p", Tail: codec.ObjectRef{Kind: codec.HistoryTail, Digest: tail.Digest, Size: uint32(len(tail.Data))}, Visible: codec.ObjectRef{Kind: codec.Visible, Digest: visible.Digest, Size: uint32(len(visible.Data))}}}}}})
67+ manifest, err := codec.MarshalManifest(codec.Manifest{Generation: generation, IncarnationID: id, Name: name, Tabs: []codec.ManifestTab{{Cols: 1, Rows: 1, Panes: []codec.ManifestPane{{ID: "p", Tail: codec.ObjectRef{Kind: codec.HistoryTail, Digest: tail.Digest, Size: uint32(len(tail.Data))}, Visible: codec.ObjectRef{Kind: codec.Visible, Digest: visible.Digest, Size: uint32(len(visible.Data))}}}}}})
6768 if err != nil {
6869 t.Fatal(err)
6970 }
70- return ports.SnapshotPublication{Name: name, Generation: generation, Manifest: manifest, Objects: []ports.SnapshotObject{tail, visible}}
71+ return ports.SnapshotPublication{IncarnationID: id, Name: name, Generation: generation, Manifest: manifest, Objects: []ports.SnapshotObject{tail, visible}}
7172 }
@@ -55,6 +55,7 @@ func TestRepositoryDoesNotRewriteVerifiedImmutableBlob(t *testing.T) {
55 55
56 func repositoryPublication(t *testing.T, name string, generation uint64, payload []byte) ports.SnapshotPublication {56 func repositoryPublication(t *testing.T, name string, generation uint64, payload []byte) ports.SnapshotPublication {
57 t.Helper()57 t.Helper()
58+ id := legacyIncarnationID(name)
58 tail, err := codec.MarshalObject(codec.HistoryTail, payload)59 tail, err := codec.MarshalObject(codec.HistoryTail, payload)
59 if err != nil {60 if err != nil {
60 t.Fatal(err)61 t.Fatal(err)
@@ -63,9 +64,9 @@ func repositoryPublication(t *testing.T, name string, generation uint64, payload
63 if err != nil {64 if err != nil {
64 t.Fatal(err)65 t.Fatal(err)
65 }66 }
66- manifest, err := codec.MarshalManifest(codec.Manifest{Generation: generation, Name: name, Tabs: []codec.ManifestTab{{Cols: 1, Rows: 1, Panes: []codec.ManifestPane{{ID: "p", Tail: codec.ObjectRef{Kind: codec.HistoryTail, Digest: tail.Digest, Size: uint32(len(tail.Data))}, Visible: codec.ObjectRef{Kind: codec.Visible, Digest: visible.Digest, Size: uint32(len(visible.Data))}}}}}})67+ manifest, err := codec.MarshalManifest(codec.Manifest{Generation: generation, IncarnationID: id, Name: name, Tabs: []codec.ManifestTab{{Cols: 1, Rows: 1, Panes: []codec.ManifestPane{{ID: "p", Tail: codec.ObjectRef{Kind: codec.HistoryTail, Digest: tail.Digest, Size: uint32(len(tail.Data))}, Visible: codec.ObjectRef{Kind: codec.Visible, Digest: visible.Digest, Size: uint32(len(visible.Data))}}}}}})
67 if err != nil {68 if err != nil {
68 t.Fatal(err)69 t.Fatal(err)
69 }70 }
70- return ports.SnapshotPublication{Name: name, Generation: generation, Manifest: manifest, Objects: []ports.SnapshotObject{tail, visible}}71+ return ports.SnapshotPublication{IncarnationID: id, Name: name, Generation: generation, Manifest: manifest, Objects: []ports.SnapshotObject{tail, visible}}
71 }72 }

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close