Fast implementation of Git in pure Go codeberg.org/lindenii/furgit
git go
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

refstore: Improve interfaces, errors, and make batch work

Runxi Yu 4a796e64 13507b77

+896 -612
+13 -2
refstore/batch.go
··· 46 46 Abort() error 47 47 } 48 48 49 + // BatchStatus reports the outcome for one queued batch operation. 50 + type BatchStatus uint8 51 + 52 + const ( 53 + BatchStatusApplied BatchStatus = iota 54 + BatchStatusRejected 55 + BatchStatusFatal 56 + BatchStatusNotAttempted 57 + ) 58 + 49 59 // BatchResult reports the outcome for one queued batch operation. 50 60 type BatchResult struct { 51 - Name string 52 - Error error 61 + Name string 62 + Status BatchStatus 63 + Error error 53 64 }
+2 -3
refstore/files/batch.go
··· 3 3 import "codeberg.org/lindenii/furgit/refstore" 4 4 5 5 type Batch struct { 6 - store *Store 7 - ops []txOp 8 - closed bool 6 + store *Store 7 + ops []queuedUpdate 9 8 } 10 9 11 10 var _ refstore.Batch = (*Batch)(nil)
-8
refstore/files/batch_abort.go
··· 1 1 package files 2 2 3 - import "errors" 4 - 5 3 func (batch *Batch) Abort() error { 6 - if batch.closed { 7 - return errors.New("refstore/files: batch already closed") 8 - } 9 - 10 - batch.closed = true 11 - 12 4 return nil 13 5 }
+102 -40
refstore/files/batch_apply.go
··· 1 1 package files 2 2 3 - import ( 4 - "errors" 5 - 6 - "codeberg.org/lindenii/furgit/refstore" 7 - ) 3 + import "codeberg.org/lindenii/furgit/refstore" 8 4 9 5 func (batch *Batch) Apply() ([]refstore.BatchResult, error) { 10 - if batch.closed { 11 - return nil, errors.New("refstore/files: batch already closed") 12 - } 13 - 14 6 results := make([]refstore.BatchResult, len(batch.ops)) 15 - seen := make(map[string]struct{}, len(batch.ops)) 7 + remainingIdx := make([]int, 0, len(batch.ops)) 8 + remainingOps := make([]queuedUpdate, 0, len(batch.ops)) 9 + seenTargets := make(map[string]struct{}, len(batch.ops)) 10 + executor := &refUpdateExecutor{store: batch.store} 16 11 17 12 for i, op := range batch.ops { 18 13 results[i].Name = op.name 19 14 20 - if _, exists := seen[op.name]; exists { 21 - batch.closed = true 15 + err := executor.validateQueuedUpdate(op) 16 + if err != nil { 17 + results[i].Status = refstore.BatchStatusRejected 18 + results[i].Error = batchResultError(err) 19 + 20 + continue 21 + } 22 + 23 + target, err := executor.resolveQueuedUpdateTarget(op) 24 + if err != nil { 25 + if isBatchRejected(err) { 26 + results[i].Status = refstore.BatchStatusRejected 27 + results[i].Error = batchResultError(err) 28 + 29 + continue 30 + } 31 + 32 + results[i].Status = refstore.BatchStatusFatal 33 + results[i].Error = batchResultError(err) 22 34 23 - err := errors.New("refstore/files: duplicate batch operation for " + `"` + op.name + `"`) 24 - for j := i; j < len(results); j++ { 35 + for j := i + 1; j < len(results); j++ { 25 36 results[j].Name = batch.ops[j].name 26 - results[j].Error = err 37 + results[j].Status = refstore.BatchStatusNotAttempted 38 + results[j].Error = batchResultError(err) 27 39 } 28 40 29 41 return results, err 30 42 } 31 43 32 - seen[op.name] = struct{}{} 33 - } 44 + targetKey := updateTargetKey(target.loc) 45 + if _, exists := seenTargets[targetKey]; exists { 46 + results[i].Status = refstore.BatchStatusRejected 47 + results[i].Error = &refstore.DuplicateUpdateError{} 34 48 35 - for i, op := range batch.ops { 36 - tx := &Transaction{ 37 - store: batch.store, 38 - ops: []txOp{op}, 49 + continue 39 50 } 40 51 41 - err := tx.validateOp(op) 52 + seenTargets[targetKey] = struct{}{} 53 + remainingIdx = append(remainingIdx, i) 54 + remainingOps = append(remainingOps, op) 55 + } 56 + 57 + for len(remainingOps) > 0 { 58 + prepared, err := executor.prepareUpdates(remainingOps) 42 59 if err != nil { 43 - results[i].Error = err 60 + if isBatchRejected(err) { 61 + name := batchResultName(err) 62 + rejectedAt := -1 44 63 45 - continue 46 - } 64 + for i, op := range remainingOps { 65 + if op.name == name { 66 + rejectedAt = i 47 67 48 - err = tx.Commit() 49 - if err == nil { 50 - continue 68 + break 69 + } 70 + } 71 + 72 + if rejectedAt < 0 { 73 + for _, idx := range remainingIdx { 74 + results[idx].Status = refstore.BatchStatusNotAttempted 75 + results[idx].Error = batchResultError(err) 76 + } 77 + 78 + return results, err 79 + } 80 + 81 + results[remainingIdx[rejectedAt]].Status = refstore.BatchStatusRejected 82 + results[remainingIdx[rejectedAt]].Error = batchResultError(err) 83 + remainingIdx = append(remainingIdx[:rejectedAt], remainingIdx[rejectedAt+1:]...) 84 + remainingOps = append(remainingOps[:rejectedAt], remainingOps[rejectedAt+1:]...) 85 + 86 + continue 87 + } 88 + 89 + fatalName := batchResultName(err) 90 + fatalMarked := false 91 + for i, idx := range remainingIdx { 92 + if !fatalMarked && remainingOps[i].name == fatalName && fatalName != "" { 93 + results[idx].Status = refstore.BatchStatusFatal 94 + results[idx].Error = batchResultError(err) 95 + fatalMarked = true 96 + 97 + continue 98 + } 99 + 100 + results[idx].Status = refstore.BatchStatusNotAttempted 101 + results[idx].Error = batchResultError(err) 102 + } 103 + 104 + return results, err 51 105 } 52 106 53 - if isBatchRejected(err) { 54 - results[i].Error = err 107 + err = executor.commitPreparedUpdates(prepared) 108 + if err != nil { 109 + fatalName := batchResultName(err) 110 + fatalMarked := false 111 + for i, idx := range remainingIdx { 112 + if !fatalMarked && remainingOps[i].name == fatalName && fatalName != "" { 113 + results[idx].Status = refstore.BatchStatusFatal 114 + results[idx].Error = batchResultError(err) 115 + fatalMarked = true 116 + 117 + continue 118 + } 119 + 120 + results[idx].Status = refstore.BatchStatusNotAttempted 121 + results[idx].Error = batchResultError(err) 122 + } 55 123 56 - continue 124 + return results, err 57 125 } 58 126 59 - batch.closed = true 60 - results[i].Error = err 61 - 62 - for j := i + 1; j < len(results); j++ { 63 - results[j].Name = batch.ops[j].name 64 - results[j].Error = err 127 + for _, idx := range remainingIdx { 128 + results[idx].Status = refstore.BatchStatusApplied 65 129 } 66 130 67 - return results, err 131 + return results, nil 68 132 } 69 - 70 - batch.closed = true 71 133 72 134 return results, nil 73 135 }
+1 -1
refstore/files/batch_begin.go
··· 8 8 func (store *Store) BeginBatch() (refstore.Batch, error) { 9 9 return &Batch{ 10 10 store: store, 11 - ops: make([]txOp, 0, 8), 11 + ops: make([]queuedUpdate, 0, 8), 12 12 }, nil 13 13 }
+1 -5
refstore/files/batch_queue.go
··· 1 1 package files 2 2 3 - func (batch *Batch) queue(op txOp) { 4 - if batch.closed { 5 - return 6 - } 7 - 3 + func (batch *Batch) queue(op queuedUpdate) { 8 4 batch.ops = append(batch.ops, op) 9 5 }
+8 -8
refstore/files/batch_queue_ops.go
··· 3 3 import "codeberg.org/lindenii/furgit/objectid" 4 4 5 5 func (batch *Batch) Create(name string, newID objectid.ObjectID) { 6 - batch.queue(txOp{name: name, kind: txCreate, newID: newID}) 6 + batch.queue(queuedUpdate{name: name, kind: updateCreate, newID: newID}) 7 7 } 8 8 9 9 func (batch *Batch) Update(name string, newID, oldID objectid.ObjectID) { 10 - batch.queue(txOp{name: name, kind: txUpdate, newID: newID, oldID: oldID}) 10 + batch.queue(queuedUpdate{name: name, kind: updateReplace, newID: newID, oldID: oldID}) 11 11 } 12 12 13 13 func (batch *Batch) Delete(name string, oldID objectid.ObjectID) { 14 - batch.queue(txOp{name: name, kind: txDelete, oldID: oldID}) 14 + batch.queue(queuedUpdate{name: name, kind: updateDelete, oldID: oldID}) 15 15 } 16 16 17 17 func (batch *Batch) Verify(name string, oldID objectid.ObjectID) { 18 - batch.queue(txOp{name: name, kind: txVerify, oldID: oldID}) 18 + batch.queue(queuedUpdate{name: name, kind: updateVerify, oldID: oldID}) 19 19 } 20 20 21 21 func (batch *Batch) CreateSymbolic(name, newTarget string) { 22 - batch.queue(txOp{name: name, kind: txCreateSymbolic, newTarget: newTarget}) 22 + batch.queue(queuedUpdate{name: name, kind: updateCreateSymbolic, newTarget: newTarget}) 23 23 } 24 24 25 25 func (batch *Batch) UpdateSymbolic(name, newTarget, oldTarget string) { 26 - batch.queue(txOp{name: name, kind: txUpdateSymbolic, newTarget: newTarget, oldTarget: oldTarget}) 26 + batch.queue(queuedUpdate{name: name, kind: updateReplaceSymbolic, newTarget: newTarget, oldTarget: oldTarget}) 27 27 } 28 28 29 29 func (batch *Batch) DeleteSymbolic(name, oldTarget string) { 30 - batch.queue(txOp{name: name, kind: txDeleteSymbolic, oldTarget: oldTarget}) 30 + batch.queue(queuedUpdate{name: name, kind: updateDeleteSymbolic, oldTarget: oldTarget}) 31 31 } 32 32 33 33 func (batch *Batch) VerifySymbolic(name, oldTarget string) { 34 - batch.queue(txOp{name: name, kind: txVerifySymbolic, oldTarget: oldTarget}) 34 + batch.queue(queuedUpdate{name: name, kind: updateVerifySymbolic, oldTarget: oldTarget}) 35 35 }
-35
refstore/files/batch_reject.go
··· 1 - package files 2 - 3 - import ( 4 - "errors" 5 - "strings" 6 - 7 - "codeberg.org/lindenii/furgit/objectid" 8 - "codeberg.org/lindenii/furgit/ref/refname" 9 - "codeberg.org/lindenii/furgit/refstore" 10 - ) 11 - 12 - func isBatchRejected(err error) bool { 13 - var nameErr *refname.NameError 14 - 15 - if errors.As(err, &nameErr) { 16 - return true 17 - } 18 - 19 - if errors.Is(err, objectid.ErrInvalidAlgorithm) || errors.Is(err, refstore.ErrReferenceNotFound) { 20 - return true 21 - } 22 - 23 - msg := err.Error() 24 - 25 - return strings.Contains(msg, "empty reference name") || 26 - strings.Contains(msg, "empty symbolic target") || 27 - strings.Contains(msg, "empty symbolic old target") || 28 - strings.Contains(msg, "already exists") || 29 - strings.Contains(msg, "is missing") || 30 - strings.Contains(msg, "is not detached") || 31 - strings.Contains(msg, "is not symbolic") || 32 - strings.Contains(msg, "expected") || 33 - strings.Contains(msg, "reference name conflict") || 34 - strings.Contains(msg, "non-empty directory blocks reference") 35 - }
+19
refstore/files/batch_rejection.go
··· 1 + package files 2 + 3 + import ( 4 + "errors" 5 + 6 + "codeberg.org/lindenii/furgit/refstore" 7 + ) 8 + 9 + func isBatchRejected(err error) bool { 10 + return errors.Is(err, refstore.ErrReferenceNotFound) || 11 + errors.As(err, new(*refstore.InvalidNameError)) || 12 + errors.As(err, new(*refstore.InvalidValueError)) || 13 + errors.As(err, new(*refstore.DuplicateUpdateError)) || 14 + errors.As(err, new(*refstore.CreateExistsError)) || 15 + errors.As(err, new(*refstore.IncorrectOldValueError)) || 16 + errors.As(err, new(*refstore.ExpectedDetachedError)) || 17 + errors.As(err, new(*refstore.ExpectedSymbolicError)) || 18 + errors.As(err, new(*refstore.NameConflictError)) 19 + }
+21
refstore/files/batch_result_error.go
··· 1 + package files 2 + 3 + import "errors" 4 + 5 + func batchResultError(err error) error { 6 + updateErr, ok := errors.AsType[*updateContextError](err) 7 + if ok { 8 + return updateErr.err 9 + } 10 + 11 + return err 12 + } 13 + 14 + func batchResultName(err error) string { 15 + updateErr, ok := errors.AsType[*updateContextError](err) 16 + if !ok { 17 + return "" 18 + } 19 + 20 + return updateErr.name 21 + }
+24 -9
refstore/files/batch_test.go
··· 1 1 package files_test 2 2 3 3 import ( 4 + "errors" 4 5 "testing" 5 6 6 7 "codeberg.org/lindenii/furgit/internal/testgit" 7 8 "codeberg.org/lindenii/furgit/objectid" 9 + "codeberg.org/lindenii/furgit/refstore" 8 10 ) 9 11 10 12 func TestBatchApplyRejectsStaleDeleteAndAppliesIndependentDelete(t *testing.T) { ··· 39 41 t.Fatalf("len(results) = %d, want 2", len(results)) 40 42 } 41 43 42 - if results[0].Error == nil { 43 - t.Fatal("stale delete unexpectedly succeeded") 44 + if results[0].Status != refstore.BatchStatusRejected { 45 + t.Fatalf("results[0].Status = %v, want rejected", results[0].Status) 46 + } 47 + 48 + if !errors.Is(results[0].Error, refstore.ErrReferenceNotFound) && 49 + errors.As(results[0].Error, new(*refstore.IncorrectOldValueError)) == false { 50 + t.Fatalf("results[0].Error = %v, want stale-value rejection", results[0].Error) 44 51 } 45 52 46 - if results[1].Error != nil { 47 - t.Fatalf("valid delete failed: %v", results[1].Error) 53 + if results[1].Status != refstore.BatchStatusApplied { 54 + t.Fatalf("results[1].Status = %v, want applied", results[1].Status) 48 55 } 49 56 50 57 _, err = store.Resolve("refs/heads/main") ··· 81 88 batch.Verify("refs/heads/main", commitID) 82 89 83 90 results, err := batch.Apply() 84 - if err == nil { 85 - t.Fatal("Apply unexpectedly succeeded") 91 + if err != nil { 92 + t.Fatalf("Apply: %v", err) 86 93 } 87 94 88 95 if len(results) != 2 { 89 96 t.Fatalf("len(results) = %d, want 2", len(results)) 90 97 } 91 98 92 - if results[1].Error == nil { 93 - t.Fatal("duplicate ref operation did not report an error") 99 + if results[0].Status != refstore.BatchStatusApplied { 100 + t.Fatalf("results[0].Status = %v, want applied", results[0].Status) 101 + } 102 + 103 + if results[1].Status != refstore.BatchStatusRejected { 104 + t.Fatalf("results[1].Status = %v, want rejected", results[1].Status) 105 + } 106 + 107 + if !errors.As(results[1].Error, new(*refstore.DuplicateUpdateError)) { 108 + t.Fatalf("results[1].Error = %v, want duplicate update error", results[1].Error) 94 109 } 95 110 96 111 _, err = store.Resolve("refs/heads/main") 97 - if err != nil { 112 + if !errors.Is(err, refstore.ErrReferenceNotFound) { 98 113 t.Fatalf("Resolve(main): %v", err) 99 114 } 100 115 })
refstore/files/errors.go refstore/files/broken_ref_error.go
+1 -1
refstore/files/root_ref_path.go refstore/files/update_path.go
··· 10 10 path string 11 11 } 12 12 13 - func (tx *Transaction) targetKey(name refPath) string { 13 + func updateTargetKey(name refPath) string { 14 14 return fmt.Sprintf("%d:%s", name.root, name.path) 15 15 } 16 16
+1 -1
refstore/files/transaction.go
··· 6 6 7 7 type Transaction struct { 8 8 store *Store 9 - ops []txOp 9 + ops []queuedUpdate 10 10 } 11 11 12 12 var _ refstore.Transaction = (*Transaction)(nil)
+1 -1
refstore/files/transaction_begin.go
··· 8 8 func (store *Store) BeginTransaction() (refstore.Transaction, error) { 9 9 return &Transaction{ 10 10 store: store, 11 - ops: make([]txOp, 0, 8), 11 + ops: make([]queuedUpdate, 0, 8), 12 12 }, nil 13 13 }
-39
refstore/files/transaction_cleanup.go
··· 1 - package files 2 - 3 - import ( 4 - "errors" 5 - "os" 6 - "slices" 7 - ) 8 - 9 - func (tx *Transaction) cleanup(prepared []preparedTxOp) error { 10 - var firstErr error 11 - 12 - lockNames := make([]string, 0, len(prepared)+1) 13 - for _, item := range prepared { 14 - lockNames = append(lockNames, tx.targetKey(item.target.loc)) 15 - } 16 - 17 - lockNames = append(lockNames, tx.targetKey(refPath{root: rootCommon, path: "packed-refs"})) 18 - slices.Sort(lockNames) 19 - lockNames = slices.Compact(lockNames) 20 - 21 - for _, lockKey := range lockNames { 22 - lockPath := refPathFromKey(lockKey) 23 - lockName := lockPath.path + ".lock" 24 - root := tx.store.rootFor(lockPath.root) 25 - 26 - err := root.Remove(lockName) 27 - if err == nil || errors.Is(err, os.ErrNotExist) { 28 - tx.tryRemoveEmptyParentPaths(lockPath.root, lockName) 29 - 30 - continue 31 - } 32 - 33 - if firstErr == nil { 34 - firstErr = err 35 - } 36 - } 37 - 38 - return firstErr 39 - }
-35
refstore/files/transaction_cleanup_parents.go
··· 1 - package files 2 - 3 - import ( 4 - "errors" 5 - "os" 6 - "path" 7 - ) 8 - 9 - func (tx *Transaction) tryRemoveEmptyParents(name string) { 10 - loc := tx.store.loosePath(name) 11 - tx.tryRemoveEmptyParentPaths(loc.root, loc.path) 12 - } 13 - 14 - func (tx *Transaction) tryRemoveEmptyParentPaths(kind rootKind, name string) { 15 - root := tx.store.rootFor(kind) 16 - dir := path.Dir(name) 17 - 18 - for dir != "." && dir != "/" { 19 - err := root.Remove(dir) 20 - if err != nil { 21 - if errors.Is(err, os.ErrNotExist) { 22 - return 23 - } 24 - 25 - var pathErr *os.PathError 26 - if errors.As(err, &pathErr) { 27 - return 28 - } 29 - 30 - return 31 - } 32 - 33 - dir = path.Dir(dir) 34 - } 35 - }
+3 -42
refstore/files/transaction_commit.go
··· 1 1 package files 2 2 3 - import ( 4 - "errors" 5 - "os" 6 - ) 7 - 8 3 func (tx *Transaction) Commit() error { 9 - prepared, err := tx.prepare() 4 + executor := &refUpdateExecutor{store: tx.store} 5 + prepared, err := executor.prepareUpdates(tx.ops) 10 6 if err != nil { 11 7 return err 12 8 } 13 9 14 - defer func() { 15 - _ = tx.cleanup(prepared) 16 - }() 17 - 18 - for _, item := range prepared { 19 - if item.op.kind == txDelete || item.op.kind == txDeleteSymbolic || item.op.kind == txVerify || item.op.kind == txVerifySymbolic { 20 - continue 21 - } 22 - 23 - err = tx.writeLoose(item) 24 - if err != nil { 25 - return err 26 - } 27 - } 28 - 29 - err = tx.applyPackedDeletes(prepared) 30 - if err != nil { 31 - return err 32 - } 33 - 34 - for _, item := range prepared { 35 - switch item.op.kind { 36 - case txDelete, txDeleteSymbolic: 37 - if item.target.ref.isLoose { 38 - err = tx.store.rootFor(item.target.loc.root).Remove(item.target.loc.path) 39 - if err != nil && !errors.Is(err, os.ErrNotExist) { 40 - return err 41 - } 42 - 43 - tx.tryRemoveEmptyParents(item.target.name) 44 - } 45 - case txCreate, txUpdate, txVerify, txCreateSymbolic, txUpdateSymbolic, txVerifySymbolic: 46 - } 47 - } 48 - 49 - return nil 10 + return executor.commitPreparedUpdates(prepared) 50 11 }
+6 -6
refstore/files/transaction_dir_tree.go refstore/files/update_dir_tree.go
··· 7 7 "path" 8 8 ) 9 9 10 - func (tx *Transaction) removeEmptyDirTree(name refPath) error { 11 - root := tx.store.rootFor(name.root) 10 + func (executor *refUpdateExecutor) removeEmptyDirTree(name refPath) error { 11 + root := executor.store.rootFor(name.root) 12 12 13 13 info, err := root.Stat(name.path) 14 14 if err != nil { ··· 23 23 return nil 24 24 } 25 25 26 - return tx.removeEmptyDirTreeRecursive(name) 26 + return executor.removeEmptyDirTreeRecursive(name) 27 27 } 28 28 29 - func (tx *Transaction) removeEmptyDirTreeRecursive(name refPath) error { 30 - root := tx.store.rootFor(name.root) 29 + func (executor *refUpdateExecutor) removeEmptyDirTreeRecursive(name refPath) error { 30 + root := executor.store.rootFor(name.root) 31 31 32 32 dir, err := root.Open(name.path) 33 33 if err != nil { ··· 46 46 return fmt.Errorf("refstore/files: non-empty directory blocks reference %q", name.path) 47 47 } 48 48 49 - err = tx.removeEmptyDirTreeRecursive(refPath{ 49 + err = executor.removeEmptyDirTreeRecursive(refPath{ 50 50 root: name.root, 51 51 path: path.Join(name.path, entry.Name()), 52 52 })
+14 -14
refstore/files/transaction_direct_read.go refstore/files/update_direct_read.go
··· 9 9 "codeberg.org/lindenii/furgit/refstore" 10 10 ) 11 11 12 - func (tx *Transaction) directRead(name string) (directRef, error) { 13 - loc := tx.store.loosePath(name) 12 + func (executor *refUpdateExecutor) directRead(name string) (directRefState, error) { 13 + loc := executor.store.loosePath(name) 14 14 hasPacked := false 15 15 16 16 if loc.root == rootCommon && refname.ParseWorktree(name).Type == refname.WorktreeShared { 17 - packed, packedErr := tx.store.readPackedRefs() 17 + packed, packedErr := executor.store.readPackedRefs() 18 18 if packedErr != nil { 19 - return directRef{}, packedErr 19 + return directRefState{}, packedErr 20 20 } 21 21 22 22 _, hasPacked = packed.byName[name] 23 23 } 24 24 25 - loose, err := tx.store.readLooseRef(name) 25 + loose, err := executor.store.readLooseRef(name) 26 26 if err == nil { 27 27 switch loose := loose.(type) { 28 28 case ref.Detached: 29 - return directRef{ 29 + return directRefState{ 30 30 kind: directDetached, 31 31 name: name, 32 32 id: loose.ID, ··· 34 34 isPacked: hasPacked, 35 35 }, nil 36 36 case ref.Symbolic: 37 - return directRef{ 37 + return directRefState{ 38 38 kind: directSymbolic, 39 39 name: name, 40 40 target: loose.Target, ··· 42 42 isPacked: hasPacked, 43 43 }, nil 44 44 default: 45 - return directRef{}, fmt.Errorf("refstore/files: unsupported reference type %T", loose) 45 + return directRefState{}, fmt.Errorf("refstore/files: unsupported reference type %T", loose) 46 46 } 47 47 } 48 48 49 49 if !errors.Is(err, refstore.ErrReferenceNotFound) { 50 - info, statErr := tx.store.rootFor(loc.root).Stat(loc.path) 50 + info, statErr := executor.store.rootFor(loc.root).Stat(loc.path) 51 51 if statErr != nil || !info.IsDir() { 52 - return directRef{}, err 52 + return directRefState{}, err 53 53 } 54 54 } 55 55 56 56 if hasPacked { 57 - packed, packedErr := tx.store.readPackedRefs() 57 + packed, packedErr := executor.store.readPackedRefs() 58 58 if packedErr != nil { 59 - return directRef{}, packedErr 59 + return directRefState{}, packedErr 60 60 } 61 61 62 62 detached := packed.byName[name] 63 63 64 - return directRef{ 64 + return directRefState{ 65 65 kind: directDetached, 66 66 name: name, 67 67 id: detached.ID, ··· 69 69 }, nil 70 70 } 71 71 72 - return directRef{ 72 + return directRefState{ 73 73 kind: directMissing, 74 74 name: name, 75 75 }, nil
+4 -4
refstore/files/transaction_direct_ref.go refstore/files/update_direct_ref.go
··· 2 2 3 3 import "codeberg.org/lindenii/furgit/objectid" 4 4 5 - type directKind uint8 5 + type directRefKind uint8 6 6 7 7 const ( 8 - directMissing directKind = iota 8 + directMissing directRefKind = iota 9 9 directDetached 10 10 directSymbolic 11 11 ) 12 12 13 - type directRef struct { 14 - kind directKind 13 + type directRefState struct { 14 + kind directRefKind 15 15 name string 16 16 id objectid.ObjectID 17 17 target string
-14
refstore/files/transaction_kind.go
··· 1 - package files 2 - 3 - type txKind uint8 4 - 5 - const ( 6 - txCreate txKind = iota 7 - txUpdate 8 - txDelete 9 - txVerify 10 - txCreateSymbolic 11 - txUpdateSymbolic 12 - txDeleteSymbolic 13 - txVerifySymbolic 14 - )
+2 -2
refstore/files/transaction_lock.go refstore/files/update_lock.go
··· 5 5 "path" 6 6 ) 7 7 8 - func (tx *Transaction) createLock(name refPath) error { 9 - root := tx.store.rootFor(name.root) 8 + func (executor *refUpdateExecutor) createUpdateLock(name refPath) error { 9 + root := executor.store.rootFor(name.root) 10 10 dir := path.Dir(name.path) 11 11 12 12 if dir != "." {
+3 -3
refstore/files/transaction_lock_packed.go refstore/files/update_lock_packed.go
··· 6 6 "time" 7 7 ) 8 8 9 - func (tx *Transaction) createPackedLock(timeout time.Duration) error { 9 + func (executor *refUpdateExecutor) createPackedRefsLock(timeout time.Duration) error { 10 10 const ( 11 11 initialBackoffMs = 1 12 12 backoffMaxMultiplier = 1000 ··· 17 17 n := 1 18 18 19 19 for { 20 - file, err := tx.store.commonRoot.OpenFile("packed-refs.lock", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) 20 + file, err := executor.store.commonRoot.OpenFile("packed-refs.lock", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) 21 21 if err == nil { 22 22 return file.Close() 23 23 } ··· 31 31 } 32 32 33 33 backoffMs := multiplier * initialBackoffMs 34 - waitMs := (750 + tx.store.lockRand.Intn(500)) * backoffMs / 1000 34 + waitMs := (750 + executor.store.lockRand.Intn(500)) * backoffMs / 1000 35 35 time.Sleep(time.Duration(waitMs) * time.Millisecond) 36 36 37 37 multiplier += 2*n + 1
-23
refstore/files/transaction_operation.go
··· 1 - package files 2 - 3 - import "codeberg.org/lindenii/furgit/objectid" 4 - 5 - type txOp struct { 6 - name string 7 - kind txKind 8 - newID objectid.ObjectID 9 - oldID objectid.ObjectID 10 - newTarget string 11 - oldTarget string 12 - } 13 - 14 - type preparedTxOp struct { 15 - op txOp 16 - target resolvedWriteTarget 17 - } 18 - 19 - type resolvedWriteTarget struct { 20 - name string 21 - loc refPath 22 - ref directRef 23 - }
-102
refstore/files/transaction_prepare.go
··· 1 - package files 2 - 3 - import ( 4 - "fmt" 5 - "slices" 6 - ) 7 - 8 - func (tx *Transaction) prepare() (prepared []preparedTxOp, err error) { 9 - prepared = make([]preparedTxOp, 0, len(tx.ops)) 10 - 11 - defer func() { 12 - if err != nil { 13 - _ = tx.cleanup(prepared) 14 - } 15 - }() 16 - 17 - targets := make(map[string]struct{}, len(tx.ops)) 18 - 19 - for _, op := range tx.ops { 20 - target, err := tx.resolveTarget(op) 21 - if err != nil { 22 - return prepared, err 23 - } 24 - 25 - targetKey := tx.targetKey(target.loc) 26 - if _, exists := targets[targetKey]; exists { 27 - return prepared, fmt.Errorf("refstore/files: duplicate transaction operation for %q", target.name) 28 - } 29 - 30 - targets[targetKey] = struct{}{} 31 - 32 - prepared = append(prepared, preparedTxOp{ 33 - op: op, 34 - target: target, 35 - }) 36 - } 37 - 38 - deleted := make(map[string]struct{}) 39 - written := make([]string, 0, len(prepared)) 40 - 41 - for _, item := range prepared { 42 - switch item.op.kind { 43 - case txDelete, txDeleteSymbolic: 44 - deleted[item.target.name] = struct{}{} 45 - case txCreate, txUpdate, txCreateSymbolic, txUpdateSymbolic: 46 - written = append(written, item.target.name) 47 - case txVerify, txVerifySymbolic: 48 - } 49 - } 50 - 51 - existing, err := tx.visibleNames() 52 - if err != nil { 53 - return prepared, err 54 - } 55 - 56 - for _, name := range written { 57 - err = verifyRefnameAvailable(name, existing, written, deleted) 58 - if err != nil { 59 - return prepared, err 60 - } 61 - } 62 - 63 - lockNames := make([]string, 0, len(prepared)) 64 - for _, item := range prepared { 65 - lockNames = append(lockNames, tx.targetKey(item.target.loc)) 66 - } 67 - 68 - slices.Sort(lockNames) 69 - 70 - for _, lockKey := range lockNames { 71 - err = tx.createLock(refPathFromKey(lockKey)) 72 - if err != nil { 73 - return prepared, err 74 - } 75 - } 76 - 77 - hasDeletes := len(deleted) > 0 78 - if hasDeletes { 79 - err = tx.createPackedLock(tx.store.packedRefsTimeout) 80 - if err != nil { 81 - return prepared, err 82 - } 83 - } 84 - 85 - for i := range prepared { 86 - item := &prepared[i] 87 - 88 - refState, err := tx.directRead(item.target.name) 89 - if err != nil { 90 - return prepared, err 91 - } 92 - 93 - item.target.ref = refState 94 - 95 - err = tx.verifyCurrent(*item) 96 - if err != nil { 97 - return prepared, err 98 - } 99 - } 100 - 101 - return prepared, nil 102 - }
+2 -2
refstore/files/transaction_queue.go
··· 1 1 package files 2 2 3 - func (tx *Transaction) queue(op txOp) error { 4 - err := tx.validateOp(op) 3 + func (tx *Transaction) queue(op queuedUpdate) error { 4 + err := (&refUpdateExecutor{store: tx.store}).validateQueuedUpdate(op) 5 5 if err != nil { 6 6 return err 7 7 }
+8 -8
refstore/files/transaction_queue_ops.go
··· 3 3 import "codeberg.org/lindenii/furgit/objectid" 4 4 5 5 func (tx *Transaction) Create(name string, newID objectid.ObjectID) error { 6 - return tx.queue(txOp{name: name, kind: txCreate, newID: newID}) 6 + return tx.queue(queuedUpdate{name: name, kind: updateCreate, newID: newID}) 7 7 } 8 8 9 9 func (tx *Transaction) Update(name string, newID, oldID objectid.ObjectID) error { 10 - return tx.queue(txOp{name: name, kind: txUpdate, newID: newID, oldID: oldID}) 10 + return tx.queue(queuedUpdate{name: name, kind: updateReplace, newID: newID, oldID: oldID}) 11 11 } 12 12 13 13 func (tx *Transaction) Delete(name string, oldID objectid.ObjectID) error { 14 - return tx.queue(txOp{name: name, kind: txDelete, oldID: oldID}) 14 + return tx.queue(queuedUpdate{name: name, kind: updateDelete, oldID: oldID}) 15 15 } 16 16 17 17 func (tx *Transaction) Verify(name string, oldID objectid.ObjectID) error { 18 - return tx.queue(txOp{name: name, kind: txVerify, oldID: oldID}) 18 + return tx.queue(queuedUpdate{name: name, kind: updateVerify, oldID: oldID}) 19 19 } 20 20 21 21 func (tx *Transaction) CreateSymbolic(name, newTarget string) error { 22 - return tx.queue(txOp{name: name, kind: txCreateSymbolic, newTarget: newTarget}) 22 + return tx.queue(queuedUpdate{name: name, kind: updateCreateSymbolic, newTarget: newTarget}) 23 23 } 24 24 25 25 func (tx *Transaction) UpdateSymbolic(name, newTarget, oldTarget string) error { 26 - return tx.queue(txOp{name: name, kind: txUpdateSymbolic, newTarget: newTarget, oldTarget: oldTarget}) 26 + return tx.queue(queuedUpdate{name: name, kind: updateReplaceSymbolic, newTarget: newTarget, oldTarget: oldTarget}) 27 27 } 28 28 29 29 func (tx *Transaction) DeleteSymbolic(name, oldTarget string) error { 30 - return tx.queue(txOp{name: name, kind: txDeleteSymbolic, oldTarget: oldTarget}) 30 + return tx.queue(queuedUpdate{name: name, kind: updateDeleteSymbolic, oldTarget: oldTarget}) 31 31 } 32 32 33 33 func (tx *Transaction) VerifySymbolic(name, oldTarget string) error { 34 - return tx.queue(txOp{name: name, kind: txVerifySymbolic, oldTarget: oldTarget}) 34 + return tx.queue(queuedUpdate{name: name, kind: updateVerifySymbolic, oldTarget: oldTarget}) 35 35 }
-21
refstore/files/transaction_resolve_target.go
··· 1 - package files 2 - 3 - import "fmt" 4 - 5 - func (tx *Transaction) resolveTarget(op txOp) (resolvedWriteTarget, error) { 6 - switch op.kind { 7 - case txCreate: 8 - return tx.resolveOrdinaryTarget(op.name, true) 9 - case txUpdate, txDelete, txVerify: 10 - return tx.resolveOrdinaryTarget(op.name, false) 11 - case txCreateSymbolic, txUpdateSymbolic, txDeleteSymbolic, txVerifySymbolic: 12 - refState, err := tx.directRead(op.name) 13 - if err != nil { 14 - return resolvedWriteTarget{}, err 15 - } 16 - 17 - return resolvedWriteTarget{name: op.name, loc: tx.store.loosePath(op.name), ref: refState}, nil 18 - default: 19 - return resolvedWriteTarget{}, fmt.Errorf("refstore/files: unsupported transaction operation %d", op.kind) 20 - } 21 - }
-46
refstore/files/transaction_resolve_target_ordinary.go
··· 1 - package files 2 - 3 - import ( 4 - "fmt" 5 - "strings" 6 - 7 - "codeberg.org/lindenii/furgit/refstore" 8 - ) 9 - 10 - func (tx *Transaction) resolveOrdinaryTarget(name string, allowMissing bool) (resolvedWriteTarget, error) { 11 - cur := name 12 - seen := make(map[string]struct{}) 13 - 14 - for { 15 - if _, ok := seen[cur]; ok { 16 - return resolvedWriteTarget{}, fmt.Errorf("refstore/files: symbolic reference cycle at %q", cur) 17 - } 18 - 19 - seen[cur] = struct{}{} 20 - 21 - refState, err := tx.directRead(cur) 22 - if err != nil { 23 - return resolvedWriteTarget{}, err 24 - } 25 - 26 - switch refState.kind { 27 - case directMissing: 28 - if !allowMissing { 29 - return resolvedWriteTarget{}, refstore.ErrReferenceNotFound 30 - } 31 - 32 - return resolvedWriteTarget{name: cur, loc: tx.store.loosePath(cur), ref: refState}, nil 33 - case directDetached: 34 - return resolvedWriteTarget{name: cur, loc: tx.store.loosePath(cur), ref: refState}, nil 35 - case directSymbolic: 36 - target := strings.TrimSpace(refState.target) 37 - if target == "" { 38 - return resolvedWriteTarget{}, fmt.Errorf("refstore/files: symbolic reference %q has empty target", cur) 39 - } 40 - 41 - cur = target 42 - default: 43 - return resolvedWriteTarget{}, fmt.Errorf("refstore/files: unsupported direct reference state %d", refState.kind) 44 - } 45 - } 46 - }
-65
refstore/files/transaction_validate.go
··· 1 - package files 2 - 3 - import ( 4 - "fmt" 5 - "strings" 6 - 7 - "codeberg.org/lindenii/furgit/objectid" 8 - "codeberg.org/lindenii/furgit/ref/refname" 9 - ) 10 - 11 - func (tx *Transaction) validateOp(op txOp) error { 12 - if op.name == "" { 13 - return fmt.Errorf("refstore/files: empty reference name") 14 - } 15 - 16 - switch op.kind { 17 - case txCreate, txUpdate: 18 - err := refname.ValidateUpdateName(op.name, true) 19 - if err != nil { 20 - return err 21 - } 22 - 23 - if op.newID.Size() == 0 { 24 - return objectid.ErrInvalidAlgorithm 25 - } 26 - case txDelete, txVerify: 27 - err := refname.ValidateUpdateName(op.name, false) 28 - if err != nil { 29 - return err 30 - } 31 - 32 - if op.oldID.Size() == 0 { 33 - return objectid.ErrInvalidAlgorithm 34 - } 35 - case txCreateSymbolic, txUpdateSymbolic: 36 - err := refname.ValidateUpdateName(op.name, true) 37 - if err != nil { 38 - return err 39 - } 40 - 41 - if strings.TrimSpace(op.newTarget) == "" { 42 - return fmt.Errorf("refstore/files: empty symbolic target") 43 - } 44 - 45 - err = refname.ValidateSymbolicTarget(op.name, strings.TrimSpace(op.newTarget)) 46 - if err != nil { 47 - return err 48 - } 49 - case txDeleteSymbolic, txVerifySymbolic: 50 - err := refname.ValidateUpdateName(op.name, false) 51 - if err != nil { 52 - return err 53 - } 54 - default: 55 - return fmt.Errorf("refstore/files: unsupported transaction operation %d", op.kind) 56 - } 57 - 58 - if op.kind == txUpdateSymbolic || op.kind == txDeleteSymbolic || op.kind == txVerifySymbolic { 59 - if strings.TrimSpace(op.oldTarget) == "" { 60 - return fmt.Errorf("refstore/files: empty symbolic old target") 61 - } 62 - } 63 - 64 - return nil 65 - }
-53
refstore/files/transaction_verify_current.go
··· 1 - package files 2 - 3 - import ( 4 - "fmt" 5 - "strings" 6 - ) 7 - 8 - func (tx *Transaction) verifyCurrent(item preparedTxOp) error { 9 - switch item.op.kind { 10 - case txCreate: 11 - if item.target.ref.kind != directMissing { 12 - return fmt.Errorf("refstore/files: reference %q already exists", item.target.name) 13 - } 14 - 15 - return nil 16 - case txUpdate, txDelete, txVerify: 17 - if item.target.ref.kind == directMissing { 18 - return fmt.Errorf("refstore/files: reference %q is missing", item.target.name) 19 - } 20 - 21 - if item.target.ref.kind != directDetached { 22 - return fmt.Errorf("refstore/files: reference %q is not detached", item.target.name) 23 - } 24 - 25 - if item.target.ref.id != item.op.oldID { 26 - return fmt.Errorf("refstore/files: reference %q is at %s but expected %s", item.target.name, item.target.ref.id, item.op.oldID) 27 - } 28 - 29 - return nil 30 - case txCreateSymbolic: 31 - if item.target.ref.kind != directMissing { 32 - return fmt.Errorf("refstore/files: reference %q already exists", item.target.name) 33 - } 34 - 35 - return nil 36 - case txUpdateSymbolic, txDeleteSymbolic, txVerifySymbolic: 37 - if item.target.ref.kind == directMissing { 38 - return fmt.Errorf("refstore/files: symbolic reference %q is missing", item.target.name) 39 - } 40 - 41 - if item.target.ref.kind != directSymbolic { 42 - return fmt.Errorf("refstore/files: reference %q is not symbolic", item.target.name) 43 - } 44 - 45 - if strings.TrimSpace(item.target.ref.target) != strings.TrimSpace(item.op.oldTarget) { 46 - return fmt.Errorf("refstore/files: reference %q points at %q, expected %q", item.target.name, item.target.ref.target, item.op.oldTarget) 47 - } 48 - 49 - return nil 50 - default: 51 - return fmt.Errorf("refstore/files: unsupported transaction operation %d", item.op.kind) 52 - } 53 - }
+4 -3
refstore/files/transaction_verify_refnames.go refstore/files/update_verify_refnames.go
··· 1 1 package files 2 2 3 3 import ( 4 - "fmt" 5 4 "strings" 5 + 6 + "codeberg.org/lindenii/furgit/refstore" 6 7 ) 7 8 8 9 func verifyRefnameAvailable(name string, existing map[string]struct{}, writes []string, deleted map[string]struct{}) error { ··· 16 17 } 17 18 18 19 if refnamesConflict(name, existingName) { 19 - return fmt.Errorf("refstore/files: reference name conflict between %q and %q", name, existingName) 20 + return wrapUpdateError(name, &refstore.NameConflictError{Other: existingName}) 20 21 } 21 22 } 22 23 ··· 26 27 } 27 28 28 29 if refnamesConflict(name, other) { 29 - return fmt.Errorf("refstore/files: reference name conflict between %q and %q", name, other) 30 + return wrapUpdateError(name, &refstore.NameConflictError{Other: other}) 30 31 } 31 32 } 32 33
+3 -3
refstore/files/transaction_visible_names.go refstore/files/update_visible_names.go
··· 1 1 package files 2 2 3 - func (tx *Transaction) visibleNames() (map[string]struct{}, error) { 3 + func (executor *refUpdateExecutor) collectVisibleNames() (map[string]struct{}, error) { 4 4 names := make(map[string]struct{}) 5 5 6 - looseNames, err := tx.store.collectLooseRefNames() 6 + looseNames, err := executor.store.collectLooseRefNames() 7 7 if err != nil { 8 8 return nil, err 9 9 } ··· 12 12 names[name] = struct{}{} 13 13 } 14 14 15 - packed, err := tx.store.readPackedRefs() 15 + packed, err := executor.store.readPackedRefs() 16 16 if err != nil { 17 17 return nil, err 18 18 }
+6 -6
refstore/files/transaction_write_loose.go refstore/files/update_write_loose.go
··· 7 7 "strings" 8 8 ) 9 9 10 - func (tx *Transaction) writeLoose(item preparedTxOp) error { 11 - root := tx.store.rootFor(item.target.loc.root) 10 + func (executor *refUpdateExecutor) writePreparedLooseUpdate(item preparedUpdate) error { 11 + root := executor.store.rootFor(item.target.loc.root) 12 12 lockName := item.target.loc.path + ".lock" 13 13 14 14 lock, err := root.OpenFile(lockName, os.O_WRONLY|os.O_TRUNC, 0o644) ··· 19 19 var content string 20 20 21 21 switch item.op.kind { 22 - case txCreate, txUpdate: 22 + case updateCreate, updateReplace: 23 23 content = item.op.newID.String() + "\n" 24 - case txCreateSymbolic, txUpdateSymbolic: 24 + case updateCreateSymbolic, updateReplaceSymbolic: 25 25 content = "ref: " + strings.TrimSpace(item.op.newTarget) + "\n" 26 - case txDelete, txVerify, txDeleteSymbolic, txVerifySymbolic: 26 + case updateDelete, updateVerify, updateDeleteSymbolic, updateVerifySymbolic: 27 27 default: 28 28 _ = lock.Close() 29 29 ··· 50 50 } 51 51 } 52 52 53 - err = tx.removeEmptyDirTree(item.target.loc) 53 + err = executor.removeEmptyDirTree(item.target.loc) 54 54 if err != nil { 55 55 return err 56 56 }
+7 -7
refstore/files/transaction_write_packed_deltas.go refstore/files/update_write_packed_refs.go
··· 5 5 "os" 6 6 ) 7 7 8 - func (tx *Transaction) applyPackedDeletes(prepared []preparedTxOp) error { 9 - _, err := tx.store.commonRoot.Stat("packed-refs.lock") 8 + func (executor *refUpdateExecutor) applyPackedRefDeletes(prepared []preparedUpdate) error { 9 + _, err := executor.store.commonRoot.Stat("packed-refs.lock") 10 10 if err != nil { 11 11 if errors.Is(err, os.ErrNotExist) { 12 12 return nil ··· 15 15 return err 16 16 } 17 17 18 - packed, err := tx.store.readPackedRefs() 18 + packed, err := executor.store.readPackedRefs() 19 19 if err != nil { 20 20 return err 21 21 } ··· 24 24 needed := false 25 25 26 26 for _, item := range prepared { 27 - if item.op.kind != txDelete && item.op.kind != txDeleteSymbolic { 27 + if item.op.kind != updateDelete && item.op.kind != updateDeleteSymbolic { 28 28 continue 29 29 } 30 30 ··· 38 38 return nil 39 39 } 40 40 41 - lock, err := tx.store.commonRoot.OpenFile("packed-refs.new", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) 41 + lock, err := executor.store.commonRoot.OpenFile("packed-refs.new", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644) 42 42 if err != nil { 43 43 return err 44 44 } ··· 50 50 return 51 51 } 52 52 53 - _ = tx.store.commonRoot.Remove("packed-refs.new") 53 + _ = executor.store.commonRoot.Remove("packed-refs.new") 54 54 }() 55 55 56 56 _, err = lock.WriteString("# pack-refs with: peeled fully-peeled sorted\n") ··· 87 87 return err 88 88 } 89 89 90 - err = tx.store.commonRoot.Rename("packed-refs.new", "packed-refs") 90 + err = executor.store.commonRoot.Rename("packed-refs.new", "packed-refs") 91 91 if err != nil { 92 92 return err 93 93 }
+39
refstore/files/update_cleanup.go
··· 1 + package files 2 + 3 + import ( 4 + "errors" 5 + "os" 6 + "slices" 7 + ) 8 + 9 + func (executor *refUpdateExecutor) cleanupPreparedUpdates(prepared []preparedUpdate) error { 10 + var firstErr error 11 + 12 + lockNames := make([]string, 0, len(prepared)+1) 13 + for _, item := range prepared { 14 + lockNames = append(lockNames, updateTargetKey(item.target.loc)) 15 + } 16 + 17 + lockNames = append(lockNames, updateTargetKey(refPath{root: rootCommon, path: "packed-refs"})) 18 + slices.Sort(lockNames) 19 + lockNames = slices.Compact(lockNames) 20 + 21 + for _, lockKey := range lockNames { 22 + lockPath := refPathFromKey(lockKey) 23 + lockName := lockPath.path + ".lock" 24 + root := executor.store.rootFor(lockPath.root) 25 + 26 + err := root.Remove(lockName) 27 + if err == nil || errors.Is(err, os.ErrNotExist) { 28 + executor.tryRemoveEmptyParentPaths(lockPath.root, lockName) 29 + 30 + continue 31 + } 32 + 33 + if firstErr == nil { 34 + firstErr = err 35 + } 36 + } 37 + 38 + return firstErr 39 + }
+35
refstore/files/update_cleanup_parents.go
··· 1 + package files 2 + 3 + import ( 4 + "errors" 5 + "os" 6 + "path" 7 + ) 8 + 9 + func (executor *refUpdateExecutor) tryRemoveEmptyParents(name string) { 10 + loc := executor.store.loosePath(name) 11 + executor.tryRemoveEmptyParentPaths(loc.root, loc.path) 12 + } 13 + 14 + func (executor *refUpdateExecutor) tryRemoveEmptyParentPaths(kind rootKind, name string) { 15 + root := executor.store.rootFor(kind) 16 + dir := path.Dir(name) 17 + 18 + for dir != "." && dir != "/" { 19 + err := root.Remove(dir) 20 + if err != nil { 21 + if errors.Is(err, os.ErrNotExist) { 22 + return 23 + } 24 + 25 + var pathErr *os.PathError 26 + if errors.As(err, &pathErr) { 27 + return 28 + } 29 + 30 + return 31 + } 32 + 33 + dir = path.Dir(dir) 34 + } 35 + }
+25
refstore/files/update_commit.go
··· 1 + package files 2 + 3 + func (executor *refUpdateExecutor) commitPreparedUpdates(prepared []preparedUpdate) (err error) { 4 + defer func() { 5 + _ = executor.cleanupPreparedUpdates(prepared) 6 + }() 7 + 8 + for _, item := range prepared { 9 + if item.op.kind == updateDelete || item.op.kind == updateDeleteSymbolic || item.op.kind == updateVerify || item.op.kind == updateVerifySymbolic { 10 + continue 11 + } 12 + 13 + err = executor.writePreparedLooseUpdate(item) 14 + if err != nil { 15 + return wrapUpdateError(item.op.name, err) 16 + } 17 + } 18 + 19 + err = executor.applyPackedRefDeletes(prepared) 20 + if err != nil { 21 + return err 22 + } 23 + 24 + return executor.removeDeletedLooseRefs(prepared) 25 + }
+25
refstore/files/update_commit_delete.go
··· 1 + package files 2 + 3 + import ( 4 + "errors" 5 + "os" 6 + ) 7 + 8 + func (executor *refUpdateExecutor) removeDeletedLooseRefs(prepared []preparedUpdate) error { 9 + for _, item := range prepared { 10 + switch item.op.kind { 11 + case updateDelete, updateDeleteSymbolic: 12 + if item.target.ref.isLoose { 13 + err := executor.store.rootFor(item.target.loc.root).Remove(item.target.loc.path) 14 + if err != nil && !errors.Is(err, os.ErrNotExist) { 15 + return wrapUpdateError(item.op.name, err) 16 + } 17 + 18 + executor.tryRemoveEmptyParents(item.target.name) 19 + } 20 + case updateCreate, updateReplace, updateVerify, updateCreateSymbolic, updateReplaceSymbolic, updateVerifySymbolic: 21 + } 22 + } 23 + 24 + return nil 25 + }
+28
refstore/files/update_error.go
··· 1 + package files 2 + 3 + import "fmt" 4 + 5 + type updateContextError struct { 6 + name string 7 + err error 8 + } 9 + 10 + func (err *updateContextError) Error() string { 11 + return fmt.Sprintf("refstore/files: update %q: %v", err.name, err.err) 12 + } 13 + 14 + func (err *updateContextError) Unwrap() error { 15 + if err == nil { 16 + return nil 17 + } 18 + 19 + return err.err 20 + } 21 + 22 + func wrapUpdateError(name string, err error) error { 23 + if err == nil || name == "" { 24 + return err 25 + } 26 + 27 + return &updateContextError{name: name, err: err} 28 + }
+5
refstore/files/update_executor.go
··· 1 + package files 2 + 3 + type refUpdateExecutor struct { 4 + store *Store 5 + }
+14
refstore/files/update_kind.go
··· 1 + package files 2 + 3 + type updateKind uint8 4 + 5 + const ( 6 + updateCreate updateKind = iota 7 + updateReplace 8 + updateDelete 9 + updateVerify 10 + updateCreateSymbolic 11 + updateReplaceSymbolic 12 + updateDeleteSymbolic 13 + updateVerifySymbolic 14 + )
+6
refstore/files/update_operation_prepared.go
··· 1 + package files 2 + 3 + type preparedUpdate struct { 4 + op queuedUpdate 5 + target resolvedUpdateTarget 6 + }
+12
refstore/files/update_operation_queue.go
··· 1 + package files 2 + 3 + import "codeberg.org/lindenii/furgit/objectid" 4 + 5 + type queuedUpdate struct { 6 + name string 7 + kind updateKind 8 + newID objectid.ObjectID 9 + oldID objectid.ObjectID 10 + newTarget string 11 + oldTarget string 12 + }
+48
refstore/files/update_prepare.go
··· 1 + package files 2 + 3 + func (executor *refUpdateExecutor) prepareUpdates(ops []queuedUpdate) (prepared []preparedUpdate, err error) { 4 + defer func() { 5 + if err != nil { 6 + _ = executor.cleanupPreparedUpdates(prepared) 7 + } 8 + }() 9 + 10 + prepared, err = executor.resolvePreparedUpdates(ops) 11 + if err != nil { 12 + return prepared, err 13 + } 14 + 15 + deleted, written := collectPreparedWrites(prepared) 16 + 17 + existing, err := executor.collectVisibleNames() 18 + if err != nil { 19 + return prepared, err 20 + } 21 + 22 + for _, name := range written { 23 + err = verifyRefnameAvailable(name, existing, written, deleted) 24 + if err != nil { 25 + return prepared, err 26 + } 27 + } 28 + 29 + err = executor.prepareUpdateLocks(prepared) 30 + if err != nil { 31 + return prepared, err 32 + } 33 + 34 + hasDeletes := len(deleted) > 0 35 + if hasDeletes { 36 + err = executor.createPackedRefsLock(executor.store.packedRefsTimeout) 37 + if err != nil { 38 + return prepared, err 39 + } 40 + } 41 + 42 + err = executor.verifyPreparedUpdates(prepared) 43 + if err != nil { 44 + return prepared, err 45 + } 46 + 47 + return prepared, nil 48 + }
+28
refstore/files/update_prepare_lock.go
··· 1 + package files 2 + 3 + import "slices" 4 + 5 + func (executor *refUpdateExecutor) prepareUpdateLocks(prepared []preparedUpdate) error { 6 + lockNames := make([]string, 0, len(prepared)) 7 + for _, item := range prepared { 8 + lockNames = append(lockNames, updateTargetKey(item.target.loc)) 9 + } 10 + 11 + slices.Sort(lockNames) 12 + 13 + for _, lockKey := range lockNames { 14 + lockPath := refPathFromKey(lockKey) 15 + err := executor.createUpdateLock(lockPath) 16 + if err != nil { 17 + for _, item := range prepared { 18 + if updateTargetKey(item.target.loc) == lockKey { 19 + return wrapUpdateError(item.op.name, err) 20 + } 21 + } 22 + 23 + return err 24 + } 25 + } 26 + 27 + return nil 28 + }
+42
refstore/files/update_prepare_resolve.go
··· 1 + package files 2 + 3 + import "codeberg.org/lindenii/furgit/refstore" 4 + 5 + func (executor *refUpdateExecutor) resolvePreparedUpdates(ops []queuedUpdate) ([]preparedUpdate, error) { 6 + prepared := make([]preparedUpdate, 0, len(ops)) 7 + targets := make(map[string]struct{}, len(ops)) 8 + 9 + for _, op := range ops { 10 + target, err := executor.resolveQueuedUpdateTarget(op) 11 + if err != nil { 12 + return prepared, err 13 + } 14 + 15 + targetKey := updateTargetKey(target.loc) 16 + if _, exists := targets[targetKey]; exists { 17 + return prepared, wrapUpdateError(op.name, &refstore.DuplicateUpdateError{}) 18 + } 19 + 20 + targets[targetKey] = struct{}{} 21 + prepared = append(prepared, preparedUpdate{op: op, target: target}) 22 + } 23 + 24 + return prepared, nil 25 + } 26 + 27 + func collectPreparedWrites(prepared []preparedUpdate) (deleted map[string]struct{}, written []string) { 28 + deleted = make(map[string]struct{}) 29 + written = make([]string, 0, len(prepared)) 30 + 31 + for _, item := range prepared { 32 + switch item.op.kind { 33 + case updateDelete, updateDeleteSymbolic: 34 + deleted[item.target.name] = struct{}{} 35 + case updateCreate, updateReplace, updateCreateSymbolic, updateReplaceSymbolic: 36 + written = append(written, item.target.name) 37 + case updateVerify, updateVerifySymbolic: 38 + } 39 + } 40 + 41 + return deleted, written 42 + }
+21
refstore/files/update_prepare_verify.go
··· 1 + package files 2 + 3 + func (executor *refUpdateExecutor) verifyPreparedUpdates(prepared []preparedUpdate) error { 4 + for i := range prepared { 5 + item := &prepared[i] 6 + 7 + refState, err := executor.directRead(item.target.name) 8 + if err != nil { 9 + return wrapUpdateError(item.op.name, err) 10 + } 11 + 12 + item.target.ref = refState 13 + 14 + err = executor.verifyPreparedUpdateCurrent(*item) 15 + if err != nil { 16 + return err 17 + } 18 + } 19 + 20 + return nil 21 + }
+21
refstore/files/update_resolve_target.go
··· 1 + package files 2 + 3 + import "fmt" 4 + 5 + func (executor *refUpdateExecutor) resolveQueuedUpdateTarget(op queuedUpdate) (resolvedUpdateTarget, error) { 6 + switch op.kind { 7 + case updateCreate: 8 + return executor.resolveOrdinaryTarget(op.name, true) 9 + case updateReplace, updateDelete, updateVerify: 10 + return executor.resolveOrdinaryTarget(op.name, false) 11 + case updateCreateSymbolic, updateReplaceSymbolic, updateDeleteSymbolic, updateVerifySymbolic: 12 + refState, err := executor.directRead(op.name) 13 + if err != nil { 14 + return resolvedUpdateTarget{}, err 15 + } 16 + 17 + return resolvedUpdateTarget{name: op.name, loc: executor.store.loosePath(op.name), ref: refState}, nil 18 + default: 19 + return resolvedUpdateTarget{}, fmt.Errorf("refstore/files: unsupported update operation %d", op.kind) 20 + } 21 + }
+48
refstore/files/update_resolve_target_ordinary.go
··· 1 + package files 2 + 3 + import ( 4 + "fmt" 5 + "strings" 6 + 7 + "codeberg.org/lindenii/furgit/refstore" 8 + ) 9 + 10 + func (executor *refUpdateExecutor) resolveOrdinaryTarget(name string, allowMissing bool) (resolvedUpdateTarget, error) { 11 + cur := name 12 + seen := make(map[string]struct{}) 13 + 14 + for { 15 + if _, ok := seen[cur]; ok { 16 + return resolvedUpdateTarget{}, fmt.Errorf("refstore/files: symbolic reference cycle at %q", cur) 17 + } 18 + 19 + seen[cur] = struct{}{} 20 + 21 + refState, err := executor.directRead(cur) 22 + if err != nil { 23 + return resolvedUpdateTarget{}, err 24 + } 25 + 26 + switch refState.kind { 27 + case directMissing: 28 + if !allowMissing { 29 + return resolvedUpdateTarget{}, wrapUpdateError(name, refstore.ErrReferenceNotFound) 30 + } 31 + 32 + return resolvedUpdateTarget{name: cur, loc: executor.store.loosePath(cur), ref: refState}, nil 33 + case directDetached: 34 + return resolvedUpdateTarget{name: cur, loc: executor.store.loosePath(cur), ref: refState}, nil 35 + case directSymbolic: 36 + target := strings.TrimSpace(refState.target) 37 + if target == "" { 38 + return resolvedUpdateTarget{}, wrapUpdateError(name, &refstore.InvalidValueError{ 39 + Err: fmt.Errorf("symbolic reference has empty target"), 40 + }) 41 + } 42 + 43 + cur = target 44 + default: 45 + return resolvedUpdateTarget{}, fmt.Errorf("refstore/files: unsupported direct reference state %d", refState.kind) 46 + } 47 + } 48 + }
+7
refstore/files/update_target_resolved.go
··· 1 + package files 2 + 3 + type resolvedUpdateTarget struct { 4 + name string 5 + loc refPath 6 + ref directRefState 7 + }
+66
refstore/files/update_validate.go
··· 1 + package files 2 + 3 + import ( 4 + "fmt" 5 + "strings" 6 + 7 + "codeberg.org/lindenii/furgit/objectid" 8 + "codeberg.org/lindenii/furgit/ref/refname" 9 + "codeberg.org/lindenii/furgit/refstore" 10 + ) 11 + 12 + func (executor *refUpdateExecutor) validateQueuedUpdate(op queuedUpdate) error { 13 + if op.name == "" { 14 + return wrapUpdateError(op.name, &refstore.InvalidNameError{Err: fmt.Errorf("empty reference name")}) 15 + } 16 + 17 + switch op.kind { 18 + case updateCreate, updateReplace: 19 + err := refname.ValidateUpdateName(op.name, true) 20 + if err != nil { 21 + return wrapUpdateError(op.name, &refstore.InvalidNameError{Err: err}) 22 + } 23 + 24 + if op.newID.Size() == 0 { 25 + return wrapUpdateError(op.name, &refstore.InvalidValueError{Err: objectid.ErrInvalidAlgorithm}) 26 + } 27 + case updateDelete, updateVerify: 28 + err := refname.ValidateUpdateName(op.name, false) 29 + if err != nil { 30 + return wrapUpdateError(op.name, &refstore.InvalidNameError{Err: err}) 31 + } 32 + 33 + if op.oldID.Size() == 0 { 34 + return wrapUpdateError(op.name, &refstore.InvalidValueError{Err: objectid.ErrInvalidAlgorithm}) 35 + } 36 + case updateCreateSymbolic, updateReplaceSymbolic: 37 + err := refname.ValidateUpdateName(op.name, true) 38 + if err != nil { 39 + return wrapUpdateError(op.name, &refstore.InvalidNameError{Err: err}) 40 + } 41 + 42 + if strings.TrimSpace(op.newTarget) == "" { 43 + return wrapUpdateError(op.name, &refstore.InvalidValueError{Err: fmt.Errorf("empty symbolic target")}) 44 + } 45 + 46 + err = refname.ValidateSymbolicTarget(op.name, strings.TrimSpace(op.newTarget)) 47 + if err != nil { 48 + return wrapUpdateError(op.name, &refstore.InvalidValueError{Err: err}) 49 + } 50 + case updateDeleteSymbolic, updateVerifySymbolic: 51 + err := refname.ValidateUpdateName(op.name, false) 52 + if err != nil { 53 + return wrapUpdateError(op.name, &refstore.InvalidNameError{Err: err}) 54 + } 55 + default: 56 + return fmt.Errorf("refstore/files: unsupported update operation %d", op.kind) 57 + } 58 + 59 + if op.kind == updateReplaceSymbolic || op.kind == updateDeleteSymbolic || op.kind == updateVerifySymbolic { 60 + if strings.TrimSpace(op.oldTarget) == "" { 61 + return wrapUpdateError(op.name, &refstore.InvalidValueError{Err: fmt.Errorf("empty symbolic old target")}) 62 + } 63 + } 64 + 65 + return nil 66 + }
+60
refstore/files/update_verify_current.go
··· 1 + package files 2 + 3 + import ( 4 + "strings" 5 + 6 + "codeberg.org/lindenii/furgit/refstore" 7 + ) 8 + 9 + func (executor *refUpdateExecutor) verifyPreparedUpdateCurrent(item preparedUpdate) error { 10 + switch item.op.kind { 11 + case updateCreate: 12 + if item.target.ref.kind != directMissing { 13 + return wrapUpdateError(item.op.name, &refstore.CreateExistsError{}) 14 + } 15 + 16 + return nil 17 + case updateReplace, updateDelete, updateVerify: 18 + if item.target.ref.kind == directMissing { 19 + return wrapUpdateError(item.op.name, refstore.ErrReferenceNotFound) 20 + } 21 + 22 + if item.target.ref.kind != directDetached { 23 + return wrapUpdateError(item.op.name, &refstore.ExpectedDetachedError{}) 24 + } 25 + 26 + if item.target.ref.id != item.op.oldID { 27 + return wrapUpdateError(item.op.name, &refstore.IncorrectOldValueError{ 28 + Actual: item.target.ref.id.String(), 29 + Expected: item.op.oldID.String(), 30 + }) 31 + } 32 + 33 + return nil 34 + case updateCreateSymbolic: 35 + if item.target.ref.kind != directMissing { 36 + return wrapUpdateError(item.op.name, &refstore.CreateExistsError{}) 37 + } 38 + 39 + return nil 40 + case updateReplaceSymbolic, updateDeleteSymbolic, updateVerifySymbolic: 41 + if item.target.ref.kind == directMissing { 42 + return wrapUpdateError(item.op.name, refstore.ErrReferenceNotFound) 43 + } 44 + 45 + if item.target.ref.kind != directSymbolic { 46 + return wrapUpdateError(item.op.name, &refstore.ExpectedSymbolicError{}) 47 + } 48 + 49 + if strings.TrimSpace(item.target.ref.target) != strings.TrimSpace(item.op.oldTarget) { 50 + return wrapUpdateError(item.op.name, &refstore.IncorrectOldValueError{ 51 + Actual: strings.TrimSpace(item.target.ref.target), 52 + Expected: strings.TrimSpace(item.op.oldTarget), 53 + }) 54 + } 55 + 56 + return nil 57 + } 58 + 59 + return nil 60 + }
+110
refstore/update_errors.go
··· 1 + package refstore 2 + 3 + import "fmt" 4 + 5 + // InvalidNameError indicates that one requested reference name is invalid. 6 + type InvalidNameError struct { 7 + Err error 8 + } 9 + 10 + func (err *InvalidNameError) Error() string { 11 + if err == nil || err.Err == nil { 12 + return "invalid reference name" 13 + } 14 + 15 + return fmt.Sprintf("invalid reference name: %v", err.Err) 16 + } 17 + 18 + func (err *InvalidNameError) Unwrap() error { 19 + if err == nil { 20 + return nil 21 + } 22 + 23 + return err.Err 24 + } 25 + 26 + // InvalidValueError indicates that one requested reference value is invalid. 27 + type InvalidValueError struct { 28 + Err error 29 + } 30 + 31 + func (err *InvalidValueError) Error() string { 32 + if err == nil || err.Err == nil { 33 + return "invalid reference value" 34 + } 35 + 36 + return fmt.Sprintf("invalid reference value: %v", err.Err) 37 + } 38 + 39 + func (err *InvalidValueError) Unwrap() error { 40 + if err == nil { 41 + return nil 42 + } 43 + 44 + return err.Err 45 + } 46 + 47 + // DuplicateUpdateError indicates that one batch or transaction includes a 48 + // duplicate update target. 49 + type DuplicateUpdateError struct{} 50 + 51 + func (err *DuplicateUpdateError) Error() string { 52 + return "duplicate reference update" 53 + } 54 + 55 + // CreateExistsError indicates that one create operation targeted an existing 56 + // reference. 57 + type CreateExistsError struct{} 58 + 59 + func (err *CreateExistsError) Error() string { 60 + return "reference already exists" 61 + } 62 + 63 + // IncorrectOldValueError indicates that one operation's expected old value did 64 + // not match the current reference value. 65 + type IncorrectOldValueError struct { 66 + Actual string 67 + Expected string 68 + } 69 + 70 + func (err *IncorrectOldValueError) Error() string { 71 + if err == nil { 72 + return "incorrect old value provided" 73 + } 74 + 75 + if err.Actual == "" && err.Expected == "" { 76 + return "incorrect old value provided" 77 + } 78 + 79 + return fmt.Sprintf("incorrect old value provided: got %q, expected %q", err.Actual, err.Expected) 80 + } 81 + 82 + // ExpectedDetachedError indicates that one operation required a detached 83 + // reference but found a different kind. 84 + type ExpectedDetachedError struct{} 85 + 86 + func (err *ExpectedDetachedError) Error() string { 87 + return "expected detached reference" 88 + } 89 + 90 + // ExpectedSymbolicError indicates that one operation required a symbolic 91 + // reference but found a different kind. 92 + type ExpectedSymbolicError struct{} 93 + 94 + func (err *ExpectedSymbolicError) Error() string { 95 + return "expected symbolic reference" 96 + } 97 + 98 + // NameConflictError indicates that one reference name conflicts with another 99 + // visible or queued reference name. 100 + type NameConflictError struct { 101 + Other string 102 + } 103 + 104 + func (err *NameConflictError) Error() string { 105 + if err == nil || err.Other == "" { 106 + return "reference name conflict" 107 + } 108 + 109 + return fmt.Sprintf("reference name conflict with %q", err.Other) 110 + }