package gitrepo import ( "context" "errors" "strings" "testing" "github.com/go-git/go-billy/v5" "github.com/go-git/go-billy/v5/memfs" "github.com/go-git/go-billy/v5/util" "github.com/go-git/go-git/v5/plumbing/protocol/packp" "gitweb/internal/store" ) // recordingStore records Put/Delete calls for assertions. Reads are unused by // writeBack (it reads file bytes from the billy fs), so they are stubs. type recordingStore struct { puts []string dels []string } func (r *recordingStore) Stat(string) (bool, error) { return false, store.ErrNotFound } func (r *recordingStore) Get(string) ([]byte, error) { return nil, store.ErrNotFound } func (r *recordingStore) List(string) ([]store.Entry, error) { return nil, store.ErrNotFound } func (r *recordingStore) Seq(string) (int64, error) { return 0, nil } func (r *recordingStore) Put(p string, _ []byte) error { r.puts = append(r.puts, p); return nil } func (r *recordingStore) Delete(p string) error { r.dels = append(r.dels, p); return nil } func writeFile(t *testing.T, fs billy.Filesystem, name, content string) { t.Helper() if err := util.WriteFile(fs, name, []byte(content), 0o644); err != nil { t.Fatal(err) } } func TestWriteBackDiffAndOrdering(t *testing.T) { fs := memfs.New() // Initial state. writeFile(t, fs, "HEAD", "ref: refs/heads/main\n") writeFile(t, fs, "objects/pack/pack-old.pack", "OLD") writeFile(t, fs, "refs/heads/main", "aaaa\n") writeFile(t, fs, "refs/heads/stale", "bbbb\n") before, err := walkManifest(fs, defaultMaxRepoBytes) if err != nil { t.Fatal(err) } // Simulate a push: new packfile, updated main ref, deleted stale ref. writeFile(t, fs, "objects/pack/pack-new.pack", "NEW") writeFile(t, fs, "refs/heads/main", "cccc\n") if err := fs.Remove("refs/heads/stale"); err != nil { t.Fatal(err) } after, err := walkManifest(fs, defaultMaxRepoBytes) if err != nil { t.Fatal(err) } rec := &recordingStore{} p := NewPusher(rec, newTestCache(t, rec, CacheOptions{})) if err := p.writeBack(fs, before, after, "u@d.com/r.git"); err != nil { t.Fatalf("writeBack: %v", err) } // Expected: the new packfile and the changed ref are Put; the removed ref is // Deleted. HEAD and the old packfile are unchanged and must not be rewritten. wantPuts := map[string]bool{ "u@d.com/r.git/objects/pack/pack-new.pack": true, "u@d.com/r.git/refs/heads/main": true, } if len(rec.puts) != len(wantPuts) { t.Fatalf("puts = %v, want keys %v", rec.puts, wantPuts) } for _, p := range rec.puts { if !wantPuts[p] { t.Errorf("unexpected put %q", p) } } // Object writes must precede ref writes. if !strings.Contains(rec.puts[0], "/objects/") { t.Errorf("expected object put first, got order %v", rec.puts) } // The stale ref must be deleted. if len(rec.dels) != 1 || rec.dels[0] != "u@d.com/r.git/refs/heads/stale" { t.Fatalf("dels = %v, want [u@d.com/r.git/refs/heads/stale]", rec.dels) } } func TestFailReport(t *testing.T) { rs := packp.NewReportStatus() rs.UnpackStatus = "ok" rs.CommandStatuses = []*packp.CommandStatus{ {ReferenceName: "refs/heads/main", Status: "ok"}, {ReferenceName: "refs/heads/other", Status: "already failed"}, } failReport(rs, errors.New("store broke:\n\tdetail")) if got := rs.CommandStatuses[0].Status; got != "store broke: detail" { t.Errorf("ok status = %q, want flattened error", got) } // A status that already carries a failure is not overwritten. if got := rs.CommandStatuses[1].Status; got != "already failed" { t.Errorf("failed status overwritten: %q", got) } if failReport(nil, errors.New("x")) != nil { t.Error("failReport(nil) != nil") } } // TestReceivePackCleansUpOnError: a failed materialization must not leave a // push-* directory behind in the cache root. func TestReceivePackCleansUpOnError(t *testing.T) { rec := &recordingStore{} // every read fails with ErrNotFound dir := t.TempDir() p := NewPusher(rec, newTestCache(t, rec, CacheOptions{Dir: dir})) _, err := p.ReceivePack(context.Background(), "u@d.com/r.git", packp.NewReferenceUpdateRequest()) if err == nil { t.Fatal("expected error from failing store") } if got := dirCount(t, dir); got != 0 { t.Fatalf("%d dirs left in cache root after failed push, want 0", got) } } // TestReceivePackSemaphoreHonorsContext: with the push semaphore full, a // cancelled context aborts instead of blocking forever. func TestReceivePackSemaphoreHonorsContext(t *testing.T) { rec := &recordingStore{} p := NewPusher(rec, newTestCache(t, rec, CacheOptions{})) for range cap(p.sem) { p.sem <- struct{}{} } ctx, cancel := context.WithCancel(context.Background()) cancel() _, err := p.ReceivePack(ctx, "u@d.com/r.git", packp.NewReferenceUpdateRequest()) if !errors.Is(err, context.Canceled) { t.Fatalf("err = %v, want context.Canceled", err) } } func TestWalkManifestSkipsTmp(t *testing.T) { fs := memfs.New() writeFile(t, fs, "HEAD", "x") writeFile(t, fs, "objects/tmp_pack_123", "scratch") writeFile(t, fs, "._packed-refs123", "scratch") man, err := walkManifest(fs, defaultMaxRepoBytes) if err != nil { t.Fatal(err) } if _, ok := man["objects/tmp_pack_123"]; ok { t.Error("walkManifest included a tmp_ scratch file") } if _, ok := man["._packed-refs123"]; ok { t.Error("walkManifest included a ._packed-refs temp file") } if _, ok := man["HEAD"]; !ok { t.Error("walkManifest missing HEAD") } }