cloudflare-native port of the tangled knot server
22

Configure Feed

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

feat(knotserver): threshold-gate and local-stage post-receive maintenance

Post-receive maintenance previously ran `git repack -a -d
--write-bitmap-index` plus `commit-graph write --reachable` in place over
the network-backed FUSE mount on every successful push, blocking the push
handler for minutes even on trivial pushes to large repos.

Gate the work behind a cheap check run on every push: a single
objects/pack listing yields pack count and bitmap presence, plus a
bounded, early-exiting loose-object scan. Repack only when pack count or
loose count crosses a threshold, or when a non-empty repo has no bitmap
(the bitmap floor keeps clones fast and is not defeatable by high
thresholds). A sub-threshold push does nothing beyond the gate.

When the gate fires, stage the repack on ephemeral disk: copy the bare
repo to a per-run dir under the configured stage dir, repack and write the
commit-graph on the copy, then write back additively. New packs install
companion-first with the .idx last so a reader never sees a pack without
its index; the commit-graph installs via temp-file rename. Only
snapshotted old packs are pruned, .idx first, and only after every new
file installs, so an interruption or concurrent push never leaves the live
repo invalid and loose objects are never removed. Pack files are
content-addressed, so same-named live files are skipped rather than
rewritten.

Add a KNOT_MAINTENANCE_ config section (loose threshold, pack threshold,
stage dir) threaded into the maintainer, create the stage dir in the
image, and keep the call synchronous with the per-repo lock.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Patch category: aerie-distribution

Jer Miller (Jun 15, 2026, 10:53 PM -0600) ef7dd226 ce22c818

+649 -12
+2 -2
deploy/cloudflare/Dockerfile
··· 29 29 COPY --from=builder /out/aerie-entrypoint /usr/local/bin/aerie-entrypoint 30 30 COPY --from=builder /out/THIRD_PARTY_LICENSES /usr/local/share/THIRD_PARTY_LICENSES 31 31 32 - RUN mkdir -p /home/git /var/lib/knot 32 + RUN mkdir -p /home/git /var/lib/knot /var/lib/knot/maintenance 33 33 34 34 WORKDIR /var/lib/knot 35 35 ··· 38 38 # Operational note: env-only `wrangler deploy` changes (e.g. new container env 39 39 # vars via worker.js) do NOT recycle warm container instances — only an image 40 40 # change rolls them. Bump this marker to force a roll when applying new env. 41 - LABEL org.solpbc.aerie.deploy-marker="260615-diag" 41 + LABEL org.solpbc.aerie.deploy-marker="260615-maint" 42 42 43 43 ENTRYPOINT ["/usr/local/bin/aerie-entrypoint"]
+7
knotserver/config/config.go
··· 49 49 S3PackKey string `env:"S3_PACK_KEY"` 50 50 } 51 51 52 + type Maintenance struct { 53 + LooseThreshold int `env:"LOOSE_THRESHOLD, default=6700"` 54 + PackThreshold int `env:"PACK_THRESHOLD, default=50"` 55 + StageDir string `env:"STAGE_DIR, default=/var/lib/knot/maintenance"` 56 + } 57 + 52 58 func (s Server) Did() syntax.DID { 53 59 return syntax.DID(fmt.Sprintf("did:web:%s", s.Hostname)) 54 60 } ··· 58 64 Server Server `env:",prefix=KNOT_SERVER_"` 59 65 Git Git `env:",prefix=KNOT_GIT_"` 60 66 Diagnostics Diagnostics `env:",prefix=KNOT_DIAGNOSTICS_"` 67 + Maintenance Maintenance `env:",prefix=KNOT_MAINTENANCE_"` 61 68 AppViewEndpoint string `env:"APPVIEW_ENDPOINT, default=https://tangled.org"` 62 69 KnotMirrors []string `env:"KNOT_MIRRORS, default=https://mirror.tangled.network"` 63 70 }
+2 -2
knotserver/diagnostics_test.go
··· 48 48 h := newHTTPSPushFixture(t, true) 49 49 h.c.Diagnostics.Token = "secret" 50 50 repoPath := resolveTestRepoPath(t, h) 51 - if err := (gitMaintainer{}).maintain(t.Context(), repoPath, testRepoDid); err != nil { 51 + if err := forcedGitMaintainer(t).maintain(t.Context(), repoPath, testRepoDid); err != nil { 52 52 t.Fatalf("maintain: %v", err) 53 53 } 54 54 ··· 131 131 runGit(t, work, "commit", "-m", "needle") 132 132 runGit(t, work, "push", "origin", "main:refs/heads/main") 133 133 134 - if err := (gitMaintainer{}).maintain(t.Context(), repoPath, testRepoDid); err != nil { 134 + if err := forcedGitMaintainer(t).maintain(t.Context(), repoPath, testRepoDid); err != nil { 135 135 t.Fatalf("maintain: %v", err) 136 136 } 137 137
+281 -5
knotserver/maintenance.go
··· 3 3 import ( 4 4 "bytes" 5 5 "context" 6 + "errors" 6 7 "fmt" 8 + "io" 9 + "os" 7 10 "os/exec" 11 + "path/filepath" 12 + "sort" 13 + "strings" 8 14 "sync" 15 + 16 + knotconfig "tangled.org/core/knotserver/config" 9 17 ) 10 18 11 19 type repoMaintainer interface { 12 20 maintain(ctx context.Context, repoPath, repoDid string) error 13 21 } 14 22 15 - type gitMaintainer struct{} 23 + type gitMaintainer struct { 24 + looseThreshold int 25 + packThreshold int 26 + stageDir string 27 + } 28 + 29 + func newGitMaintainer(cfg knotconfig.Maintenance) gitMaintainer { 30 + return gitMaintainer{ 31 + looseThreshold: cfg.LooseThreshold, 32 + packThreshold: cfg.PackThreshold, 33 + stageDir: cfg.StageDir, 34 + } 35 + } 16 36 17 37 type repoLocks struct { 18 38 mu sync.Mutex ··· 36 56 37 57 var maintenanceLocks = &repoLocks{m: map[string]*sync.Mutex{}} 38 58 39 - func (gitMaintainer) maintain(ctx context.Context, repoPath, repoDid string) error { 59 + func (m gitMaintainer) maintain(ctx context.Context, repoPath, repoDid string) error { 40 60 mu := maintenanceLocks.get(repoDid) 41 61 if !mu.TryLock() { 42 62 // Sequential pushes never contend; concurrent maintenance on one repo is ··· 46 66 } 47 67 defer mu.Unlock() 48 68 69 + signals, err := gateSignals(repoPath, m.looseThreshold) 70 + if err != nil { 71 + return fmt.Errorf("gate: %w", err) 72 + } 73 + 74 + nonEmpty := signals.packCount > 0 || signals.looseCount > 0 75 + // Why: bitmap floor keeps non-empty repos clone-friendly and is not 76 + // operator-defeatable by high count thresholds. 77 + trigger := signals.packCount >= m.packThreshold || 78 + signals.looseCount >= m.looseThreshold || 79 + (nonEmpty && !signals.bitmapPresent) 80 + if !trigger { 81 + return nil 82 + } 83 + 49 84 gitBin, err := exec.LookPath("git") 50 85 if err != nil { 51 - return fmt.Errorf("find git: %w", err) 86 + return fmt.Errorf("repack: find git: %w", err) 87 + } 88 + 89 + if err := os.MkdirAll(m.stageDir, 0o755); err != nil { 90 + return fmt.Errorf("mkdir: %w", err) 91 + } 92 + runDir, err := os.MkdirTemp(m.stageDir, "aerie-maintenance-*") 93 + if err != nil { 94 + return fmt.Errorf("mkdir: %w", err) 95 + } 96 + defer os.RemoveAll(runDir) 97 + 98 + copyPath := filepath.Join(runDir, "repo.git") 99 + if err := copyDir(repoPath, copyPath); err != nil { 100 + return fmt.Errorf("copy: %w", err) 101 + } 102 + 103 + if err := runMaintenanceGit(ctx, gitBin, copyPath, "repack", "-a", "-d", "--write-bitmap-index"); err != nil { 104 + return fmt.Errorf("repack: %w", err) 105 + } 106 + if err := runMaintenanceGit(ctx, gitBin, copyPath, "commit-graph", "write", "--reachable"); err != nil { 107 + return fmt.Errorf("repack: %w", err) 108 + } 109 + return writeBackMaintenance(repoPath, copyPath, signals.snapshotBases) 110 + } 111 + 112 + type maintenanceGateSignals struct { 113 + packCount int 114 + looseCount int 115 + bitmapPresent bool 116 + snapshotBases map[string]struct{} 117 + } 118 + 119 + func gateSignals(repoPath string, looseThreshold int) (maintenanceGateSignals, error) { 120 + packDir := filepath.Join(repoPath, "objects", "pack") 121 + entries, err := os.ReadDir(packDir) 122 + if err != nil && !errors.Is(err, os.ErrNotExist) { 123 + return maintenanceGateSignals{}, err 124 + } 125 + 126 + signals := maintenanceGateSignals{snapshotBases: map[string]struct{}{}} 127 + for _, entry := range entries { 128 + name := entry.Name() 129 + if strings.HasSuffix(name, ".bitmap") { 130 + signals.bitmapPresent = true 131 + } 132 + if base, ok := packBaseFromPackName(name); ok { 133 + signals.packCount++ 134 + signals.snapshotBases[base] = struct{}{} 135 + } 136 + } 137 + signals.looseCount = countLooseObjects(repoPath, looseThreshold) 138 + return signals, nil 139 + } 140 + 141 + func countLooseObjects(repoPath string, threshold int) int { 142 + if threshold <= 0 { 143 + return 0 144 + } 145 + 146 + objectsDir := filepath.Join(repoPath, "objects") 147 + count := 0 148 + for i := 0; i < 256; i++ { 149 + // Why: this is a best-effort threshold signal that avoids a git 150 + // subprocess on every push. 151 + entries, err := os.ReadDir(filepath.Join(objectsDir, fmt.Sprintf("%02x", i))) 152 + if err != nil { 153 + continue 154 + } 155 + for _, entry := range entries { 156 + if entry.Type().IsRegular() { 157 + count++ 158 + if count >= threshold { 159 + return count 160 + } 161 + } 162 + } 163 + } 164 + return count 165 + } 166 + 167 + func writeBackMaintenance(liveRepo, copyRepo string, snapshotBases map[string]struct{}) error { 168 + livePackDir := filepath.Join(liveRepo, "objects", "pack") 169 + copyPackDir := filepath.Join(copyRepo, "objects", "pack") 170 + 171 + copyBases, err := packBasesFromDir(copyPackDir) 172 + if err != nil { 173 + return fmt.Errorf("write-back: enumerate packs: %w", err) 174 + } 175 + 176 + // Why: a one-pack repack can keep the same pack hash while adding 177 + // companions such as .bitmap. Copy bases are the desired final set; 178 + // installPack only fills live files that are missing. 179 + installBases := make([]string, 0, len(copyBases)) 180 + for base := range copyBases { 181 + installBases = append(installBases, base) 52 182 } 183 + sort.Strings(installBases) 53 184 54 - if err := runMaintenanceGit(ctx, gitBin, repoPath, "repack", "-a", "-d", "--write-bitmap-index"); err != nil { 185 + for _, base := range installBases { 186 + if err := installPack(livePackDir, copyPackDir, base); err != nil { 187 + return fmt.Errorf("write-back: install pack %s: %w", base, err) 188 + } 189 + } 190 + if err := installCommitGraph(liveRepo, copyRepo); err != nil { 191 + return fmt.Errorf("write-back: commit-graph: %w", err) 192 + } 193 + if err := prunePacks(livePackDir, snapshotBases, copyBases); err != nil { 194 + return fmt.Errorf("prune: %w", err) 195 + } 196 + return nil 197 + } 198 + 199 + func packBasesFromDir(packDir string) (map[string]struct{}, error) { 200 + entries, err := os.ReadDir(packDir) 201 + if err != nil { 202 + if errors.Is(err, os.ErrNotExist) { 203 + return map[string]struct{}{}, nil 204 + } 205 + return nil, err 206 + } 207 + 208 + bases := map[string]struct{}{} 209 + for _, entry := range entries { 210 + if base, ok := packBaseFromPackName(entry.Name()); ok { 211 + bases[base] = struct{}{} 212 + } 213 + } 214 + return bases, nil 215 + } 216 + 217 + func packBaseFromPackName(name string) (string, bool) { 218 + if !strings.HasPrefix(name, "pack-") || !strings.HasSuffix(name, ".pack") { 219 + return "", false 220 + } 221 + return strings.TrimSuffix(name, ".pack"), true 222 + } 223 + 224 + func installPack(livePackDir, copyPackDir, base string) error { 225 + for _, ext := range []string{".pack", ".rev", ".bitmap", ".idx"} { 226 + src := filepath.Join(copyPackDir, base+ext) 227 + dst := filepath.Join(livePackDir, base+ext) 228 + // Why: pack files are content-addressed, so a same-named live file is 229 + // byte-identical. Skipping existing files keeps write-back additive. 230 + if _, err := os.Stat(dst); err == nil { 231 + continue 232 + } else if !errors.Is(err, os.ErrNotExist) { 233 + return err 234 + } 235 + if ext != ".pack" && ext != ".idx" { 236 + if _, err := os.Stat(src); err != nil { 237 + if errors.Is(err, os.ErrNotExist) { 238 + continue 239 + } 240 + return err 241 + } 242 + } 243 + if err := installFileAtomically(src, dst); err != nil { 244 + return fmt.Errorf("%s: %w", ext, err) 245 + } 246 + } 247 + return nil 248 + } 249 + 250 + func installCommitGraph(liveRepo, copyRepo string) error { 251 + src := filepath.Join(copyRepo, "objects", "info", "commit-graph") 252 + if _, err := os.Stat(src); err != nil { 253 + if errors.Is(err, os.ErrNotExist) { 254 + return nil 255 + } 55 256 return err 56 257 } 57 - return runMaintenanceGit(ctx, gitBin, repoPath, "commit-graph", "write", "--reachable") 258 + 259 + dstDir := filepath.Join(liveRepo, "objects", "info") 260 + if err := os.MkdirAll(dstDir, 0o755); err != nil { 261 + return err 262 + } 263 + return installFileAtomically(src, filepath.Join(dstDir, "commit-graph")) 264 + } 265 + 266 + func installFileAtomically(src, dst string) error { 267 + info, err := os.Stat(src) 268 + if err != nil { 269 + return err 270 + } 271 + if info.IsDir() { 272 + return fmt.Errorf("%s is a directory", src) 273 + } 274 + 275 + in, err := os.Open(src) 276 + if err != nil { 277 + return err 278 + } 279 + defer in.Close() 280 + 281 + tmp, err := os.CreateTemp(filepath.Dir(dst), "tmp-") 282 + if err != nil { 283 + return err 284 + } 285 + tmpName := tmp.Name() 286 + removeTmp := true 287 + defer func() { 288 + if removeTmp { 289 + _ = os.Remove(tmpName) 290 + } 291 + }() 292 + 293 + if _, err := io.Copy(tmp, in); err != nil { 294 + _ = tmp.Close() 295 + return err 296 + } 297 + if err := tmp.Chmod(info.Mode().Perm()); err != nil { 298 + _ = tmp.Close() 299 + return err 300 + } 301 + if err := tmp.Sync(); err != nil { 302 + _ = tmp.Close() 303 + return err 304 + } 305 + if err := tmp.Close(); err != nil { 306 + return err 307 + } 308 + if err := os.Rename(tmpName, dst); err != nil { 309 + return err 310 + } 311 + removeTmp = false 312 + return nil 313 + } 314 + 315 + func prunePacks(packDir string, snapshotBases, keepBases map[string]struct{}) error { 316 + bases := make([]string, 0, len(snapshotBases)) 317 + for base := range snapshotBases { 318 + if _, keep := keepBases[base]; keep { 319 + continue 320 + } 321 + bases = append(bases, base) 322 + } 323 + sort.Strings(bases) 324 + 325 + for _, base := range bases { 326 + for _, ext := range []string{".idx", ".pack", ".bitmap", ".rev"} { 327 + path := filepath.Join(packDir, base+ext) 328 + if err := os.Remove(path); err != nil && !errors.Is(err, os.ErrNotExist) { 329 + return fmt.Errorf("remove %s: %w", path, err) 330 + } 331 + } 332 + } 333 + return nil 58 334 } 59 335 60 336 func runMaintenanceGit(ctx context.Context, gitBin, repoPath string, args ...string) error {
+356 -2
knotserver/maintenance_test.go
··· 8 8 "net/http/httptest" 9 9 "os" 10 10 "path/filepath" 11 + "sort" 12 + "strings" 11 13 "sync" 12 14 "sync/atomic" 13 15 "testing" ··· 58 60 repoPath := filepath.Join(t.TempDir(), "repo.git") 59 61 seedBareRepo(t, repoPath) 60 62 61 - if err := (gitMaintainer{}).maintain(context.Background(), repoPath, "did:plc:maintenance-a2"); err != nil { 63 + if err := forcedGitMaintainer(t).maintain(context.Background(), repoPath, "did:plc:maintenance-a2"); err != nil { 62 64 t.Fatalf("maintain: %v", err) 63 65 } 64 66 65 67 assertMaintenanceArtifacts(t, repoPath) 68 + runGit(t, "", "-C", repoPath, "fsck") 66 69 } 67 70 68 71 func TestReceivePackMaintainerFailureDoesNotSuppressEvents(t *testing.T) { ··· 108 111 func TestGitMaintainerConcurrentSameRepo(t *testing.T) { 109 112 repoPath := filepath.Join(t.TempDir(), "repo.git") 110 113 seedBareRepo(t, repoPath) 114 + maintainer := forcedGitMaintainer(t) 111 115 112 116 var wg sync.WaitGroup 113 117 errs := make(chan error, 2) ··· 115 119 wg.Add(1) 116 120 go func() { 117 121 defer wg.Done() 118 - errs <- (gitMaintainer{}).maintain(context.Background(), repoPath, "did:plc:maintenance-a5") 122 + errs <- maintainer.maintain(context.Background(), repoPath, "did:plc:maintenance-a5") 119 123 }() 120 124 } 121 125 wg.Wait() ··· 130 134 assertMaintenanceArtifacts(t, repoPath) 131 135 } 132 136 137 + func TestGitMaintainerSubThresholdSkipDoesNoWork(t *testing.T) { 138 + repoPath := filepath.Join(t.TempDir(), "repo.git") 139 + seedPackedRepoWithBitmap(t, repoPath) 140 + removeCommitGraph(t, repoPath) 141 + before := packInventory(t, repoPath) 142 + 143 + stageDir := filepath.Join(t.TempDir(), "maintenance-stage") 144 + maintainer := gitMaintainer{looseThreshold: 1 << 30, packThreshold: 1 << 30, stageDir: stageDir} 145 + if err := maintainer.maintain(context.Background(), repoPath, "did:plc:maintenance-skip"); err != nil { 146 + t.Fatalf("maintain: %v", err) 147 + } 148 + 149 + if _, err := os.Stat(stageDir); !errors.Is(err, os.ErrNotExist) { 150 + t.Fatalf("stageDir stat err = %v, want not exist", err) 151 + } 152 + if commitGraphExists(t, repoPath) { 153 + t.Fatalf("commit-graph exists after sub-threshold skip") 154 + } 155 + assertStringSlicesEqual(t, packInventory(t, repoPath), before, "pack inventory") 156 + } 157 + 158 + func TestGitMaintainerPackThresholdTriggers(t *testing.T) { 159 + repoPath := filepath.Join(t.TempDir(), "repo.git") 160 + seedPackedRepoWithBitmap(t, repoPath) 161 + removeCommitGraph(t, repoPath) 162 + 163 + maintainer := gitMaintainer{looseThreshold: 1 << 30, packThreshold: 1, stageDir: t.TempDir()} 164 + if err := maintainer.maintain(context.Background(), repoPath, "did:plc:maintenance-pack-threshold"); err != nil { 165 + t.Fatalf("maintain: %v", err) 166 + } 167 + 168 + assertMaintenanceArtifacts(t, repoPath) 169 + runGit(t, "", "-C", repoPath, "fsck") 170 + } 171 + 172 + func TestGitMaintainerBitmapFloorTriggers(t *testing.T) { 173 + repoPath := filepath.Join(t.TempDir(), "repo.git") 174 + seedPackedRepoWithBitmap(t, repoPath) 175 + removeBitmaps(t, repoPath) 176 + removeCommitGraph(t, repoPath) 177 + 178 + maintainer := gitMaintainer{looseThreshold: 1 << 30, packThreshold: 1 << 30, stageDir: t.TempDir()} 179 + if err := maintainer.maintain(context.Background(), repoPath, "did:plc:maintenance-bitmap-floor"); err != nil { 180 + t.Fatalf("maintain: %v", err) 181 + } 182 + 183 + assertMaintenanceArtifacts(t, repoPath) 184 + runGit(t, "", "-C", repoPath, "fsck") 185 + } 186 + 187 + func TestWriteBackMaintenancePreservesPostSnapshotObjects(t *testing.T) { 188 + liveRepo := filepath.Join(t.TempDir(), "live.git") 189 + seedMultiPackRepo(t, liveRepo) 190 + snapshotBases := packBasesForRepo(t, liveRepo) 191 + if len(snapshotBases) < 2 { 192 + t.Fatalf("snapshot packs = %d, want at least 2", len(snapshotBases)) 193 + } 194 + copyRepo := buildStagedMaintenanceCopy(t, liveRepo) 195 + copyBases := packBasesForRepo(t, copyRepo) 196 + 197 + injectedPackBase := injectExtraPack(t, liveRepo, snapshotBases) 198 + looseObjectPath := injectLooseObject(t, liveRepo) 199 + 200 + if err := writeBackMaintenance(liveRepo, copyRepo, snapshotBases); err != nil { 201 + t.Fatalf("writeBackMaintenance: %v", err) 202 + } 203 + 204 + assertPackBaseExists(t, liveRepo, injectedPackBase, ".pack", ".idx") 205 + if _, err := os.Stat(looseObjectPath); err != nil { 206 + t.Fatalf("loose object stat: %v", err) 207 + } 208 + assertMaintenanceArtifacts(t, liveRepo) 209 + for base := range snapshotBases { 210 + if _, kept := copyBases[base]; kept { 211 + continue 212 + } 213 + assertPackBaseMissing(t, liveRepo, base, ".pack", ".idx") 214 + } 215 + runGit(t, "", "-C", liveRepo, "fsck") 216 + } 217 + 218 + func TestWriteBackMaintenanceInstallErrorDoesNotPrune(t *testing.T) { 219 + liveRepo := filepath.Join(t.TempDir(), "live.git") 220 + seedMultiPackRepo(t, liveRepo) 221 + snapshotBases := packBasesForRepo(t, liveRepo) 222 + copyRepo := buildStagedMaintenanceCopy(t, liveRepo) 223 + copyBases := packBasesForRepo(t, copyRepo) 224 + 225 + commitGraphPath := filepath.Join(copyRepo, "objects", "info", "commit-graph") 226 + if err := os.Remove(commitGraphPath); err != nil { 227 + t.Fatalf("Remove commit-graph: %v", err) 228 + } 229 + if err := os.Mkdir(commitGraphPath, 0o755); err != nil { 230 + t.Fatalf("Mkdir commit-graph fault: %v", err) 231 + } 232 + 233 + err := writeBackMaintenance(liveRepo, copyRepo, snapshotBases) 234 + if err == nil || !strings.Contains(err.Error(), "write-back") { 235 + t.Fatalf("writeBackMaintenance err = %v, want write-back error", err) 236 + } 237 + 238 + for base := range copyBases { 239 + assertPackBaseExists(t, liveRepo, base, ".pack", ".idx") 240 + } 241 + for base := range snapshotBases { 242 + assertPackBaseExists(t, liveRepo, base, ".pack", ".idx") 243 + } 244 + runGit(t, "", "-C", liveRepo, "fsck") 245 + } 246 + 247 + func TestGitMaintainerStageFailureLeavesRepoUntouched(t *testing.T) { 248 + repoPath := filepath.Join(t.TempDir(), "repo.git") 249 + seedBareRepo(t, repoPath) 250 + before := packInventory(t, repoPath) 251 + 252 + stageParent := filepath.Join(t.TempDir(), "stage-parent") 253 + if err := os.WriteFile(stageParent, []byte("not a directory\n"), 0o644); err != nil { 254 + t.Fatalf("WriteFile stage parent: %v", err) 255 + } 256 + maintainer := gitMaintainer{looseThreshold: 1, packThreshold: 1, stageDir: filepath.Join(stageParent, "child")} 257 + err := maintainer.maintain(context.Background(), repoPath, "did:plc:maintenance-stage-fail") 258 + if err == nil || !strings.Contains(err.Error(), "mkdir") { 259 + t.Fatalf("maintain err = %v, want mkdir error", err) 260 + } 261 + 262 + assertStringSlicesEqual(t, packInventory(t, repoPath), before, "pack inventory") 263 + if commitGraphExists(t, repoPath) { 264 + t.Fatalf("commit-graph exists after failed staging") 265 + } 266 + runGit(t, "", "-C", repoPath, "fsck") 267 + } 268 + 269 + func TestGitMaintainerEmptyBareRepo(t *testing.T) { 270 + repoPath := filepath.Join(t.TempDir(), "empty.git") 271 + runGit(t, "", "init", "--bare", repoPath) 272 + 273 + stageDir := filepath.Join(t.TempDir(), "maintenance-stage") 274 + maintainer := gitMaintainer{looseThreshold: 1 << 30, packThreshold: 1 << 30, stageDir: stageDir} 275 + if err := maintainer.maintain(context.Background(), repoPath, "did:plc:maintenance-empty-skip"); err != nil { 276 + t.Fatalf("maintain skip: %v", err) 277 + } 278 + if _, err := os.Stat(stageDir); !errors.Is(err, os.ErrNotExist) { 279 + t.Fatalf("stageDir stat err = %v, want not exist", err) 280 + } 281 + assertNoMaintenanceArtifacts(t, repoPath) 282 + 283 + forced := gitMaintainer{looseThreshold: 1 << 30, packThreshold: 0, stageDir: t.TempDir()} 284 + if err := forced.maintain(context.Background(), repoPath, "did:plc:maintenance-empty-forced"); err != nil { 285 + t.Fatalf("maintain forced: %v", err) 286 + } 287 + assertNoMaintenanceArtifacts(t, repoPath) 288 + if got := packInventory(t, repoPath); len(got) != 0 { 289 + t.Fatalf("pack inventory = %v, want empty", got) 290 + } 291 + } 292 + 133 293 func assertMaintenanceArtifacts(t *testing.T, repoPath string) { 134 294 t.Helper() 135 295 ··· 145 305 t.Fatalf("commit-graph stat: %v", err) 146 306 } 147 307 } 308 + 309 + func forcedGitMaintainer(t *testing.T) gitMaintainer { 310 + t.Helper() 311 + return gitMaintainer{looseThreshold: 1, packThreshold: 1, stageDir: t.TempDir()} 312 + } 313 + 314 + func seedPackedRepoWithBitmap(t *testing.T, repoPath string) { 315 + t.Helper() 316 + seedBareRepo(t, repoPath) 317 + runGit(t, "", "-C", repoPath, "repack", "-a", "-d", "--write-bitmap-index") 318 + } 319 + 320 + func seedMultiPackRepo(t *testing.T, repoPath string) { 321 + t.Helper() 322 + 323 + runGit(t, "", "init", "--bare", repoPath) 324 + work := t.TempDir() 325 + runGit(t, work, "init") 326 + runGit(t, work, "checkout", "-b", "main") 327 + runGit(t, work, "remote", "add", "origin", repoPath) 328 + 329 + for i := 0; i < 3; i++ { 330 + name := filepath.Join(work, "file.txt") 331 + if err := os.WriteFile(name, []byte(strings.Repeat("x", i+1)+"\n"), 0o644); err != nil { 332 + t.Fatalf("WriteFile seed file: %v", err) 333 + } 334 + runGit(t, work, "add", "file.txt") 335 + runGit(t, work, "commit", "-m", "commit") 336 + runGit(t, work, "push", "origin", "HEAD:refs/heads/main") 337 + runGit(t, "", "-C", repoPath, "repack", "-d") 338 + } 339 + } 340 + 341 + func buildStagedMaintenanceCopy(t *testing.T, liveRepo string) string { 342 + t.Helper() 343 + 344 + copyRepo := filepath.Join(t.TempDir(), "repo.git") 345 + if err := copyDir(liveRepo, copyRepo); err != nil { 346 + t.Fatalf("copyDir: %v", err) 347 + } 348 + runGit(t, "", "-C", copyRepo, "repack", "-a", "-d", "--write-bitmap-index") 349 + runGit(t, "", "-C", copyRepo, "commit-graph", "write", "--reachable") 350 + return copyRepo 351 + } 352 + 353 + func injectExtraPack(t *testing.T, repoPath string, snapshotBases map[string]struct{}) string { 354 + t.Helper() 355 + 356 + work := t.TempDir() 357 + runGit(t, work, "init") 358 + runGit(t, work, "remote", "add", "origin", repoPath) 359 + runGit(t, work, "fetch", "origin", "main") 360 + runGit(t, work, "checkout", "-b", "main", "FETCH_HEAD") 361 + if err := os.WriteFile(filepath.Join(work, "post-snapshot.txt"), []byte("post snapshot\n"), 0o644); err != nil { 362 + t.Fatalf("WriteFile post-snapshot: %v", err) 363 + } 364 + runGit(t, work, "add", "post-snapshot.txt") 365 + runGit(t, work, "commit", "-m", "post snapshot") 366 + runGit(t, work, "push", "origin", "HEAD:refs/heads/main") 367 + runGit(t, "", "-C", repoPath, "repack", "-d") 368 + 369 + for base := range packBasesForRepo(t, repoPath) { 370 + if _, snapshotted := snapshotBases[base]; !snapshotted { 371 + return base 372 + } 373 + } 374 + t.Fatalf("no post-snapshot pack found") 375 + return "" 376 + } 377 + 378 + func injectLooseObject(t *testing.T, repoPath string) string { 379 + t.Helper() 380 + 381 + tmp := filepath.Join(t.TempDir(), "loose.txt") 382 + if err := os.WriteFile(tmp, []byte("loose object\n"), 0o644); err != nil { 383 + t.Fatalf("WriteFile loose object: %v", err) 384 + } 385 + oid := runGit(t, "", "--git-dir", repoPath, "hash-object", "-w", tmp) 386 + if len(oid) < 3 { 387 + t.Fatalf("hash-object oid = %q, want full oid", oid) 388 + } 389 + return filepath.Join(repoPath, "objects", oid[:2], oid[2:]) 390 + } 391 + 392 + func packBasesForRepo(t *testing.T, repoPath string) map[string]struct{} { 393 + t.Helper() 394 + 395 + bases, err := packBasesFromDir(filepath.Join(repoPath, "objects", "pack")) 396 + if err != nil { 397 + t.Fatalf("packBasesFromDir: %v", err) 398 + } 399 + return bases 400 + } 401 + 402 + func packInventory(t *testing.T, repoPath string) []string { 403 + t.Helper() 404 + 405 + entries, err := os.ReadDir(filepath.Join(repoPath, "objects", "pack")) 406 + if err != nil { 407 + t.Fatalf("ReadDir pack: %v", err) 408 + } 409 + names := make([]string, 0, len(entries)) 410 + for _, entry := range entries { 411 + names = append(names, entry.Name()) 412 + } 413 + sort.Strings(names) 414 + return names 415 + } 416 + 417 + func removeBitmaps(t *testing.T, repoPath string) { 418 + t.Helper() 419 + 420 + matches, err := filepath.Glob(filepath.Join(repoPath, "objects", "pack", "*.bitmap")) 421 + if err != nil { 422 + t.Fatalf("Glob bitmap: %v", err) 423 + } 424 + for _, path := range matches { 425 + if err := os.Remove(path); err != nil { 426 + t.Fatalf("Remove bitmap: %v", err) 427 + } 428 + } 429 + } 430 + 431 + func removeCommitGraph(t *testing.T, repoPath string) { 432 + t.Helper() 433 + 434 + if err := os.Remove(filepath.Join(repoPath, "objects", "info", "commit-graph")); err != nil && !errors.Is(err, os.ErrNotExist) { 435 + t.Fatalf("Remove commit-graph: %v", err) 436 + } 437 + } 438 + 439 + func commitGraphExists(t *testing.T, repoPath string) bool { 440 + t.Helper() 441 + 442 + _, err := os.Stat(filepath.Join(repoPath, "objects", "info", "commit-graph")) 443 + if err == nil { 444 + return true 445 + } 446 + if errors.Is(err, os.ErrNotExist) { 447 + return false 448 + } 449 + t.Fatalf("Stat commit-graph: %v", err) 450 + return false 451 + } 452 + 453 + func assertNoMaintenanceArtifacts(t *testing.T, repoPath string) { 454 + t.Helper() 455 + 456 + bitmaps, err := filepath.Glob(filepath.Join(repoPath, "objects", "pack", "*.bitmap")) 457 + if err != nil { 458 + t.Fatalf("Glob bitmap: %v", err) 459 + } 460 + if len(bitmaps) != 0 { 461 + t.Fatalf("bitmap files = %v, want empty", bitmaps) 462 + } 463 + if commitGraphExists(t, repoPath) { 464 + t.Fatalf("commit-graph exists, want absent") 465 + } 466 + } 467 + 468 + func assertPackBaseExists(t *testing.T, repoPath, base string, exts ...string) { 469 + t.Helper() 470 + 471 + for _, ext := range exts { 472 + path := filepath.Join(repoPath, "objects", "pack", base+ext) 473 + if _, err := os.Stat(path); err != nil { 474 + t.Fatalf("pack file %s stat: %v", path, err) 475 + } 476 + } 477 + } 478 + 479 + func assertPackBaseMissing(t *testing.T, repoPath, base string, exts ...string) { 480 + t.Helper() 481 + 482 + for _, ext := range exts { 483 + path := filepath.Join(repoPath, "objects", "pack", base+ext) 484 + if _, err := os.Stat(path); !errors.Is(err, os.ErrNotExist) { 485 + t.Fatalf("pack file %s stat err = %v, want not exist", path, err) 486 + } 487 + } 488 + } 489 + 490 + func assertStringSlicesEqual(t *testing.T, got, want []string, name string) { 491 + t.Helper() 492 + 493 + if len(got) != len(want) { 494 + t.Fatalf("%s = %v, want %v", name, got, want) 495 + } 496 + for i := range got { 497 + if got[i] != want[i] { 498 + t.Fatalf("%s = %v, want %v", name, got, want) 499 + } 500 + } 501 + }
+1 -1
knotserver/router.go
··· 48 48 n: n, 49 49 resolver: resolver, 50 50 motd: defaultMotd, 51 - maintainer: gitMaintainer{}, 51 + maintainer: newGitMaintainer(c.Maintenance), 52 52 } 53 53 h.ServiceAuth = serviceauth.NewServiceAuth(h.l, h.resolver, h.c.Server.Did().String()) 54 54