diff options
| author | 2026-03-23 03:25:44 +0000 | |
|---|---|---|
| committer | 2026-03-23 03:27:52 +0000 | |
| commit | 4a796e64ac576d6a3e3f2fe6174c4aa476ea0c5c (patch) | |
| tree | 44d72a20076ceab0981d0b553693d26ca36cc0be /refstore/files | |
| parent | receivepack: Lifecycle/ownership docs (diff) | |
| signature | No signature | |
refstore: Improve interfaces, errors, and make batch work v0.1.92
Diffstat (limited to 'refstore/files')
51 files changed, 709 insertions, 546 deletions
diff --git a/refstore/files/batch.go b/refstore/files/batch.go index fb804432..43eb1a08 100644 --- a/refstore/files/batch.go +++ b/refstore/files/batch.go @@ -3,9 +3,8 @@ package files import "codeberg.org/lindenii/furgit/refstore" type Batch struct { - store *Store - ops []txOp - closed bool + store *Store + ops []queuedUpdate } var _ refstore.Batch = (*Batch)(nil) diff --git a/refstore/files/batch_abort.go b/refstore/files/batch_abort.go index 74aaa439..0cbd1651 100644 --- a/refstore/files/batch_abort.go +++ b/refstore/files/batch_abort.go @@ -1,13 +1,5 @@ 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 index 0c217c56..55224b36 100644 --- a/refstore/files/batch_apply.go +++ b/refstore/files/batch_apply.go @@ -1,73 +1,135 @@ package files -import ( - "errors" - - "codeberg.org/lindenii/furgit/refstore" -) +import "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)) + remainingIdx := make([]int, 0, len(batch.ops)) + remainingOps := make([]queuedUpdate, 0, len(batch.ops)) + seenTargets := make(map[string]struct{}, len(batch.ops)) + executor := &refUpdateExecutor{store: batch.store} for i, op := range batch.ops { results[i].Name = op.name - if _, exists := seen[op.name]; exists { - batch.closed = true + 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) { + results[i].Status = refstore.BatchStatusRejected + results[i].Error = batchResultError(err) + + continue + } + + results[i].Status = refstore.BatchStatusFatal + results[i].Error = batchResultError(err) - err := errors.New("refstore/files: duplicate batch operation for " + `"` + op.name + `"`) - for j := i; j < len(results); j++ { + for j := i + 1; j < len(results); j++ { results[j].Name = batch.ops[j].name - results[j].Error = err + results[j].Status = refstore.BatchStatusNotAttempted + results[j].Error = batchResultError(err) } return results, err } - seen[op.name] = struct{}{} - } + targetKey := updateTargetKey(target.loc) + if _, exists := seenTargets[targetKey]; exists { + results[i].Status = refstore.BatchStatusRejected + results[i].Error = &refstore.DuplicateUpdateError{} - for i, op := range batch.ops { - tx := &Transaction{ - store: batch.store, - ops: []txOp{op}, + continue } - err := tx.validateOp(op) + seenTargets[targetKey] = struct{}{} + remainingIdx = append(remainingIdx, i) + remainingOps = append(remainingOps, op) + } + + for len(remainingOps) > 0 { + prepared, err := executor.prepareUpdates(remainingOps) if err != nil { - results[i].Error = err + if isBatchRejected(err) { + name := batchResultName(err) + rejectedAt := -1 - continue - } + for i, op := range remainingOps { + if op.name == name { + rejectedAt = i - err = tx.Commit() - if err == nil { - continue - } + break + } + } - if isBatchRejected(err) { - results[i].Error = err + if rejectedAt < 0 { + for _, idx := range remainingIdx { + results[idx].Status = refstore.BatchStatusNotAttempted + results[idx].Error = batchResultError(err) + } - continue + return results, err + } + + results[remainingIdx[rejectedAt]].Status = refstore.BatchStatusRejected + results[remainingIdx[rejectedAt]].Error = batchResultError(err) + remainingIdx = append(remainingIdx[:rejectedAt], remainingIdx[rejectedAt+1:]...) + remainingOps = append(remainingOps[:rejectedAt], remainingOps[rejectedAt+1:]...) + + continue + } + + fatalName := batchResultName(err) + fatalMarked := false + for i, idx := range remainingIdx { + if !fatalMarked && remainingOps[i].name == fatalName && fatalName != "" { + results[idx].Status = refstore.BatchStatusFatal + results[idx].Error = batchResultError(err) + fatalMarked = true + + continue + } + + results[idx].Status = refstore.BatchStatusNotAttempted + results[idx].Error = batchResultError(err) + } + + return results, err } - batch.closed = true - results[i].Error = err + err = executor.commitPreparedUpdates(prepared) + if err != nil { + fatalName := batchResultName(err) + fatalMarked := false + for i, idx := range remainingIdx { + if !fatalMarked && remainingOps[i].name == fatalName && fatalName != "" { + results[idx].Status = refstore.BatchStatusFatal + results[idx].Error = batchResultError(err) + fatalMarked = true + + continue + } + + results[idx].Status = refstore.BatchStatusNotAttempted + results[idx].Error = batchResultError(err) + } - for j := i + 1; j < len(results); j++ { - results[j].Name = batch.ops[j].name - results[j].Error = err + return results, err } - return results, err - } + for _, idx := range remainingIdx { + results[idx].Status = refstore.BatchStatusApplied + } - batch.closed = true + return results, nil + } return results, nil } diff --git a/refstore/files/batch_begin.go b/refstore/files/batch_begin.go index d45af9d3..06459b2c 100644 --- a/refstore/files/batch_begin.go +++ b/refstore/files/batch_begin.go @@ -8,6 +8,6 @@ import "codeberg.org/lindenii/furgit/refstore" func (store *Store) BeginBatch() (refstore.Batch, error) { return &Batch{ store: store, - ops: make([]txOp, 0, 8), + ops: make([]queuedUpdate, 0, 8), }, nil } diff --git a/refstore/files/batch_queue.go b/refstore/files/batch_queue.go index 4a3b3cf1..5937c6fb 100644 --- a/refstore/files/batch_queue.go +++ b/refstore/files/batch_queue.go @@ -1,9 +1,5 @@ package files -func (batch *Batch) queue(op txOp) { - if batch.closed { - return - } - +func (batch *Batch) queue(op queuedUpdate) { batch.ops = append(batch.ops, op) } diff --git a/refstore/files/batch_queue_ops.go b/refstore/files/batch_queue_ops.go index b381a7ee..b74157c1 100644 --- a/refstore/files/batch_queue_ops.go +++ b/refstore/files/batch_queue_ops.go @@ -3,33 +3,33 @@ 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}) + batch.queue(queuedUpdate{name: name, kind: updateCreate, newID: newID}) } func (batch *Batch) Update(name string, newID, oldID objectid.ObjectID) { - batch.queue(txOp{name: name, kind: txUpdate, newID: newID, oldID: oldID}) + batch.queue(queuedUpdate{name: name, kind: updateReplace, newID: newID, oldID: oldID}) } func (batch *Batch) Delete(name string, oldID objectid.ObjectID) { - batch.queue(txOp{name: name, kind: txDelete, oldID: oldID}) + batch.queue(queuedUpdate{name: name, kind: updateDelete, oldID: oldID}) } func (batch *Batch) Verify(name string, oldID objectid.ObjectID) { - batch.queue(txOp{name: name, kind: txVerify, oldID: oldID}) + batch.queue(queuedUpdate{name: name, kind: updateVerify, oldID: oldID}) } func (batch *Batch) CreateSymbolic(name, newTarget string) { - batch.queue(txOp{name: name, kind: txCreateSymbolic, newTarget: newTarget}) + batch.queue(queuedUpdate{name: name, kind: updateCreateSymbolic, newTarget: newTarget}) } func (batch *Batch) UpdateSymbolic(name, newTarget, oldTarget string) { - batch.queue(txOp{name: name, kind: txUpdateSymbolic, newTarget: newTarget, oldTarget: oldTarget}) + batch.queue(queuedUpdate{name: name, kind: updateReplaceSymbolic, newTarget: newTarget, oldTarget: oldTarget}) } func (batch *Batch) DeleteSymbolic(name, oldTarget string) { - batch.queue(txOp{name: name, kind: txDeleteSymbolic, oldTarget: oldTarget}) + batch.queue(queuedUpdate{name: name, kind: updateDeleteSymbolic, oldTarget: oldTarget}) } func (batch *Batch) VerifySymbolic(name, oldTarget string) { - batch.queue(txOp{name: name, kind: txVerifySymbolic, oldTarget: oldTarget}) + batch.queue(queuedUpdate{name: name, kind: updateVerifySymbolic, oldTarget: oldTarget}) } diff --git a/refstore/files/batch_reject.go b/refstore/files/batch_reject.go deleted file mode 100644 index 27715c2d..00000000 --- a/refstore/files/batch_reject.go +++ /dev/null @@ -1,35 +0,0 @@ -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_rejection.go b/refstore/files/batch_rejection.go new file mode 100644 index 00000000..3f3569d6 --- /dev/null +++ b/refstore/files/batch_rejection.go @@ -0,0 +1,19 @@ +package files + +import ( + "errors" + + "codeberg.org/lindenii/furgit/refstore" +) + +func isBatchRejected(err error) bool { + return errors.Is(err, refstore.ErrReferenceNotFound) || + errors.As(err, new(*refstore.InvalidNameError)) || + errors.As(err, new(*refstore.InvalidValueError)) || + errors.As(err, new(*refstore.DuplicateUpdateError)) || + errors.As(err, new(*refstore.CreateExistsError)) || + errors.As(err, new(*refstore.IncorrectOldValueError)) || + errors.As(err, new(*refstore.ExpectedDetachedError)) || + errors.As(err, new(*refstore.ExpectedSymbolicError)) || + errors.As(err, new(*refstore.NameConflictError)) +} diff --git a/refstore/files/batch_result_error.go b/refstore/files/batch_result_error.go new file mode 100644 index 00000000..06d68273 --- /dev/null +++ b/refstore/files/batch_result_error.go @@ -0,0 +1,21 @@ +package files + +import "errors" + +func batchResultError(err error) error { + updateErr, ok := errors.AsType[*updateContextError](err) + if ok { + return updateErr.err + } + + return err +} + +func batchResultName(err error) string { + updateErr, ok := errors.AsType[*updateContextError](err) + if !ok { + return "" + } + + return updateErr.name +} diff --git a/refstore/files/batch_test.go b/refstore/files/batch_test.go index 9a507919..d9ce9ac9 100644 --- a/refstore/files/batch_test.go +++ b/refstore/files/batch_test.go @@ -1,10 +1,12 @@ package files_test import ( + "errors" "testing" "codeberg.org/lindenii/furgit/internal/testgit" "codeberg.org/lindenii/furgit/objectid" + "codeberg.org/lindenii/furgit/refstore" ) func TestBatchApplyRejectsStaleDeleteAndAppliesIndependentDelete(t *testing.T) { @@ -39,12 +41,17 @@ func TestBatchApplyRejectsStaleDeleteAndAppliesIndependentDelete(t *testing.T) { t.Fatalf("len(results) = %d, want 2", len(results)) } - if results[0].Error == nil { - t.Fatal("stale delete unexpectedly succeeded") + if results[0].Status != refstore.BatchStatusRejected { + t.Fatalf("results[0].Status = %v, want rejected", results[0].Status) } - if results[1].Error != nil { - t.Fatalf("valid delete failed: %v", results[1].Error) + if !errors.Is(results[0].Error, refstore.ErrReferenceNotFound) && + errors.As(results[0].Error, new(*refstore.IncorrectOldValueError)) == false { + t.Fatalf("results[0].Error = %v, want stale-value rejection", results[0].Error) + } + + if results[1].Status != refstore.BatchStatusApplied { + t.Fatalf("results[1].Status = %v, want applied", results[1].Status) } _, err = store.Resolve("refs/heads/main") @@ -81,20 +88,28 @@ func TestBatchApplyRejectsDuplicateQueuedRef(t *testing.T) { batch.Verify("refs/heads/main", commitID) results, err := batch.Apply() - if err == nil { - t.Fatal("Apply unexpectedly succeeded") + if err != nil { + t.Fatalf("Apply: %v", err) } 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 results[0].Status != refstore.BatchStatusApplied { + t.Fatalf("results[0].Status = %v, want applied", results[0].Status) + } + + if results[1].Status != refstore.BatchStatusRejected { + t.Fatalf("results[1].Status = %v, want rejected", results[1].Status) + } + + if !errors.As(results[1].Error, new(*refstore.DuplicateUpdateError)) { + t.Fatalf("results[1].Error = %v, want duplicate update error", results[1].Error) } _, err = store.Resolve("refs/heads/main") - if err != nil { + if !errors.Is(err, refstore.ErrReferenceNotFound) { t.Fatalf("Resolve(main): %v", err) } }) diff --git a/refstore/files/errors.go b/refstore/files/broken_ref_error.go index daa40849..daa40849 100644 --- a/refstore/files/errors.go +++ b/refstore/files/broken_ref_error.go diff --git a/refstore/files/transaction.go b/refstore/files/transaction.go index b3132d3e..1babfe60 100644 --- a/refstore/files/transaction.go +++ b/refstore/files/transaction.go @@ -6,7 +6,7 @@ import ( type Transaction struct { store *Store - ops []txOp + ops []queuedUpdate } var _ refstore.Transaction = (*Transaction)(nil) diff --git a/refstore/files/transaction_begin.go b/refstore/files/transaction_begin.go index 73bc9767..95834a33 100644 --- a/refstore/files/transaction_begin.go +++ b/refstore/files/transaction_begin.go @@ -8,6 +8,6 @@ import "codeberg.org/lindenii/furgit/refstore" func (store *Store) BeginTransaction() (refstore.Transaction, error) { return &Transaction{ store: store, - ops: make([]txOp, 0, 8), + ops: make([]queuedUpdate, 0, 8), }, nil } diff --git a/refstore/files/transaction_commit.go b/refstore/files/transaction_commit.go index dae4d8ee..4839936a 100644 --- a/refstore/files/transaction_commit.go +++ b/refstore/files/transaction_commit.go @@ -1,50 +1,11 @@ package files -import ( - "errors" - "os" -) - func (tx *Transaction) Commit() error { - prepared, err := tx.prepare() - if err != nil { - return err - } - - defer func() { - _ = tx.cleanup(prepared) - }() - - for _, item := range prepared { - if item.op.kind == txDelete || item.op.kind == txDeleteSymbolic || item.op.kind == txVerify || item.op.kind == txVerifySymbolic { - continue - } - - err = tx.writeLoose(item) - if err != nil { - return err - } - } - - err = tx.applyPackedDeletes(prepared) + executor := &refUpdateExecutor{store: tx.store} + prepared, err := executor.prepareUpdates(tx.ops) if err != nil { return err } - for _, item := range prepared { - switch item.op.kind { - case txDelete, txDeleteSymbolic: - if item.target.ref.isLoose { - err = tx.store.rootFor(item.target.loc.root).Remove(item.target.loc.path) - if err != nil && !errors.Is(err, os.ErrNotExist) { - return err - } - - tx.tryRemoveEmptyParents(item.target.name) - } - case txCreate, txUpdate, txVerify, txCreateSymbolic, txUpdateSymbolic, txVerifySymbolic: - } - } - - return nil + return executor.commitPreparedUpdates(prepared) } diff --git a/refstore/files/transaction_kind.go b/refstore/files/transaction_kind.go deleted file mode 100644 index d4f84031..00000000 --- a/refstore/files/transaction_kind.go +++ /dev/null @@ -1,14 +0,0 @@ -package files - -type txKind uint8 - -const ( - txCreate txKind = iota - txUpdate - txDelete - txVerify - txCreateSymbolic - txUpdateSymbolic - txDeleteSymbolic - txVerifySymbolic -) diff --git a/refstore/files/transaction_operation.go b/refstore/files/transaction_operation.go deleted file mode 100644 index bb24c5a2..00000000 --- a/refstore/files/transaction_operation.go +++ /dev/null @@ -1,23 +0,0 @@ -package files - -import "codeberg.org/lindenii/furgit/objectid" - -type txOp struct { - name string - kind txKind - newID objectid.ObjectID - oldID objectid.ObjectID - newTarget string - oldTarget string -} - -type preparedTxOp struct { - op txOp - target resolvedWriteTarget -} - -type resolvedWriteTarget struct { - name string - loc refPath - ref directRef -} diff --git a/refstore/files/transaction_prepare.go b/refstore/files/transaction_prepare.go deleted file mode 100644 index 38eea9d8..00000000 --- a/refstore/files/transaction_prepare.go +++ /dev/null @@ -1,102 +0,0 @@ -package files - -import ( - "fmt" - "slices" -) - -func (tx *Transaction) prepare() (prepared []preparedTxOp, err error) { - prepared = make([]preparedTxOp, 0, len(tx.ops)) - - defer func() { - if err != nil { - _ = tx.cleanup(prepared) - } - }() - - targets := make(map[string]struct{}, len(tx.ops)) - - for _, op := range tx.ops { - target, err := tx.resolveTarget(op) - if err != nil { - return prepared, err - } - - targetKey := tx.targetKey(target.loc) - if _, exists := targets[targetKey]; exists { - return prepared, fmt.Errorf("refstore/files: duplicate transaction operation for %q", target.name) - } - - targets[targetKey] = struct{}{} - - prepared = append(prepared, preparedTxOp{ - op: op, - target: target, - }) - } - - deleted := make(map[string]struct{}) - written := make([]string, 0, len(prepared)) - - for _, item := range prepared { - switch item.op.kind { - case txDelete, txDeleteSymbolic: - deleted[item.target.name] = struct{}{} - case txCreate, txUpdate, txCreateSymbolic, txUpdateSymbolic: - written = append(written, item.target.name) - case txVerify, txVerifySymbolic: - } - } - - existing, err := tx.visibleNames() - if err != nil { - return prepared, err - } - - for _, name := range written { - err = verifyRefnameAvailable(name, existing, written, deleted) - if err != nil { - return prepared, err - } - } - - lockNames := make([]string, 0, len(prepared)) - for _, item := range prepared { - lockNames = append(lockNames, tx.targetKey(item.target.loc)) - } - - slices.Sort(lockNames) - - for _, lockKey := range lockNames { - err = tx.createLock(refPathFromKey(lockKey)) - if err != nil { - return prepared, err - } - } - - hasDeletes := len(deleted) > 0 - if hasDeletes { - err = tx.createPackedLock(tx.store.packedRefsTimeout) - if err != nil { - return prepared, err - } - } - - for i := range prepared { - item := &prepared[i] - - refState, err := tx.directRead(item.target.name) - if err != nil { - return prepared, err - } - - item.target.ref = refState - - err = tx.verifyCurrent(*item) - if err != nil { - return prepared, err - } - } - - return prepared, nil -} diff --git a/refstore/files/transaction_queue.go b/refstore/files/transaction_queue.go index 0000bc27..aa2004c3 100644 --- a/refstore/files/transaction_queue.go +++ b/refstore/files/transaction_queue.go @@ -1,7 +1,7 @@ package files -func (tx *Transaction) queue(op txOp) error { - err := tx.validateOp(op) +func (tx *Transaction) queue(op queuedUpdate) error { + err := (&refUpdateExecutor{store: tx.store}).validateQueuedUpdate(op) if err != nil { return err } diff --git a/refstore/files/transaction_queue_ops.go b/refstore/files/transaction_queue_ops.go index ff966559..e7000c6a 100644 --- a/refstore/files/transaction_queue_ops.go +++ b/refstore/files/transaction_queue_ops.go @@ -3,33 +3,33 @@ package files import "codeberg.org/lindenii/furgit/objectid" func (tx *Transaction) Create(name string, newID objectid.ObjectID) error { - return tx.queue(txOp{name: name, kind: txCreate, newID: newID}) + return tx.queue(queuedUpdate{name: name, kind: updateCreate, newID: newID}) } func (tx *Transaction) Update(name string, newID, oldID objectid.ObjectID) error { - return tx.queue(txOp{name: name, kind: txUpdate, newID: newID, oldID: oldID}) + return tx.queue(queuedUpdate{name: name, kind: updateReplace, newID: newID, oldID: oldID}) } func (tx *Transaction) Delete(name string, oldID objectid.ObjectID) error { - return tx.queue(txOp{name: name, kind: txDelete, oldID: oldID}) + return tx.queue(queuedUpdate{name: name, kind: updateDelete, oldID: oldID}) } func (tx *Transaction) Verify(name string, oldID objectid.ObjectID) error { - return tx.queue(txOp{name: name, kind: txVerify, oldID: oldID}) + return tx.queue(queuedUpdate{name: name, kind: updateVerify, oldID: oldID}) } func (tx *Transaction) CreateSymbolic(name, newTarget string) error { - return tx.queue(txOp{name: name, kind: txCreateSymbolic, newTarget: newTarget}) + return tx.queue(queuedUpdate{name: name, kind: updateCreateSymbolic, newTarget: newTarget}) } func (tx *Transaction) UpdateSymbolic(name, newTarget, oldTarget string) error { - return tx.queue(txOp{name: name, kind: txUpdateSymbolic, newTarget: newTarget, oldTarget: oldTarget}) + return tx.queue(queuedUpdate{name: name, kind: updateReplaceSymbolic, newTarget: newTarget, oldTarget: oldTarget}) } func (tx *Transaction) DeleteSymbolic(name, oldTarget string) error { - return tx.queue(txOp{name: name, kind: txDeleteSymbolic, oldTarget: oldTarget}) + return tx.queue(queuedUpdate{name: name, kind: updateDeleteSymbolic, oldTarget: oldTarget}) } func (tx *Transaction) VerifySymbolic(name, oldTarget string) error { - return tx.queue(txOp{name: name, kind: txVerifySymbolic, oldTarget: oldTarget}) + return tx.queue(queuedUpdate{name: name, kind: updateVerifySymbolic, oldTarget: oldTarget}) } diff --git a/refstore/files/transaction_resolve_target.go b/refstore/files/transaction_resolve_target.go deleted file mode 100644 index 08f24b1c..00000000 --- a/refstore/files/transaction_resolve_target.go +++ /dev/null @@ -1,21 +0,0 @@ -package files - -import "fmt" - -func (tx *Transaction) resolveTarget(op txOp) (resolvedWriteTarget, error) { - switch op.kind { - case txCreate: - return tx.resolveOrdinaryTarget(op.name, true) - case txUpdate, txDelete, txVerify: - return tx.resolveOrdinaryTarget(op.name, false) - case txCreateSymbolic, txUpdateSymbolic, txDeleteSymbolic, txVerifySymbolic: - refState, err := tx.directRead(op.name) - if err != nil { - return resolvedWriteTarget{}, err - } - - return resolvedWriteTarget{name: op.name, loc: tx.store.loosePath(op.name), ref: refState}, nil - default: - return resolvedWriteTarget{}, fmt.Errorf("refstore/files: unsupported transaction operation %d", op.kind) - } -} diff --git a/refstore/files/transaction_resolve_target_ordinary.go b/refstore/files/transaction_resolve_target_ordinary.go deleted file mode 100644 index a495b2af..00000000 --- a/refstore/files/transaction_resolve_target_ordinary.go +++ /dev/null @@ -1,46 +0,0 @@ -package files - -import ( - "fmt" - "strings" - - "codeberg.org/lindenii/furgit/refstore" -) - -func (tx *Transaction) resolveOrdinaryTarget(name string, allowMissing bool) (resolvedWriteTarget, error) { - cur := name - seen := make(map[string]struct{}) - - for { - if _, ok := seen[cur]; ok { - return resolvedWriteTarget{}, fmt.Errorf("refstore/files: symbolic reference cycle at %q", cur) - } - - seen[cur] = struct{}{} - - refState, err := tx.directRead(cur) - if err != nil { - return resolvedWriteTarget{}, err - } - - switch refState.kind { - case directMissing: - if !allowMissing { - return resolvedWriteTarget{}, refstore.ErrReferenceNotFound - } - - return resolvedWriteTarget{name: cur, loc: tx.store.loosePath(cur), ref: refState}, nil - case directDetached: - return resolvedWriteTarget{name: cur, loc: tx.store.loosePath(cur), ref: refState}, nil - case directSymbolic: - target := strings.TrimSpace(refState.target) - if target == "" { - return resolvedWriteTarget{}, fmt.Errorf("refstore/files: symbolic reference %q has empty target", cur) - } - - cur = target - default: - return resolvedWriteTarget{}, fmt.Errorf("refstore/files: unsupported direct reference state %d", refState.kind) - } - } -} diff --git a/refstore/files/transaction_validate.go b/refstore/files/transaction_validate.go deleted file mode 100644 index 784db2a4..00000000 --- a/refstore/files/transaction_validate.go +++ /dev/null @@ -1,65 +0,0 @@ -package files - -import ( - "fmt" - "strings" - - "codeberg.org/lindenii/furgit/objectid" - "codeberg.org/lindenii/furgit/ref/refname" -) - -func (tx *Transaction) validateOp(op txOp) error { - if op.name == "" { - return fmt.Errorf("refstore/files: empty reference name") - } - - switch op.kind { - case txCreate, txUpdate: - err := refname.ValidateUpdateName(op.name, true) - if err != nil { - return err - } - - if op.newID.Size() == 0 { - return objectid.ErrInvalidAlgorithm - } - case txDelete, txVerify: - err := refname.ValidateUpdateName(op.name, false) - if err != nil { - return err - } - - if op.oldID.Size() == 0 { - return objectid.ErrInvalidAlgorithm - } - case txCreateSymbolic, txUpdateSymbolic: - err := refname.ValidateUpdateName(op.name, true) - if err != nil { - return err - } - - if strings.TrimSpace(op.newTarget) == "" { - return fmt.Errorf("refstore/files: empty symbolic target") - } - - err = refname.ValidateSymbolicTarget(op.name, strings.TrimSpace(op.newTarget)) - if err != nil { - return err - } - case txDeleteSymbolic, txVerifySymbolic: - err := refname.ValidateUpdateName(op.name, false) - if err != nil { - return err - } - default: - return fmt.Errorf("refstore/files: unsupported transaction operation %d", op.kind) - } - - if op.kind == txUpdateSymbolic || op.kind == txDeleteSymbolic || op.kind == txVerifySymbolic { - if strings.TrimSpace(op.oldTarget) == "" { - return fmt.Errorf("refstore/files: empty symbolic old target") - } - } - - return nil -} diff --git a/refstore/files/transaction_verify_current.go b/refstore/files/transaction_verify_current.go deleted file mode 100644 index 03ee3e9c..00000000 --- a/refstore/files/transaction_verify_current.go +++ /dev/null @@ -1,53 +0,0 @@ -package files - -import ( - "fmt" - "strings" -) - -func (tx *Transaction) verifyCurrent(item preparedTxOp) error { - switch item.op.kind { - case txCreate: - if item.target.ref.kind != directMissing { - return fmt.Errorf("refstore/files: reference %q already exists", item.target.name) - } - - return nil - case txUpdate, txDelete, txVerify: - if item.target.ref.kind == directMissing { - return fmt.Errorf("refstore/files: reference %q is missing", item.target.name) - } - - if item.target.ref.kind != directDetached { - return fmt.Errorf("refstore/files: reference %q is not detached", item.target.name) - } - - if item.target.ref.id != item.op.oldID { - return fmt.Errorf("refstore/files: reference %q is at %s but expected %s", item.target.name, item.target.ref.id, item.op.oldID) - } - - return nil - case txCreateSymbolic: - if item.target.ref.kind != directMissing { - return fmt.Errorf("refstore/files: reference %q already exists", item.target.name) - } - - return nil - case txUpdateSymbolic, txDeleteSymbolic, txVerifySymbolic: - if item.target.ref.kind == directMissing { - return fmt.Errorf("refstore/files: symbolic reference %q is missing", item.target.name) - } - - if item.target.ref.kind != directSymbolic { - return fmt.Errorf("refstore/files: reference %q is not symbolic", item.target.name) - } - - if strings.TrimSpace(item.target.ref.target) != strings.TrimSpace(item.op.oldTarget) { - return fmt.Errorf("refstore/files: reference %q points at %q, expected %q", item.target.name, item.target.ref.target, item.op.oldTarget) - } - - return nil - default: - return fmt.Errorf("refstore/files: unsupported transaction operation %d", item.op.kind) - } -} diff --git a/refstore/files/transaction_cleanup.go b/refstore/files/update_cleanup.go index 8de1d19f..5df2d967 100644 --- a/refstore/files/transaction_cleanup.go +++ b/refstore/files/update_cleanup.go @@ -6,26 +6,26 @@ import ( "slices" ) -func (tx *Transaction) cleanup(prepared []preparedTxOp) error { +func (executor *refUpdateExecutor) cleanupPreparedUpdates(prepared []preparedUpdate) error { var firstErr error lockNames := make([]string, 0, len(prepared)+1) for _, item := range prepared { - lockNames = append(lockNames, tx.targetKey(item.target.loc)) + lockNames = append(lockNames, updateTargetKey(item.target.loc)) } - lockNames = append(lockNames, tx.targetKey(refPath{root: rootCommon, path: "packed-refs"})) + lockNames = append(lockNames, updateTargetKey(refPath{root: rootCommon, path: "packed-refs"})) slices.Sort(lockNames) lockNames = slices.Compact(lockNames) for _, lockKey := range lockNames { lockPath := refPathFromKey(lockKey) lockName := lockPath.path + ".lock" - root := tx.store.rootFor(lockPath.root) + root := executor.store.rootFor(lockPath.root) err := root.Remove(lockName) if err == nil || errors.Is(err, os.ErrNotExist) { - tx.tryRemoveEmptyParentPaths(lockPath.root, lockName) + executor.tryRemoveEmptyParentPaths(lockPath.root, lockName) continue } diff --git a/refstore/files/transaction_cleanup_parents.go b/refstore/files/update_cleanup_parents.go index 1e62e637..c62681fa 100644 --- a/refstore/files/transaction_cleanup_parents.go +++ b/refstore/files/update_cleanup_parents.go @@ -6,13 +6,13 @@ import ( "path" ) -func (tx *Transaction) tryRemoveEmptyParents(name string) { - loc := tx.store.loosePath(name) - tx.tryRemoveEmptyParentPaths(loc.root, loc.path) +func (executor *refUpdateExecutor) tryRemoveEmptyParents(name string) { + loc := executor.store.loosePath(name) + executor.tryRemoveEmptyParentPaths(loc.root, loc.path) } -func (tx *Transaction) tryRemoveEmptyParentPaths(kind rootKind, name string) { - root := tx.store.rootFor(kind) +func (executor *refUpdateExecutor) tryRemoveEmptyParentPaths(kind rootKind, name string) { + root := executor.store.rootFor(kind) dir := path.Dir(name) for dir != "." && dir != "/" { diff --git a/refstore/files/update_commit.go b/refstore/files/update_commit.go new file mode 100644 index 00000000..3d39e990 --- /dev/null +++ b/refstore/files/update_commit.go @@ -0,0 +1,25 @@ +package files + +func (executor *refUpdateExecutor) commitPreparedUpdates(prepared []preparedUpdate) (err error) { + defer func() { + _ = executor.cleanupPreparedUpdates(prepared) + }() + + for _, item := range prepared { + if item.op.kind == updateDelete || item.op.kind == updateDeleteSymbolic || item.op.kind == updateVerify || item.op.kind == updateVerifySymbolic { + continue + } + + err = executor.writePreparedLooseUpdate(item) + if err != nil { + return wrapUpdateError(item.op.name, err) + } + } + + err = executor.applyPackedRefDeletes(prepared) + if err != nil { + return err + } + + return executor.removeDeletedLooseRefs(prepared) +} diff --git a/refstore/files/update_commit_delete.go b/refstore/files/update_commit_delete.go new file mode 100644 index 00000000..47a600fb --- /dev/null +++ b/refstore/files/update_commit_delete.go @@ -0,0 +1,25 @@ +package files + +import ( + "errors" + "os" +) + +func (executor *refUpdateExecutor) removeDeletedLooseRefs(prepared []preparedUpdate) error { + for _, item := range prepared { + switch item.op.kind { + case updateDelete, updateDeleteSymbolic: + if item.target.ref.isLoose { + err := executor.store.rootFor(item.target.loc.root).Remove(item.target.loc.path) + if err != nil && !errors.Is(err, os.ErrNotExist) { + return wrapUpdateError(item.op.name, err) + } + + executor.tryRemoveEmptyParents(item.target.name) + } + case updateCreate, updateReplace, updateVerify, updateCreateSymbolic, updateReplaceSymbolic, updateVerifySymbolic: + } + } + + return nil +} diff --git a/refstore/files/transaction_dir_tree.go b/refstore/files/update_dir_tree.go index e317f604..51fb5cfb 100644 --- a/refstore/files/transaction_dir_tree.go +++ b/refstore/files/update_dir_tree.go @@ -7,8 +7,8 @@ import ( "path" ) -func (tx *Transaction) removeEmptyDirTree(name refPath) error { - root := tx.store.rootFor(name.root) +func (executor *refUpdateExecutor) removeEmptyDirTree(name refPath) error { + root := executor.store.rootFor(name.root) info, err := root.Stat(name.path) if err != nil { @@ -23,11 +23,11 @@ func (tx *Transaction) removeEmptyDirTree(name refPath) error { return nil } - return tx.removeEmptyDirTreeRecursive(name) + return executor.removeEmptyDirTreeRecursive(name) } -func (tx *Transaction) removeEmptyDirTreeRecursive(name refPath) error { - root := tx.store.rootFor(name.root) +func (executor *refUpdateExecutor) removeEmptyDirTreeRecursive(name refPath) error { + root := executor.store.rootFor(name.root) dir, err := root.Open(name.path) if err != nil { @@ -46,7 +46,7 @@ func (tx *Transaction) removeEmptyDirTreeRecursive(name refPath) error { return fmt.Errorf("refstore/files: non-empty directory blocks reference %q", name.path) } - err = tx.removeEmptyDirTreeRecursive(refPath{ + err = executor.removeEmptyDirTreeRecursive(refPath{ root: name.root, path: path.Join(name.path, entry.Name()), }) diff --git a/refstore/files/transaction_direct_read.go b/refstore/files/update_direct_read.go index 4da6a499..4efecdca 100644 --- a/refstore/files/transaction_direct_read.go +++ b/refstore/files/update_direct_read.go @@ -9,24 +9,24 @@ import ( "codeberg.org/lindenii/furgit/refstore" ) -func (tx *Transaction) directRead(name string) (directRef, error) { - loc := tx.store.loosePath(name) +func (executor *refUpdateExecutor) directRead(name string) (directRefState, error) { + loc := executor.store.loosePath(name) hasPacked := false if loc.root == rootCommon && refname.ParseWorktree(name).Type == refname.WorktreeShared { - packed, packedErr := tx.store.readPackedRefs() + packed, packedErr := executor.store.readPackedRefs() if packedErr != nil { - return directRef{}, packedErr + return directRefState{}, packedErr } _, hasPacked = packed.byName[name] } - loose, err := tx.store.readLooseRef(name) + loose, err := executor.store.readLooseRef(name) if err == nil { switch loose := loose.(type) { case ref.Detached: - return directRef{ + return directRefState{ kind: directDetached, name: name, id: loose.ID, @@ -34,7 +34,7 @@ func (tx *Transaction) directRead(name string) (directRef, error) { isPacked: hasPacked, }, nil case ref.Symbolic: - return directRef{ + return directRefState{ kind: directSymbolic, name: name, target: loose.Target, @@ -42,26 +42,26 @@ func (tx *Transaction) directRead(name string) (directRef, error) { isPacked: hasPacked, }, nil default: - return directRef{}, fmt.Errorf("refstore/files: unsupported reference type %T", loose) + return directRefState{}, fmt.Errorf("refstore/files: unsupported reference type %T", loose) } } if !errors.Is(err, refstore.ErrReferenceNotFound) { - info, statErr := tx.store.rootFor(loc.root).Stat(loc.path) + info, statErr := executor.store.rootFor(loc.root).Stat(loc.path) if statErr != nil || !info.IsDir() { - return directRef{}, err + return directRefState{}, err } } if hasPacked { - packed, packedErr := tx.store.readPackedRefs() + packed, packedErr := executor.store.readPackedRefs() if packedErr != nil { - return directRef{}, packedErr + return directRefState{}, packedErr } detached := packed.byName[name] - return directRef{ + return directRefState{ kind: directDetached, name: name, id: detached.ID, @@ -69,7 +69,7 @@ func (tx *Transaction) directRead(name string) (directRef, error) { }, nil } - return directRef{ + return directRefState{ kind: directMissing, name: name, }, nil diff --git a/refstore/files/transaction_direct_ref.go b/refstore/files/update_direct_ref.go index 970e7b6a..fb9a83ae 100644 --- a/refstore/files/transaction_direct_ref.go +++ b/refstore/files/update_direct_ref.go @@ -2,16 +2,16 @@ package files import "codeberg.org/lindenii/furgit/objectid" -type directKind uint8 +type directRefKind uint8 const ( - directMissing directKind = iota + directMissing directRefKind = iota directDetached directSymbolic ) -type directRef struct { - kind directKind +type directRefState struct { + kind directRefKind name string id objectid.ObjectID target string diff --git a/refstore/files/update_error.go b/refstore/files/update_error.go new file mode 100644 index 00000000..d8841d44 --- /dev/null +++ b/refstore/files/update_error.go @@ -0,0 +1,28 @@ +package files + +import "fmt" + +type updateContextError struct { + name string + err error +} + +func (err *updateContextError) Error() string { + return fmt.Sprintf("refstore/files: update %q: %v", err.name, err.err) +} + +func (err *updateContextError) Unwrap() error { + if err == nil { + return nil + } + + return err.err +} + +func wrapUpdateError(name string, err error) error { + if err == nil || name == "" { + return err + } + + return &updateContextError{name: name, err: err} +} diff --git a/refstore/files/update_executor.go b/refstore/files/update_executor.go new file mode 100644 index 00000000..749f7061 --- /dev/null +++ b/refstore/files/update_executor.go @@ -0,0 +1,5 @@ +package files + +type refUpdateExecutor struct { + store *Store +} diff --git a/refstore/files/update_kind.go b/refstore/files/update_kind.go new file mode 100644 index 00000000..f04719db --- /dev/null +++ b/refstore/files/update_kind.go @@ -0,0 +1,14 @@ +package files + +type updateKind uint8 + +const ( + updateCreate updateKind = iota + updateReplace + updateDelete + updateVerify + updateCreateSymbolic + updateReplaceSymbolic + updateDeleteSymbolic + updateVerifySymbolic +) diff --git a/refstore/files/transaction_lock.go b/refstore/files/update_lock.go index 20a89c78..1ce9adbb 100644 --- a/refstore/files/transaction_lock.go +++ b/refstore/files/update_lock.go @@ -5,8 +5,8 @@ import ( "path" ) -func (tx *Transaction) createLock(name refPath) error { - root := tx.store.rootFor(name.root) +func (executor *refUpdateExecutor) createUpdateLock(name refPath) error { + root := executor.store.rootFor(name.root) dir := path.Dir(name.path) if dir != "." { diff --git a/refstore/files/transaction_lock_packed.go b/refstore/files/update_lock_packed.go index 4538e5e5..f74a4f5e 100644 --- a/refstore/files/transaction_lock_packed.go +++ b/refstore/files/update_lock_packed.go @@ -6,7 +6,7 @@ import ( "time" ) -func (tx *Transaction) createPackedLock(timeout time.Duration) error { +func (executor *refUpdateExecutor) createPackedRefsLock(timeout time.Duration) error { const ( initialBackoffMs = 1 backoffMaxMultiplier = 1000 @@ -17,7 +17,7 @@ func (tx *Transaction) createPackedLock(timeout time.Duration) error { n := 1 for { - file, err := tx.store.commonRoot.OpenFile("packed-refs.lock", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) + file, err := executor.store.commonRoot.OpenFile("packed-refs.lock", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) if err == nil { return file.Close() } @@ -31,7 +31,7 @@ func (tx *Transaction) createPackedLock(timeout time.Duration) error { } backoffMs := multiplier * initialBackoffMs - waitMs := (750 + tx.store.lockRand.Intn(500)) * backoffMs / 1000 + waitMs := (750 + executor.store.lockRand.Intn(500)) * backoffMs / 1000 time.Sleep(time.Duration(waitMs) * time.Millisecond) multiplier += 2*n + 1 diff --git a/refstore/files/update_operation_prepared.go b/refstore/files/update_operation_prepared.go new file mode 100644 index 00000000..c50fea4e --- /dev/null +++ b/refstore/files/update_operation_prepared.go @@ -0,0 +1,6 @@ +package files + +type preparedUpdate struct { + op queuedUpdate + target resolvedUpdateTarget +} diff --git a/refstore/files/update_operation_queue.go b/refstore/files/update_operation_queue.go new file mode 100644 index 00000000..05039ce3 --- /dev/null +++ b/refstore/files/update_operation_queue.go @@ -0,0 +1,12 @@ +package files + +import "codeberg.org/lindenii/furgit/objectid" + +type queuedUpdate struct { + name string + kind updateKind + newID objectid.ObjectID + oldID objectid.ObjectID + newTarget string + oldTarget string +} diff --git a/refstore/files/root_ref_path.go b/refstore/files/update_path.go index ed79ca3b..2bd42535 100644 --- a/refstore/files/root_ref_path.go +++ b/refstore/files/update_path.go @@ -10,7 +10,7 @@ type refPath struct { path string } -func (tx *Transaction) targetKey(name refPath) string { +func updateTargetKey(name refPath) string { return fmt.Sprintf("%d:%s", name.root, name.path) } diff --git a/refstore/files/update_prepare.go b/refstore/files/update_prepare.go new file mode 100644 index 00000000..035c0bc2 --- /dev/null +++ b/refstore/files/update_prepare.go @@ -0,0 +1,48 @@ +package files + +func (executor *refUpdateExecutor) prepareUpdates(ops []queuedUpdate) (prepared []preparedUpdate, err error) { + defer func() { + if err != nil { + _ = executor.cleanupPreparedUpdates(prepared) + } + }() + + prepared, err = executor.resolvePreparedUpdates(ops) + if err != nil { + return prepared, err + } + + deleted, written := collectPreparedWrites(prepared) + + existing, err := executor.collectVisibleNames() + if err != nil { + return prepared, err + } + + for _, name := range written { + err = verifyRefnameAvailable(name, existing, written, deleted) + if err != nil { + return prepared, err + } + } + + err = executor.prepareUpdateLocks(prepared) + if err != nil { + return prepared, err + } + + hasDeletes := len(deleted) > 0 + if hasDeletes { + err = executor.createPackedRefsLock(executor.store.packedRefsTimeout) + if err != nil { + return prepared, err + } + } + + err = executor.verifyPreparedUpdates(prepared) + if err != nil { + return prepared, err + } + + return prepared, nil +} diff --git a/refstore/files/update_prepare_lock.go b/refstore/files/update_prepare_lock.go new file mode 100644 index 00000000..d958fc0a --- /dev/null +++ b/refstore/files/update_prepare_lock.go @@ -0,0 +1,28 @@ +package files + +import "slices" + +func (executor *refUpdateExecutor) prepareUpdateLocks(prepared []preparedUpdate) error { + lockNames := make([]string, 0, len(prepared)) + for _, item := range prepared { + lockNames = append(lockNames, updateTargetKey(item.target.loc)) + } + + slices.Sort(lockNames) + + for _, lockKey := range lockNames { + lockPath := refPathFromKey(lockKey) + err := executor.createUpdateLock(lockPath) + if err != nil { + for _, item := range prepared { + if updateTargetKey(item.target.loc) == lockKey { + return wrapUpdateError(item.op.name, err) + } + } + + return err + } + } + + return nil +} diff --git a/refstore/files/update_prepare_resolve.go b/refstore/files/update_prepare_resolve.go new file mode 100644 index 00000000..492f5157 --- /dev/null +++ b/refstore/files/update_prepare_resolve.go @@ -0,0 +1,42 @@ +package files + +import "codeberg.org/lindenii/furgit/refstore" + +func (executor *refUpdateExecutor) resolvePreparedUpdates(ops []queuedUpdate) ([]preparedUpdate, error) { + prepared := make([]preparedUpdate, 0, len(ops)) + targets := make(map[string]struct{}, len(ops)) + + for _, op := range ops { + target, err := executor.resolveQueuedUpdateTarget(op) + if err != nil { + return prepared, err + } + + targetKey := updateTargetKey(target.loc) + if _, exists := targets[targetKey]; exists { + return prepared, wrapUpdateError(op.name, &refstore.DuplicateUpdateError{}) + } + + targets[targetKey] = struct{}{} + prepared = append(prepared, preparedUpdate{op: op, target: target}) + } + + return prepared, nil +} + +func collectPreparedWrites(prepared []preparedUpdate) (deleted map[string]struct{}, written []string) { + deleted = make(map[string]struct{}) + written = make([]string, 0, len(prepared)) + + for _, item := range prepared { + switch item.op.kind { + case updateDelete, updateDeleteSymbolic: + deleted[item.target.name] = struct{}{} + case updateCreate, updateReplace, updateCreateSymbolic, updateReplaceSymbolic: + written = append(written, item.target.name) + case updateVerify, updateVerifySymbolic: + } + } + + return deleted, written +} diff --git a/refstore/files/update_prepare_verify.go b/refstore/files/update_prepare_verify.go new file mode 100644 index 00000000..dcd14945 --- /dev/null +++ b/refstore/files/update_prepare_verify.go @@ -0,0 +1,21 @@ +package files + +func (executor *refUpdateExecutor) verifyPreparedUpdates(prepared []preparedUpdate) error { + for i := range prepared { + item := &prepared[i] + + refState, err := executor.directRead(item.target.name) + if err != nil { + return wrapUpdateError(item.op.name, err) + } + + item.target.ref = refState + + err = executor.verifyPreparedUpdateCurrent(*item) + if err != nil { + return err + } + } + + return nil +} diff --git a/refstore/files/update_resolve_target.go b/refstore/files/update_resolve_target.go new file mode 100644 index 00000000..7cfb9aa1 --- /dev/null +++ b/refstore/files/update_resolve_target.go @@ -0,0 +1,21 @@ +package files + +import "fmt" + +func (executor *refUpdateExecutor) resolveQueuedUpdateTarget(op queuedUpdate) (resolvedUpdateTarget, error) { + switch op.kind { + case updateCreate: + return executor.resolveOrdinaryTarget(op.name, true) + case updateReplace, updateDelete, updateVerify: + return executor.resolveOrdinaryTarget(op.name, false) + case updateCreateSymbolic, updateReplaceSymbolic, updateDeleteSymbolic, updateVerifySymbolic: + refState, err := executor.directRead(op.name) + if err != nil { + return resolvedUpdateTarget{}, err + } + + return resolvedUpdateTarget{name: op.name, loc: executor.store.loosePath(op.name), ref: refState}, nil + default: + return resolvedUpdateTarget{}, fmt.Errorf("refstore/files: unsupported update operation %d", op.kind) + } +} diff --git a/refstore/files/update_resolve_target_ordinary.go b/refstore/files/update_resolve_target_ordinary.go new file mode 100644 index 00000000..34206e0b --- /dev/null +++ b/refstore/files/update_resolve_target_ordinary.go @@ -0,0 +1,48 @@ +package files + +import ( + "fmt" + "strings" + + "codeberg.org/lindenii/furgit/refstore" +) + +func (executor *refUpdateExecutor) resolveOrdinaryTarget(name string, allowMissing bool) (resolvedUpdateTarget, error) { + cur := name + seen := make(map[string]struct{}) + + for { + if _, ok := seen[cur]; ok { + return resolvedUpdateTarget{}, fmt.Errorf("refstore/files: symbolic reference cycle at %q", cur) + } + + seen[cur] = struct{}{} + + refState, err := executor.directRead(cur) + if err != nil { + return resolvedUpdateTarget{}, err + } + + switch refState.kind { + case directMissing: + if !allowMissing { + return resolvedUpdateTarget{}, wrapUpdateError(name, refstore.ErrReferenceNotFound) + } + + return resolvedUpdateTarget{name: cur, loc: executor.store.loosePath(cur), ref: refState}, nil + case directDetached: + return resolvedUpdateTarget{name: cur, loc: executor.store.loosePath(cur), ref: refState}, nil + case directSymbolic: + target := strings.TrimSpace(refState.target) + if target == "" { + return resolvedUpdateTarget{}, wrapUpdateError(name, &refstore.InvalidValueError{ + Err: fmt.Errorf("symbolic reference has empty target"), + }) + } + + cur = target + default: + return resolvedUpdateTarget{}, fmt.Errorf("refstore/files: unsupported direct reference state %d", refState.kind) + } + } +} diff --git a/refstore/files/update_target_resolved.go b/refstore/files/update_target_resolved.go new file mode 100644 index 00000000..c29e5938 --- /dev/null +++ b/refstore/files/update_target_resolved.go @@ -0,0 +1,7 @@ +package files + +type resolvedUpdateTarget struct { + name string + loc refPath + ref directRefState +} diff --git a/refstore/files/update_validate.go b/refstore/files/update_validate.go new file mode 100644 index 00000000..9449fda5 --- /dev/null +++ b/refstore/files/update_validate.go @@ -0,0 +1,66 @@ +package files + +import ( + "fmt" + "strings" + + "codeberg.org/lindenii/furgit/objectid" + "codeberg.org/lindenii/furgit/ref/refname" + "codeberg.org/lindenii/furgit/refstore" +) + +func (executor *refUpdateExecutor) validateQueuedUpdate(op queuedUpdate) error { + if op.name == "" { + return wrapUpdateError(op.name, &refstore.InvalidNameError{Err: fmt.Errorf("empty reference name")}) + } + + switch op.kind { + case updateCreate, updateReplace: + err := refname.ValidateUpdateName(op.name, true) + if err != nil { + return wrapUpdateError(op.name, &refstore.InvalidNameError{Err: err}) + } + + if op.newID.Size() == 0 { + return wrapUpdateError(op.name, &refstore.InvalidValueError{Err: objectid.ErrInvalidAlgorithm}) + } + case updateDelete, updateVerify: + err := refname.ValidateUpdateName(op.name, false) + if err != nil { + return wrapUpdateError(op.name, &refstore.InvalidNameError{Err: err}) + } + + if op.oldID.Size() == 0 { + return wrapUpdateError(op.name, &refstore.InvalidValueError{Err: objectid.ErrInvalidAlgorithm}) + } + case updateCreateSymbolic, updateReplaceSymbolic: + err := refname.ValidateUpdateName(op.name, true) + if err != nil { + return wrapUpdateError(op.name, &refstore.InvalidNameError{Err: err}) + } + + if strings.TrimSpace(op.newTarget) == "" { + return wrapUpdateError(op.name, &refstore.InvalidValueError{Err: fmt.Errorf("empty symbolic target")}) + } + + err = refname.ValidateSymbolicTarget(op.name, strings.TrimSpace(op.newTarget)) + if err != nil { + return wrapUpdateError(op.name, &refstore.InvalidValueError{Err: err}) + } + case updateDeleteSymbolic, updateVerifySymbolic: + err := refname.ValidateUpdateName(op.name, false) + if err != nil { + return wrapUpdateError(op.name, &refstore.InvalidNameError{Err: err}) + } + default: + return fmt.Errorf("refstore/files: unsupported update operation %d", op.kind) + } + + if op.kind == updateReplaceSymbolic || op.kind == updateDeleteSymbolic || op.kind == updateVerifySymbolic { + if strings.TrimSpace(op.oldTarget) == "" { + return wrapUpdateError(op.name, &refstore.InvalidValueError{Err: fmt.Errorf("empty symbolic old target")}) + } + } + + return nil +} diff --git a/refstore/files/update_verify_current.go b/refstore/files/update_verify_current.go new file mode 100644 index 00000000..f8035994 --- /dev/null +++ b/refstore/files/update_verify_current.go @@ -0,0 +1,60 @@ +package files + +import ( + "strings" + + "codeberg.org/lindenii/furgit/refstore" +) + +func (executor *refUpdateExecutor) verifyPreparedUpdateCurrent(item preparedUpdate) error { + switch item.op.kind { + case updateCreate: + if item.target.ref.kind != directMissing { + return wrapUpdateError(item.op.name, &refstore.CreateExistsError{}) + } + + return nil + case updateReplace, updateDelete, updateVerify: + if item.target.ref.kind == directMissing { + return wrapUpdateError(item.op.name, refstore.ErrReferenceNotFound) + } + + if item.target.ref.kind != directDetached { + return wrapUpdateError(item.op.name, &refstore.ExpectedDetachedError{}) + } + + if item.target.ref.id != item.op.oldID { + return wrapUpdateError(item.op.name, &refstore.IncorrectOldValueError{ + Actual: item.target.ref.id.String(), + Expected: item.op.oldID.String(), + }) + } + + return nil + case updateCreateSymbolic: + if item.target.ref.kind != directMissing { + return wrapUpdateError(item.op.name, &refstore.CreateExistsError{}) + } + + return nil + case updateReplaceSymbolic, updateDeleteSymbolic, updateVerifySymbolic: + if item.target.ref.kind == directMissing { + return wrapUpdateError(item.op.name, refstore.ErrReferenceNotFound) + } + + if item.target.ref.kind != directSymbolic { + return wrapUpdateError(item.op.name, &refstore.ExpectedSymbolicError{}) + } + + if strings.TrimSpace(item.target.ref.target) != strings.TrimSpace(item.op.oldTarget) { + return wrapUpdateError(item.op.name, &refstore.IncorrectOldValueError{ + Actual: strings.TrimSpace(item.target.ref.target), + Expected: strings.TrimSpace(item.op.oldTarget), + }) + } + + return nil + } + + return nil +} diff --git a/refstore/files/transaction_verify_refnames.go b/refstore/files/update_verify_refnames.go index 2efc872a..12d67c5f 100644 --- a/refstore/files/transaction_verify_refnames.go +++ b/refstore/files/update_verify_refnames.go @@ -1,8 +1,9 @@ package files import ( - "fmt" "strings" + + "codeberg.org/lindenii/furgit/refstore" ) func verifyRefnameAvailable(name string, existing map[string]struct{}, writes []string, deleted map[string]struct{}) error { @@ -16,7 +17,7 @@ func verifyRefnameAvailable(name string, existing map[string]struct{}, writes [] } if refnamesConflict(name, existingName) { - return fmt.Errorf("refstore/files: reference name conflict between %q and %q", name, existingName) + return wrapUpdateError(name, &refstore.NameConflictError{Other: existingName}) } } @@ -26,7 +27,7 @@ func verifyRefnameAvailable(name string, existing map[string]struct{}, writes [] } if refnamesConflict(name, other) { - return fmt.Errorf("refstore/files: reference name conflict between %q and %q", name, other) + return wrapUpdateError(name, &refstore.NameConflictError{Other: other}) } } diff --git a/refstore/files/transaction_visible_names.go b/refstore/files/update_visible_names.go index ef5941c2..f5792f93 100644 --- a/refstore/files/transaction_visible_names.go +++ b/refstore/files/update_visible_names.go @@ -1,9 +1,9 @@ package files -func (tx *Transaction) visibleNames() (map[string]struct{}, error) { +func (executor *refUpdateExecutor) collectVisibleNames() (map[string]struct{}, error) { names := make(map[string]struct{}) - looseNames, err := tx.store.collectLooseRefNames() + looseNames, err := executor.store.collectLooseRefNames() if err != nil { return nil, err } @@ -12,7 +12,7 @@ func (tx *Transaction) visibleNames() (map[string]struct{}, error) { names[name] = struct{}{} } - packed, err := tx.store.readPackedRefs() + packed, err := executor.store.readPackedRefs() if err != nil { return nil, err } diff --git a/refstore/files/transaction_write_loose.go b/refstore/files/update_write_loose.go index b2a0e5d6..212be9a8 100644 --- a/refstore/files/transaction_write_loose.go +++ b/refstore/files/update_write_loose.go @@ -7,8 +7,8 @@ import ( "strings" ) -func (tx *Transaction) writeLoose(item preparedTxOp) error { - root := tx.store.rootFor(item.target.loc.root) +func (executor *refUpdateExecutor) writePreparedLooseUpdate(item preparedUpdate) error { + root := executor.store.rootFor(item.target.loc.root) lockName := item.target.loc.path + ".lock" lock, err := root.OpenFile(lockName, os.O_WRONLY|os.O_TRUNC, 0o644) @@ -19,11 +19,11 @@ func (tx *Transaction) writeLoose(item preparedTxOp) error { var content string switch item.op.kind { - case txCreate, txUpdate: + case updateCreate, updateReplace: content = item.op.newID.String() + "\n" - case txCreateSymbolic, txUpdateSymbolic: + case updateCreateSymbolic, updateReplaceSymbolic: content = "ref: " + strings.TrimSpace(item.op.newTarget) + "\n" - case txDelete, txVerify, txDeleteSymbolic, txVerifySymbolic: + case updateDelete, updateVerify, updateDeleteSymbolic, updateVerifySymbolic: default: _ = lock.Close() @@ -50,7 +50,7 @@ func (tx *Transaction) writeLoose(item preparedTxOp) error { } } - err = tx.removeEmptyDirTree(item.target.loc) + err = executor.removeEmptyDirTree(item.target.loc) if err != nil { return err } diff --git a/refstore/files/transaction_write_packed_deltas.go b/refstore/files/update_write_packed_refs.go index 5fe07a7a..c7eea780 100644 --- a/refstore/files/transaction_write_packed_deltas.go +++ b/refstore/files/update_write_packed_refs.go @@ -5,8 +5,8 @@ import ( "os" ) -func (tx *Transaction) applyPackedDeletes(prepared []preparedTxOp) error { - _, err := tx.store.commonRoot.Stat("packed-refs.lock") +func (executor *refUpdateExecutor) applyPackedRefDeletes(prepared []preparedUpdate) error { + _, err := executor.store.commonRoot.Stat("packed-refs.lock") if err != nil { if errors.Is(err, os.ErrNotExist) { return nil @@ -15,7 +15,7 @@ func (tx *Transaction) applyPackedDeletes(prepared []preparedTxOp) error { return err } - packed, err := tx.store.readPackedRefs() + packed, err := executor.store.readPackedRefs() if err != nil { return err } @@ -24,7 +24,7 @@ func (tx *Transaction) applyPackedDeletes(prepared []preparedTxOp) error { needed := false for _, item := range prepared { - if item.op.kind != txDelete && item.op.kind != txDeleteSymbolic { + if item.op.kind != updateDelete && item.op.kind != updateDeleteSymbolic { continue } @@ -38,7 +38,7 @@ func (tx *Transaction) applyPackedDeletes(prepared []preparedTxOp) error { return nil } - lock, err := tx.store.commonRoot.OpenFile("packed-refs.new", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) + lock, err := executor.store.commonRoot.OpenFile("packed-refs.new", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) if err != nil { return err } @@ -50,7 +50,7 @@ func (tx *Transaction) applyPackedDeletes(prepared []preparedTxOp) error { return } - _ = tx.store.commonRoot.Remove("packed-refs.new") + _ = executor.store.commonRoot.Remove("packed-refs.new") }() _, err = lock.WriteString("# pack-refs with: peeled fully-peeled sorted\n") @@ -87,7 +87,7 @@ func (tx *Transaction) applyPackedDeletes(prepared []preparedTxOp) error { return err } - err = tx.store.commonRoot.Rename("packed-refs.new", "packed-refs") + err = executor.store.commonRoot.Rename("packed-refs.new", "packed-refs") if err != nil { return err } |
