ref: 0fb1520c3119ed6aedc5cb25098c0fd0b4cacf90
parent: c09ab35a972e0c9dd57fd4c931ef9bd80c91a20d
author: Runxi Yu <me@runxiyu.org>
date: Sat Mar 7 17:38:17 EST 2026
*: Package sorting and updates
--- /dev/null
+++ b/protocol/doc.go
@@ -1,0 +1,2 @@
+// Package protocol encapsulates network protocol implementations.
+package protocol
--- /dev/null
+++ b/protocol/v0v1/doc.go
@@ -1,0 +1,2 @@
+// Package v0v1 provides common constants and routines for the V0 and V1 protocols.
+package v0v1
--- a/receivepack/commands.go
+++ b/receivepack/commands.go
@@ -2,7 +2,7 @@
import (
protoreceive "codeberg.org/lindenii/furgit/protocol/v0v1/server/receivepack"
- "codeberg.org/lindenii/furgit/receivepack/internal/service"
+ "codeberg.org/lindenii/furgit/receivepack/service"
)
func translateCommands(commands []protoreceive.Command) []service.Command {--- a/receivepack/hook.go
+++ b/receivepack/hook.go
@@ -6,7 +6,7 @@
"codeberg.org/lindenii/furgit/objectid"
"codeberg.org/lindenii/furgit/objectstore"
- "codeberg.org/lindenii/furgit/receivepack/internal/service"
+ "codeberg.org/lindenii/furgit/receivepack/service"
"codeberg.org/lindenii/furgit/refstore"
)
--- a/receivepack/internal/service/apply.go
+++ /dev/null
@@ -1,108 +1,0 @@
-package service
-
-import (
- "codeberg.org/lindenii/furgit/objectid"
- "codeberg.org/lindenii/furgit/refstore"
-)
-
-func (service *Service) applyAtomic(result *Result, commands []Command) error {- tx, err := service.opts.Refs.BeginTransaction()
- if err != nil {- return err
- }
-
- for _, command := range commands {- err = queueWriteTransaction(tx, command)
- if err != nil {- _ = tx.Abort()
-
- fillCommandErrors(result, commands, err.Error())
-
- return nil
- }
- }
-
- err = tx.Commit()
- if err != nil {- fillCommandErrors(result, commands, err.Error())
-
- return nil
- }
-
- result.Applied = true
- for _, command := range commands {- result.Commands = append(result.Commands, successCommandResult(command))
- }
-
- return nil
-}
-
-func (service *Service) applyBatch(result *Result, commands []Command) error {- 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 {- return err
- }
-
- appliedAny := false
-
- for i, command := range commands {- item := successCommandResult(command)
- if i < len(batchResults) && batchResults[i].Error != nil {- item.Error = batchResults[i].Error.Error()
- } else {- appliedAny = true
- }
-
- result.Commands = append(result.Commands, item)
- }
-
- result.Applied = appliedAny
-
- 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),
- }
-}
--- a/receivepack/internal/service/command.go
+++ /dev/null
@@ -1,32 +1,0 @@
-package service
-
-import "codeberg.org/lindenii/furgit/objectid"
-
-// 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
-}
--- a/receivepack/internal/service/command_result.go
+++ /dev/null
@@ -1,13 +1,0 @@
-package service
-
-import "codeberg.org/lindenii/furgit/objectid"
-
-// CommandResult is one per-command execution result.
-type CommandResult struct {- Name string
- Error string
- RefName string
- OldID *objectid.ObjectID
- NewID *objectid.ObjectID
- ForcedUpdate bool
-}
--- a/receivepack/internal/service/doc.go
+++ /dev/null
@@ -1,2 +1,0 @@
-// Package service implements the protocol-independent receive-pack service.
-package service
--- a/receivepack/internal/service/execute.go
+++ /dev/null
@@ -1,115 +1,0 @@
-package service
-
-import (
- "context"
- "os"
-)
-
-// 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 {- // Git migrates quarantined objects into permanent storage immediately
- // before starting ref updates.
- err = service.promoteQuarantine(quarantineName, quarantineRoot)
- if err != nil {- result.UnpackError = err.Error()
- fillCommandErrors(result, req.Commands, err.Error())
-
- return result, nil
- }
- }
-
- 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
-}
--- a/receivepack/internal/service/hook.go
+++ /dev/null
@@ -1,37 +1,0 @@
-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
- NewID objectid.ObjectID
-}
-
-type UpdateDecision struct {- Accept bool
- Message string
-}
-
-type HookRequest struct {- Refs refstore.ReadingStore
- ExistingObjects objectstore.Store
- QuarantinedObjects objectstore.Store
- Updates []RefUpdate
- PushOptions []string
- IO HookIO
-}
-
-type Hook func(context.Context, HookRequest) ([]UpdateDecision, error)
--- a/receivepack/internal/service/hook_apply.go
+++ /dev/null
@@ -1,44 +1,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
-}
--- a/receivepack/internal/service/ingest_quarantine.go
+++ /dev/null
@@ -1,75 +1,0 @@
-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/options.go
+++ /dev/null
@@ -1,26 +1,0 @@
-package service
-
-import (
- "io/fs"
- "os"
-
- "codeberg.org/lindenii/furgit/objectid"
- "codeberg.org/lindenii/furgit/objectstore"
- "codeberg.org/lindenii/furgit/refstore"
-)
-
-type PromotedObjectPermissions struct {- DirMode fs.FileMode
- FileMode fs.FileMode
-}
-
-// Options configures one protocol-independent receive-pack service.
-type Options struct {- Algorithm objectid.Algorithm
- Refs refstore.ReadWriteStore
- ExistingObjects objectstore.Store
- ObjectsRoot *os.Root
- PromotedObjectPermissions *PromotedObjectPermissions
- Hook Hook
- HookIO HookIO
-}
--- a/receivepack/internal/service/quarantine.go
+++ /dev/null
@@ -1,269 +1,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.
-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
- }
- }
-}
--- a/receivepack/internal/service/quarantine_objects.go
+++ /dev/null
@@ -1,50 +1,0 @@
-package service
-
-import (
- "os"
-
- "codeberg.org/lindenii/furgit/objectstore"
- "codeberg.org/lindenii/furgit/objectstore/loose"
- "codeberg.org/lindenii/furgit/objectstore/memory"
- objectmix "codeberg.org/lindenii/furgit/objectstore/mix"
- "codeberg.org/lindenii/furgit/objectstore/packed"
-)
-
-func (service *Service) openQuarantinedObjects(quarantineName string) (objectstore.Store, error) {- if quarantineName == "" {- return memory.New(service.opts.Algorithm), nil
- }
-
- looseRoot, err := service.opts.ObjectsRoot.OpenRoot(quarantineName)
- if err != nil {- return nil, err
- }
-
- looseStore, err := loose.New(looseRoot, service.opts.Algorithm)
- if err != nil {- _ = looseRoot.Close()
-
- return nil, err
- }
-
- packRoot, err := looseRoot.OpenRoot("pack")- if err == nil {- packedStore, packedErr := packed.New(packRoot, service.opts.Algorithm)
- if packedErr != nil {- _ = packRoot.Close()
- _ = looseStore.Close()
-
- return nil, packedErr
- }
-
- return objectmix.New(looseStore, packedStore), nil
- }
-
- if !os.IsNotExist(err) {- _ = looseStore.Close()
-
- return nil, err
- }
-
- return looseStore, nil
-}
--- a/receivepack/internal/service/quarantine_test.go
+++ /dev/null
@@ -1,184 +1,0 @@
-package service //nolint:testpackage
-
-// because we need access to quarantine internals
-
-import (
- "os"
- "path"
- "testing"
-
- "codeberg.org/lindenii/furgit/objectid"
- "codeberg.org/lindenii/furgit/objectstore/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)- }
-}
--- a/receivepack/internal/service/request.go
+++ /dev/null
@@ -1,13 +1,0 @@
-package service
-
-import "io"
-
-// Request is one protocol-independent receive-pack execution request.
-type Request struct {- Commands []Command
- PushOptions []string
- Atomic bool
- DeleteOnly bool
- PackExpected bool
- Pack io.Reader
-}
--- a/receivepack/internal/service/result.go
+++ /dev/null
@@ -1,14 +1,0 @@
-package service
-
-import (
- "codeberg.org/lindenii/furgit/format/pack/ingest"
-)
-
-// Result is one receive-pack execution result.
-type Result struct {- UnpackError string
- Commands []CommandResult
- Ingest *ingest.Result
- Planned []PlannedUpdate
- Applied bool
-}
--- a/receivepack/internal/service/run_hook.go
+++ /dev/null
@@ -1,74 +1,0 @@
-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...),
- IO: service.opts.HookIO,
- })
- 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, ""
-}
--- a/receivepack/internal/service/service.go
+++ /dev/null
@@ -1,11 +1,0 @@
-package service
-
-// Service executes protocol-independent receive-pack requests.
-type Service struct {- opts Options
-}
-
-// New creates one receive-pack service.
-func New(opts Options) *Service {- return &Service{opts: opts}-}
--- a/receivepack/internal/service/service_test.go
+++ /dev/null
@@ -1,99 +1,0 @@
-package service_test
-
-import (
- "context"
- "io/fs"
- "os"
- "strings"
- "testing"
-
- "codeberg.org/lindenii/furgit/internal/testgit"
- "codeberg.org/lindenii/furgit/objectid"
- "codeberg.org/lindenii/furgit/objectstore/memory"
- "codeberg.org/lindenii/furgit/receivepack/internal/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))- }
- })
-}
--- a/receivepack/internal/service/update.go
+++ /dev/null
@@ -1,12 +1,0 @@
-package service
-
-import "codeberg.org/lindenii/furgit/objectid"
-
-// 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
-}
--- a/receivepack/permissions.go
+++ b/receivepack/permissions.go
@@ -3,7 +3,7 @@
import (
"io/fs"
- "codeberg.org/lindenii/furgit/receivepack/internal/service"
+ "codeberg.org/lindenii/furgit/receivepack/service"
)
// PromotedObjectPermissions configures the destination permissions applied to
--- a/receivepack/receivepack.go
+++ b/receivepack/receivepack.go
@@ -7,7 +7,7 @@
"codeberg.org/lindenii/furgit/format/pktline"
common "codeberg.org/lindenii/furgit/protocol/v0v1/server"
protoreceive "codeberg.org/lindenii/furgit/protocol/v0v1/server/receivepack"
- "codeberg.org/lindenii/furgit/receivepack/internal/service"
+ "codeberg.org/lindenii/furgit/receivepack/service"
)
// TODO: Some more designing to do. In particular, we'd like to have access to
--- a/receivepack/results.go
+++ b/receivepack/results.go
@@ -2,7 +2,7 @@
import (
protoreceive "codeberg.org/lindenii/furgit/protocol/v0v1/server/receivepack"
- "codeberg.org/lindenii/furgit/receivepack/internal/service"
+ "codeberg.org/lindenii/furgit/receivepack/service"
)
func translateResult(result *service.Result) protoreceive.ReportStatusResult {--- /dev/null
+++ b/receivepack/service/apply.go
@@ -1,0 +1,108 @@
+package service
+
+import (
+ "codeberg.org/lindenii/furgit/objectid"
+ "codeberg.org/lindenii/furgit/refstore"
+)
+
+func (service *Service) applyAtomic(result *Result, commands []Command) error {+ tx, err := service.opts.Refs.BeginTransaction()
+ if err != nil {+ return err
+ }
+
+ for _, command := range commands {+ err = queueWriteTransaction(tx, command)
+ if err != nil {+ _ = tx.Abort()
+
+ fillCommandErrors(result, commands, err.Error())
+
+ return nil
+ }
+ }
+
+ err = tx.Commit()
+ if err != nil {+ fillCommandErrors(result, commands, err.Error())
+
+ return nil
+ }
+
+ result.Applied = true
+ for _, command := range commands {+ result.Commands = append(result.Commands, successCommandResult(command))
+ }
+
+ return nil
+}
+
+func (service *Service) applyBatch(result *Result, commands []Command) error {+ 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 {+ return err
+ }
+
+ appliedAny := false
+
+ for i, command := range commands {+ item := successCommandResult(command)
+ if i < len(batchResults) && batchResults[i].Error != nil {+ item.Error = batchResults[i].Error.Error()
+ } else {+ appliedAny = true
+ }
+
+ result.Commands = append(result.Commands, item)
+ }
+
+ result.Applied = appliedAny
+
+ 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),
+ }
+}
--- /dev/null
+++ b/receivepack/service/command.go
@@ -1,0 +1,32 @@
+package service
+
+import "codeberg.org/lindenii/furgit/objectid"
+
+// 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
+}
--- /dev/null
+++ b/receivepack/service/command_result.go
@@ -1,0 +1,13 @@
+package service
+
+import "codeberg.org/lindenii/furgit/objectid"
+
+// CommandResult is one per-command execution result.
+type CommandResult struct {+ Name string
+ Error string
+ RefName string
+ OldID *objectid.ObjectID
+ NewID *objectid.ObjectID
+ ForcedUpdate bool
+}
--- /dev/null
+++ b/receivepack/service/doc.go
@@ -1,0 +1,2 @@
+// Package service implements the protocol-independent receive-pack service.
+package service
--- /dev/null
+++ b/receivepack/service/execute.go
@@ -1,0 +1,115 @@
+package service
+
+import (
+ "context"
+ "os"
+)
+
+// 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 {+ // Git migrates quarantined objects into permanent storage immediately
+ // before starting ref updates.
+ err = service.promoteQuarantine(quarantineName, quarantineRoot)
+ if err != nil {+ result.UnpackError = err.Error()
+ fillCommandErrors(result, req.Commands, err.Error())
+
+ return result, nil
+ }
+ }
+
+ 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
+}
--- /dev/null
+++ b/receivepack/service/hook.go
@@ -1,0 +1,37 @@
+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
+ NewID objectid.ObjectID
+}
+
+type UpdateDecision struct {+ Accept bool
+ Message string
+}
+
+type HookRequest struct {+ Refs refstore.ReadingStore
+ ExistingObjects objectstore.Store
+ QuarantinedObjects objectstore.Store
+ Updates []RefUpdate
+ PushOptions []string
+ IO HookIO
+}
+
+type Hook func(context.Context, HookRequest) ([]UpdateDecision, error)
--- /dev/null
+++ b/receivepack/service/hook_apply.go
@@ -1,0 +1,44 @@
+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
+}
--- /dev/null
+++ b/receivepack/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
+}
--- /dev/null
+++ b/receivepack/service/options.go
@@ -1,0 +1,26 @@
+package service
+
+import (
+ "io/fs"
+ "os"
+
+ "codeberg.org/lindenii/furgit/objectid"
+ "codeberg.org/lindenii/furgit/objectstore"
+ "codeberg.org/lindenii/furgit/refstore"
+)
+
+type PromotedObjectPermissions struct {+ DirMode fs.FileMode
+ FileMode fs.FileMode
+}
+
+// Options configures one protocol-independent receive-pack service.
+type Options struct {+ Algorithm objectid.Algorithm
+ Refs refstore.ReadWriteStore
+ ExistingObjects objectstore.Store
+ ObjectsRoot *os.Root
+ PromotedObjectPermissions *PromotedObjectPermissions
+ Hook Hook
+ HookIO HookIO
+}
--- /dev/null
+++ b/receivepack/service/quarantine.go
@@ -1,0 +1,269 @@
+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.
+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
+ }
+ }
+}
--- /dev/null
+++ b/receivepack/service/quarantine_objects.go
@@ -1,0 +1,50 @@
+package service
+
+import (
+ "os"
+
+ "codeberg.org/lindenii/furgit/objectstore"
+ "codeberg.org/lindenii/furgit/objectstore/loose"
+ "codeberg.org/lindenii/furgit/objectstore/memory"
+ objectmix "codeberg.org/lindenii/furgit/objectstore/mix"
+ "codeberg.org/lindenii/furgit/objectstore/packed"
+)
+
+func (service *Service) openQuarantinedObjects(quarantineName string) (objectstore.Store, error) {+ if quarantineName == "" {+ return memory.New(service.opts.Algorithm), nil
+ }
+
+ looseRoot, err := service.opts.ObjectsRoot.OpenRoot(quarantineName)
+ if err != nil {+ return nil, err
+ }
+
+ looseStore, err := loose.New(looseRoot, service.opts.Algorithm)
+ if err != nil {+ _ = looseRoot.Close()
+
+ return nil, err
+ }
+
+ packRoot, err := looseRoot.OpenRoot("pack")+ if err == nil {+ packedStore, packedErr := packed.New(packRoot, service.opts.Algorithm)
+ if packedErr != nil {+ _ = packRoot.Close()
+ _ = looseStore.Close()
+
+ return nil, packedErr
+ }
+
+ return objectmix.New(looseStore, packedStore), nil
+ }
+
+ if !os.IsNotExist(err) {+ _ = looseStore.Close()
+
+ return nil, err
+ }
+
+ return looseStore, nil
+}
--- /dev/null
+++ b/receivepack/service/quarantine_test.go
@@ -1,0 +1,184 @@
+package service //nolint:testpackage
+
+// because we need access to quarantine internals
+
+import (
+ "os"
+ "path"
+ "testing"
+
+ "codeberg.org/lindenii/furgit/objectid"
+ "codeberg.org/lindenii/furgit/objectstore/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)+ }
+}
--- /dev/null
+++ b/receivepack/service/request.go
@@ -1,0 +1,13 @@
+package service
+
+import "io"
+
+// Request is one protocol-independent receive-pack execution request.
+type Request struct {+ Commands []Command
+ PushOptions []string
+ Atomic bool
+ DeleteOnly bool
+ PackExpected bool
+ Pack io.Reader
+}
--- /dev/null
+++ b/receivepack/service/result.go
@@ -1,0 +1,14 @@
+package service
+
+import (
+ "codeberg.org/lindenii/furgit/format/pack/ingest"
+)
+
+// Result is one receive-pack execution result.
+type Result struct {+ UnpackError string
+ Commands []CommandResult
+ Ingest *ingest.Result
+ Planned []PlannedUpdate
+ Applied bool
+}
--- /dev/null
+++ b/receivepack/service/run_hook.go
@@ -1,0 +1,74 @@
+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...),
+ IO: service.opts.HookIO,
+ })
+ 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, ""
+}
--- /dev/null
+++ b/receivepack/service/service.go
@@ -1,0 +1,11 @@
+package service
+
+// Service executes protocol-independent receive-pack requests.
+type Service struct {+ opts Options
+}
+
+// New creates one receive-pack service.
+func New(opts Options) *Service {+ return &Service{opts: opts}+}
--- /dev/null
+++ b/receivepack/service/service_test.go
@@ -1,0 +1,99 @@
+package service_test
+
+import (
+ "context"
+ "io/fs"
+ "os"
+ "strings"
+ "testing"
+
+ "codeberg.org/lindenii/furgit/internal/testgit"
+ "codeberg.org/lindenii/furgit/objectid"
+ "codeberg.org/lindenii/furgit/objectstore/memory"
+ "codeberg.org/lindenii/furgit/receivepack/internal/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))+ }
+ })
+}
--- /dev/null
+++ b/receivepack/service/update.go
@@ -1,0 +1,12 @@
+package service
+
+import "codeberg.org/lindenii/furgit/objectid"
+
+// 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
+}
--
⑨