shithub: furgit

Download patch

ref: 5b8941986d4c3f398fc1fc2d1314e80510be346c
parent: 344d0c4d3c968506f5641da40fce581ea5bcdbbc
author: Runxi Yu <me@runxiyu.org>
date: Sat Mar 7 16:21:58 EST 2026

receivepack: Fix lint

--- a/receivepack/int_test.go
+++ b/receivepack/int_test.go
@@ -52,7 +52,8 @@
 			t.Fatalf("unexpected receive-pack output %q", got)
 		}
 
-		if _, err := repo.Refs().Resolve("refs/heads/main"); err == nil {
+		_, err = repo.Refs().Resolve("refs/heads/main")
+		if err == nil {
 			t.Fatal("refs/heads/main still exists after delete push")
 		}
 	})
@@ -101,11 +102,13 @@
 			t.Fatalf("unexpected receive-pack output %q", got)
 		}
 
-		if _, err := repo.Refs().Resolve("refs/heads/main"); err != nil {
+		_, err = repo.Refs().Resolve("refs/heads/main")
+		if err != nil {
 			t.Fatalf("Resolve(main): %v", err)
 		}
 
-		if _, err := repo.Refs().Resolve("refs/heads/topic"); err == nil {
+		_, err = repo.Refs().Resolve("refs/heads/topic")
+		if err == nil {
 			t.Fatal("refs/heads/topic still exists after successful delete")
 		}
 	})
@@ -154,11 +157,13 @@
 			t.Fatalf("unexpected receive-pack output %q", got)
 		}
 
-		if _, err := repo.Refs().Resolve("refs/heads/main"); err != nil {
+		_, err = repo.Refs().Resolve("refs/heads/main")
+		if err != nil {
 			t.Fatalf("Resolve(main): %v", err)
 		}
 
-		if _, err := repo.Refs().Resolve("refs/heads/topic"); err != nil {
+		_, err = repo.Refs().Resolve("refs/heads/topic")
+		if err != nil {
 			t.Fatalf("Resolve(topic): %v", err)
 		}
 	})
@@ -449,11 +454,13 @@
 						t.Fatalf("unexpected hook updates: %+v", req.Updates)
 					}
 
-					if _, _, err := req.ExistingObjects.ReadHeader(commitID); err == nil {
+					_, _, err := req.ExistingObjects.ReadHeader(commitID)
+					if err == nil {
 						t.Fatalf("existing objects unexpectedly contained quarantined commit %s", commitID)
 					}
 
-					if _, _, err := req.QuarantinedObjects.ReadHeader(commitID); err != nil {
+					_, _, err = req.QuarantinedObjects.ReadHeader(commitID)
+					if err != nil {
 						t.Fatalf("quarantined objects missing commit %s: %v", commitID, err)
 					}
 
@@ -477,7 +484,8 @@
 			t.Fatalf("unexpected receive-pack output %q", got)
 		}
 
-		if _, err := repo.Refs().Resolve("refs/heads/main"); err == nil {
+		_, err = repo.Refs().Resolve("refs/heads/main")
+		if err == nil {
 			t.Fatal("refs/heads/main exists after hook rejection")
 		}
 
@@ -535,11 +543,13 @@
 			t.Fatalf("unexpected receive-pack output %q", got)
 		}
 
-		if _, err := repo.Refs().Resolve("refs/heads/main"); err != nil {
+		_, err = repo.Refs().Resolve("refs/heads/main")
+		if err != nil {
 			t.Fatalf("Resolve(main): %v", err)
 		}
 
-		if _, err := repo.Refs().Resolve("refs/heads/topic"); err == nil {
+		_, err = repo.Refs().Resolve("refs/heads/topic")
+		if err == nil {
 			t.Fatal("refs/heads/topic still exists after successful delete")
 		}
 	})
--- a/receivepack/internal/service/apply.go
+++ b/receivepack/internal/service/apply.go
@@ -15,6 +15,7 @@
 		err = queueWriteTransaction(tx, command)
 		if err != nil {
 			_ = tx.Abort()
+
 			fillCommandErrors(result, commands, err.Error())
 
 			return nil
--- a/receivepack/internal/service/execute.go
+++ b/receivepack/internal/service/execute.go
@@ -3,8 +3,6 @@
 import (
 	"context"
 	"os"
-
-	"codeberg.org/lindenii/furgit/format/pack/ingest"
 )
 
 // Execute validates one receive-pack request, optionally ingests its pack into
@@ -13,6 +11,7 @@
 	result := &Result{
 		Commands: make([]CommandResult, 0, len(req.Commands)),
 	}
+
 	var (
 		quarantineName string
 		quarantineRoot *os.Root
@@ -19,62 +18,16 @@
 		err            error
 	)
 
-	if req.PackExpected {
-		if req.Pack == nil {
-			result.UnpackError = "missing pack stream"
-			fillCommandErrors(result, req.Commands, "missing pack stream")
+	quarantineName, quarantineRoot, ok := service.ingestQuarantine(result, req.Commands, req)
+	if !ok {
+		return result, nil
+	}
 
-			return result, nil
-		}
-
-		if service.opts.ObjectsRoot == nil {
-			result.UnpackError = "objects root not configured"
-			fillCommandErrors(result, req.Commands, "objects root not configured")
-
-			return result, nil
-		}
-
-		quarantineName, quarantineRoot, err = service.createQuarantineRoot()
-		if err != nil {
-			result.UnpackError = err.Error()
-			fillCommandErrors(result, req.Commands, err.Error())
-
-			return result, nil
-		}
-
+	if quarantineRoot != nil {
 		defer func() {
 			_ = quarantineRoot.Close()
 			_ = service.opts.ObjectsRoot.RemoveAll(quarantineName)
 		}()
-
-		quarantinePackRoot, err := service.openQuarantinePackRoot(quarantineRoot)
-		if err != nil {
-			result.UnpackError = err.Error()
-			fillCommandErrors(result, req.Commands, err.Error())
-
-			return result, nil
-		}
-
-		defer func() {
-			_ = quarantinePackRoot.Close()
-		}()
-
-		ingested, err := ingest.Ingest(
-			req.Pack,
-			quarantinePackRoot,
-			service.opts.Algorithm,
-			true,
-			true,
-			service.opts.ExistingObjects,
-		)
-		if err != nil {
-			result.UnpackError = err.Error()
-			fillCommandErrors(result, req.Commands, err.Error())
-
-			return result, nil
-		}
-
-		result.Ingest = &ingested
 	}
 
 	for _, command := range req.Commands {
@@ -90,75 +43,30 @@
 		return result, nil
 	}
 
-	allowedCommands := append([]Command(nil), req.Commands...)
-	allowedIndices := make([]int, 0, len(req.Commands))
-	for index := range req.Commands {
-		allowedIndices = append(allowedIndices, index)
+	allowedCommands, allowedIndices, rejected, ok, errText := service.runHook(
+		ctx,
+		req,
+		req.Commands,
+		quarantineName,
+	)
+	if !ok {
+		fillCommandErrors(result, req.Commands, errText)
+
+		return result, nil
 	}
-	rejected := make(map[int]string)
 
-	if service.opts.Hook != nil {
-		quarantinedObjects, err := service.openQuarantinedObjects(quarantineName)
-		if err != nil {
-			fillCommandErrors(result, req.Commands, err.Error())
-
-			return result, nil
-		}
-
-		defer func() {
-			_ = quarantinedObjects.Close()
-		}()
-
-		decisions, err := service.opts.Hook(ctx, HookRequest{
-			Refs:               service.opts.Refs,
-			ExistingObjects:    service.opts.ExistingObjects,
-			QuarantinedObjects: quarantinedObjects,
-			Updates:            buildHookUpdates(req.Commands),
-			PushOptions:        append([]string(nil), req.PushOptions...),
-		})
-		if err != nil {
-			fillCommandErrors(result, req.Commands, err.Error())
-
-			return result, nil
-		}
-
-		if len(decisions) != len(req.Commands) {
-			fillCommandErrors(result, req.Commands, "hook returned wrong number of update decisions")
-
-			return result, nil
-		}
-
-		allowedCommands = allowedCommands[:0]
-		allowedIndices = allowedIndices[:0]
-		for index, decision := range decisions {
-			if decision.Accept {
-				allowedCommands = append(allowedCommands, req.Commands[index])
-				allowedIndices = append(allowedIndices, index)
-
-				continue
-			}
-
-			message := decision.Message
+	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 = "rejected by hook"
+				message = "atomic push rejected by hook"
 			}
 
-			rejected[index] = message
+			result.Commands = append(result.Commands, resultForHookRejection(command, message))
 		}
 
-		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
-		}
+		return result, nil
 	}
 
 	if len(allowedCommands) == 0 {
@@ -181,6 +89,7 @@
 
 	if req.Atomic {
 		subresult := &Result{}
+
 		err := service.applyAtomic(subresult, allowedCommands)
 		if err != nil {
 			return result, err
@@ -193,6 +102,7 @@
 	}
 
 	subresult := &Result{}
+
 	err = service.applyBatch(subresult, allowedCommands)
 	if err != nil {
 		return result, err
--- /dev/null
+++ b/receivepack/internal/service/ingest_quarantine.go
@@ -1,0 +1,75 @@
+package service
+
+import (
+	"os"
+
+	"codeberg.org/lindenii/furgit/format/pack/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 {
+		result.UnpackError = "missing pack stream"
+		fillCommandErrors(result, commands, "missing pack stream")
+
+		return "", nil, false
+	}
+
+	if service.opts.ObjectsRoot == nil {
+		result.UnpackError = "objects root not configured"
+		fillCommandErrors(result, commands, "objects root not configured")
+
+		return "", nil, false
+	}
+
+	quarantineName, quarantineRoot, err := service.createQuarantineRoot()
+	if err != nil {
+		result.UnpackError = err.Error()
+		fillCommandErrors(result, commands, err.Error())
+
+		return "", nil, false
+	}
+
+	quarantinePackRoot, err := service.openQuarantinePackRoot(quarantineRoot)
+	if err != nil {
+		result.UnpackError = err.Error()
+		fillCommandErrors(result, commands, err.Error())
+
+		_ = quarantineRoot.Close()
+		_ = service.opts.ObjectsRoot.RemoveAll(quarantineName)
+
+		return "", nil, false
+	}
+
+	ingested, err := ingest.Ingest(
+		req.Pack,
+		quarantinePackRoot,
+		service.opts.Algorithm,
+		true,
+		true,
+		service.opts.ExistingObjects,
+	)
+
+	_ = quarantinePackRoot.Close()
+
+	if err != nil {
+		result.UnpackError = err.Error()
+		fillCommandErrors(result, commands, err.Error())
+
+		_ = quarantineRoot.Close()
+		_ = service.opts.ObjectsRoot.RemoveAll(quarantineName)
+
+		return "", nil, false
+	}
+
+	result.Ingest = &ingested
+
+	return quarantineName, quarantineRoot, true
+}
--- a/receivepack/internal/service/quarantine.go
+++ b/receivepack/internal/service/quarantine.go
@@ -169,9 +169,10 @@
 			return applyPromotedFilePermissions(root, dst, perms)
 		case !errors.Is(err, fs.ErrExist):
 			_, statErr := root.Stat(dst)
-			if statErr == nil {
+			switch {
+			case statErr == nil:
 				err = fs.ErrExist
-			} else if errors.Is(statErr, fs.ErrNotExist) {
+			case errors.Is(statErr, fs.ErrNotExist):
 				renameErr := root.Rename(src, dst)
 				if renameErr == nil {
 					return applyPromotedFilePermissions(root, dst, perms)
@@ -178,7 +179,7 @@
 				}
 
 				err = renameErr
-			} else {
+			default:
 				_ = root.Remove(src)
 
 				return statErr
--- a/receivepack/internal/service/quarantine_test.go
+++ b/receivepack/internal/service/quarantine_test.go
@@ -1,5 +1,7 @@
-package service
+package service //nolint:testpackage
 
+// because we need access to quarantine internals
+
 import (
 	"os"
 	"path"
@@ -9,167 +11,161 @@
 	"codeberg.org/lindenii/furgit/objectstore/memory"
 )
 
-func TestPromoteQuarantineAppliesConfiguredPermissions(t *testing.T) {
-	t.Parallel()
+type quarantineFixture struct {
+	svc            *Service
+	objectsRoot    *os.Root
+	quarantineName string
+	quarantineRoot *os.Root
+}
 
-	objectsDir := t.TempDir()
-	objectsRoot, err := os.OpenRoot(objectsDir)
+func newQuarantineFixture(tb testing.TB, opts Options) *quarantineFixture {
+	tb.Helper()
+
+	objectsRoot, err := os.OpenRoot(tb.TempDir())
 	if err != nil {
-		t.Fatalf("os.OpenRoot: %v", err)
+		tb.Fatalf("os.OpenRoot: %v", err)
 	}
 
-	t.Cleanup(func() {
+	tb.Cleanup(func() {
 		_ = objectsRoot.Close()
 	})
 
-	svc := New(Options{
-		Algorithm:       objectid.AlgorithmSHA1,
-		ExistingObjects: memory.New(objectid.AlgorithmSHA1),
-		ObjectsRoot:     objectsRoot,
-		PromotedObjectPermissions: &PromotedObjectPermissions{
-			DirMode:  0o751,
-			FileMode: 0o640,
-		},
-	})
+	opts.Algorithm = objectid.AlgorithmSHA1
+	opts.ExistingObjects = memory.New(objectid.AlgorithmSHA1)
+	opts.ObjectsRoot = objectsRoot
 
+	svc := New(opts)
+
 	quarantineName, quarantineRoot, err := svc.createQuarantineRoot()
 	if err != nil {
-		t.Fatalf("createQuarantineRoot: %v", err)
+		tb.Fatalf("createQuarantineRoot: %v", err)
 	}
 
-	t.Cleanup(func() {
+	tb.Cleanup(func() {
 		_ = quarantineRoot.Close()
 		_ = objectsRoot.RemoveAll(quarantineName)
 	})
 
-	if err := quarantineRoot.Mkdir("ab", 0o700); err != nil {
-		t.Fatalf("Mkdir(ab): %v", err)
+	return &quarantineFixture{
+		svc:            svc,
+		objectsRoot:    objectsRoot,
+		quarantineName: quarantineName,
+		quarantineRoot: quarantineRoot,
 	}
+}
 
-	if err := quarantineRoot.WriteFile(path.Join("ab", "cdef"), []byte("payload"), 0o600); err != nil {
-		t.Fatalf("WriteFile(quarantine loose): %v", err)
-	}
+func writeMatchingPromotedFile(
+	tb testing.TB,
+	quarantineRoot, objectsRoot *os.Root,
+	dir, name, payload string,
+) {
+	tb.Helper()
 
-	if err := svc.promoteQuarantine(quarantineName, quarantineRoot); err != nil {
-		t.Fatalf("promoteQuarantine: %v", err)
+	err := quarantineRoot.Mkdir(dir, 0o755)
+	if err != nil {
+		tb.Fatalf("Mkdir(%s): %v", dir, err)
 	}
 
-	dirInfo, err := objectsRoot.Stat("ab")
+	err = objectsRoot.Mkdir(dir, 0o755)
 	if err != nil {
-		t.Fatalf("Stat(ab): %v", err)
+		tb.Fatalf("Mkdir(dst %s): %v", dir, err)
 	}
 
-	if got := dirInfo.Mode().Perm(); got != 0o751 {
-		t.Fatalf("dir mode = %o, want 751", got)
-	}
+	rel := path.Join(dir, name)
 
-	fileInfo, err := objectsRoot.Stat(path.Join("ab", "cdef"))
+	err = quarantineRoot.WriteFile(rel, []byte(payload), 0o644)
 	if err != nil {
-		t.Fatalf("Stat(ab/cdef): %v", err)
+		tb.Fatalf("WriteFile(quarantine %s): %v", rel, err)
 	}
 
-	if got := fileInfo.Mode().Perm(); got != 0o640 {
-		t.Fatalf("file mode = %o, want 640", got)
+	err = objectsRoot.WriteFile(rel, []byte(payload), 0o644)
+	if err != nil {
+		tb.Fatalf("WriteFile(permanent %s): %v", rel, err)
 	}
 }
 
-func TestPromoteQuarantineTreatsExistingLooseObjectAsSuccess(t *testing.T) {
+func TestPromoteQuarantineAppliesConfiguredPermissions(t *testing.T) {
 	t.Parallel()
 
-	objectsDir := t.TempDir()
-	objectsRoot, err := os.OpenRoot(objectsDir)
+	fx := newQuarantineFixture(t, Options{
+		PromotedObjectPermissions: &PromotedObjectPermissions{
+			DirMode:  0o751,
+			FileMode: 0o640,
+		},
+	})
+
+	err := fx.quarantineRoot.Mkdir("ab", 0o700)
 	if err != nil {
-		t.Fatalf("os.OpenRoot: %v", err)
+		t.Fatalf("Mkdir(ab): %v", err)
 	}
 
-	t.Cleanup(func() {
-		_ = objectsRoot.Close()
-	})
-
-	svc := New(Options{
-		Algorithm:       objectid.AlgorithmSHA1,
-		ExistingObjects: memory.New(objectid.AlgorithmSHA1),
-		ObjectsRoot:     objectsRoot,
-	})
-
-	quarantineName, quarantineRoot, err := svc.createQuarantineRoot()
+	err = fx.quarantineRoot.WriteFile(path.Join("ab", "cdef"), []byte("payload"), 0o600)
 	if err != nil {
-		t.Fatalf("createQuarantineRoot: %v", err)
+		t.Fatalf("WriteFile(quarantine loose): %v", err)
 	}
 
-	t.Cleanup(func() {
-		_ = quarantineRoot.Close()
-		_ = objectsRoot.RemoveAll(quarantineName)
-	})
-
-	if err := quarantineRoot.Mkdir("ab", 0o755); err != nil {
-		t.Fatalf("Mkdir(ab): %v", err)
+	err = fx.svc.promoteQuarantine(fx.quarantineName, fx.quarantineRoot)
+	if err != nil {
+		t.Fatalf("promoteQuarantine: %v", err)
 	}
 
-	if err := objectsRoot.Mkdir("ab", 0o755); err != nil {
-		t.Fatalf("Mkdir(dst ab): %v", err)
+	dirInfo, err := fx.objectsRoot.Stat("ab")
+	if err != nil {
+		t.Fatalf("Stat(ab): %v", err)
 	}
 
-	const payload = "same object bytes"
-	if err := quarantineRoot.WriteFile(path.Join("ab", "cdef"), []byte(payload), 0o644); err != nil {
-		t.Fatalf("WriteFile(quarantine loose): %v", err)
+	if got := dirInfo.Mode().Perm(); got != 0o751 {
+		t.Fatalf("dir mode = %o, want 751", got)
 	}
 
-	if err := objectsRoot.WriteFile(path.Join("ab", "cdef"), []byte(payload), 0o644); err != nil {
-		t.Fatalf("WriteFile(permanent loose): %v", err)
+	fileInfo, err := fx.objectsRoot.Stat(path.Join("ab", "cdef"))
+	if err != nil {
+		t.Fatalf("Stat(ab/cdef): %v", err)
 	}
 
-	if err := svc.promoteQuarantine(quarantineName, quarantineRoot); err != nil {
-		t.Fatalf("promoteQuarantine: %v", err)
+	if got := fileInfo.Mode().Perm(); got != 0o640 {
+		t.Fatalf("file mode = %o, want 640", got)
 	}
 }
 
-func TestPromoteQuarantineRejectsDifferentExistingPackFile(t *testing.T) {
+func TestPromoteQuarantineTreatsExistingLooseObjectAsSuccess(t *testing.T) {
 	t.Parallel()
 
-	objectsDir := t.TempDir()
-	objectsRoot, err := os.OpenRoot(objectsDir)
+	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("os.OpenRoot: %v", err)
+		t.Fatalf("promoteQuarantine: %v", err)
 	}
+}
 
-	t.Cleanup(func() {
-		_ = objectsRoot.Close()
-	})
+func TestPromoteQuarantineRejectsDifferentExistingPackFile(t *testing.T) {
+	t.Parallel()
 
-	svc := New(Options{
-		Algorithm:       objectid.AlgorithmSHA1,
-		ExistingObjects: memory.New(objectid.AlgorithmSHA1),
-		ObjectsRoot:     objectsRoot,
-	})
+	fx := newQuarantineFixture(t, Options{})
 
-	quarantineName, quarantineRoot, err := svc.createQuarantineRoot()
+	err := fx.quarantineRoot.Mkdir("pack", 0o755)
 	if err != nil {
-		t.Fatalf("createQuarantineRoot: %v", err)
-	}
-
-	t.Cleanup(func() {
-		_ = quarantineRoot.Close()
-		_ = objectsRoot.RemoveAll(quarantineName)
-	})
-
-	if err := quarantineRoot.Mkdir("pack", 0o755); err != nil {
 		t.Fatalf("Mkdir(pack): %v", err)
 	}
 
-	if err := objectsRoot.Mkdir("pack", 0o755); err != nil {
+	err = fx.objectsRoot.Mkdir("pack", 0o755)
+	if err != nil {
 		t.Fatalf("Mkdir(dst pack): %v", err)
 	}
 
-	if err := quarantineRoot.WriteFile(path.Join("pack", "pack-a.pack"), []byte("new bytes"), 0o644); err != nil {
+	err = fx.quarantineRoot.WriteFile(path.Join("pack", "pack-a.pack"), []byte("new bytes"), 0o644)
+	if err != nil {
 		t.Fatalf("WriteFile(quarantine pack): %v", err)
 	}
 
-	if err := objectsRoot.WriteFile(path.Join("pack", "pack-a.pack"), []byte("old bytes"), 0o644); err != nil {
+	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 = svc.promoteQuarantine(quarantineName, quarantineRoot)
+	err = fx.svc.promoteQuarantine(fx.quarantineName, fx.quarantineRoot)
 	if err == nil {
 		t.Fatal("promoteQuarantine unexpectedly succeeded")
 	}
@@ -178,50 +174,11 @@
 func TestPromoteQuarantineAcceptsMatchingExistingPackFile(t *testing.T) {
 	t.Parallel()
 
-	objectsDir := t.TempDir()
-	objectsRoot, err := os.OpenRoot(objectsDir)
-	if err != nil {
-		t.Fatalf("os.OpenRoot: %v", err)
-	}
+	fx := newQuarantineFixture(t, Options{})
+	writeMatchingPromotedFile(t, fx.quarantineRoot, fx.objectsRoot, "pack", "pack-a.pack", "identical pack bytes")
 
-	t.Cleanup(func() {
-		_ = objectsRoot.Close()
-	})
-
-	svc := New(Options{
-		Algorithm:       objectid.AlgorithmSHA1,
-		ExistingObjects: memory.New(objectid.AlgorithmSHA1),
-		ObjectsRoot:     objectsRoot,
-	})
-
-	quarantineName, quarantineRoot, err := svc.createQuarantineRoot()
+	err := fx.svc.promoteQuarantine(fx.quarantineName, fx.quarantineRoot)
 	if err != nil {
-		t.Fatalf("createQuarantineRoot: %v", err)
-	}
-
-	t.Cleanup(func() {
-		_ = quarantineRoot.Close()
-		_ = objectsRoot.RemoveAll(quarantineName)
-	})
-
-	if err := quarantineRoot.Mkdir("pack", 0o755); err != nil {
-		t.Fatalf("Mkdir(pack): %v", err)
-	}
-
-	if err := objectsRoot.Mkdir("pack", 0o755); err != nil {
-		t.Fatalf("Mkdir(dst pack): %v", err)
-	}
-
-	const payload = "identical pack bytes"
-	if err := quarantineRoot.WriteFile(path.Join("pack", "pack-a.pack"), []byte(payload), 0o644); err != nil {
-		t.Fatalf("WriteFile(quarantine pack): %v", err)
-	}
-
-	if err := objectsRoot.WriteFile(path.Join("pack", "pack-a.pack"), []byte(payload), 0o644); err != nil {
-		t.Fatalf("WriteFile(permanent pack): %v", err)
-	}
-
-	if err := svc.promoteQuarantine(quarantineName, quarantineRoot); err != nil {
 		t.Fatalf("promoteQuarantine: %v", err)
 	}
 }
--- /dev/null
+++ b/receivepack/internal/service/run_hook.go
@@ -1,0 +1,73 @@
+package service
+
+import "context"
+
+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, ""
+	}
+
+	quarantinedObjects, err := service.openQuarantinedObjects(quarantineName)
+	if err != nil {
+		return nil, nil, nil, false, err.Error()
+	}
+
+	defer func() {
+		_ = quarantinedObjects.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...),
+	})
+	if err != nil {
+		return nil, nil, nil, false, err.Error()
+	}
+
+	if len(decisions) != len(commands) {
+		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
+	}
+
+	return allowedCommands, allowedIndices, rejected, true, ""
+}
--