git@cosmocr.at/gitweb.git @ main · summary refs log
main / internal / gitrepo / push_test.go /
internal/gitrepo/push_test.go · 5.3 KiB · raw
view: SourceMarkdown ☑ force
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
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")
	}
}