aboutsummaryrefslogtreecommitdiff
path: root/receivepack
diff options
context:
space:
mode:
authorGravatar Runxi Yu2026-03-07 22:00:51 +0800
committerGravatar Runxi Yu2026-03-07 22:03:26 +0800
commit7d0f942b3ae4903dded72a9524f6bd4ffa16feb9 (patch)
tree32cbfc7841dc59759618c07905675cc3ece1c5a1 /receivepack
parentprotocol/v0v1/server: Add ProgessWriter and ErrorWriter (diff)
signatureNo signature
receivepack: Add HookIO
Diffstat (limited to 'receivepack')
-rw-r--r--receivepack/hook.go11
-rw-r--r--receivepack/int_test.go132
-rw-r--r--receivepack/internal/service/hook.go7
-rw-r--r--receivepack/internal/service/options.go1
-rw-r--r--receivepack/internal/service/run_hook.go1
-rw-r--r--receivepack/receivepack.go12
6 files changed, 164 insertions, 0 deletions
diff --git a/receivepack/hook.go b/receivepack/hook.go
index c96911ac..bdb0b087 100644
--- a/receivepack/hook.go
+++ b/receivepack/hook.go
@@ -2,6 +2,7 @@ package receivepack
import (
"context"
+ "io"
"codeberg.org/lindenii/furgit/objectid"
"codeberg.org/lindenii/furgit/objectstore"
@@ -9,6 +10,11 @@ import (
"codeberg.org/lindenii/furgit/refstore"
)
+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
@@ -30,6 +36,7 @@ type HookRequest struct {
QuarantinedObjects objectstore.Store
Updates []RefUpdate
PushOptions []string
+ IO HookIO
}
// Hook decides whether each requested update should proceed.
@@ -60,6 +67,10 @@ func translateHook(hook Hook) service.Hook {
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
diff --git a/receivepack/int_test.go b/receivepack/int_test.go
index c2d260bb..ae8b71a7 100644
--- a/receivepack/int_test.go
+++ b/receivepack/int_test.go
@@ -7,9 +7,11 @@ import (
"strings"
"testing"
+ "codeberg.org/lindenii/furgit/format/sideband64k"
"codeberg.org/lindenii/furgit/internal/testgit"
"codeberg.org/lindenii/furgit/objectid"
receivepack "codeberg.org/lindenii/furgit/receivepack"
+ receivepackhooks "codeberg.org/lindenii/furgit/receivepack/hooks"
)
// TODO: actually test with send-pack
@@ -555,6 +557,136 @@ func TestReceivePackHookCanRejectSubsetOfNonAtomicDeleteOnlyPush(t *testing.T) {
})
}
+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{})
+
+ frame, err := dec.ReadFrame()
+ if err != nil {
+ t.Fatalf("ReadFrame(progress): %v", err)
+ }
+
+ if frame.Type != sideband64k.FrameProgress || string(frame.Payload) != "hook says hello\n" {
+ t.Fatalf("first frame = %#v", frame)
+ }
+
+ frame, err = dec.ReadFrame()
+ if err != nil {
+ t.Fatalf("ReadFrame(unpack): %v", err)
+ }
+
+ if frame.Type != sideband64k.FrameData || string(frame.Payload) != "unpack ok\n" {
+ t.Fatalf("second frame = %#v", frame)
+ }
+ })
+}
+
+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().ResolveFully("refs/heads/main")
+ if err != nil {
+ t.Fatalf("ResolveFully(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()
diff --git a/receivepack/internal/service/hook.go b/receivepack/internal/service/hook.go
index 85295e15..748a00b9 100644
--- a/receivepack/internal/service/hook.go
+++ b/receivepack/internal/service/hook.go
@@ -2,12 +2,18 @@ package service
import (
"context"
+ "io"
"codeberg.org/lindenii/furgit/objectid"
"codeberg.org/lindenii/furgit/objectstore"
"codeberg.org/lindenii/furgit/refstore"
)
+type HookIO struct {
+ Progress io.Writer
+ Error io.Writer
+}
+
type RefUpdate struct {
Name string
OldID objectid.ObjectID
@@ -25,6 +31,7 @@ type HookRequest struct {
QuarantinedObjects objectstore.Store
Updates []RefUpdate
PushOptions []string
+ IO HookIO
}
type Hook func(context.Context, HookRequest) ([]UpdateDecision, error)
diff --git a/receivepack/internal/service/options.go b/receivepack/internal/service/options.go
index d9f8a265..b8dda2f7 100644
--- a/receivepack/internal/service/options.go
+++ b/receivepack/internal/service/options.go
@@ -22,4 +22,5 @@ type Options struct {
ObjectsRoot *os.Root
PromotedObjectPermissions *PromotedObjectPermissions
Hook Hook
+ HookIO HookIO
}
diff --git a/receivepack/internal/service/run_hook.go b/receivepack/internal/service/run_hook.go
index cf934664..3c76906e 100644
--- a/receivepack/internal/service/run_hook.go
+++ b/receivepack/internal/service/run_hook.go
@@ -41,6 +41,7 @@ func (service *Service) runHook(
QuarantinedObjects: quarantinedObjects,
Updates: buildHookUpdates(commands),
PushOptions: append([]string(nil), req.PushOptions...),
+ IO: service.opts.HookIO,
})
if err != nil {
return nil, nil, nil, false, err.Error()
diff --git a/receivepack/receivepack.go b/receivepack/receivepack.go
index d1e54e58..4c1912cf 100644
--- a/receivepack/receivepack.go
+++ b/receivepack/receivepack.go
@@ -10,6 +10,14 @@ import (
"codeberg.org/lindenii/furgit/receivepack/internal/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.
func ReceivePack(
ctx context.Context,
@@ -75,6 +83,10 @@ func ReceivePack(
opts.PromotedObjectPermissions,
),
Hook: translateHook(opts.Hook),
+ HookIO: service.HookIO{
+ Progress: base.ProgressWriter(),
+ Error: base.ErrorWriter(),
+ },
})
result, err := svc.Execute(ctx, serviceReq)