diff options
| author | 2026-03-07 19:37:20 +0800 | |
|---|---|---|
| committer | 2026-03-07 19:37:20 +0800 | |
| commit | 563a4dfb78aaa97febd0763e9f81a740af0dd666 (patch) | |
| tree | f7ede5d05f1363021fa5b1902e9e569c2b274bf1 /refstore | |
| parent | refstore: Batch should also be staged (diff) | |
| signature | No signature | |
refstore/files: Implement batching
Diffstat (limited to 'refstore')
| -rw-r--r-- | refstore/files/batch.go | 11 | ||||
| -rw-r--r-- | refstore/files/batch_abort.go | 13 | ||||
| -rw-r--r-- | refstore/files/batch_apply.go | 70 | ||||
| -rw-r--r-- | refstore/files/batch_begin.go | 13 | ||||
| -rw-r--r-- | refstore/files/batch_queue.go | 9 | ||||
| -rw-r--r-- | refstore/files/batch_queue_ops.go | 35 | ||||
| -rw-r--r-- | refstore/files/batch_reject.go | 35 | ||||
| -rw-r--r-- | refstore/files/batch_test.go | 98 | ||||
| -rw-r--r-- | refstore/files/store.go | 1 |
9 files changed, 285 insertions, 0 deletions
diff --git a/refstore/files/batch.go b/refstore/files/batch.go new file mode 100644 index 00000000..fb804432 --- /dev/null +++ b/refstore/files/batch.go @@ -0,0 +1,11 @@ +package files + +import "codeberg.org/lindenii/furgit/refstore" + +type Batch struct { + store *Store + ops []txOp + closed bool +} + +var _ refstore.Batch = (*Batch)(nil) diff --git a/refstore/files/batch_abort.go b/refstore/files/batch_abort.go new file mode 100644 index 00000000..74aaa439 --- /dev/null +++ b/refstore/files/batch_abort.go @@ -0,0 +1,13 @@ +package files + +import "errors" + +func (batch *Batch) Abort() error { + if batch.closed { + return errors.New("refstore/files: batch already closed") + } + + batch.closed = true + + return nil +} diff --git a/refstore/files/batch_apply.go b/refstore/files/batch_apply.go new file mode 100644 index 00000000..4274df0b --- /dev/null +++ b/refstore/files/batch_apply.go @@ -0,0 +1,70 @@ +package files + +import ( + "errors" + + "codeberg.org/lindenii/furgit/refstore" +) + +func (batch *Batch) Apply() ([]refstore.BatchResult, error) { + if batch.closed { + return nil, errors.New("refstore/files: batch already closed") + } + + results := make([]refstore.BatchResult, len(batch.ops)) + seen := make(map[string]struct{}, len(batch.ops)) + + for i, op := range batch.ops { + results[i].Name = op.name + + if _, exists := seen[op.name]; exists { + batch.closed = true + + err := errors.New("refstore/files: duplicate batch operation for " + `"` + op.name + `"`) + for j := i; j < len(results); j++ { + results[j].Name = batch.ops[j].name + results[j].Error = err + } + + return results, err + } + + seen[op.name] = struct{}{} + } + + for i, op := range batch.ops { + tx := &Transaction{ + store: batch.store, + ops: []txOp{op}, + } + + if err := tx.validateOp(op); err != nil { + results[i].Error = err + continue + } + + err := tx.Commit() + if err == nil { + continue + } + + if isBatchRejected(err) { + results[i].Error = err + continue + } + + batch.closed = true + results[i].Error = err + + for j := i + 1; j < len(results); j++ { + results[j].Name = batch.ops[j].name + results[j].Error = err + } + + return results, err + } + + batch.closed = true + + return results, nil +} diff --git a/refstore/files/batch_begin.go b/refstore/files/batch_begin.go new file mode 100644 index 00000000..d45af9d3 --- /dev/null +++ b/refstore/files/batch_begin.go @@ -0,0 +1,13 @@ +package files + +import "codeberg.org/lindenii/furgit/refstore" + +// BeginBatch creates one new files batch. +// +//nolint:ireturn +func (store *Store) BeginBatch() (refstore.Batch, error) { + return &Batch{ + store: store, + ops: make([]txOp, 0, 8), + }, nil +} diff --git a/refstore/files/batch_queue.go b/refstore/files/batch_queue.go new file mode 100644 index 00000000..4a3b3cf1 --- /dev/null +++ b/refstore/files/batch_queue.go @@ -0,0 +1,9 @@ +package files + +func (batch *Batch) queue(op txOp) { + if batch.closed { + return + } + + batch.ops = append(batch.ops, op) +} diff --git a/refstore/files/batch_queue_ops.go b/refstore/files/batch_queue_ops.go new file mode 100644 index 00000000..b381a7ee --- /dev/null +++ b/refstore/files/batch_queue_ops.go @@ -0,0 +1,35 @@ +package files + +import "codeberg.org/lindenii/furgit/objectid" + +func (batch *Batch) Create(name string, newID objectid.ObjectID) { + batch.queue(txOp{name: name, kind: txCreate, newID: newID}) +} + +func (batch *Batch) Update(name string, newID, oldID objectid.ObjectID) { + batch.queue(txOp{name: name, kind: txUpdate, newID: newID, oldID: oldID}) +} + +func (batch *Batch) Delete(name string, oldID objectid.ObjectID) { + batch.queue(txOp{name: name, kind: txDelete, oldID: oldID}) +} + +func (batch *Batch) Verify(name string, oldID objectid.ObjectID) { + batch.queue(txOp{name: name, kind: txVerify, oldID: oldID}) +} + +func (batch *Batch) CreateSymbolic(name, newTarget string) { + batch.queue(txOp{name: name, kind: txCreateSymbolic, newTarget: newTarget}) +} + +func (batch *Batch) UpdateSymbolic(name, newTarget, oldTarget string) { + batch.queue(txOp{name: name, kind: txUpdateSymbolic, newTarget: newTarget, oldTarget: oldTarget}) +} + +func (batch *Batch) DeleteSymbolic(name, oldTarget string) { + batch.queue(txOp{name: name, kind: txDeleteSymbolic, oldTarget: oldTarget}) +} + +func (batch *Batch) VerifySymbolic(name, oldTarget string) { + batch.queue(txOp{name: name, kind: txVerifySymbolic, oldTarget: oldTarget}) +} diff --git a/refstore/files/batch_reject.go b/refstore/files/batch_reject.go new file mode 100644 index 00000000..27715c2d --- /dev/null +++ b/refstore/files/batch_reject.go @@ -0,0 +1,35 @@ +package files + +import ( + "errors" + "strings" + + "codeberg.org/lindenii/furgit/objectid" + "codeberg.org/lindenii/furgit/ref/refname" + "codeberg.org/lindenii/furgit/refstore" +) + +func isBatchRejected(err error) bool { + var nameErr *refname.NameError + + if errors.As(err, &nameErr) { + return true + } + + if errors.Is(err, objectid.ErrInvalidAlgorithm) || errors.Is(err, refstore.ErrReferenceNotFound) { + return true + } + + msg := err.Error() + + return strings.Contains(msg, "empty reference name") || + strings.Contains(msg, "empty symbolic target") || + strings.Contains(msg, "empty symbolic old target") || + strings.Contains(msg, "already exists") || + strings.Contains(msg, "is missing") || + strings.Contains(msg, "is not detached") || + strings.Contains(msg, "is not symbolic") || + strings.Contains(msg, "expected") || + strings.Contains(msg, "reference name conflict") || + strings.Contains(msg, "non-empty directory blocks reference") +} diff --git a/refstore/files/batch_test.go b/refstore/files/batch_test.go new file mode 100644 index 00000000..17be3850 --- /dev/null +++ b/refstore/files/batch_test.go @@ -0,0 +1,98 @@ +package files_test + +import ( + "testing" + + "codeberg.org/lindenii/furgit/internal/testgit" + "codeberg.org/lindenii/furgit/objectid" +) + +func TestBatchApplyRejectsStaleDeleteAndAppliesIndependentDelete(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) + + store := openFilesStore(t, testRepo, algo) + + batch, err := store.BeginBatch() + if err != nil { + t.Fatalf("BeginBatch: %v", err) + } + + batch.Delete("refs/heads/main", staleID) + batch.Delete("refs/heads/topic", commitID) + + results, err := batch.Apply() + if err != nil { + t.Fatalf("Apply: %v", err) + } + + if len(results) != 2 { + t.Fatalf("len(results) = %d, want 2", len(results)) + } + + if results[0].Error == nil { + t.Fatal("stale delete unexpectedly succeeded") + } + + if results[1].Error != nil { + t.Fatalf("valid delete failed: %v", results[1].Error) + } + + if _, err := store.Resolve("refs/heads/main"); err != nil { + t.Fatalf("Resolve(main): %v", err) + } + + if _, err := store.Resolve("refs/heads/topic"); err == nil { + t.Fatal("refs/heads/topic still exists") + } + }) +} + +func TestBatchApplyRejectsDuplicateQueuedRef(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) + + store := openFilesStore(t, testRepo, algo) + + batch, err := store.BeginBatch() + if err != nil { + t.Fatalf("BeginBatch: %v", err) + } + + batch.Delete("refs/heads/main", commitID) + batch.Verify("refs/heads/main", commitID) + + results, err := batch.Apply() + if err == nil { + t.Fatal("Apply unexpectedly succeeded") + } + + if len(results) != 2 { + t.Fatalf("len(results) = %d, want 2", len(results)) + } + + if results[1].Error == nil { + t.Fatal("duplicate ref operation did not report an error") + } + + if _, err := store.Resolve("refs/heads/main"); err != nil { + t.Fatalf("Resolve(main): %v", err) + } + }) +} diff --git a/refstore/files/store.go b/refstore/files/store.go index 0328bc65..6091c000 100644 --- a/refstore/files/store.go +++ b/refstore/files/store.go @@ -27,4 +27,5 @@ type Store struct { var ( _ refstore.ReadingStore = (*Store)(nil) _ refstore.TransactionalStore = (*Store)(nil) + _ refstore.BatchStore = (*Store)(nil) ) |
