diff options
| author | 2026-03-26 09:14:59 +0000 | |
|---|---|---|
| committer | 2026-03-26 09:14:59 +0000 | |
| commit | 3d25bda9d5da6814661828adabe8a09f9d01aefb (patch) | |
| tree | d034e28079333f85e5d7b96d921282eddd4798d6 /receivepack/int_test.go | |
| parent | object/id: Empty tree (diff) | |
| signature | No signature | |
network/receivepack: Rename from receivepack
Diffstat (limited to 'receivepack/int_test.go')
| -rw-r--r-- | receivepack/int_test.go | 1047 |
1 files changed, 0 insertions, 1047 deletions
diff --git a/receivepack/int_test.go b/receivepack/int_test.go deleted file mode 100644 index cf3ee870..00000000 --- a/receivepack/int_test.go +++ /dev/null @@ -1,1047 +0,0 @@ -package receivepack_test - -import ( - "context" - "fmt" - "io" - "os" - "strings" - "testing" - "time" - - "codeberg.org/lindenii/furgit/internal/testgit" - "codeberg.org/lindenii/furgit/network/protocol/pktline" - "codeberg.org/lindenii/furgit/network/protocol/sideband64k" - objectid "codeberg.org/lindenii/furgit/object/id" - receivepack "codeberg.org/lindenii/furgit/receivepack" - receivepackhooks "codeberg.org/lindenii/furgit/receivepack/hooks" -) - -func TestReceivePackDeleteOnlyAtomicDeleteSucceeds(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - - repo := testRepo.OpenRepository(t) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - commitID.String() + " " + objectid.Zero(algo).String() + " refs/heads/main\x00report-status atomic delete-refs object-format=" + algo.String() + "\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{ - GitProtocol: "", - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - }) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - got := output.String() - if !strings.Contains(got, "ok refs/heads/main\n") { - t.Fatalf("unexpected receive-pack output %q", got) - } - - _, err = repo.Refs().Resolve("refs/heads/main") - if err == nil { - t.Fatal("refs/heads/main still exists after delete push") - } - }) -} - -func TestReceivePackDeleteOnlyNonAtomicAppliesIndependentDeletes(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - _, _, staleID := testRepo.MakeCommit(t, "stale") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - testRepo.UpdateRef(t, "refs/heads/topic", commitID) - - repo := testRepo.OpenRepository(t) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - staleID.String() + " " + objectid.Zero(algo).String() + " refs/heads/main\x00report-status delete-refs object-format=" + algo.String() + "\n", - )) - input.WriteString(pktlineData( - commitID.String() + " " + objectid.Zero(algo).String() + " refs/heads/topic\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{ - GitProtocol: "", - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - }) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - got := output.String() - if !strings.Contains(got, "ng refs/heads/main ") || !strings.Contains(got, "ok refs/heads/topic\n") { - t.Fatalf("unexpected receive-pack output %q", got) - } - - _, err = repo.Refs().Resolve("refs/heads/main") - if err != nil { - t.Fatalf("Resolve(main): %v", err) - } - - _, err = repo.Refs().Resolve("refs/heads/topic") - if err == nil { - t.Fatal("refs/heads/topic still exists after successful delete") - } - }) -} - -func TestReceivePackDeleteOnlyAtomicFailureLeavesAllRefsUntouched(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - _, _, staleID := testRepo.MakeCommit(t, "stale") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - testRepo.UpdateRef(t, "refs/heads/topic", commitID) - - repo := testRepo.OpenRepository(t) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - staleID.String() + " " + objectid.Zero(algo).String() + " refs/heads/main\x00report-status atomic delete-refs object-format=" + algo.String() + "\n", - )) - input.WriteString(pktlineData( - commitID.String() + " " + objectid.Zero(algo).String() + " refs/heads/topic\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{ - GitProtocol: "", - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - }) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - got := output.String() - if !strings.Contains(got, "ng refs/heads/main ") || !strings.Contains(got, "ng refs/heads/topic ") { - t.Fatalf("unexpected receive-pack output %q", got) - } - - _, err = repo.Refs().Resolve("refs/heads/main") - if err != nil { - t.Fatalf("Resolve(main): %v", err) - } - - _, err = repo.Refs().Resolve("refs/heads/topic") - if err != nil { - t.Fatalf("Resolve(topic): %v", err) - } - }) -} - -func TestReceivePackAdvertisesResolvedHEAD(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - testRepo.SymbolicRef(t, "HEAD", "refs/heads/main") - - repo := testRepo.OpenRepository(t) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString("0000") - - err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - }) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - got := output.String() - - want := commitID.String() + " HEAD" - if !strings.Contains(got, want) { - t.Fatalf("HEAD advertisement missing %q in %q", want, got) - } - }) -} - -func TestReceivePackVersion2FallsBackToV0(t *testing.T) { - t.Parallel() - - testReceivePackProtocolFallback(t, "version=2") -} - -func TestReceivePackHighestRequestedVersionFallsBackToV0ForV2(t *testing.T) { - t.Parallel() - - testReceivePackProtocolFallback(t, "version=1:version=2") -} - -func TestReceivePackWithoutReportStatusWritesNoStatusPayload(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - - repo := testRepo.OpenRepository(t) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - commitID.String() + " " + objectid.Zero(algo).String() + " refs/heads/main\x00delete-refs atomic object-format=" + algo.String() + "\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - }) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - got := output.String() - if strings.Contains(got, "unpack ") || strings.Contains(got, "ng refs/heads/main ") || strings.Contains(got, "ok refs/heads/main\n") { - t.Fatalf("unexpected status payload %q", got) - } - }) -} - -func testReceivePackProtocolFallback(t *testing.T, gitProtocol string) { - t.Helper() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - - repo := testRepo.OpenRepository(t) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - commitID.String() + " " + objectid.Zero(algo).String() + " refs/heads/main\x00report-status atomic delete-refs object-format=" + algo.String() + "\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{ - GitProtocol: gitProtocol, - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - }) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - if strings.HasPrefix(output.String(), pktlineData("version 1\n")) { - t.Fatalf("receive-pack output started with protocol v1 preface for %q: %q", gitProtocol, output.String()) - } - }) -} - -func TestReceivePackPackRequestWithoutObjectsRootReportsNotConfigured(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - - repo := testRepo.OpenRepository(t) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - commitID.String() + " " + commitID.String() + " refs/heads/main\x00report-status object-format=" + algo.String() + "\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - }) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - got := output.String() - if !strings.Contains(got, "unpack objects root not configured\n") { - t.Fatalf("unexpected receive-pack output %q", got) - } - }) -} - -func TestReceivePackPackCreatePromotesObjectsAndUpdatesRef(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - sender := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := sender.MakeCommit(t, "pushed commit") - - receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - repo := receiver.OpenRepository(t) - objectsRoot := receiver.OpenObjectsRoot(t) - - packStream := sender.PackObjectsReader(t, []string{commitID.String()}, false) - t.Cleanup(func() { - _ = packStream.Close() - }) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - objectid.Zero(algo).String() + " " + commitID.String() + " refs/heads/main\x00report-status-v2 atomic object-format=" + algo.String() + "\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack( - context.Background(), - &output, - io.MultiReader(strings.NewReader(input.String()), packStream), - receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - ObjectsRoot: objectsRoot, - }, - ) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - got := output.String() - if !strings.Contains(got, "unpack ok\n") || !strings.Contains(got, "ok refs/heads/main\n") { - t.Fatalf("unexpected receive-pack output %q", got) - } - - reopened := receiver.OpenRepository(t) - - resolved, err := reopened.Refs().ResolveToDetached("refs/heads/main") - if err != nil { - t.Fatalf("ResolveToDetached(main): %v", err) - } - - if resolved.ID != commitID { - t.Fatalf("refs/heads/main = %s, want %s", resolved.ID, commitID) - } - - if gotType := receiver.Run(t, "cat-file", "-t", commitID.String()); gotType != "commit" { - t.Fatalf("cat-file -t = %q, want commit", gotType) - } - - packs := receiver.Run(t, "count-objects", "-v") - if !strings.Contains(packs, "packs: 1") { - t.Fatalf("count-objects output missing promoted pack: %q", packs) - } - }) -} - -func TestReceivePackHookSeesQuarantinedObjectsAndCanRejectBeforePromotion(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - sender := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := sender.MakeCommit(t, "pushed commit") - - receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - repo := receiver.OpenRepository(t) - objectsRoot := receiver.OpenObjectsRoot(t) - - packStream := sender.PackObjectsReader(t, []string{commitID.String()}, false) - t.Cleanup(func() { - _ = packStream.Close() - }) - - var ( - input strings.Builder - output bufferWriteFlusher - hookCalled bool - ) - - input.WriteString(pktlineData( - objectid.Zero(algo).String() + " " + commitID.String() + " refs/heads/main\x00report-status-v2 atomic object-format=" + algo.String() + "\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack( - context.Background(), - &output, - io.MultiReader(strings.NewReader(input.String()), packStream), - receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - ObjectsRoot: objectsRoot, - Hook: func(ctx context.Context, req receivepack.HookRequest) ([]receivepack.UpdateDecision, error) { - hookCalled = true - - if len(req.Updates) != 1 || req.Updates[0].NewID != commitID { - t.Fatalf("unexpected hook updates: %+v", req.Updates) - } - - _, _, err := req.ExistingObjects.ReadHeader(commitID) - if err == nil { - t.Fatalf("existing objects unexpectedly contained quarantined commit %s", commitID) - } - - _, _, err = req.QuarantinedObjects.ReadHeader(commitID) - if err != nil { - t.Fatalf("quarantined objects missing commit %s: %v", commitID, err) - } - - return []receivepack.UpdateDecision{{ - Accept: false, - Message: "blocked by hook", - }}, nil - }, - }, - ) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - if !hookCalled { - t.Fatal("hook was not called") - } - - got := output.String() - if !strings.Contains(got, "unpack ok\n") || !strings.Contains(got, "ng refs/heads/main blocked by hook\n") { - t.Fatalf("unexpected receive-pack output %q", got) - } - - _, err = repo.Refs().Resolve("refs/heads/main") - if err == nil { - t.Fatal("refs/heads/main exists after hook rejection") - } - - packs := receiver.Run(t, "count-objects", "-v") - if !strings.Contains(packs, "packs: 0") { - t.Fatalf("count-objects output shows unexpected promoted pack: %q", packs) - } - }) -} - -func TestReceivePackHookCanRejectSubsetOfNonAtomicDeleteOnlyPush(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - testRepo.UpdateRef(t, "refs/heads/topic", commitID) - - repo := testRepo.OpenRepository(t) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - commitID.String() + " " + objectid.Zero(algo).String() + " refs/heads/main\x00report-status delete-refs object-format=" + algo.String() + "\n", - )) - input.WriteString(pktlineData( - commitID.String() + " " + objectid.Zero(algo).String() + " refs/heads/topic\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - Hook: func(ctx context.Context, req receivepack.HookRequest) ([]receivepack.UpdateDecision, error) { - return []receivepack.UpdateDecision{ - {Accept: false, Message: "leave main alone"}, - {Accept: true}, - }, nil - }, - }) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - got := output.String() - if !strings.Contains(got, "ng refs/heads/main leave main alone\n") || !strings.Contains(got, "ok refs/heads/topic\n") { - t.Fatalf("unexpected receive-pack output %q", got) - } - - _, err = repo.Refs().Resolve("refs/heads/main") - if err != nil { - t.Fatalf("Resolve(main): %v", err) - } - - _, err = repo.Refs().Resolve("refs/heads/topic") - if err == nil { - t.Fatal("refs/heads/topic still exists after successful delete") - } - }) -} - -func TestReceivePackHookProgressUsesSideBand64K(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - - repo := testRepo.OpenRepository(t) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - commitID.String() + " " + objectid.Zero(algo).String() + " refs/heads/main\x00report-status side-band-64k atomic delete-refs object-format=" + algo.String() + "\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - Hook: func(ctx context.Context, req receivepack.HookRequest) ([]receivepack.UpdateDecision, error) { - _, err := io.WriteString(req.IO.Progress, "hook says hello\n") - if err != nil { - return nil, err - } - - return []receivepack.UpdateDecision{{Accept: true}}, nil - }, - }) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - _, sidebandWire, ok := strings.Cut(output.String(), "0000") - if !ok { - t.Fatalf("output missing advertisement flush: %q", output.String()) - } - - dec := sideband64k.NewDecoder(strings.NewReader(sidebandWire), sideband64k.ReadOptions{}) - - sawHookProgress := false - - var frame sideband64k.Frame - - for { - var err error - - frame, err = dec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame: %v", err) - } - - if frame.Type == sideband64k.FrameProgress && string(frame.Payload) == "hook says hello\n" { - sawHookProgress = true - } - - if frame.Type == sideband64k.FrameData { - break - } - } - - if !sawHookProgress { - t.Fatal("missing hook progress frame") - } - - statusDec := pktline.NewDecoder(strings.NewReader(string(frame.Payload)), pktline.ReadOptions{}) - - statusFrame, err := statusDec.ReadFrame() - if err != nil { - t.Fatalf("ReadFrame(status unpack): %v", err) - } - - if statusFrame.Type != pktline.PacketData || string(statusFrame.Payload) != "unpack ok\n" { - t.Fatalf("status frame = %#v", statusFrame) - } - }) -} - -func TestReceivePackPredefinedRejectForcePushHookRejectsNonFastForward(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - _, treeID := testRepo.MakeSingleFileTree(t, "base.txt", []byte("base\n")) - baseID := testRepo.CommitTree(t, treeID, "base") - currentID := testRepo.CommitTree(t, treeID, "current", baseID) - forcedID := testRepo.CommitTree(t, treeID, "forced", baseID) - testRepo.UpdateRef(t, "refs/heads/main", currentID) - - repo := testRepo.OpenRepository(t) - objectsRoot := testRepo.OpenObjectsRoot(t) - packStream := testRepo.PackObjectsReader(t, []string{forcedID.String(), "^" + currentID.String()}, false) - t.Cleanup(func() { - _ = packStream.Close() - }) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - currentID.String() + " " + forcedID.String() + " refs/heads/main\x00report-status atomic object-format=" + algo.String() + "\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack( - context.Background(), - &output, - io.MultiReader(strings.NewReader(input.String()), packStream), - receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - ObjectsRoot: objectsRoot, - Hook: receivepackhooks.RejectForcePush(), - }, - ) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - got := output.String() - if !strings.Contains(got, "ng refs/heads/main non-fast-forward\n") { - t.Fatalf("unexpected receive-pack output %q", got) - } - - resolved, err := repo.Refs().ResolveToDetached("refs/heads/main") - if err != nil { - t.Fatalf("ResolveToDetached(main): %v", err) - } - - if resolved.ID != currentID { - t.Fatalf("refs/heads/main = %s, want %s", resolved.ID, currentID) - } - }) -} - -func TestReceivePackReportStatusV2IncludesRefDetails(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := testRepo.MakeCommit(t, "base") - testRepo.UpdateRef(t, "refs/heads/main", commitID) - - repo := testRepo.OpenRepository(t) - - var ( - input strings.Builder - output bufferWriteFlusher - ) - - input.WriteString(pktlineData( - commitID.String() + " " + objectid.Zero(algo).String() + " refs/heads/main\x00report-status-v2 atomic delete-refs object-format=" + algo.String() + "\n", - )) - input.WriteString("0000") - - err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - }) - if err != nil { - t.Fatalf("ReceivePack: %v", err) - } - - got := output.String() - if !strings.Contains(got, "option refname refs/heads/main\n") { - t.Fatalf("missing option refname in %q", got) - } - - if !strings.Contains(got, "option old-oid "+commitID.String()+"\n") { - t.Fatalf("missing option old-oid in %q", got) - } - - if !strings.Contains(got, "option new-oid "+objectid.Zero(algo).String()+"\n") { - t.Fatalf("missing option new-oid in %q", got) - } - }) -} - -func TestReceivePackGitPushCreatesBranch(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - t.Parallel() - - sender := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - _, _, commitID := sender.MakeCommit(t, "pushed commit") - sender.UpdateRef(t, "refs/heads/main", commitID) - - receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - repo := receiver.OpenRepository(t) - objectsRoot := receiver.OpenObjectsRoot(t) - - stdout, stderr, clientErr, serverErr := runGitPushFD( - t, - sender, - receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - ObjectsRoot: objectsRoot, - }, - "push", "--porcelain", "fd::3,4/test", "refs/heads/main:refs/heads/main", - ) - if clientErr != nil { - t.Fatalf("git push failed: %v\nstdout=%s\nstderr=%s", clientErr, stdout, stderr) - } - - if serverErr != nil { - t.Fatalf("ReceivePack: %v", serverErr) - } - - resolved, err := receiver.OpenRepository(t).Refs().ResolveToDetached("refs/heads/main") - if err != nil { - t.Fatalf("ResolveToDetached(main): %v", err) - } - - if resolved.ID != commitID { - t.Fatalf("refs/heads/main = %s, want %s", resolved.ID, commitID) - } - }) -} - -func TestReceivePackGitPushRefUpdateWithoutNewObjectsSucceeds(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - t.Parallel() - - sender := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - blobID, treeID := sender.MakeSingleFileTree(t, "base.txt", []byte("base\n")) - commitID := sender.CommitTree(t, treeID, "base") - sender.UpdateRef(t, "refs/heads/main", commitID) - - receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - receiver.HashObject(t, "blob", sender.RunBytes(t, "cat-file", "blob", blobID.String())) - receiver.HashObject(t, "tree", sender.RunBytes(t, "cat-file", "tree", treeID.String())) - receiver.HashObject(t, "commit", sender.RunBytes(t, "cat-file", "commit", commitID.String())) - receiver.UpdateRef(t, "refs/heads/main", commitID) - - repo := receiver.OpenRepository(t) - objectsRoot := receiver.OpenObjectsRoot(t) - - stdout, stderr, clientErr, serverErr := runGitPushFD( - t, - sender, - receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - ObjectsRoot: objectsRoot, - }, - "push", "--porcelain", "fd::3,4/test", "refs/heads/main:refs/heads/topic", - ) - if clientErr != nil { - t.Fatalf("git push failed: %v\nstdout=%s\nstderr=%s", clientErr, stdout, stderr) - } - - if serverErr != nil { - t.Fatalf("ReceivePack: %v", serverErr) - } - - resolved, err := receiver.OpenRepository(t).Refs().ResolveToDetached("refs/heads/topic") - if err != nil { - t.Fatalf("ResolveToDetached(topic): %v", err) - } - - if resolved.ID != commitID { - t.Fatalf("refs/heads/topic = %s, want %s", resolved.ID, commitID) - } - - packs := receiver.Run(t, "count-objects", "-v") - if !strings.Contains(packs, "packs: 0") { - t.Fatalf("count-objects output shows unexpected promoted pack: %q", packs) - } - }) -} - -func TestReceivePackGitPushAtomicDelete(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - t.Parallel() - - sender := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - _, _, commitID := receiver.MakeCommit(t, "base") - receiver.UpdateRef(t, "refs/heads/main", commitID) - - repo := receiver.OpenRepository(t) - - stdout, stderr, clientErr, serverErr := runGitPushFD( - t, - sender, - receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - }, - "push", "--porcelain", "--atomic", "fd::3,4/test", ":refs/heads/main", - ) - if clientErr != nil { - t.Fatalf("git push failed: %v\nstdout=%s\nstderr=%s", clientErr, stdout, stderr) - } - - if serverErr != nil { - t.Fatalf("ReceivePack: %v", serverErr) - } - - _, err := receiver.OpenRepository(t).Refs().Resolve("refs/heads/main") - if err == nil { - t.Fatal("refs/heads/main still exists after delete push") - } - }) -} - -func TestReceivePackGitPushRejectsForcedUpdateViaHook(t *testing.T) { - t.Parallel() - - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { //nolint:thelper - t.Parallel() - - sender := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo}) - blobID, treeID := sender.MakeSingleFileTree(t, "base.txt", []byte("base\n")) - baseID := sender.CommitTree(t, treeID, "base") - currentID := sender.CommitTree(t, treeID, "current", baseID) - forcedID := sender.CommitTree(t, treeID, "forced", baseID) - sender.UpdateRef(t, "refs/heads/main", forcedID) - - receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true}) - receiver.HashObject(t, "blob", sender.RunBytes(t, "cat-file", "blob", blobID.String())) - receiver.HashObject(t, "tree", sender.RunBytes(t, "cat-file", "tree", treeID.String())) - receiver.HashObject(t, "commit", sender.RunBytes(t, "cat-file", "commit", baseID.String())) - receiver.HashObject(t, "commit", sender.RunBytes(t, "cat-file", "commit", currentID.String())) - receiver.UpdateRef(t, "refs/heads/main", currentID) - - repo := receiver.OpenRepository(t) - objectsRoot := receiver.OpenObjectsRoot(t) - - stdout, stderr, clientErr, serverErr := runGitPushFD( - t, - sender, - receivepack.Options{ - Algorithm: algo, - Refs: repo.Refs(), - ExistingObjects: repo.Objects(), - ObjectsRoot: objectsRoot, - Hook: receivepackhooks.RejectForcePush(), - }, - "push", "--porcelain", "--force", "fd::3,4/test", "refs/heads/main:refs/heads/main", - ) - if clientErr == nil { - t.Fatalf("git push unexpectedly succeeded\nstdout=%s\nstderr=%s", stdout, stderr) - } - - if serverErr != nil { - t.Fatalf("ReceivePack: %v", serverErr) - } - - if !strings.Contains(stdout, "non-fast-forward") && !strings.Contains(stderr, "non-fast-forward") { - t.Fatalf("git push output missing non-fast-forward message\nstdout=%s\nstderr=%s", stdout, stderr) - } - - resolved, err := receiver.OpenRepository(t).Refs().ResolveToDetached("refs/heads/main") - if err != nil { - t.Fatalf("ResolveToDetached(main): %v", err) - } - - if resolved.ID != currentID { - t.Fatalf("refs/heads/main = %s, want %s", resolved.ID, currentID) - } - }) -} - -type bufferWriteFlusher struct { - strings.Builder -} - -func (bufferWriteFlusher) Flush() error { - return nil -} - -func pktlineData(payload string) string { - return fmt.Sprintf("%04x%s", len(payload)+4, payload) -} - -type fileWriteFlusher struct { - *os.File -} - -func (fileWriteFlusher) Flush() error { - return nil -} - -func runGitPushFD( - tb testing.TB, - sender *testgit.TestRepo, - opts receivepack.Options, - gitArgs ...string, -) (stdout string, stderr string, clientErr error, serverErr error) { - tb.Helper() - - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - serverRead, clientWrite, err := os.Pipe() - if err != nil { - tb.Fatalf("os.Pipe(serverRead/clientWrite): %v", err) - } - - clientRead, serverWrite, err := os.Pipe() - if err != nil { - tb.Fatalf("os.Pipe(clientRead/serverWrite): %v", err) - } - - tb.Cleanup(func() { - _ = serverRead.Close() - _ = clientWrite.Close() - _ = clientRead.Close() - _ = serverWrite.Close() - }) - - go func() { - <-ctx.Done() - - _ = serverRead.Close() - _ = clientWrite.Close() - _ = clientRead.Close() - _ = serverWrite.Close() - }() - - serverErrCh := make(chan error, 1) - - go func() { - defer func() { - _ = serverRead.Close() - _ = serverWrite.Close() - }() - - serverErrCh <- receivepack.ReceivePack( - ctx, - fileWriteFlusher{serverWrite}, - serverRead, - opts, - ) - }() - - stdoutBytes, stderrBytes, clientErr := sender.RunWithExtraFilesEnvContextE( - tb, - ctx, - nil, - []*os.File{clientRead, clientWrite}, - gitArgs..., - ) - _ = clientRead.Close() - _ = clientWrite.Close() - - serverErr = <-serverErrCh - - if ctx.Err() != nil { - tb.Fatalf( - "git push fd:: timed out\nstdout=%s\nstderr=%s\nclientErr=%v\nserverErr=%v", - stdoutBytes, - stderrBytes, - clientErr, - serverErr, - ) - } - - return string(stdoutBytes), string(stderrBytes), clientErr, serverErr -} |
