shithub: furgit

Download patch

ref: 6634953afb1af520ed6e523d96e58031b55058ac
parent: ab174c473618dd3743881cf44e02c2db4d1ecd5f
author: Runxi Yu <runxiyu@umich.edu>
date: Mon Mar 30 22:09:47 EDT 2026

ref/store: Shape Batch a bit more like Transaction and eagerly validate what we could.

--- a/network/receivepack/service/apply.go
+++ b/network/receivepack/service/apply.go
@@ -57,7 +57,15 @@
 	}
 
 	for _, command := range commands {
-		queueWriteBatch(batch, command)
+		err = queueWriteBatch(batch, command)
+		if err != nil {
+			_ = batch.Abort()
+
+			fillCommandErrors(result, commands, err.Error())
+			utils.BestEffortFprintf(service.opts.Progress, "updating refs: failed while queueing batch.\n")
+
+			return nil
+		}
 	}
 
 	batchResults, err := batch.Apply()
@@ -107,20 +115,16 @@
 	return tx.Update(command.Name, command.NewID, command.OldID)
 }
 
-func queueWriteBatch(batch refstore.Batch, command Command) {
+func queueWriteBatch(batch refstore.Batch, command Command) error {
 	if isDelete(command) {
-		batch.Delete(command.Name, command.OldID)
-
-		return
+		return batch.Delete(command.Name, command.OldID)
 	}
 
 	if command.OldID == command.OldID.Algorithm().Zero() {
-		batch.Create(command.Name, command.NewID)
-
-		return
+		return batch.Create(command.Name, command.NewID)
 	}
 
-	batch.Update(command.Name, command.NewID, command.OldID)
+	return batch.Update(command.Name, command.NewID, command.OldID)
 }
 
 func successCommandResult(command Command) CommandResult {
--- a/ref/store/batch.go
+++ b/ref/store/batch.go
@@ -14,31 +14,34 @@
 type Batch interface {
 	// Create creates one detached reference, requiring that the logical
 	// reference does not already exist.
-	Create(name string, newID objectid.ObjectID)
+	Create(name string, newID objectid.ObjectID) error
 	// Update updates one detached reference, requiring that the current logical
 	// reference value matches oldID.
-	Update(name string, newID, oldID objectid.ObjectID)
+	Update(name string, newID, oldID objectid.ObjectID) error
 	// Delete deletes one detached reference, requiring that the current logical
 	// reference value matches oldID.
-	Delete(name string, oldID objectid.ObjectID)
+	Delete(name string, oldID objectid.ObjectID) error
 	// Verify verifies that the current logical reference value matches oldID.
-	Verify(name string, oldID objectid.ObjectID)
+	Verify(name string, oldID objectid.ObjectID) error
 
 	// CreateSymbolic creates one symbolic reference, requiring that the named
 	// reference does not already exist.
-	CreateSymbolic(name, newTarget string)
+	CreateSymbolic(name, newTarget string) error
 	// UpdateSymbolic updates one symbolic reference directly, requiring that its
 	// current target matches oldTarget.
-	UpdateSymbolic(name, newTarget, oldTarget string)
+	UpdateSymbolic(name, newTarget, oldTarget string) error
 	// DeleteSymbolic deletes one symbolic reference directly, requiring that its
 	// current target matches oldTarget.
-	DeleteSymbolic(name, oldTarget string)
+	DeleteSymbolic(name, oldTarget string) error
 	// VerifySymbolic verifies that the named symbolic reference currently points
 	// at oldTarget.
-	VerifySymbolic(name, oldTarget string)
+	VerifySymbolic(name, oldTarget string) error
 
 	// Apply validates and applies queued operations, returning one result per
 	// queued operation in order. Fatal backend failures are returned separately.
+	//
+	// Malformed operations are rejected by the queueing methods above and do not
+	// enter the batch.
 	//
 	// Apply invalidates the receiver.
 	Apply() ([]BatchResult, error)
--- a/ref/store/files/batch_apply.go
+++ b/ref/store/files/batch_apply.go
@@ -13,14 +13,6 @@
 	for i, op := range batch.ops {
 		results[i].Name = op.name
 
-		err := executor.validateQueuedUpdate(op)
-		if err != nil {
-			results[i].Status = refstore.BatchStatusRejected
-			results[i].Error = batchResultError(err)
-
-			continue
-		}
-
 		target, err := executor.resolveQueuedUpdateTarget(op)
 		if err != nil {
 			if isBatchRejected(err) {
--- a/ref/store/files/batch_queue.go
+++ b/ref/store/files/batch_queue.go
@@ -1,5 +1,12 @@
 package files
 
-func (batch *Batch) queue(op queuedUpdate) {
+func (batch *Batch) queue(op queuedUpdate) error {
+	err := (&refUpdateExecutor{store: batch.store}).validateQueuedUpdate(op)
+	if err != nil {
+		return err
+	}
+
 	batch.ops = append(batch.ops, op)
+
+	return nil
 }
--- a/ref/store/files/batch_queue_ops.go
+++ b/ref/store/files/batch_queue_ops.go
@@ -3,41 +3,41 @@
 import objectid "codeberg.org/lindenii/furgit/object/id"
 
 // Create queues a detached reference creation.
-func (batch *Batch) Create(name string, newID objectid.ObjectID) {
-	batch.queue(queuedUpdate{name: name, kind: updateCreate, newID: newID})
+func (batch *Batch) Create(name string, newID objectid.ObjectID) error {
+	return batch.queue(queuedUpdate{name: name, kind: updateCreate, newID: newID})
 }
 
 // Update queues a detached reference update.
-func (batch *Batch) Update(name string, newID, oldID objectid.ObjectID) {
-	batch.queue(queuedUpdate{name: name, kind: updateReplace, newID: newID, oldID: oldID})
+func (batch *Batch) Update(name string, newID, oldID objectid.ObjectID) error {
+	return batch.queue(queuedUpdate{name: name, kind: updateReplace, newID: newID, oldID: oldID})
 }
 
 // Delete queues a detached reference deletion.
-func (batch *Batch) Delete(name string, oldID objectid.ObjectID) {
-	batch.queue(queuedUpdate{name: name, kind: updateDelete, oldID: oldID})
+func (batch *Batch) Delete(name string, oldID objectid.ObjectID) error {
+	return batch.queue(queuedUpdate{name: name, kind: updateDelete, oldID: oldID})
 }
 
 // Verify queues a detached reference verification.
-func (batch *Batch) Verify(name string, oldID objectid.ObjectID) {
-	batch.queue(queuedUpdate{name: name, kind: updateVerify, oldID: oldID})
+func (batch *Batch) Verify(name string, oldID objectid.ObjectID) error {
+	return batch.queue(queuedUpdate{name: name, kind: updateVerify, oldID: oldID})
 }
 
 // CreateSymbolic queues a symbolic reference creation.
-func (batch *Batch) CreateSymbolic(name, newTarget string) {
-	batch.queue(queuedUpdate{name: name, kind: updateCreateSymbolic, newTarget: newTarget})
+func (batch *Batch) CreateSymbolic(name, newTarget string) error {
+	return batch.queue(queuedUpdate{name: name, kind: updateCreateSymbolic, newTarget: newTarget})
 }
 
 // UpdateSymbolic queues a symbolic reference update.
-func (batch *Batch) UpdateSymbolic(name, newTarget, oldTarget string) {
-	batch.queue(queuedUpdate{name: name, kind: updateReplaceSymbolic, newTarget: newTarget, oldTarget: oldTarget})
+func (batch *Batch) UpdateSymbolic(name, newTarget, oldTarget string) error {
+	return batch.queue(queuedUpdate{name: name, kind: updateReplaceSymbolic, newTarget: newTarget, oldTarget: oldTarget})
 }
 
 // DeleteSymbolic queues a symbolic reference deletion.
-func (batch *Batch) DeleteSymbolic(name, oldTarget string) {
-	batch.queue(queuedUpdate{name: name, kind: updateDeleteSymbolic, oldTarget: oldTarget})
+func (batch *Batch) DeleteSymbolic(name, oldTarget string) error {
+	return batch.queue(queuedUpdate{name: name, kind: updateDeleteSymbolic, oldTarget: oldTarget})
 }
 
 // VerifySymbolic queues a symbolic reference verification.
-func (batch *Batch) VerifySymbolic(name, oldTarget string) {
-	batch.queue(queuedUpdate{name: name, kind: updateVerifySymbolic, oldTarget: oldTarget})
+func (batch *Batch) VerifySymbolic(name, oldTarget string) error {
+	return batch.queue(queuedUpdate{name: name, kind: updateVerifySymbolic, oldTarget: oldTarget})
 }
--- a/ref/store/files/batch_test.go
+++ b/ref/store/files/batch_test.go
@@ -29,9 +29,16 @@
 			t.Fatalf("BeginBatch: %v", err)
 		}
 
-		batch.Delete("refs/heads/main", staleID)
-		batch.Delete("refs/heads/topic", commitID)
+		err = batch.Delete("refs/heads/main", staleID)
+		if err != nil {
+			t.Fatalf("Delete(main) queue: %v", err)
+		}
 
+		err = batch.Delete("refs/heads/topic", commitID)
+		if err != nil {
+			t.Fatalf("Delete(topic) queue: %v", err)
+		}
+
 		results, err := batch.Apply()
 		if err != nil {
 			t.Fatalf("Apply: %v", err)
@@ -83,8 +90,15 @@
 			t.Fatalf("BeginBatch: %v", err)
 		}
 
-		batch.Delete("refs/heads/main", commitID)
-		batch.Verify("refs/heads/main", commitID)
+		err = batch.Delete("refs/heads/main", commitID)
+		if err != nil {
+			t.Fatalf("Delete(main) queue: %v", err)
+		}
+
+		err = batch.Verify("refs/heads/main", commitID)
+		if err != nil {
+			t.Fatalf("Verify(main) queue: %v", err)
+		}
 
 		results, err := batch.Apply()
 		if err != nil {
--