ref: da621b97d0aa209e7e502e9e898e0a7a89857216
parent: 84342973be94c2981fac56c6680d9f87eb9fa9ce
author: Runxi Yu <runxiyu@umich.edu>
date: Mon Mar 30 15:51:58 EDT 2026
network/receivepack: Use dual
--- a/cmd/receivepack9418/conn.go
+++ b/cmd/receivepack9418/conn.go
@@ -6,9 +6,13 @@
"fmt"
"log"
"net"
+ "os"
"strings"
"codeberg.org/lindenii/furgit/network/receivepack"
+ objectdual "codeberg.org/lindenii/furgit/object/store/dual"
+ objectloose "codeberg.org/lindenii/furgit/object/store/loose"
+ objectpacked "codeberg.org/lindenii/furgit/object/store/packed"
)
func (srv *server) handleConn(conn net.Conn) {@@ -38,12 +42,24 @@
gitProtocol := strings.Join(req.ExtraParameters, ":")
+ objectIngress, cleanupObjectIngress, err := srv.openObjectIngress()
+ if err != nil {+ writeErrPkt(writer, fmt.Sprintf("object ingress unavailable: %v", err))+ _ = writer.Flush()
+
+ log.Printf("receivepack9418: %s: object ingress unavailable: %v", conn.RemoteAddr(), err)+
+ return
+ }
+
+ defer cleanupObjectIngress()
+
opts := receivepack.Options{GitProtocol: gitProtocol,
Algorithm: srv.repo.Algorithm(),
Refs: srv.repo.Refs(),
ExistingObjects: srv.repo.Objects(),
- ObjectsRoot: srv.objectsRoot,
+ ObjectIngress: objectIngress,
}
err = receivepack.ReceivePack(context.Background(), writer, reader, opts)
@@ -68,4 +84,39 @@
return
}
+}
+
+func (srv *server) openObjectIngress() (*objectdual.Dual, func(), error) {+ err := srv.objectsRoot.Mkdir("pack", 0o755)+ if err != nil && !os.IsExist(err) {+ return nil, nil, err
+ }
+
+ packRoot, err := srv.objectsRoot.OpenRoot("pack")+ if err != nil {+ return nil, nil, err
+ }
+
+ looseStore, err := objectloose.New(srv.objectsRoot, srv.repo.Algorithm())
+ if err != nil {+ _ = packRoot.Close()
+
+ return nil, nil, err
+ }
+
+ packedStore, err := objectpacked.New(packRoot, srv.repo.Algorithm(), objectpacked.Options{WriteRev: true})+ if err != nil {+ _ = looseStore.Close()
+ _ = packRoot.Close()
+
+ return nil, nil, err
+ }
+
+ cleanup := func() {+ _ = packedStore.Close()
+ _ = looseStore.Close()
+ _ = packRoot.Close()
+ }
+
+ return objectdual.New(looseStore, packedStore), cleanup, nil
}
--- a/network/receivepack/hooks/reject_force_push.go
+++ b/network/receivepack/hooks/reject_force_push.go
@@ -23,8 +23,6 @@
objects := objectmix.New(req.QuarantinedObjects, req.ExistingObjects)
- defer func() { _ = objects.Close() }()-
queries := commitquery.New(fetch.New(objects), req.CommitGraph)
decisions := make([]receivepack.UpdateDecision, len(req.Updates))
--- a/network/receivepack/int_test.go
+++ b/network/receivepack/int_test.go
@@ -15,6 +15,10 @@
receivepack "codeberg.org/lindenii/furgit/network/receivepack"
receivepackhooks "codeberg.org/lindenii/furgit/network/receivepack/hooks"
objectid "codeberg.org/lindenii/furgit/object/id"
+ objectstore "codeberg.org/lindenii/furgit/object/store"
+ objectdual "codeberg.org/lindenii/furgit/object/store/dual"
+ objectloose "codeberg.org/lindenii/furgit/object/store/loose"
+ objectpacked "codeberg.org/lindenii/furgit/object/store/packed"
)
func TestReceivePackDeleteOnlyAtomicDeleteSucceeds(t *testing.T) {@@ -301,7 +305,7 @@
})
}
-func TestReceivePackPackRequestWithoutObjectsRootReportsNotConfigured(t *testing.T) {+func TestReceivePackPackRequestWithoutObjectIngressReportsNotConfigured(t *testing.T) {t.Parallel()
//nolint:thelper
@@ -334,7 +338,7 @@
}
got := output.String()
- if !strings.Contains(got, "unpack objects root not configured\n") {+ if !strings.Contains(got, "unpack object ingress not configured\n") { t.Fatalf("unexpected receive-pack output %q", got)}
})
@@ -352,7 +356,7 @@
receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})repo := receiver.OpenRepository(t)
- objectsRoot := receiver.OpenObjectsRoot(t)
+ objectIngress := openReceivePackIngress(t, receiver, algo)
packStream := sender.PackObjectsReader(t, []string{commitID.String()}, false) t.Cleanup(func() {@@ -377,7 +381,7 @@
Algorithm: algo,
Refs: repo.Refs(),
ExistingObjects: repo.Objects(),
- ObjectsRoot: objectsRoot,
+ ObjectIngress: objectIngress,
},
)
if err != nil {@@ -423,7 +427,7 @@
receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})repo := receiver.OpenRepository(t)
- objectsRoot := receiver.OpenObjectsRoot(t)
+ objectIngress := openReceivePackIngress(t, receiver, algo)
packStream := sender.PackObjectsReader(t, []string{commitID.String()}, false) t.Cleanup(func() {@@ -449,7 +453,7 @@
Algorithm: algo,
Refs: repo.Refs(),
ExistingObjects: repo.Objects(),
- ObjectsRoot: objectsRoot,
+ ObjectIngress: objectIngress,
Hook: func(ctx context.Context, req receivepack.HookRequest) ([]receivepack.UpdateDecision, error) {hookCalled = true
@@ -658,7 +662,7 @@
testRepo.UpdateRef(t, "refs/heads/main", currentID)
repo := testRepo.OpenRepository(t)
- objectsRoot := testRepo.OpenObjectsRoot(t)
+ objectIngress := openReceivePackIngress(t, testRepo, algo)
packStream := testRepo.PackObjectsReader(t, []string{forcedID.String(), "^" + currentID.String()}, false) t.Cleanup(func() {_ = packStream.Close()
@@ -682,7 +686,7 @@
Algorithm: algo,
Refs: repo.Refs(),
ExistingObjects: repo.Objects(),
- ObjectsRoot: objectsRoot,
+ ObjectIngress: objectIngress,
Hook: receivepackhooks.RejectForcePush(),
},
)
@@ -765,7 +769,7 @@
receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})repo := receiver.OpenRepository(t)
- objectsRoot := receiver.OpenObjectsRoot(t)
+ objectIngress := openReceivePackIngress(t, receiver, algo)
stdout, stderr, clientErr, serverErr := runGitPushFD(
t,
@@ -774,7 +778,7 @@
Algorithm: algo,
Refs: repo.Refs(),
ExistingObjects: repo.Objects(),
- ObjectsRoot: objectsRoot,
+ ObjectIngress: objectIngress,
},
"push", "--porcelain", "fd::3,4/test", "refs/heads/main:refs/heads/main",
)
@@ -815,7 +819,7 @@
receiver.UpdateRef(t, "refs/heads/main", commitID)
repo := receiver.OpenRepository(t)
- objectsRoot := receiver.OpenObjectsRoot(t)
+ objectIngress := openReceivePackIngress(t, receiver, algo)
stdout, stderr, clientErr, serverErr := runGitPushFD(
t,
@@ -824,7 +828,7 @@
Algorithm: algo,
Refs: repo.Refs(),
ExistingObjects: repo.Objects(),
- ObjectsRoot: objectsRoot,
+ ObjectIngress: objectIngress,
},
"push", "--porcelain", "fd::3,4/test", "refs/heads/main:refs/heads/topic",
)
@@ -911,7 +915,7 @@
receiver.UpdateRef(t, "refs/heads/main", currentID)
repo := receiver.OpenRepository(t)
- objectsRoot := receiver.OpenObjectsRoot(t)
+ objectIngress := openReceivePackIngress(t, receiver, algo)
stdout, stderr, clientErr, serverErr := runGitPushFD(
t,
@@ -920,7 +924,7 @@
Algorithm: algo,
Refs: repo.Refs(),
ExistingObjects: repo.Objects(),
- ObjectsRoot: objectsRoot,
+ ObjectIngress: objectIngress,
Hook: receivepackhooks.RejectForcePush(),
},
"push", "--porcelain", "--force", "fd::3,4/test", "refs/heads/main:refs/heads/main",
@@ -958,6 +962,50 @@
func pktlineData(payload string) string { return fmt.Sprintf("%04x%s", len(payload)+4, payload)+}
+
+func openReceivePackIngress(
+ tb testing.TB,
+ testRepo *testgit.TestRepo,
+ algo objectid.Algorithm,
+) objectstore.Quarantiner {+ tb.Helper()
+
+ objectsRoot := testRepo.OpenObjectsRoot(tb)
+
+ err := objectsRoot.Mkdir("pack", 0o755)+ if err != nil && !os.IsExist(err) {+ tb.Fatalf("Mkdir(pack): %v", err)+ }
+
+ packRoot, err := objectsRoot.OpenRoot("pack")+ if err != nil {+ tb.Fatalf("OpenRoot(pack): %v", err)+ }
+
+ tb.Cleanup(func() {+ _ = packRoot.Close()
+ })
+
+ looseStore, err := objectloose.New(objectsRoot, algo)
+ if err != nil {+ tb.Fatalf("loose.New: %v", err)+ }
+
+ tb.Cleanup(func() {+ _ = looseStore.Close()
+ })
+
+ packedStore, err := objectpacked.New(packRoot, algo, objectpacked.Options{WriteRev: true})+ if err != nil {+ tb.Fatalf("packed.New: %v", err)+ }
+
+ tb.Cleanup(func() {+ _ = packedStore.Close()
+ })
+
+ return objectdual.New(looseStore, packedStore)
}
type fileWriteFlusher struct {--- a/network/receivepack/options.go
+++ b/network/receivepack/options.go
@@ -1,8 +1,6 @@
package receivepack
import (
- "os"
-
commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read"
objectid "codeberg.org/lindenii/furgit/object/id"
objectstore "codeberg.org/lindenii/furgit/object/store"
@@ -14,7 +12,7 @@
// 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
+// ObjectIngress is required if the invocation may need to ingest or quarantine a
// pack.
type Options struct {// GitProtocol is the raw Git protocol version string from the transport,
@@ -31,15 +29,12 @@
// ExistingObjects is the object store visible to the push before any newly
// uploaded quarantined objects are promoted.
ExistingObjects objectstore.Reader
+ // ObjectIngress creates coordinated quarantines for quarantined object and
+ // pack ingestion during the push.
+ ObjectIngress objectstore.Quarantiner
// CommitGraph is an optional commit-graph snapshot corresponding to
// ExistingObjects.
CommitGraph *commitgraphread.Reader
- // 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.
--- a/network/receivepack/permissions.go
+++ /dev/null
@@ -1,27 +1,0 @@
-package receivepack
-
-import (
- "io/fs"
-
- "codeberg.org/lindenii/furgit/network/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,
- }
-}
--- a/network/receivepack/receivepack.go
+++ b/network/receivepack/receivepack.go
@@ -108,13 +108,10 @@
Algorithm: opts.Algorithm,
Refs: opts.Refs,
ExistingObjects: opts.ExistingObjects,
+ ObjectIngress: opts.ObjectIngress,
CommitGraph: opts.CommitGraph,
- ObjectsRoot: opts.ObjectsRoot,
Progress: progress,
- PromotedObjectPermissions: translatePromotedObjectPermissions(
- opts.PromotedObjectPermissions,
- ),
- Hook: translateHook(opts.Hook),
+ Hook: translateHook(opts.Hook),
HookIO: service.HookIO{Progress: progress,
Error: protoSession.ErrorWriter(),
--- a/network/receivepack/service/execute.go
+++ b/network/receivepack/service/execute.go
@@ -2,9 +2,9 @@
import (
"context"
- "os"
"codeberg.org/lindenii/furgit/internal/utils"
+ objectstore "codeberg.org/lindenii/furgit/object/store"
)
// Execute validates one receive-pack request, optionally ingests its pack into
@@ -15,23 +15,17 @@
result := &Result{Commands: make([]CommandResult, 0, len(req.Commands)),
}
+ var err error
- var (
- quarantineName string
- quarantineRoot *os.Root
- err error
- )
-
- quarantineName, quarantineRoot, ok := service.ingestQuarantine(result, req.Commands, req)
+ quarantine, ok := service.ingestQuarantine(result, req.Commands, req)
if !ok {return result, nil
}
- if quarantineRoot != nil {- defer func() {- _ = quarantineRoot.Close()
- _ = service.opts.ObjectsRoot.RemoveAll(quarantineName)
- }()
+ if quarantine != nil {+ defer func(q objectstore.Quarantine) {+ _ = q.Discard()
+ }(quarantine)
}
for _, command := range req.Commands {@@ -51,7 +45,7 @@
ctx,
req,
req.Commands,
- quarantineName,
+ quarantine,
)
if !ok {fillCommandErrors(result, req.Commands, errText)
@@ -79,12 +73,12 @@
return result, nil
}
- if req.PackExpected && quarantineRoot != nil {+ if req.PackExpected && quarantine != 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)
+ err := quarantine.Promote()
if err != nil {utils.BestEffortFprintf(service.opts.Progress, "promoting quarantine: failed: %v.\n", err)
@@ -94,6 +88,7 @@
return result, nil
}
+ quarantine = nil
utils.BestEffortFprintf(service.opts.Progress, "promoting quarantine: done.\n")
}
--- a/network/receivepack/service/ingest_quarantine.go
+++ b/network/receivepack/service/ingest_quarantine.go
@@ -1,10 +1,8 @@
package service
import (
- "os"
-
- "codeberg.org/lindenii/furgit/format/packfile/ingest"
"codeberg.org/lindenii/furgit/internal/utils"
+ objectstore "codeberg.org/lindenii/furgit/object/store"
)
func (service *Service) ingestQuarantine(
@@ -11,9 +9,9 @@
result *Result,
commands []Command,
req *Request,
-) (string, *os.Root, bool) {+) (objectstore.Quarantine, bool) { if !req.PackExpected {- return "", nil, true
+ return nil, true
}
if req.Pack == nil {@@ -22,16 +20,16 @@
result.UnpackError = "missing pack stream"
fillCommandErrors(result, commands, "missing pack stream")
- return "", nil, false
+ return nil, false
}
- if service.opts.ObjectsRoot == nil {- utils.BestEffortFprintf(service.opts.Progress, "unpack failed: objects root not configured.\n")
+ if service.opts.ObjectIngress == nil {+ utils.BestEffortFprintf(service.opts.Progress, "unpack failed: object ingress not configured.\n")
- result.UnpackError = "objects root not configured"
- fillCommandErrors(result, commands, "objects root not configured")
+ result.UnpackError = "object ingress not configured"
+ fillCommandErrors(result, commands, "object ingress not configured")
- return "", nil, false
+ return nil, false
}
var err error
@@ -43,57 +41,12 @@
result.UnpackError = err.Error()
fillCommandErrors(result, commands, err.Error())
- return "", nil, false
+ 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,
- },
- )
- 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()
+ quarantine, err := service.opts.ObjectIngress.BeginQuarantine(objectstore.QuarantineOptions{}) if err != nil {utils.BestEffortFprintf(service.opts.Progress, "unpack failed: %v.\n", err)
@@ -100,29 +53,17 @@
result.UnpackError = err.Error()
fillCommandErrors(result, commands, err.Error())
- return "", nil, false
+ 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()
-
+ err = quarantine.WritePack(req.Pack, objectstore.PackWriteOptions{+ ThinBase: service.opts.ExistingObjects,
+ Progress: service.opts.Progress,
+ RequireTrailingEOF: false,
+ })
if err != nil {utils.BestEffortFprintf(service.opts.Progress, "unpack failed: %v.\n", err)
@@ -129,15 +70,12 @@
result.UnpackError = err.Error()
fillCommandErrors(result, commands, err.Error())
- _ = quarantineRoot.Close()
- _ = service.opts.ObjectsRoot.RemoveAll(quarantineName)
+ _ = quarantine.Discard()
- return "", nil, false
+ return nil, false
}
- utils.BestEffortFprintf(service.opts.Progress, "unpacking: done (%d objects, %s).\n", ingested.ObjectCount, ingested.PackHash)
+ utils.BestEffortFprintf(service.opts.Progress, "unpacking: done.\n")
- result.Ingest = &ingested
-
- return quarantineName, quarantineRoot, true
+ return quarantine, true
}
--- a/network/receivepack/service/options.go
+++ b/network/receivepack/service/options.go
@@ -1,9 +1,6 @@
package service
import (
- "io/fs"
- "os"
-
"codeberg.org/lindenii/furgit/common/iowrap"
commitgraphread "codeberg.org/lindenii/furgit/format/commitgraph/read"
objectid "codeberg.org/lindenii/furgit/object/id"
@@ -11,18 +8,14 @@
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, Hook, and HookIO are optional; when provided they are also
+// ObjectIngress is required if Execute may need to ingest or quarantine a
+// pack.
+// CommitGraph, Progress, Hook, and HookIO are optional; when provided they are also
// borrowed for the duration of Execute.
type Options struct {Algorithm objectid.Algorithm
@@ -31,11 +24,10 @@
refstore.TransactionalStore
refstore.BatchStore
}
- ExistingObjects objectstore.Reader
- CommitGraph *commitgraphread.Reader
- ObjectsRoot *os.Root
- Progress iowrap.WriteFlusher
- PromotedObjectPermissions *PromotedObjectPermissions
- Hook Hook
- HookIO HookIO
+ ExistingObjects objectstore.Reader
+ ObjectIngress objectstore.Quarantiner
+ CommitGraph *commitgraphread.Reader
+ Progress iowrap.WriteFlusher
+ Hook Hook
+ HookIO HookIO
}
--- a/network/receivepack/service/quarantine.go
+++ /dev/null
@@ -1,274 +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.
-//
-// 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
- }
- }
-}
--- a/network/receivepack/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"
-
- objectid "codeberg.org/lindenii/furgit/object/id"
- "codeberg.org/lindenii/furgit/object/store/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/network/receivepack/service/result.go
+++ b/network/receivepack/service/result.go
@@ -1,14 +1,9 @@
package service
-import (
- "codeberg.org/lindenii/furgit/format/packfile/ingest"
-)
-
// Result is one receive-pack execution result.
type Result struct {UnpackError string
Commands []CommandResult
- Ingest *ingest.Result
Planned []PlannedUpdate
Applied bool
}
--- a/network/receivepack/service/run_hook.go
+++ b/network/receivepack/service/run_hook.go
@@ -2,13 +2,9 @@
import (
"context"
- "os"
"codeberg.org/lindenii/furgit/internal/utils"
objectstore "codeberg.org/lindenii/furgit/object/store"
- "codeberg.org/lindenii/furgit/object/store/loose"
- objectmix "codeberg.org/lindenii/furgit/object/store/mix"
- "codeberg.org/lindenii/furgit/object/store/packed"
)
func (service *Service) runHook(
@@ -15,7 +11,7 @@
ctx context.Context,
req *Request,
commands []Command,
- quarantineName string,
+ quarantinedObjects objectstore.Reader,
) (
allowedCommands []Command,
allowedIndices []int,
@@ -36,82 +32,6 @@
}
utils.BestEffortFprintf(service.opts.Progress, "running hooks...\r")
-
- quarantinedObjects := service.opts.ExistingObjects
-
- var (
- quarantineObjectsStore objectstore.Reader
- 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()
- }
-
- 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 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,
--- a/network/receivepack/service/service_test.go
+++ b/network/receivepack/service/service_test.go
@@ -2,7 +2,6 @@
import (
"context"
- "io/fs"
"os"
"strings"
"testing"
@@ -10,10 +9,14 @@
"codeberg.org/lindenii/furgit/internal/testgit"
"codeberg.org/lindenii/furgit/network/receivepack/service"
objectid "codeberg.org/lindenii/furgit/object/id"
+ objectstore "codeberg.org/lindenii/furgit/object/store"
+ objectdual "codeberg.org/lindenii/furgit/object/store/dual"
+ objectloose "codeberg.org/lindenii/furgit/object/store/loose"
"codeberg.org/lindenii/furgit/object/store/memory"
+ objectpacked "codeberg.org/lindenii/furgit/object/store/packed"
)
-func TestExecutePackExpectedWithoutObjectsRoot(t *testing.T) {+func TestExecutePackExpectedWithoutObjectIngress(t *testing.T) {t.Parallel()
//nolint:thelper
@@ -39,13 +42,13 @@
t.Fatalf("Execute: %v", err)}
- if result.UnpackError != "objects root not configured" {+ if result.UnpackError != "object ingress not configured" { t.Fatalf("unexpected unpack error %q", result.UnpackError)}
})
}
-func TestExecuteRemovesDerivedQuarantineAfterIngestFailure(t *testing.T) {+func TestExecuteDiscardedQuarantineAfterIngestFailure(t *testing.T) {t.Parallel()
//nolint:thelper
@@ -53,21 +56,12 @@
t.Parallel()
store := memory.New(algo)
- objectsDir := t.TempDir()
+ objectIngress := newDualIngress(t, algo)
- 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,
+ ObjectIngress: objectIngress,
})
result, err := svc.Execute(context.Background(), &service.Request{@@ -86,14 +80,52 @@
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)- }
+func newDualIngress(tb testing.TB, algo objectid.Algorithm) objectstore.Quarantiner {+ tb.Helper()
- if len(entries) != 0 {- t.Fatalf("objects root still has entries after failed ingest: %d", len(entries))- }
+ objectsRoot, err := os.OpenRoot(tb.TempDir())
+ if err != nil {+ tb.Fatalf("os.OpenRoot: %v", err)+ }
+
+ tb.Cleanup(func() {+ _ = objectsRoot.Close()
})
+
+ err = objectsRoot.Mkdir("pack", 0o755)+ if err != nil {+ tb.Fatalf("Mkdir(pack): %v", err)+ }
+
+ packRoot, err := objectsRoot.OpenRoot("pack")+ if err != nil {+ tb.Fatalf("OpenRoot(pack): %v", err)+ }
+
+ tb.Cleanup(func() {+ _ = packRoot.Close()
+ })
+
+ looseStore, err := objectloose.New(objectsRoot, algo)
+ if err != nil {+ tb.Fatalf("loose.New: %v", err)+ }
+
+ tb.Cleanup(func() {+ _ = looseStore.Close()
+ })
+
+ packedStore, err := objectpacked.New(packRoot, algo, objectpacked.Options{WriteRev: true})+ if err != nil {+ tb.Fatalf("packed.New: %v", err)+ }
+
+ tb.Cleanup(func() {+ _ = packedStore.Close()
+ })
+
+ return objectdual.New(looseStore, packedStore)
}
--
⑨