shithub: furgit

Download patch

ref: 446993c94dc34c0374e00f3f5f21ece72b18a9f6
parent: d86d30116b7509e616d4dfedc8b9aca2db5ae3e8
author: Runxi Yu <me@runxiyu.org>
date: Sat Mar 7 16:05:30 EST 2026

receivepack: Set permissions properly

--- a/receivepack/internal/service/options.go
+++ b/receivepack/internal/service/options.go
@@ -1,6 +1,7 @@
 package service
 
 import (
+	"io/fs"
 	"os"
 
 	"codeberg.org/lindenii/furgit/objectid"
@@ -8,11 +9,17 @@
 	"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
+	Algorithm                  objectid.Algorithm
+	Refs                       refstore.ReadWriteStore
+	ExistingObjects            objectstore.Store
+	ObjectsRoot                *os.Root
+	PromotedObjectPermissions  *PromotedObjectPermissions
 	// TODO: Hook and such callbacks.
 }
--- a/receivepack/internal/service/quarantine.go
+++ b/receivepack/internal/service/quarantine.go
@@ -71,6 +71,11 @@
 				return err
 			}
 
+			err = service.applyPromotedDirectoryPermissions(childRel)
+			if err != nil {
+				return err
+			}
+
 			err = service.promoteQuarantineDir(quarantineName, quarantineRoot, childRel)
 			if err != nil {
 				return err
@@ -84,6 +89,7 @@
 			path.Join(quarantineName, childRel),
 			childRel,
 			isLooseObjectShardPath(rel),
+			service.opts.PromotedObjectPermissions,
 		)
 		if err == nil {
 			continue
@@ -126,7 +132,32 @@
 	return ('0' <= ch && ch <= '9') || ('a' <= ch && ch <= 'f') || ('A' <= ch && ch <= 'F')
 }
 
-func finalizeQuarantineFile(root *os.Root, src, dst string, skipCollisionCheck bool) error {
+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++ {
@@ -135,7 +166,7 @@
 		case err == nil:
 			_ = root.Remove(src)
 
-			return nil
+			return applyPromotedFilePermissions(root, dst, perms)
 		case !errors.Is(err, fs.ErrExist):
 			_, statErr := root.Stat(dst)
 			if statErr == nil {
@@ -143,7 +174,7 @@
 			} else if errors.Is(statErr, fs.ErrNotExist) {
 				renameErr := root.Rename(src, dst)
 				if renameErr == nil {
-					return nil
+					return applyPromotedFilePermissions(root, dst, perms)
 				}
 
 				err = renameErr
@@ -163,7 +194,7 @@
 		if skipCollisionCheck {
 			_ = root.Remove(src)
 
-			return nil
+			return applyPromotedFilePermissions(root, dst, perms)
 		}
 
 		equal, vanished, cmpErr := compareRootFiles(root, src, dst)
@@ -185,7 +216,7 @@
 
 		_ = root.Remove(src)
 
-		return nil
+		return applyPromotedFilePermissions(root, dst, perms)
 	}
 }
 
--- a/receivepack/internal/service/quarantine_test.go
+++ b/receivepack/internal/service/quarantine_test.go
@@ -9,6 +9,70 @@
 	"codeberg.org/lindenii/furgit/objectstore/memory"
 )
 
+func TestPromoteQuarantineAppliesConfiguredPermissions(t *testing.T) {
+	t.Parallel()
+
+	objectsDir := t.TempDir()
+	objectsRoot, err := os.OpenRoot(objectsDir)
+	if err != nil {
+		t.Fatalf("os.OpenRoot: %v", err)
+	}
+
+	t.Cleanup(func() {
+		_ = objectsRoot.Close()
+	})
+
+	svc := New(Options{
+		Algorithm:       objectid.AlgorithmSHA1,
+		ExistingObjects: memory.New(objectid.AlgorithmSHA1),
+		ObjectsRoot:     objectsRoot,
+		PromotedObjectPermissions: &PromotedObjectPermissions{
+			DirMode:  0o751,
+			FileMode: 0o640,
+		},
+	})
+
+	quarantineName, quarantineRoot, err := svc.createQuarantineRoot()
+	if err != nil {
+		t.Fatalf("createQuarantineRoot: %v", err)
+	}
+
+	t.Cleanup(func() {
+		_ = quarantineRoot.Close()
+		_ = objectsRoot.RemoveAll(quarantineName)
+	})
+
+	if err := quarantineRoot.Mkdir("ab", 0o700); err != nil {
+		t.Fatalf("Mkdir(ab): %v", err)
+	}
+
+	if err := quarantineRoot.WriteFile(path.Join("ab", "cdef"), []byte("payload"), 0o600); err != nil {
+		t.Fatalf("WriteFile(quarantine loose): %v", err)
+	}
+
+	if err := svc.promoteQuarantine(quarantineName, quarantineRoot); err != nil {
+		t.Fatalf("promoteQuarantine: %v", err)
+	}
+
+	dirInfo, err := 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 := 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()
 
--- a/receivepack/options.go
+++ b/receivepack/options.go
@@ -1,6 +1,7 @@
 package receivepack
 
 import (
+	"io/fs"
 	"os"
 
 	"codeberg.org/lindenii/furgit/objectid"
@@ -8,6 +9,13 @@
 	"codeberg.org/lindenii/furgit/refstore"
 )
 
+// PromotedObjectPermissions configures the destination permissions applied to
+// objects and directories promoted out of quarantine.
+type PromotedObjectPermissions struct {
+	DirMode  fs.FileMode
+	FileMode fs.FileMode
+}
+
 // Options configures one receive-pack invocation.
 type Options struct {
 	// GitProtocol is the raw Git protocol version string from the transport,
@@ -23,6 +31,9 @@
 	// 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
 	// TODO: Hook and policy callbacks.
 }
 
--- a/receivepack/receivepack.go
+++ b/receivepack/receivepack.go
@@ -71,6 +71,9 @@
 		Refs:            opts.Refs,
 		ExistingObjects: opts.ExistingObjects,
 		ObjectsRoot:     opts.ObjectsRoot,
+		PromotedObjectPermissions: translatePromotedObjectPermissions(
+			opts.PromotedObjectPermissions,
+		),
 	})
 
 	result, err := svc.Execute(ctx, serviceReq)
@@ -89,4 +92,17 @@
 	}
 
 	return nil
+}
+
+func translatePromotedObjectPermissions(
+	perms *PromotedObjectPermissions,
+) *service.PromotedObjectPermissions {
+	if perms == nil {
+		return nil
+	}
+
+	return &service.PromotedObjectPermissions{
+		DirMode:  perms.DirMode,
+		FileMode: perms.FileMode,
+	}
 }
--