shithub: furgit

Download patch

ref: 8aa2e9f0903a80c90a9d8308138439d6f8732050
parent: 563a4dfb78aaa97febd0763e9f81a740af0dd666
author: Runxi Yu <me@runxiyu.org>
date: Sat Mar 7 14:40:27 EST 2026

receivepack: Use refs

--- a/receivepack/int_test.go
+++ b/receivepack/int_test.go
@@ -3,6 +3,7 @@
 import (
 	"context"
 	"fmt"
+	"io"
 	"strings"
 	"testing"
 
@@ -13,7 +14,7 @@
 
 // TODO: actually test with send-pack
 
-func TestReceivePackDeleteOnlyReportsNotImplemented(t *testing.T) {
+func TestReceivePackDeleteOnlyAtomicDeleteSucceeds(t *testing.T) {
 	t.Parallel()
 
 	//nolint:thelper
@@ -32,11 +33,11 @@
 		)
 
 		input.WriteString(pktlineData(
-			commitID.String() + " " + objectid.Zero(algo).String() + " refs/heads/main\x00report-status delete-refs object-format=" + algo.String() + "\n",
+			commitID.String() + " " + objectid.Zero(algo).String() + " refs/heads/main\x00report-status atomic delete-refs object-format=" + algo.String() + "\n",
 		))
 		input.WriteString("0000")
 
-		err := receivepack.ReceivePack(context.Background(), &output, &strings.Builder{}, strings.NewReader(input.String()), receivepack.Options{
+		err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{
 			GitProtocol:     "",
 			Algorithm:       algo,
 			Refs:            repo.Refs(),
@@ -47,12 +48,122 @@
 		}
 
 		got := output.String()
-		if !strings.Contains(got, "ng refs/heads/main ref updates not implemented yet\n") {
+		if !strings.Contains(got, "ok refs/heads/main\n") {
 			t.Fatalf("unexpected receive-pack output %q", got)
 		}
+
+		if _, err := repo.Refs().Resolve("refs/heads/main"); err == nil {
+			t.Fatal("refs/heads/main still exists after delete push")
+		}
 	})
 }
 
+func TestReceivePackDeleteOnlyNonAtomicAppliesIndependentDeletes(t *testing.T) {
+	t.Parallel()
+
+	//nolint:thelper
+	testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) {
+		t.Parallel()
+
+		testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo})
+		_, _, commitID := testRepo.MakeCommit(t, "base")
+		_, _, staleID := testRepo.MakeCommit(t, "stale")
+		testRepo.UpdateRef(t, "refs/heads/main", commitID)
+		testRepo.UpdateRef(t, "refs/heads/topic", commitID)
+
+		repo := testRepo.OpenRepository(t)
+
+		var (
+			input  strings.Builder
+			output bufferWriteFlusher
+		)
+
+		input.WriteString(pktlineData(
+			staleID.String() + " " + objectid.Zero(algo).String() + " refs/heads/main\x00report-status delete-refs object-format=" + algo.String() + "\n",
+		))
+		input.WriteString(pktlineData(
+			commitID.String() + " " + objectid.Zero(algo).String() + " refs/heads/topic\n",
+		))
+		input.WriteString("0000")
+
+		err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{
+			GitProtocol:     "",
+			Algorithm:       algo,
+			Refs:            repo.Refs(),
+			ExistingObjects: repo.Objects(),
+		})
+		if err != nil {
+			t.Fatalf("ReceivePack: %v", err)
+		}
+
+		got := output.String()
+		if !strings.Contains(got, "ng refs/heads/main ") || !strings.Contains(got, "ok refs/heads/topic\n") {
+			t.Fatalf("unexpected receive-pack output %q", got)
+		}
+
+		if _, err := repo.Refs().Resolve("refs/heads/main"); err != nil {
+			t.Fatalf("Resolve(main): %v", err)
+		}
+
+		if _, err := repo.Refs().Resolve("refs/heads/topic"); err == nil {
+			t.Fatal("refs/heads/topic still exists after successful delete")
+		}
+	})
+}
+
+func TestReceivePackDeleteOnlyAtomicFailureLeavesAllRefsUntouched(t *testing.T) {
+	t.Parallel()
+
+	//nolint:thelper
+	testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) {
+		t.Parallel()
+
+		testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo})
+		_, _, commitID := testRepo.MakeCommit(t, "base")
+		_, _, staleID := testRepo.MakeCommit(t, "stale")
+		testRepo.UpdateRef(t, "refs/heads/main", commitID)
+		testRepo.UpdateRef(t, "refs/heads/topic", commitID)
+
+		repo := testRepo.OpenRepository(t)
+
+		var (
+			input  strings.Builder
+			output bufferWriteFlusher
+		)
+
+		input.WriteString(pktlineData(
+			staleID.String() + " " + objectid.Zero(algo).String() + " refs/heads/main\x00report-status atomic delete-refs object-format=" + algo.String() + "\n",
+		))
+		input.WriteString(pktlineData(
+			commitID.String() + " " + objectid.Zero(algo).String() + " refs/heads/topic\n",
+		))
+		input.WriteString("0000")
+
+		err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{
+			GitProtocol:     "",
+			Algorithm:       algo,
+			Refs:            repo.Refs(),
+			ExistingObjects: repo.Objects(),
+		})
+		if err != nil {
+			t.Fatalf("ReceivePack: %v", err)
+		}
+
+		got := output.String()
+		if !strings.Contains(got, "ng refs/heads/main ") || !strings.Contains(got, "ng refs/heads/topic ") {
+			t.Fatalf("unexpected receive-pack output %q", got)
+		}
+
+		if _, err := repo.Refs().Resolve("refs/heads/main"); err != nil {
+			t.Fatalf("Resolve(main): %v", err)
+		}
+
+		if _, err := repo.Refs().Resolve("refs/heads/topic"); err != nil {
+			t.Fatalf("Resolve(topic): %v", err)
+		}
+	})
+}
+
 func TestReceivePackAdvertisesResolvedHEAD(t *testing.T) {
 	t.Parallel()
 
@@ -74,7 +185,7 @@
 
 		input.WriteString("0000")
 
-		err := receivepack.ReceivePack(context.Background(), &output, &strings.Builder{}, strings.NewReader(input.String()), receivepack.Options{
+		err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{
 			Algorithm:       algo,
 			Refs:            repo.Refs(),
 			ExistingObjects: repo.Objects(),
@@ -123,11 +234,11 @@
 		)
 
 		input.WriteString(pktlineData(
-			commitID.String() + " " + objectid.Zero(algo).String() + " refs/heads/main\x00delete-refs object-format=" + algo.String() + "\n",
+			commitID.String() + " " + objectid.Zero(algo).String() + " refs/heads/main\x00delete-refs atomic object-format=" + algo.String() + "\n",
 		))
 		input.WriteString("0000")
 
-		err := receivepack.ReceivePack(context.Background(), &output, &strings.Builder{}, strings.NewReader(input.String()), receivepack.Options{
+		err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{
 			Algorithm:       algo,
 			Refs:            repo.Refs(),
 			ExistingObjects: repo.Objects(),
@@ -162,11 +273,11 @@
 		)
 
 		input.WriteString(pktlineData(
-			commitID.String() + " " + objectid.Zero(algo).String() + " refs/heads/main\x00report-status delete-refs object-format=" + algo.String() + "\n",
+			commitID.String() + " " + objectid.Zero(algo).String() + " refs/heads/main\x00report-status atomic delete-refs object-format=" + algo.String() + "\n",
 		))
 		input.WriteString("0000")
 
-		err := receivepack.ReceivePack(context.Background(), &output, &strings.Builder{}, strings.NewReader(input.String()), receivepack.Options{
+		err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{
 			GitProtocol:     gitProtocol,
 			Algorithm:       algo,
 			Refs:            repo.Refs(),
@@ -205,7 +316,7 @@
 		))
 		input.WriteString("0000")
 
-		err := receivepack.ReceivePack(context.Background(), &output, &strings.Builder{}, strings.NewReader(input.String()), receivepack.Options{
+		err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{
 			Algorithm:       algo,
 			Refs:            repo.Refs(),
 			ExistingObjects: repo.Objects(),
@@ -217,6 +328,124 @@
 		got := output.String()
 		if !strings.Contains(got, "unpack objects root not configured\n") {
 			t.Fatalf("unexpected receive-pack output %q", got)
+		}
+	})
+}
+
+func TestReceivePackPackCreatePromotesObjectsAndUpdatesRef(t *testing.T) {
+	t.Parallel()
+
+	//nolint:thelper
+	testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) {
+		t.Parallel()
+
+		sender := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo})
+		_, _, commitID := sender.MakeCommit(t, "pushed commit")
+
+		receiver := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo, Bare: true})
+		repo := receiver.OpenRepository(t)
+		objectsRoot := receiver.OpenObjectsRoot(t)
+
+		packStream := sender.PackObjectsReader(t, []string{commitID.String()}, false)
+		t.Cleanup(func() {
+			_ = packStream.Close()
+		})
+
+		var (
+			input  strings.Builder
+			output bufferWriteFlusher
+		)
+
+		input.WriteString(pktlineData(
+			objectid.Zero(algo).String() + " " + commitID.String() + " refs/heads/main\x00report-status-v2 atomic object-format=" + algo.String() + "\n",
+		))
+		input.WriteString("0000")
+
+		err := receivepack.ReceivePack(
+			context.Background(),
+			&output,
+			io.MultiReader(strings.NewReader(input.String()), packStream),
+			receivepack.Options{
+				Algorithm:       algo,
+				Refs:            repo.Refs(),
+				ExistingObjects: repo.Objects(),
+				ObjectsRoot:     objectsRoot,
+			},
+		)
+		if err != nil {
+			t.Fatalf("ReceivePack: %v", err)
+		}
+
+		got := output.String()
+		if !strings.Contains(got, "unpack ok\n") || !strings.Contains(got, "ok refs/heads/main\n") {
+			t.Fatalf("unexpected receive-pack output %q", got)
+		}
+
+		reopened := receiver.OpenRepository(t)
+
+		resolved, err := reopened.Refs().ResolveFully("refs/heads/main")
+		if err != nil {
+			t.Fatalf("ResolveFully(main): %v", err)
+		}
+
+		if resolved.ID != commitID {
+			t.Fatalf("refs/heads/main = %s, want %s", resolved.ID, commitID)
+		}
+
+		if gotType := receiver.Run(t, "cat-file", "-t", commitID.String()); gotType != "commit" {
+			t.Fatalf("cat-file -t = %q, want commit", gotType)
+		}
+
+		packs := receiver.Run(t, "count-objects", "-v")
+		if !strings.Contains(packs, "packs: 1") {
+			t.Fatalf("count-objects output missing promoted pack: %q", packs)
+		}
+	})
+}
+
+func TestReceivePackReportStatusV2IncludesRefDetails(t *testing.T) {
+	t.Parallel()
+
+	//nolint:thelper
+	testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) {
+		t.Parallel()
+
+		testRepo := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: algo})
+		_, _, commitID := testRepo.MakeCommit(t, "base")
+		testRepo.UpdateRef(t, "refs/heads/main", commitID)
+
+		repo := testRepo.OpenRepository(t)
+
+		var (
+			input  strings.Builder
+			output bufferWriteFlusher
+		)
+
+		input.WriteString(pktlineData(
+			commitID.String() + " " + objectid.Zero(algo).String() + " refs/heads/main\x00report-status-v2 atomic delete-refs object-format=" + algo.String() + "\n",
+		))
+		input.WriteString("0000")
+
+		err := receivepack.ReceivePack(context.Background(), &output, strings.NewReader(input.String()), receivepack.Options{
+			Algorithm:       algo,
+			Refs:            repo.Refs(),
+			ExistingObjects: repo.Objects(),
+		})
+		if err != nil {
+			t.Fatalf("ReceivePack: %v", err)
+		}
+
+		got := output.String()
+		if !strings.Contains(got, "option refname refs/heads/main\n") {
+			t.Fatalf("missing option refname in %q", got)
+		}
+
+		if !strings.Contains(got, "option old-oid "+commitID.String()+"\n") {
+			t.Fatalf("missing option old-oid in %q", got)
+		}
+
+		if !strings.Contains(got, "option new-oid "+objectid.Zero(algo).String()+"\n") {
+			t.Fatalf("missing option new-oid in %q", got)
 		}
 	})
 }
--- /dev/null
+++ b/receivepack/internal/service/apply.go
@@ -1,0 +1,107 @@
+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
+++ b/receivepack/internal/service/command.go
@@ -12,8 +12,11 @@
 func fillCommandErrors(result *Result, commands []Command, errText string) {
 	for _, command := range commands {
 		result.Commands = append(result.Commands, CommandResult{
-			Name:  command.Name,
-			Error: errText,
+			Name:    command.Name,
+			Error:   errText,
+			RefName: command.Name,
+			OldID:   objectIDPointer(command.OldID),
+			NewID:   objectIDPointer(command.NewID),
 		})
 	}
 }
@@ -20,4 +23,10 @@
 
 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
+++ b/receivepack/internal/service/command_result.go
@@ -1,7 +1,13 @@
 package service
 
+import "codeberg.org/lindenii/furgit/objectid"
+
 // CommandResult is one per-command execution result.
 type CommandResult struct {
-	Name  string
-	Error string
+	Name         string
+	Error        string
+	RefName      string
+	OldID        *objectid.ObjectID
+	NewID        *objectid.ObjectID
+	ForcedUpdate bool
 }
--- a/receivepack/internal/service/execute.go
+++ b/receivepack/internal/service/execute.go
@@ -2,7 +2,7 @@
 
 import (
 	"context"
-	"log"
+	"os"
 
 	"codeberg.org/lindenii/furgit/format/pack/ingest"
 )
@@ -12,8 +12,6 @@
 //
 // TODO: Invoke hook or policy callbacks to decide whether each planned update
 // should be allowed.
-// TODO: Apply planned ref updates with one atomic compare-and-swap ref
-// transaction once ref writing exists.
 func (service *Service) Execute(ctx context.Context, req *Request) (*Result, error) {
 	_ = ctx
 
@@ -20,6 +18,11 @@
 	result := &Result{
 		Commands: make([]CommandResult, 0, len(req.Commands)),
 	}
+	var (
+		quarantineName string
+		quarantineRoot *os.Root
+		err            error
+	)
 
 	if req.PackExpected {
 		if req.Pack == nil {
@@ -36,7 +39,7 @@
 			return result, nil
 		}
 
-		quarantineName, quarantineRoot, err := service.createQuarantineRoot()
+		quarantineName, quarantineRoot, err = service.createQuarantineRoot()
 		if err != nil {
 			result.UnpackError = err.Error()
 			fillCommandErrors(result, req.Commands, err.Error())
@@ -46,14 +49,24 @@
 
 		defer func() {
 			_ = quarantineRoot.Close()
-			// TODO: Promote accepted quarantined objects into the permanent object
-			// store once atomic ref application exists.
 			_ = service.opts.ObjectsRoot.RemoveAll(quarantineName)
 		}()
 
+		quarantinePackRoot, err := service.openQuarantinePackRoot(quarantineRoot)
+		if err != nil {
+			result.UnpackError = err.Error()
+			fillCommandErrors(result, req.Commands, err.Error())
+
+			return result, nil
+		}
+
+		defer func() {
+			_ = quarantinePackRoot.Close()
+		}()
+
 		ingested, err := ingest.Ingest(
 			req.Pack,
-			quarantineRoot,
+			quarantinePackRoot,
 			service.opts.Algorithm,
 			true,
 			true,
@@ -78,11 +91,35 @@
 		})
 	}
 
-	fillCommandErrors(result, req.Commands, "ref updates not implemented yet")
-	log.Printf(
-		"receivepack: planned %d ref updates, but hook/policy checks and atomic ref writes are not implemented yet",
-		len(result.Planned),
-	)
+	if len(req.Commands) == 0 {
+		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 {
+		err := service.applyAtomic(result, req.Commands)
+		if err != nil {
+			return result, err
+		}
+
+		return result, nil
+	}
+
+	err = service.applyBatch(result, req.Commands)
+	if err != nil {
+		return result, err
+	}
 
 	return result, nil
 }
--- a/receivepack/internal/service/options.go
+++ b/receivepack/internal/service/options.go
@@ -11,7 +11,7 @@
 // Options configures one protocol-independent receive-pack service.
 type Options struct {
 	Algorithm       objectid.Algorithm
-	Refs            refstore.ReadingStore
+	Refs            refstore.ReadWriteStore
 	ExistingObjects objectstore.Store
 	ObjectsRoot     *os.Root
 	// TODO: Hook and such callbacks.
--- a/receivepack/internal/service/quarantine.go
+++ b/receivepack/internal/service/quarantine.go
@@ -1,8 +1,15 @@
 package service
 
 import (
+	"bytes"
 	"crypto/rand"
+	"errors"
+	"fmt"
+	"io"
+	"io/fs"
 	"os"
+	"path"
+	"slices"
 )
 
 // createQuarantineRoot creates one per-push quarantine directory beneath the
@@ -23,4 +30,199 @@
 	}
 
 	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.promoteQuarantineDir(quarantineName, quarantineRoot, childRel)
+			if err != nil {
+				return err
+			}
+
+			continue
+		}
+
+		err = finalizeQuarantineFile(
+			service.opts.ObjectsRoot,
+			path.Join(quarantineName, childRel),
+			childRel,
+			isLooseObjectShardPath(rel),
+		)
+		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 finalizeQuarantineFile(root *os.Root, src, dst string, skipCollisionCheck bool) error {
+	const maxVanishedRetries = 5
+
+	for retries := 0; ; retries++ {
+		err := root.Link(src, dst)
+		switch {
+		case err == nil:
+			_ = root.Remove(src)
+
+			return nil
+		case !errors.Is(err, fs.ErrExist):
+			_, statErr := root.Stat(dst)
+			if statErr == nil {
+				err = fs.ErrExist
+			} else if errors.Is(statErr, fs.ErrNotExist) {
+				if renameErr := root.Rename(src, dst); renameErr == nil {
+					return nil
+				}
+
+				return fmt.Errorf("promote quarantine %q -> %q: %w", src, dst, err)
+			} else {
+				return statErr
+			}
+		}
+
+		if skipCollisionCheck {
+			_ = root.Remove(src)
+
+			return nil
+		}
+
+		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 nil
+	}
+}
+
+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/internal/service/quarantine_test.go
@@ -1,0 +1,163 @@
+package service
+
+import (
+	"os"
+	"path"
+	"testing"
+
+	"codeberg.org/lindenii/furgit/objectid"
+	"codeberg.org/lindenii/furgit/objectstore/memory"
+)
+
+func TestPromoteQuarantineTreatsExistingLooseObjectAsSuccess(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,
+	})
+
+	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", 0o755); err != nil {
+		t.Fatalf("Mkdir(ab): %v", err)
+	}
+
+	if err := objectsRoot.Mkdir("ab", 0o755); err != nil {
+		t.Fatalf("Mkdir(dst ab): %v", err)
+	}
+
+	const payload = "same object bytes"
+	if err := quarantineRoot.WriteFile(path.Join("ab", "cdef"), []byte(payload), 0o644); err != nil {
+		t.Fatalf("WriteFile(quarantine loose): %v", err)
+	}
+
+	if err := objectsRoot.WriteFile(path.Join("ab", "cdef"), []byte(payload), 0o644); err != nil {
+		t.Fatalf("WriteFile(permanent loose): %v", err)
+	}
+
+	if err := svc.promoteQuarantine(quarantineName, quarantineRoot); err != nil {
+		t.Fatalf("promoteQuarantine: %v", err)
+	}
+}
+
+func TestPromoteQuarantineRejectsDifferentExistingPackFile(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,
+	})
+
+	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("pack", 0o755); err != nil {
+		t.Fatalf("Mkdir(pack): %v", err)
+	}
+
+	if err := objectsRoot.Mkdir("pack", 0o755); err != nil {
+		t.Fatalf("Mkdir(dst pack): %v", err)
+	}
+
+	if err := quarantineRoot.WriteFile(path.Join("pack", "pack-a.pack"), []byte("new bytes"), 0o644); err != nil {
+		t.Fatalf("WriteFile(quarantine pack): %v", err)
+	}
+
+	if err := objectsRoot.WriteFile(path.Join("pack", "pack-a.pack"), []byte("old bytes"), 0o644); err != nil {
+		t.Fatalf("WriteFile(permanent pack): %v", err)
+	}
+
+	err = svc.promoteQuarantine(quarantineName, quarantineRoot)
+	if err == nil {
+		t.Fatal("promoteQuarantine unexpectedly succeeded")
+	}
+}
+
+func TestPromoteQuarantineAcceptsMatchingExistingPackFile(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,
+	})
+
+	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("pack", 0o755); err != nil {
+		t.Fatalf("Mkdir(pack): %v", err)
+	}
+
+	if err := objectsRoot.Mkdir("pack", 0o755); err != nil {
+		t.Fatalf("Mkdir(dst pack): %v", err)
+	}
+
+	const payload = "identical pack bytes"
+	if err := quarantineRoot.WriteFile(path.Join("pack", "pack-a.pack"), []byte(payload), 0o644); err != nil {
+		t.Fatalf("WriteFile(quarantine pack): %v", err)
+	}
+
+	if err := objectsRoot.WriteFile(path.Join("pack", "pack-a.pack"), []byte(payload), 0o644); err != nil {
+		t.Fatalf("WriteFile(permanent pack): %v", err)
+	}
+
+	if err := svc.promoteQuarantine(quarantineName, quarantineRoot); err != nil {
+		t.Fatalf("promoteQuarantine: %v", err)
+	}
+}
--- a/receivepack/internal/service/request.go
+++ b/receivepack/internal/service/request.go
@@ -6,6 +6,7 @@
 type Request struct {
 	Commands     []Command
 	PushOptions  []string
+	Atomic       bool
 	DeleteOnly   bool
 	PackExpected bool
 	Pack         io.Reader
--- a/receivepack/options.go
+++ b/receivepack/options.go
@@ -16,7 +16,7 @@
 	// Algorithm is the repository object ID algorithm used by the push session.
 	Algorithm objectid.Algorithm
 	// Refs is the reference store visible to the push.
-	Refs refstore.ReadingStore
+	Refs refstore.ReadWriteStore
 	// ExistingObjects is the object store visible to the push before any newly
 	// uploaded quarantined objects are promoted.
 	ExistingObjects objectstore.Store
--- a/receivepack/receivepack.go
+++ b/receivepack/receivepack.go
@@ -14,12 +14,9 @@
 func ReceivePack(
 	ctx context.Context,
 	w pktline.WriteFlusher,
-	e io.Writer,
 	r io.Reader,
 	opts Options,
 ) error {
-	_ = e // TODO: Use stderr/progress sink explicitly as hook/progress behavior expands.
-
 	err := validateOptions(opts)
 	if err != nil {
 		return err
@@ -63,6 +60,7 @@
 	serviceReq := &service.Request{
 		Commands:     translateCommands(req.Commands),
 		PushOptions:  append([]string(nil), req.PushOptions...),
+		Atomic:       req.Capabilities.Atomic,
 		DeleteOnly:   req.DeleteOnly,
 		PackExpected: req.PackExpected,
 		Pack:         r,
--- a/receivepack/translate.go
+++ b/receivepack/translate.go
@@ -26,8 +26,12 @@
 
 	for _, command := range result.Commands {
 		out.Commands = append(out.Commands, protoreceive.CommandResult{
-			Name:  command.Name,
-			Error: command.Error,
+			Name:         command.Name,
+			Error:        command.Error,
+			RefName:      command.RefName,
+			OldID:        command.OldID,
+			NewID:        command.NewID,
+			ForcedUpdate: command.ForcedUpdate,
 		})
 	}
 
--