diff options
| author | 2026-03-26 09:14:59 +0000 | |
|---|---|---|
| committer | 2026-03-26 09:14:59 +0000 | |
| commit | 3d25bda9d5da6814661828adabe8a09f9d01aefb (patch) | |
| tree | d034e28079333f85e5d7b96d921282eddd4798d6 /receivepack | |
| parent | object/id: Empty tree (diff) | |
| signature | No signature | |
network/receivepack: Rename from receivepack
Diffstat (limited to 'receivepack')
32 files changed, 0 insertions, 3031 deletions
diff --git a/receivepack/advertise.go b/receivepack/advertise.go deleted file mode 100644 index 0fa010bf..00000000 --- a/receivepack/advertise.go +++ /dev/null @@ -1,57 +0,0 @@ -package receivepack - -import ( - "errors" - - common "codeberg.org/lindenii/furgit/network/protocol/v0v1/server" - "codeberg.org/lindenii/furgit/ref" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -func advertisedRefs(opts Options) ([]common.AdvertisedRef, error) { - listed, err := opts.Refs.List("") - if err != nil { - return nil, err - } - - return buildAdvertisedRefs(opts, listed) -} - -func buildAdvertisedRefs(opts Options, listed []ref.Ref) ([]common.AdvertisedRef, error) { - refs := make([]common.AdvertisedRef, 0, len(listed)) - for _, entry := range listed { - switch resolved := entry.(type) { - case ref.Detached: - advertised := common.AdvertisedRef{ - Name: resolved.Name(), - ID: resolved.ID, - } - - if resolved.Peeled != nil { - advertised.Peeled = resolved.Peeled - } - - refs = append(refs, advertised) - case ref.Symbolic: - if resolved.Name() != "HEAD" { - continue - } - - head, err := opts.Refs.ResolveToDetached("HEAD") - if err != nil { - if errors.Is(err, refstore.ErrReferenceNotFound) { - continue - } - - return nil, err - } - - refs = append(refs, common.AdvertisedRef{ - Name: "HEAD", - ID: head.ID, - }) - } - } - - return refs, nil -} diff --git a/receivepack/capabilities_defaults.go b/receivepack/capabilities_defaults.go deleted file mode 100644 index 72c36c30..00000000 --- a/receivepack/capabilities_defaults.go +++ /dev/null @@ -1,17 +0,0 @@ -package receivepack - -import ( - "crypto/rand" -) - -func defaultAgent() string { - return "furgit" -} - -func defaultSessionID() string { - return "furgit-" + rand.Text() -} - -func defaultPushCertNonce() string { - return "furgit-" + rand.Text() -} diff --git a/receivepack/commands.go b/receivepack/commands.go deleted file mode 100644 index 9c38ec73..00000000 --- a/receivepack/commands.go +++ /dev/null @@ -1,19 +0,0 @@ -package receivepack - -import ( - protoreceive "codeberg.org/lindenii/furgit/network/protocol/v0v1/server/receivepack" - "codeberg.org/lindenii/furgit/receivepack/service" -) - -func translateCommands(commands []protoreceive.Command) []service.Command { - out := make([]service.Command, 0, len(commands)) - for _, command := range commands { - out = append(out, service.Command{ - OldID: command.OldID, - NewID: command.NewID, - Name: command.Name, - }) - } - - return out -} diff --git a/receivepack/doc.go b/receivepack/doc.go deleted file mode 100644 index b63f49d5..00000000 --- a/receivepack/doc.go +++ /dev/null @@ -1,3 +0,0 @@ -// Package receivepack provides the application-facing server-side push entry -// point. -package receivepack diff --git a/receivepack/errors.go b/receivepack/errors.go deleted file mode 100644 index 18e7a135..00000000 --- a/receivepack/errors.go +++ /dev/null @@ -1,15 +0,0 @@ -package receivepack - -import "errors" - -var ( - // ErrMissingAlgorithm reports one missing repository hash algorithm. - ErrMissingAlgorithm = errors.New("receivepack: missing object id algorithm") - // ErrMissingRefs reports one missing reference store dependency. - ErrMissingRefs = errors.New("receivepack: missing refs store") - // ErrMissingObjects reports one missing object store dependency. - ErrMissingObjects = errors.New("receivepack: missing objects store") - // ErrUnsupportedProtocol reports one unsupported requested Git protocol - // version. - ErrUnsupportedProtocol = errors.New("receivepack: unsupported protocol version") -) diff --git a/receivepack/hook.go b/receivepack/hook.go deleted file mode 100644 index 431fd66f..00000000 --- a/receivepack/hook.go +++ /dev/null @@ -1,93 +0,0 @@ -package receivepack - -import ( - "context" - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstorer "codeberg.org/lindenii/furgit/object/storer" - "codeberg.org/lindenii/furgit/receivepack/service" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -type HookIO struct { - Progress io.Writer - Error io.Writer -} - -// RefUpdate is one requested reference update presented to a receive-pack hook. -type RefUpdate struct { - Name string - OldID objectid.ObjectID - NewID objectid.ObjectID -} - -// UpdateDecision is one hook decision for a requested reference update. -type UpdateDecision struct { - Accept bool - Message string -} - -// HookRequest is the input presented to a receive-pack hook before quarantine -// promotion and ref updates. -// -// Refs, ExistingObjects, and QuarantinedObjects are borrowed and are only -// valid for the duration of the hook call. -type HookRequest struct { - Refs refstore.ReadingStore - ExistingObjects objectstorer.Store - QuarantinedObjects objectstorer.Store - Updates []RefUpdate - PushOptions []string - IO HookIO -} - -// Hook decides whether each requested update should proceed. -// -// The hook runs after pack ingestion into quarantine and before quarantine -// promotion or ref updates. The returned decisions must have the same length as -// HookRequest.Updates. Hook borrows the data and stores in HookRequest only for -// the duration of the call. -type Hook func(context.Context, HookRequest) ([]UpdateDecision, error) - -func translateHook(hook Hook) service.Hook { - if hook == nil { - return nil - } - - return func(ctx context.Context, req service.HookRequest) ([]service.UpdateDecision, error) { - translatedUpdates := make([]RefUpdate, 0, len(req.Updates)) - for _, update := range req.Updates { - translatedUpdates = append(translatedUpdates, RefUpdate{ - Name: update.Name, - OldID: update.OldID, - NewID: update.NewID, - }) - } - - decisions, err := hook(ctx, HookRequest{ - Refs: req.Refs, - ExistingObjects: req.ExistingObjects, - QuarantinedObjects: req.QuarantinedObjects, - Updates: translatedUpdates, - PushOptions: append([]string(nil), req.PushOptions...), - IO: HookIO{ - Progress: req.IO.Progress, - Error: req.IO.Error, - }, - }) - if err != nil { - return nil, err - } - - out := make([]service.UpdateDecision, 0, len(decisions)) - for _, decision := range decisions { - out = append(out, service.UpdateDecision{ - Accept: decision.Accept, - Message: decision.Message, - }) - } - - return out, nil - } -} diff --git a/receivepack/hooks/chain.go b/receivepack/hooks/chain.go deleted file mode 100644 index 4ce65064..00000000 --- a/receivepack/hooks/chain.go +++ /dev/null @@ -1,51 +0,0 @@ -package hooks - -import ( - "context" - "fmt" - - receivepack "codeberg.org/lindenii/furgit/receivepack" -) - -// Chain combines hooks by running them in order and intersecting their -// decisions. The first rejecting message for each update is preserved. -func Chain(hooks ...receivepack.Hook) receivepack.Hook { - return func( - ctx context.Context, - req receivepack.HookRequest, - ) ([]receivepack.UpdateDecision, error) { - decisions := make([]receivepack.UpdateDecision, len(req.Updates)) - for i := range decisions { - decisions[i].Accept = true - } - - for _, hook := range hooks { - if hook == nil { - continue - } - - hookDecisions, err := hook(ctx, req) - if err != nil { - return nil, err - } - - if len(hookDecisions) != len(req.Updates) { - return nil, fmt.Errorf("hook returned %d decisions for %d updates", len(hookDecisions), len(req.Updates)) - } - - for i, decision := range hookDecisions { - if decision.Accept { - continue - } - - if decisions[i].Accept { - decisions[i].Message = decision.Message - } - - decisions[i].Accept = false - } - } - - return decisions, nil - } -} diff --git a/receivepack/hooks/doc.go b/receivepack/hooks/doc.go deleted file mode 100644 index bef2baf9..00000000 --- a/receivepack/hooks/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package hooks provides a few pre-defined hooks that callers might find useful. -package hooks diff --git a/receivepack/hooks/reject_force_push.go b/receivepack/hooks/reject_force_push.go deleted file mode 100644 index e6b112ea..00000000 --- a/receivepack/hooks/reject_force_push.go +++ /dev/null @@ -1,64 +0,0 @@ -package hooks - -import ( - "context" - "errors" - "fmt" - - "codeberg.org/lindenii/furgit/commitquery" - objectid "codeberg.org/lindenii/furgit/object/id" - objectmix "codeberg.org/lindenii/furgit/object/storer/mix" - receivepack "codeberg.org/lindenii/furgit/receivepack" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -// RejectForcePush rejects updates whose new value is not a fast-forward of the -// currently resolved reference. -func RejectForcePush() receivepack.Hook { - return func( - ctx context.Context, - req receivepack.HookRequest, - ) ([]receivepack.UpdateDecision, error) { - _ = ctx - - objects := objectmix.New(req.QuarantinedObjects, req.ExistingObjects) - - decisions := make([]receivepack.UpdateDecision, len(req.Updates)) - for i := range decisions { - decisions[i].Accept = true - } - - for i, update := range req.Updates { - if update.OldID == objectid.Zero(update.OldID.Algorithm()) || update.NewID == objectid.Zero(update.NewID.Algorithm()) { - continue - } - - current, err := req.Refs.ResolveToDetached(update.Name) - switch { - case err == nil: - case errors.Is(err, refstore.ErrReferenceNotFound): - continue - default: - return nil, fmt.Errorf("resolve %s: %w", update.Name, err) - } - - if current.ID == update.NewID { - continue - } - - ok, err := commitquery.New(objects, nil).IsAncestor(current.ID, update.NewID) - if err != nil { - return nil, fmt.Errorf("check fast-forward %s: %w", update.Name, err) - } - - if !ok { - decisions[i] = receivepack.UpdateDecision{ - Accept: false, - Message: "non-fast-forward", - } - } - } - - return decisions, nil - } -} 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 -} diff --git a/receivepack/options.go b/receivepack/options.go deleted file mode 100644 index 139c3839..00000000 --- a/receivepack/options.go +++ /dev/null @@ -1,68 +0,0 @@ -package receivepack - -import ( - "os" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstorer "codeberg.org/lindenii/furgit/object/storer" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -// Options configures one receive-pack invocation. -// -// ReceivePack borrows all configured dependencies. -// -// Refs and ExistingObjects are required and must be non-nil. -// ObjectsRoot is required if the invocation may need to ingest or promote a -// pack. -type Options struct { - // GitProtocol is the raw Git protocol version string from the transport, - // such as "version=1". - GitProtocol string - // Algorithm is the repository object ID algorithm used by the push session. - Algorithm objectid.Algorithm - // Refs is the reference store visible to the push. - Refs refstore.ReadWriteStore - // ExistingObjects is the object store visible to the push before any newly - // uploaded quarantined objects are promoted. - ExistingObjects objectstorer.Store - // ObjectsRoot is the permanent object storage root beneath which per-push - // quarantine directories are derived. - ObjectsRoot *os.Root - // PromotedObjectPermissions, when non-nil, is applied to objects and - // directories moved from quarantine into the permanent object store. - PromotedObjectPermissions *PromotedObjectPermissions - // Hook, when non-nil, runs after pack ingestion into quarantine and before - // quarantine promotion or ref updates. Hook is borrowed for the duration of - // ReceivePack. - Hook Hook - // Agent is the receive-pack agent string advertised via capability. - // - // When empty, ReceivePack derives one from build info and falls back to - // "furgit". - Agent string - // SessionID is the advertised receive-pack session-id capability value. - // - // When empty, ReceivePack generates one random value per invocation. - SessionID string - // PushCertNonce is the advertised push-cert nonce capability value. - // - // When empty, ReceivePack generates one random value per invocation. - PushCertNonce string -} - -func validateOptions(opts Options) error { - if opts.Algorithm == 0 { - return ErrMissingAlgorithm - } - - if opts.Refs == nil { - return ErrMissingRefs - } - - if opts.ExistingObjects == nil { - return ErrMissingObjects - } - - return nil -} diff --git a/receivepack/permissions.go b/receivepack/permissions.go deleted file mode 100644 index 55eb5390..00000000 --- a/receivepack/permissions.go +++ /dev/null @@ -1,27 +0,0 @@ -package receivepack - -import ( - "io/fs" - - "codeberg.org/lindenii/furgit/receivepack/service" -) - -// PromotedObjectPermissions configures the destination permissions applied to -// objects and directories promoted out of quarantine. -type PromotedObjectPermissions struct { - DirMode fs.FileMode - FileMode fs.FileMode -} - -func translatePromotedObjectPermissions( - perms *PromotedObjectPermissions, -) *service.PromotedObjectPermissions { - if perms == nil { - return nil - } - - return &service.PromotedObjectPermissions{ - DirMode: perms.DirMode, - FileMode: perms.FileMode, - } -} diff --git a/receivepack/receivepack.go b/receivepack/receivepack.go deleted file mode 100644 index d6969bf3..00000000 --- a/receivepack/receivepack.go +++ /dev/null @@ -1,147 +0,0 @@ -package receivepack - -import ( - "context" - "io" - - "codeberg.org/lindenii/furgit/network/protocol/pktline" - common "codeberg.org/lindenii/furgit/network/protocol/v0v1/server" - protoreceive "codeberg.org/lindenii/furgit/network/protocol/v0v1/server/receivepack" - "codeberg.org/lindenii/furgit/receivepack/service" -) - -// TODO: Some more designing to do. In particular, we'd like to have access to -// commit graphs and stored object abstractions and such here, especially because -// hooks might want to access full repos, but we risk creating -// circular dependencies if we import repository/ here. Might need an interface-ish -// design, but that risks being over-complicated. -// Theoretically we could also just give the hooks an os.Root but that -// feels a bit ugly. - -// ReceivePack serves one receive-pack session over r/w. -// -// ReceivePack borrows r, w, and all dependencies reachable through opts for -// the duration of the call. It does not close any of them. -func ReceivePack( - ctx context.Context, - w pktline.WriteFlusher, - r io.Reader, - opts Options, -) error { - err := validateOptions(opts) - if err != nil { - return err - } - - version := parseVersion(opts.GitProtocol) - - base := common.NewSession(r, w, common.Options{ - Version: version, - Algorithm: opts.Algorithm, - }) - - agent := opts.Agent - if agent == "" { - agent = defaultAgent() - } - - sessionID := opts.SessionID - if sessionID == "" { - sessionID = defaultSessionID() - } - - pushCertNonce := opts.PushCertNonce - if pushCertNonce == "" { - pushCertNonce = defaultPushCertNonce() - } - - protoSession := protoreceive.NewSession(base, protoreceive.Capabilities{ - ReportStatus: true, - ReportStatusV2: true, - DeleteRefs: true, - SideBand64K: true, - Quiet: true, - Atomic: true, - OfsDelta: true, - PushOptions: true, - PushCertNonce: pushCertNonce, - SessionID: sessionID, - ObjectFormat: opts.Algorithm, - Agent: agent, - }) - - refs, err := advertisedRefs(opts) - if err != nil { - return err - } - - err = protoSession.AdvertiseRefs(common.Advertisement{Refs: refs}) - if err != nil { - return err - } - - err = base.FlushIO() - if err != nil { - return err - } - - req, err := protoSession.ReadRequest() - if err != nil { - return err - } - - progressWriter := protoSession.ProgressWriter() - progressFlush := base.FlushIO - - if req.Capabilities.Quiet { - progressWriter = io.Discard - progressFlush = nil - } - - serviceReq := &service.Request{ - Commands: translateCommands(req.Commands), - PushOptions: append([]string(nil), req.PushOptions...), - Atomic: req.Capabilities.Atomic, - DeleteOnly: req.DeleteOnly, - PackExpected: req.PackExpected, - Pack: r, - } - - svc := service.New(service.Options{ - Algorithm: opts.Algorithm, - Refs: opts.Refs, - ExistingObjects: opts.ExistingObjects, - ObjectsRoot: opts.ObjectsRoot, - Progress: progressWriter, - ProgressFlush: progressFlush, - PromotedObjectPermissions: translatePromotedObjectPermissions( - opts.PromotedObjectPermissions, - ), - Hook: translateHook(opts.Hook), - HookIO: service.HookIO{ - Progress: progressWriter, - Error: protoSession.ErrorWriter(), - }, - }) - - result, err := svc.Execute(ctx, serviceReq) - if err != nil { - return err - } - - protoResult := translateResult(result) - - if req.Capabilities.ReportStatusV2 { - err = protoSession.WriteReportStatusV2(protoResult) - if err != nil { - return err - } - } else if req.Capabilities.ReportStatus { - err = protoSession.WriteReportStatus(protoResult) - if err != nil { - return err - } - } - - return base.FlushIO() -} diff --git a/receivepack/results.go b/receivepack/results.go deleted file mode 100644 index ce31ddcd..00000000 --- a/receivepack/results.go +++ /dev/null @@ -1,26 +0,0 @@ -package receivepack - -import ( - protoreceive "codeberg.org/lindenii/furgit/network/protocol/v0v1/server/receivepack" - "codeberg.org/lindenii/furgit/receivepack/service" -) - -func translateResult(result *service.Result) protoreceive.ReportStatusResult { - out := protoreceive.ReportStatusResult{ - UnpackError: result.UnpackError, - Commands: make([]protoreceive.CommandResult, 0, len(result.Commands)), - } - - for _, command := range result.Commands { - out.Commands = append(out.Commands, protoreceive.CommandResult{ - Name: command.Name, - Error: command.Error, - RefName: command.RefName, - OldID: command.OldID, - NewID: command.NewID, - ForcedUpdate: command.ForcedUpdate, - }) - } - - return out -} diff --git a/receivepack/service/apply.go b/receivepack/service/apply.go deleted file mode 100644 index 8fa500ca..00000000 --- a/receivepack/service/apply.go +++ /dev/null @@ -1,134 +0,0 @@ -package service - -import ( - "codeberg.org/lindenii/furgit/internal/utils" - objectid "codeberg.org/lindenii/furgit/object/id" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -func (service *Service) applyAtomic(result *Result, commands []Command) error { - total := len(commands) - utils.BestEffortFprintf(service.opts.Progress, "updating refs: 0/%d\r", total) - - tx, err := service.opts.Refs.BeginTransaction() - if err != nil { - return err - } - - for i, command := range commands { - err = queueWriteTransaction(tx, command) - if err != nil { - _ = tx.Abort() - - fillCommandErrors(result, commands, err.Error()) - utils.BestEffortFprintf(service.opts.Progress, "updating refs: failed at %d/%d.\n", i+1, total) - - return nil - } - - utils.BestEffortFprintf(service.opts.Progress, "updating refs: %d/%d\r", i+1, total) - } - - err = tx.Commit() - if err != nil { - fillCommandErrors(result, commands, err.Error()) - utils.BestEffortFprintf(service.opts.Progress, "updating refs: failed at commit.\n") - - return nil - } - - result.Applied = true - for _, command := range commands { - result.Commands = append(result.Commands, successCommandResult(command)) - } - - utils.BestEffortFprintf(service.opts.Progress, "updating refs: done.\n") - - return nil -} - -func (service *Service) applyBatch(result *Result, commands []Command) error { - total := len(commands) - - utils.BestEffortFprintf(service.opts.Progress, "updating refs...\r") - - batch, err := service.opts.Refs.BeginBatch() - if err != nil { - return err - } - - for _, command := range commands { - queueWriteBatch(batch, command) - } - - batchResults, err := batch.Apply() - if err != nil && len(batchResults) == 0 { - utils.BestEffortFprintf(service.opts.Progress, "updating refs: failed at apply.\n") - - return err - } - - appliedAny := false - failedCount := 0 - - for i, command := range commands { - item := successCommandResult(command) - if i < len(batchResults) && batchResults[i].Error != nil { - item.Error = batchResults[i].Error.Error() - failedCount++ - } else { - appliedAny = true - } - - result.Commands = append(result.Commands, item) - - utils.BestEffortFprintf(service.opts.Progress, "updating refs: %d/%d\r", i+1, total) - } - - result.Applied = appliedAny - - if failedCount == 0 { - utils.BestEffortFprintf(service.opts.Progress, "updating refs: done.\n") - } else { - utils.BestEffortFprintf(service.opts.Progress, "updating refs: failed (%d/%d).\n", failedCount, total) - } - - return nil -} - -func queueWriteTransaction(tx refstore.Transaction, command Command) error { - if isDelete(command) { - return tx.Delete(command.Name, command.OldID) - } - - if command.OldID == objectid.Zero(command.OldID.Algorithm()) { - return tx.Create(command.Name, command.NewID) - } - - return tx.Update(command.Name, command.NewID, command.OldID) -} - -func queueWriteBatch(batch refstore.Batch, command Command) { - if isDelete(command) { - batch.Delete(command.Name, command.OldID) - - return - } - - if command.OldID == objectid.Zero(command.OldID.Algorithm()) { - batch.Create(command.Name, command.NewID) - - return - } - - batch.Update(command.Name, command.NewID, command.OldID) -} - -func successCommandResult(command Command) CommandResult { - return CommandResult{ - Name: command.Name, - RefName: command.Name, - OldID: objectIDPointer(command.OldID), - NewID: objectIDPointer(command.NewID), - } -} diff --git a/receivepack/service/command.go b/receivepack/service/command.go deleted file mode 100644 index 0fd8961e..00000000 --- a/receivepack/service/command.go +++ /dev/null @@ -1,32 +0,0 @@ -package service - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// Command is one protocol-independent requested ref update. -type Command struct { - OldID objectid.ObjectID - NewID objectid.ObjectID - Name string -} - -func fillCommandErrors(result *Result, commands []Command, errText string) { - for _, command := range commands { - result.Commands = append(result.Commands, CommandResult{ - Name: command.Name, - Error: errText, - RefName: command.Name, - OldID: objectIDPointer(command.OldID), - NewID: objectIDPointer(command.NewID), - }) - } -} - -func isDelete(command Command) bool { - return command.NewID == objectid.Zero(command.NewID.Algorithm()) -} - -func objectIDPointer(id objectid.ObjectID) *objectid.ObjectID { - out := id - - return &out -} diff --git a/receivepack/service/command_result.go b/receivepack/service/command_result.go deleted file mode 100644 index 37549f08..00000000 --- a/receivepack/service/command_result.go +++ /dev/null @@ -1,13 +0,0 @@ -package service - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// CommandResult is one per-command execution result. -type CommandResult struct { - Name string - Error string - RefName string - OldID *objectid.ObjectID - NewID *objectid.ObjectID - ForcedUpdate bool -} diff --git a/receivepack/service/doc.go b/receivepack/service/doc.go deleted file mode 100644 index 37be23f4..00000000 --- a/receivepack/service/doc.go +++ /dev/null @@ -1,6 +0,0 @@ -// Package service implements the protocol-independent receive-pack service. -// -// A Service borrows the stores, roots, hooks, and I/O endpoints supplied in -// Options. Callers retain ownership of those dependencies and must keep them -// valid for each Execute call that uses them. -package service diff --git a/receivepack/service/execute.go b/receivepack/service/execute.go deleted file mode 100644 index 9f373e0d..00000000 --- a/receivepack/service/execute.go +++ /dev/null @@ -1,123 +0,0 @@ -package service - -import ( - "context" - "os" - - "codeberg.org/lindenii/furgit/internal/utils" -) - -// Execute validates one receive-pack request, optionally ingests its pack into -// quarantine, runs the optional hook, and applies allowed ref updates. -func (service *Service) Execute(ctx context.Context, req *Request) (*Result, error) { - result := &Result{ - Commands: make([]CommandResult, 0, len(req.Commands)), - } - - var ( - quarantineName string - quarantineRoot *os.Root - err error - ) - - quarantineName, quarantineRoot, ok := service.ingestQuarantine(result, req.Commands, req) - if !ok { - return result, nil - } - - if quarantineRoot != nil { - defer func() { - _ = quarantineRoot.Close() - _ = service.opts.ObjectsRoot.RemoveAll(quarantineName) - }() - } - - for _, command := range req.Commands { - result.Planned = append(result.Planned, PlannedUpdate{ - Name: command.Name, - OldID: command.OldID, - NewID: command.NewID, - Delete: isDelete(command), - }) - } - - if len(req.Commands) == 0 { - return result, nil - } - - allowedCommands, allowedIndices, rejected, ok, errText := service.runHook( - ctx, - req, - req.Commands, - quarantineName, - ) - if !ok { - fillCommandErrors(result, req.Commands, errText) - - return result, nil - } - - if req.Atomic && len(rejected) != 0 { - result.Commands = make([]CommandResult, 0, len(req.Commands)) - for index, command := range req.Commands { - message := rejected[index] - if message == "" { - message = "atomic push rejected by hook" - } - - result.Commands = append(result.Commands, resultForHookRejection(command, message)) - } - - return result, nil - } - - if len(allowedCommands) == 0 { - result.Commands = mergeCommandResults(req.Commands, rejected, nil, nil) - - return result, nil - } - - if req.PackExpected && quarantineRoot != nil { - // Git migrates quarantined objects into permanent storage immediately - // before starting ref updates. - utils.BestEffortFprintf(service.opts.Progress, "promoting quarantine...\r") - - err = service.promoteQuarantine(quarantineName, quarantineRoot) - if err != nil { - utils.BestEffortFprintf(service.opts.Progress, "promoting quarantine: failed: %v.\n", err) - - result.UnpackError = err.Error() - fillCommandErrors(result, req.Commands, err.Error()) - - return result, nil - } - - utils.BestEffortFprintf(service.opts.Progress, "promoting quarantine: done.\n") - } - - if req.Atomic { - subresult := &Result{} - - err := service.applyAtomic(subresult, allowedCommands) - if err != nil { - return result, err - } - - result.Commands = mergeCommandResults(req.Commands, rejected, subresult.Commands, allowedIndices) - result.Applied = subresult.Applied - - return result, nil - } - - subresult := &Result{} - - err = service.applyBatch(subresult, allowedCommands) - if err != nil { - return result, err - } - - result.Commands = mergeCommandResults(req.Commands, rejected, subresult.Commands, allowedIndices) - result.Applied = subresult.Applied - - return result, nil -} diff --git a/receivepack/service/hook.go b/receivepack/service/hook.go deleted file mode 100644 index 750720dd..00000000 --- a/receivepack/service/hook.go +++ /dev/null @@ -1,45 +0,0 @@ -package service - -import ( - "context" - "io" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstorer "codeberg.org/lindenii/furgit/object/storer" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -type HookIO struct { - Progress io.Writer - Error io.Writer -} - -type RefUpdate struct { - Name string - OldID objectid.ObjectID - NewID objectid.ObjectID -} - -type UpdateDecision struct { - Accept bool - Message string -} - -// HookRequest is the borrowed view passed to one Hook invocation. -// -// Refs, ExistingObjects, and QuarantinedObjects are borrowed and are only -// valid for the duration of the hook call. -type HookRequest struct { - Refs refstore.ReadingStore - ExistingObjects objectstorer.Store - QuarantinedObjects objectstorer.Store - Updates []RefUpdate - PushOptions []string - IO HookIO -} - -// Hook is an optional per-request validation hook. -// -// Hook borrows the data and stores in HookRequest only for the duration of the -// call. -type Hook func(context.Context, HookRequest) ([]UpdateDecision, error) diff --git a/receivepack/service/hook_apply.go b/receivepack/service/hook_apply.go deleted file mode 100644 index 5bd8f596..00000000 --- a/receivepack/service/hook_apply.go +++ /dev/null @@ -1,44 +0,0 @@ -package service - -func buildHookUpdates(commands []Command) []RefUpdate { - updates := make([]RefUpdate, 0, len(commands)) - for _, command := range commands { - updates = append(updates, RefUpdate{ - Name: command.Name, - OldID: command.OldID, - NewID: command.NewID, - }) - } - - return updates -} - -func resultForHookRejection(command Command, message string) CommandResult { - result := successCommandResult(command) - result.Error = message - - return result -} - -func mergeCommandResults( - commands []Command, - rejected map[int]string, - applied []CommandResult, - appliedIndices []int, -) []CommandResult { - out := make([]CommandResult, len(commands)) - - for index, message := range rejected { - out[index] = resultForHookRejection(commands[index], message) - } - - for i, appliedResult := range applied { - if i >= len(appliedIndices) { - break - } - - out[appliedIndices[i]] = appliedResult - } - - return out -} diff --git a/receivepack/service/ingest_quarantine.go b/receivepack/service/ingest_quarantine.go deleted file mode 100644 index 8e3e2455..00000000 --- a/receivepack/service/ingest_quarantine.go +++ /dev/null @@ -1,144 +0,0 @@ -package service - -import ( - "os" - - "codeberg.org/lindenii/furgit/internal/utils" - "codeberg.org/lindenii/furgit/packfile/ingest" -) - -func (service *Service) ingestQuarantine( - result *Result, - commands []Command, - req *Request, -) (string, *os.Root, bool) { - if !req.PackExpected { - return "", nil, true - } - - if req.Pack == nil { - utils.BestEffortFprintf(service.opts.Progress, "unpack failed: missing pack stream.\n") - - result.UnpackError = "missing pack stream" - fillCommandErrors(result, commands, "missing pack stream") - - return "", nil, false - } - - if service.opts.ObjectsRoot == nil { - utils.BestEffortFprintf(service.opts.Progress, "unpack failed: objects root not configured.\n") - - result.UnpackError = "objects root not configured" - fillCommandErrors(result, commands, "objects root not configured") - - return "", nil, false - } - - var err error - - err = service.opts.ExistingObjects.Refresh() - if err != nil { - utils.BestEffortFprintf(service.opts.Progress, "unpack failed: refresh existing objects: %v.\n", err) - - result.UnpackError = err.Error() - fillCommandErrors(result, commands, err.Error()) - - return "", nil, false - } - - pending, err := ingest.Ingest( - req.Pack, - service.opts.Algorithm, - ingest.Options{ - FixThin: true, - WriteRev: true, - Base: service.opts.ExistingObjects, - Progress: service.opts.Progress, - ProgressFlush: service.opts.ProgressFlush, - }, - ) - if err != nil { - utils.BestEffortFprintf(service.opts.Progress, "unpack failed: %v.\n", err) - - result.UnpackError = err.Error() - fillCommandErrors(result, commands, err.Error()) - - return "", nil, false - } - - if pending.Header().ObjectCount == 0 { - discarded, err := pending.Discard() - if err != nil { - utils.BestEffortFprintf(service.opts.Progress, "unpack failed: %v.\n", err) - - result.UnpackError = err.Error() - fillCommandErrors(result, commands, err.Error()) - - return "", nil, false - } - - result.Ingest = &ingest.Result{ - PackHash: discarded.PackHash, - ObjectCount: discarded.ObjectCount, - } - - utils.BestEffortFprintf( - service.opts.Progress, - "unpacking: done (%d objects, %s).\n", - discarded.ObjectCount, - discarded.PackHash, - ) - - return "", nil, true - } - - utils.BestEffortFprintf(service.opts.Progress, "creating quarantine...\r") - - quarantineName, quarantineRoot, err := service.createQuarantineRoot() - if err != nil { - utils.BestEffortFprintf(service.opts.Progress, "unpack failed: %v.\n", err) - - result.UnpackError = err.Error() - fillCommandErrors(result, commands, err.Error()) - - return "", nil, false - } - - quarantinePackRoot, err := service.openQuarantinePackRoot(quarantineRoot) - if err != nil { - utils.BestEffortFprintf(service.opts.Progress, "unpack failed: %v.\n", err) - - result.UnpackError = err.Error() - fillCommandErrors(result, commands, err.Error()) - - _ = quarantineRoot.Close() - _ = service.opts.ObjectsRoot.RemoveAll(quarantineName) - - return "", nil, false - } - - utils.BestEffortFprintf(service.opts.Progress, "creating quarantine: done.\n") - utils.BestEffortFprintf(service.opts.Progress, "unpacking...\r") - - ingested, err := pending.Continue(quarantinePackRoot) - - _ = quarantinePackRoot.Close() - - if err != nil { - utils.BestEffortFprintf(service.opts.Progress, "unpack failed: %v.\n", err) - - result.UnpackError = err.Error() - fillCommandErrors(result, commands, err.Error()) - - _ = quarantineRoot.Close() - _ = service.opts.ObjectsRoot.RemoveAll(quarantineName) - - return "", nil, false - } - - utils.BestEffortFprintf(service.opts.Progress, "unpacking: done (%d objects, %s).\n", ingested.ObjectCount, ingested.PackHash) - - result.Ingest = &ingested - - return quarantineName, quarantineRoot, true -} diff --git a/receivepack/service/options.go b/receivepack/service/options.go deleted file mode 100644 index 7edf4f06..00000000 --- a/receivepack/service/options.go +++ /dev/null @@ -1,36 +0,0 @@ -package service - -import ( - "io" - "io/fs" - "os" - - objectid "codeberg.org/lindenii/furgit/object/id" - objectstorer "codeberg.org/lindenii/furgit/object/storer" - refstore "codeberg.org/lindenii/furgit/ref/store" -) - -type PromotedObjectPermissions struct { - DirMode fs.FileMode - FileMode fs.FileMode -} - -// Options configures one protocol-independent receive-pack service. -// -// Service borrows all configured dependencies. -// -// Refs and ExistingObjects are required and must be non-nil. -// ObjectsRoot is required if Execute may need to ingest or promote a pack. -// Progress, ProgressFlush, Hook, and HookIO are optional; when provided they -// are also borrowed for the duration of Execute. -type Options struct { - Algorithm objectid.Algorithm - Refs refstore.ReadWriteStore - ExistingObjects objectstorer.Store - ObjectsRoot *os.Root - Progress io.Writer - ProgressFlush func() error - PromotedObjectPermissions *PromotedObjectPermissions - Hook Hook - HookIO HookIO -} diff --git a/receivepack/service/quarantine.go b/receivepack/service/quarantine.go deleted file mode 100644 index 0bd98aeb..00000000 --- a/receivepack/service/quarantine.go +++ /dev/null @@ -1,274 +0,0 @@ -package service - -import ( - "bytes" - "crypto/rand" - "errors" - "fmt" - "io" - "io/fs" - "os" - "path" - "slices" -) - -// createQuarantineRoot creates one per-push quarantine directory beneath the -// permanent objects root. -// -// It returns both the quarantine directory name relative to ObjectsRoot and an -// opened root for that directory. Callers use the name for later promotion or -// removal relative to ObjectsRoot, and use the opened root for capability-based -// access within the quarantine itself. -func (service *Service) createQuarantineRoot() (string, *os.Root, error) { - name := "tmp_objdir-incoming-" + rand.Text() - - err := service.opts.ObjectsRoot.Mkdir(name, 0o700) - if err != nil { - return "", nil, err - } - - root, err := service.opts.ObjectsRoot.OpenRoot(name) - if err != nil { - _ = service.opts.ObjectsRoot.RemoveAll(name) - - return "", nil, err - } - - return name, root, nil -} - -func (service *Service) openQuarantinePackRoot(quarantineRoot *os.Root) (*os.Root, error) { - err := quarantineRoot.Mkdir("pack", 0o755) - if err != nil && !os.IsExist(err) { - return nil, err - } - - return quarantineRoot.OpenRoot("pack") -} - -func (service *Service) promoteQuarantine(quarantineName string, quarantineRoot *os.Root) error { - if quarantineName == "" || quarantineRoot == nil { - return nil - } - - return service.promoteQuarantineDir(quarantineName, quarantineRoot, ".") -} - -func (service *Service) promoteQuarantineDir(quarantineName string, quarantineRoot *os.Root, rel string) error { - entries, err := fs.ReadDir(quarantineRoot.FS(), rel) - if err != nil && !os.IsNotExist(err) { - return err - } - - slices.SortFunc(entries, func(left, right fs.DirEntry) int { - return packCopyPriority(left.Name()) - packCopyPriority(right.Name()) - }) - - for _, entry := range entries { - childRel := entry.Name() - if rel != "." { - childRel = path.Join(rel, entry.Name()) - } - - if entry.IsDir() { - err = service.opts.ObjectsRoot.Mkdir(childRel, 0o755) - if err != nil && !os.IsExist(err) { - return err - } - - err = service.applyPromotedDirectoryPermissions(childRel) - if err != nil { - return err - } - - err = service.promoteQuarantineDir(quarantineName, quarantineRoot, childRel) - if err != nil { - return err - } - - continue - } - - err = finalizeQuarantineFile( - service.opts.ObjectsRoot, - path.Join(quarantineName, childRel), - childRel, - isLooseObjectShardPath(rel), - service.opts.PromotedObjectPermissions, - ) - if err == nil { - continue - } - - return err - } - - return nil -} - -func packCopyPriority(name string) int { - if !pathHasPackPrefix(name) { - return 0 - } - - switch { - case path.Ext(name) == ".keep": - return 1 - case path.Ext(name) == ".pack": - return 2 - case path.Ext(name) == ".rev": - return 3 - case path.Ext(name) == ".idx": - return 4 - default: - return 5 - } -} - -func pathHasPackPrefix(name string) bool { - return len(name) >= 4 && name[:4] == "pack" -} - -func isLooseObjectShardPath(rel string) bool { - return len(rel) == 2 && isHex(rel[0]) && isHex(rel[1]) -} - -func isHex(ch byte) bool { - return ('0' <= ch && ch <= '9') || ('a' <= ch && ch <= 'f') || ('A' <= ch && ch <= 'F') -} - -func (service *Service) applyPromotedDirectoryPermissions(name string) error { - if service.opts.PromotedObjectPermissions == nil { - return nil - } - - return service.opts.ObjectsRoot.Chmod(name, service.opts.PromotedObjectPermissions.DirMode) -} - -func applyPromotedFilePermissions( - root *os.Root, - name string, - perms *PromotedObjectPermissions, -) error { - if perms == nil { - return nil - } - - return root.Chmod(name, perms.FileMode) -} - -func finalizeQuarantineFile( - root *os.Root, - src, dst string, - skipCollisionCheck bool, - perms *PromotedObjectPermissions, -) error { - const maxVanishedRetries = 5 - - for retries := 0; ; retries++ { - err := root.Link(src, dst) - switch { - case err == nil: - _ = root.Remove(src) - - return applyPromotedFilePermissions(root, dst, perms) - case !errors.Is(err, fs.ErrExist): - _, statErr := root.Stat(dst) - switch { - case statErr == nil: - err = fs.ErrExist - case errors.Is(statErr, fs.ErrNotExist): - renameErr := root.Rename(src, dst) - if renameErr == nil { - return applyPromotedFilePermissions(root, dst, perms) - } - - err = renameErr - default: - _ = root.Remove(src) - - return statErr - } - } - - if !errors.Is(err, fs.ErrExist) { - _ = root.Remove(src) - - return fmt.Errorf("promote quarantine %q -> %q: %w", src, dst, err) - } - - if skipCollisionCheck { - _ = root.Remove(src) - - return applyPromotedFilePermissions(root, dst, perms) - } - - equal, vanished, cmpErr := compareRootFiles(root, src, dst) - if vanished { - if retries >= maxVanishedRetries { - return fmt.Errorf("promote quarantine %q -> %q: destination repeatedly vanished", src, dst) - } - - continue - } - - if cmpErr != nil { - return cmpErr - } - - if !equal { - return fmt.Errorf("promote quarantine %q -> %q: files differ in contents", src, dst) - } - - _ = root.Remove(src) - - return applyPromotedFilePermissions(root, dst, perms) - } -} - -func compareRootFiles(root *os.Root, left, right string) (equal bool, vanished bool, err error) { - leftFile, err := root.Open(left) - if err != nil { - return false, false, err - } - - defer func() { - _ = leftFile.Close() - }() - - rightFile, err := root.Open(right) - if err != nil { - if errors.Is(err, fs.ErrNotExist) { - return false, true, nil - } - - return false, false, err - } - - defer func() { - _ = rightFile.Close() - }() - - var leftBuf, rightBuf [4096]byte - - for { - leftN, leftErr := leftFile.Read(leftBuf[:]) - rightN, rightErr := rightFile.Read(rightBuf[:]) - - if leftErr != nil && !errors.Is(leftErr, io.EOF) { - return false, false, leftErr - } - - if rightErr != nil && !errors.Is(rightErr, io.EOF) { - return false, false, rightErr - } - - if leftN != rightN || !bytes.Equal(leftBuf[:leftN], rightBuf[:rightN]) { - return false, false, nil - } - - if leftErr != nil || rightErr != nil { - return true, false, nil - } - } -} diff --git a/receivepack/service/quarantine_test.go b/receivepack/service/quarantine_test.go deleted file mode 100644 index 86299be2..00000000 --- a/receivepack/service/quarantine_test.go +++ /dev/null @@ -1,184 +0,0 @@ -package service //nolint:testpackage - -// because we need access to quarantine internals - -import ( - "os" - "path" - "testing" - - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/storer/memory" -) - -type quarantineFixture struct { - svc *Service - objectsRoot *os.Root - quarantineName string - quarantineRoot *os.Root -} - -func newQuarantineFixture(tb testing.TB, opts Options) *quarantineFixture { - tb.Helper() - - objectsRoot, err := os.OpenRoot(tb.TempDir()) - if err != nil { - tb.Fatalf("os.OpenRoot: %v", err) - } - - tb.Cleanup(func() { - _ = objectsRoot.Close() - }) - - opts.Algorithm = objectid.AlgorithmSHA1 - opts.ExistingObjects = memory.New(objectid.AlgorithmSHA1) - opts.ObjectsRoot = objectsRoot - - svc := New(opts) - - quarantineName, quarantineRoot, err := svc.createQuarantineRoot() - if err != nil { - tb.Fatalf("createQuarantineRoot: %v", err) - } - - tb.Cleanup(func() { - _ = quarantineRoot.Close() - _ = objectsRoot.RemoveAll(quarantineName) - }) - - return &quarantineFixture{ - svc: svc, - objectsRoot: objectsRoot, - quarantineName: quarantineName, - quarantineRoot: quarantineRoot, - } -} - -func writeMatchingPromotedFile( - tb testing.TB, - quarantineRoot, objectsRoot *os.Root, - dir, name, payload string, -) { - tb.Helper() - - err := quarantineRoot.Mkdir(dir, 0o755) - if err != nil { - tb.Fatalf("Mkdir(%s): %v", dir, err) - } - - err = objectsRoot.Mkdir(dir, 0o755) - if err != nil { - tb.Fatalf("Mkdir(dst %s): %v", dir, err) - } - - rel := path.Join(dir, name) - - err = quarantineRoot.WriteFile(rel, []byte(payload), 0o644) - if err != nil { - tb.Fatalf("WriteFile(quarantine %s): %v", rel, err) - } - - err = objectsRoot.WriteFile(rel, []byte(payload), 0o644) - if err != nil { - tb.Fatalf("WriteFile(permanent %s): %v", rel, err) - } -} - -func TestPromoteQuarantineAppliesConfiguredPermissions(t *testing.T) { - t.Parallel() - - fx := newQuarantineFixture(t, Options{ - PromotedObjectPermissions: &PromotedObjectPermissions{ - DirMode: 0o751, - FileMode: 0o640, - }, - }) - - err := fx.quarantineRoot.Mkdir("ab", 0o700) - if err != nil { - t.Fatalf("Mkdir(ab): %v", err) - } - - err = fx.quarantineRoot.WriteFile(path.Join("ab", "cdef"), []byte("payload"), 0o600) - if err != nil { - t.Fatalf("WriteFile(quarantine loose): %v", err) - } - - err = fx.svc.promoteQuarantine(fx.quarantineName, fx.quarantineRoot) - if err != nil { - t.Fatalf("promoteQuarantine: %v", err) - } - - dirInfo, err := fx.objectsRoot.Stat("ab") - if err != nil { - t.Fatalf("Stat(ab): %v", err) - } - - if got := dirInfo.Mode().Perm(); got != 0o751 { - t.Fatalf("dir mode = %o, want 751", got) - } - - fileInfo, err := fx.objectsRoot.Stat(path.Join("ab", "cdef")) - if err != nil { - t.Fatalf("Stat(ab/cdef): %v", err) - } - - if got := fileInfo.Mode().Perm(); got != 0o640 { - t.Fatalf("file mode = %o, want 640", got) - } -} - -func TestPromoteQuarantineTreatsExistingLooseObjectAsSuccess(t *testing.T) { - t.Parallel() - - fx := newQuarantineFixture(t, Options{}) - writeMatchingPromotedFile(t, fx.quarantineRoot, fx.objectsRoot, "ab", "cdef", "same object bytes") - - err := fx.svc.promoteQuarantine(fx.quarantineName, fx.quarantineRoot) - if err != nil { - t.Fatalf("promoteQuarantine: %v", err) - } -} - -func TestPromoteQuarantineRejectsDifferentExistingPackFile(t *testing.T) { - t.Parallel() - - fx := newQuarantineFixture(t, Options{}) - - err := fx.quarantineRoot.Mkdir("pack", 0o755) - if err != nil { - t.Fatalf("Mkdir(pack): %v", err) - } - - err = fx.objectsRoot.Mkdir("pack", 0o755) - if err != nil { - t.Fatalf("Mkdir(dst pack): %v", err) - } - - err = fx.quarantineRoot.WriteFile(path.Join("pack", "pack-a.pack"), []byte("new bytes"), 0o644) - if err != nil { - t.Fatalf("WriteFile(quarantine pack): %v", err) - } - - err = fx.objectsRoot.WriteFile(path.Join("pack", "pack-a.pack"), []byte("old bytes"), 0o644) - if err != nil { - t.Fatalf("WriteFile(permanent pack): %v", err) - } - - err = fx.svc.promoteQuarantine(fx.quarantineName, fx.quarantineRoot) - if err == nil { - t.Fatal("promoteQuarantine unexpectedly succeeded") - } -} - -func TestPromoteQuarantineAcceptsMatchingExistingPackFile(t *testing.T) { - t.Parallel() - - fx := newQuarantineFixture(t, Options{}) - writeMatchingPromotedFile(t, fx.quarantineRoot, fx.objectsRoot, "pack", "pack-a.pack", "identical pack bytes") - - err := fx.svc.promoteQuarantine(fx.quarantineName, fx.quarantineRoot) - if err != nil { - t.Fatalf("promoteQuarantine: %v", err) - } -} diff --git a/receivepack/service/request.go b/receivepack/service/request.go deleted file mode 100644 index 7f9bcc2f..00000000 --- a/receivepack/service/request.go +++ /dev/null @@ -1,16 +0,0 @@ -package service - -import "io" - -// Request is one protocol-independent receive-pack execution request. -// -// If PackExpected is true, Pack must be non-nil and remain valid until -// Execute finishes consuming it. -type Request struct { - Commands []Command - PushOptions []string - Atomic bool - DeleteOnly bool - PackExpected bool - Pack io.Reader -} diff --git a/receivepack/service/result.go b/receivepack/service/result.go deleted file mode 100644 index 17fc0b6b..00000000 --- a/receivepack/service/result.go +++ /dev/null @@ -1,14 +0,0 @@ -package service - -import ( - "codeberg.org/lindenii/furgit/packfile/ingest" -) - -// Result is one receive-pack execution result. -type Result struct { - UnpackError string - Commands []CommandResult - Ingest *ingest.Result - Planned []PlannedUpdate - Applied bool -} diff --git a/receivepack/service/run_hook.go b/receivepack/service/run_hook.go deleted file mode 100644 index 94467078..00000000 --- a/receivepack/service/run_hook.go +++ /dev/null @@ -1,168 +0,0 @@ -package service - -import ( - "context" - "os" - - "codeberg.org/lindenii/furgit/internal/utils" - objectstorer "codeberg.org/lindenii/furgit/object/storer" - "codeberg.org/lindenii/furgit/object/storer/loose" - objectmix "codeberg.org/lindenii/furgit/object/storer/mix" - "codeberg.org/lindenii/furgit/object/storer/packed" -) - -func (service *Service) runHook( - ctx context.Context, - req *Request, - commands []Command, - quarantineName string, -) ( - allowedCommands []Command, - allowedIndices []int, - rejected map[int]string, - ok bool, - errText string, -) { - allowedCommands = append([]Command(nil), commands...) - - allowedIndices = make([]int, 0, len(commands)) - for index := range commands { - allowedIndices = append(allowedIndices, index) - } - - rejected = make(map[int]string) - if service.opts.Hook == nil { - return allowedCommands, allowedIndices, rejected, true, "" - } - - utils.BestEffortFprintf(service.opts.Progress, "running hooks...\r") - - quarantinedObjects := service.opts.ExistingObjects - - var ( - quarantineObjectsStore objectstorer.Store - quarantineLooseStore *loose.Store - quarantinePackedStore *packed.Store - quarantineLooseRoot *os.Root - quarantinePackRoot *os.Root - err error - ) - - //nolint:nestif - if quarantineName != "" { - quarantineLooseRoot, err = service.opts.ObjectsRoot.OpenRoot(quarantineName) - if err != nil { - utils.BestEffortFprintf(service.opts.Progress, "running hooks: failed: %v.\n", err) - - return nil, nil, nil, false, err.Error() - } - - quarantineLooseStore, err = loose.New(quarantineLooseRoot, service.opts.Algorithm) - if err != nil { - _ = quarantineLooseRoot.Close() - - utils.BestEffortFprintf(service.opts.Progress, "running hooks: failed: %v.\n", err) - - return nil, nil, nil, false, err.Error() - } - - quarantineObjectsStore = quarantineLooseStore - quarantinedObjects = quarantineLooseStore - - quarantinePackRoot, err = quarantineLooseRoot.OpenRoot("pack") - if err == nil { - var packedErr error - - quarantinePackedStore, packedErr = packed.New(quarantinePackRoot, service.opts.Algorithm, packed.Options{}) - if packedErr != nil { - _ = quarantineLooseStore.Close() - _ = quarantinePackRoot.Close() - _ = quarantineLooseRoot.Close() - - utils.BestEffortFprintf(service.opts.Progress, "running hooks: failed: %v.\n", packedErr) - - return nil, nil, nil, false, packedErr.Error() - } - - quarantineObjectsStore = objectmix.New(quarantineLooseStore, quarantinePackedStore) - quarantinedObjects = quarantineObjectsStore - } else if !os.IsNotExist(err) { - _ = quarantineLooseStore.Close() - _ = quarantineLooseRoot.Close() - - utils.BestEffortFprintf(service.opts.Progress, "running hooks: failed: %v.\n", err) - - return nil, nil, nil, false, err.Error() - } - - defer func() { - if quarantineObjectsStore != nil { - _ = quarantineObjectsStore.Close() - } - - if quarantinePackedStore != nil { - _ = quarantinePackedStore.Close() - } - - if quarantineLooseStore != nil { - _ = quarantineLooseStore.Close() - } - - if quarantinePackRoot != nil { - _ = quarantinePackRoot.Close() - } - - if quarantineLooseRoot != nil { - _ = quarantineLooseRoot.Close() - } - }() - } - - decisions, err := service.opts.Hook(ctx, HookRequest{ - Refs: service.opts.Refs, - ExistingObjects: service.opts.ExistingObjects, - QuarantinedObjects: quarantinedObjects, - Updates: buildHookUpdates(commands), - PushOptions: append([]string(nil), req.PushOptions...), - IO: service.opts.HookIO, - }) - if err != nil { - utils.BestEffortFprintf(service.opts.Progress, "running hooks: failed: %v.\n", err) - - return nil, nil, nil, false, err.Error() - } - - if len(decisions) != len(commands) { - utils.BestEffortFprintf(service.opts.Progress, "running hooks: failed: wrong decision count.\n") - - return nil, nil, nil, false, "hook returned wrong number of update decisions" - } - - allowedCommands = allowedCommands[:0] - allowedIndices = allowedIndices[:0] - - for index, decision := range decisions { - if decision.Accept { - allowedCommands = append(allowedCommands, commands[index]) - allowedIndices = append(allowedIndices, index) - - continue - } - - message := decision.Message - if message == "" { - message = "rejected by hook" - } - - rejected[index] = message - } - - utils.BestEffortFprintf( - service.opts.Progress, - "running hooks: done (%d/%d accepted).\n", - len(allowedCommands), - len(commands), - ) - - return allowedCommands, allowedIndices, rejected, true, "" -} diff --git a/receivepack/service/service.go b/receivepack/service/service.go deleted file mode 100644 index a57fd354..00000000 --- a/receivepack/service/service.go +++ /dev/null @@ -1,16 +0,0 @@ -package service - -// Service executes protocol-independent receive-pack requests. -// -// Service borrows all dependencies supplied in Options. -type Service struct { - opts Options -} - -// New creates one receive-pack service. -// -// The returned service borrows opts and does not take ownership of any stores, -// roots, hooks, or I/O endpoints reachable through it. -func New(opts Options) *Service { - return &Service{opts: opts} -} diff --git a/receivepack/service/service_test.go b/receivepack/service/service_test.go deleted file mode 100644 index 74a242f8..00000000 --- a/receivepack/service/service_test.go +++ /dev/null @@ -1,99 +0,0 @@ -package service_test - -import ( - "context" - "io/fs" - "os" - "strings" - "testing" - - "codeberg.org/lindenii/furgit/internal/testgit" - objectid "codeberg.org/lindenii/furgit/object/id" - "codeberg.org/lindenii/furgit/object/storer/memory" - "codeberg.org/lindenii/furgit/receivepack/service" -) - -func TestExecutePackExpectedWithoutObjectsRoot(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - store := memory.New(algo) - svc := service.New(service.Options{ - Algorithm: algo, - ExistingObjects: store, - }) - - result, err := svc.Execute(context.Background(), &service.Request{ - Commands: []service.Command{{ - Name: "refs/heads/main", - OldID: objectid.Zero(algo), - NewID: objectid.Zero(algo), - }}, - PackExpected: true, - Pack: strings.NewReader("not a pack"), - }) - if err != nil { - t.Fatalf("Execute: %v", err) - } - - if result.UnpackError != "objects root not configured" { - t.Fatalf("unexpected unpack error %q", result.UnpackError) - } - }) -} - -func TestExecuteRemovesDerivedQuarantineAfterIngestFailure(t *testing.T) { - t.Parallel() - - //nolint:thelper - testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) { - t.Parallel() - - store := memory.New(algo) - objectsDir := t.TempDir() - - objectsRoot, err := os.OpenRoot(objectsDir) - if err != nil { - t.Fatalf("os.OpenRoot: %v", err) - } - - t.Cleanup(func() { - _ = objectsRoot.Close() - }) - - svc := service.New(service.Options{ - Algorithm: algo, - ExistingObjects: store, - ObjectsRoot: objectsRoot, - }) - - result, err := svc.Execute(context.Background(), &service.Request{ - Commands: []service.Command{{ - Name: "refs/heads/main", - OldID: objectid.Zero(algo), - NewID: objectid.Zero(algo), - }}, - PackExpected: true, - Pack: strings.NewReader("not a pack"), - }) - if err != nil { - t.Fatalf("Execute: %v", err) - } - - if result.UnpackError == "" { - t.Fatal("Execute returned empty unpack error for invalid pack") - } - - entries, err := fs.ReadDir(objectsRoot.FS(), ".") - if err != nil { - t.Fatalf("fs.ReadDir: %v", err) - } - - if len(entries) != 0 { - t.Fatalf("objects root still has entries after failed ingest: %d", len(entries)) - } - }) -} diff --git a/receivepack/service/update.go b/receivepack/service/update.go deleted file mode 100644 index 043f3d51..00000000 --- a/receivepack/service/update.go +++ /dev/null @@ -1,12 +0,0 @@ -package service - -import objectid "codeberg.org/lindenii/furgit/object/id" - -// PlannedUpdate is one ref update that would be applied once ref writing -// exists. -type PlannedUpdate struct { - Name string - OldID objectid.ObjectID - NewID objectid.ObjectID - Delete bool -} diff --git a/receivepack/version.go b/receivepack/version.go deleted file mode 100644 index 9a4544dc..00000000 --- a/receivepack/version.go +++ /dev/null @@ -1,35 +0,0 @@ -package receivepack - -import ( - "strings" - - common "codeberg.org/lindenii/furgit/network/protocol/v0v1/server" -) - -func parseVersion(gitProtocol string) common.Version { - if gitProtocol == "" { - return common.Version0 - } - - var highestRequested uint8 - - for field := range strings.SplitSeq(gitProtocol, ":") { - switch field { - case "version=0": - case "version=1": - if highestRequested < 1 { - highestRequested = 1 - } - case "version=2": - if highestRequested < 2 { - highestRequested = 2 - } - } - } - - if highestRequested == 1 { - return common.Version1 - } - - return common.Version0 -} |
