ref: 4a796e64ac576d6a3e3f2fe6174c4aa476ea0c5c
parent: 13507b7704415332e2b00f5f4d291f3be8bd18fa
author: Runxi Yu <runxiyu@umich.edu>
date: Sun Mar 22 23:25:44 EDT 2026
refstore: Improve interfaces, errors, and make batch work
--- a/refstore/batch.go
+++ b/refstore/batch.go
@@ -46,8 +46,19 @@
Abort() error
}
+// BatchStatus reports the outcome for one queued batch operation.
+type BatchStatus uint8
+
+const (
+ BatchStatusApplied BatchStatus = iota
+ BatchStatusRejected
+ BatchStatusFatal
+ BatchStatusNotAttempted
+)
+
// BatchResult reports the outcome for one queued batch operation.
type BatchResult struct {- Name string
- Error error
+ Name string
+ Status BatchStatus
+ Error error
}
--- a/refstore/files/batch.go
+++ b/refstore/files/batch.go
@@ -3,9 +3,8 @@
import "codeberg.org/lindenii/furgit/refstore"
type Batch struct {- store *Store
- ops []txOp
- closed bool
+ store *Store
+ ops []queuedUpdate
}
var _ refstore.Batch = (*Batch)(nil)
--- 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
}
--- a/refstore/files/batch_apply.go
+++ b/refstore/files/batch_apply.go
@@ -1,73 +1,135 @@
package files
-import (
- "errors"
+import "codeberg.org/lindenii/furgit/refstore"
- "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)
- err := errors.New("refstore/files: duplicate batch operation for " + `"` + op.name + `"`)- for j := i; j < len(results); j++ {+ 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)
+
+ 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
- for j := i + 1; j < len(results); j++ {- results[j].Name = batch.ops[j].name
- results[j].Error = err
+ continue
+ }
+
+ results[idx].Status = refstore.BatchStatusNotAttempted
+ results[idx].Error = batchResultError(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
}
--- a/refstore/files/batch_begin.go
+++ b/refstore/files/batch_begin.go
@@ -8,6 +8,6 @@
func (store *Store) BeginBatch() (refstore.Batch, error) { return &Batch{store: store,
- ops: make([]txOp, 0, 8),
+ ops: make([]queuedUpdate, 0, 8),
}, nil
}
--- 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)
}
--- a/refstore/files/batch_queue_ops.go
+++ b/refstore/files/batch_queue_ops.go
@@ -3,33 +3,33 @@
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})}
--- a/refstore/files/batch_reject.go
+++ /dev/null
@@ -1,35 +1,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")
-}
--- /dev/null
+++ b/refstore/files/batch_rejection.go
@@ -1,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))
+}
--- /dev/null
+++ b/refstore/files/batch_result_error.go
@@ -1,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
+}
--- 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,14 +41,19 @@
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") if err != nil { t.Fatalf("Resolve(main): %v", err)@@ -81,8 +88,8 @@
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 {@@ -89,12 +96,20 @@
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)}
})
--- /dev/null
+++ b/refstore/files/broken_ref_error.go
@@ -1,0 +1,16 @@
+package files
+
+import "fmt"
+
+type brokenRefError struct {+ name string
+ err error
+}
+
+func (err brokenRefError) Error() string {+ return fmt.Sprintf("refstore/files: broken reference %q: %v", err.name, err.err)+}
+
+func (err brokenRefError) Unwrap() error {+ return err.err
+}
--- a/refstore/files/errors.go
+++ /dev/null
@@ -1,16 +1,0 @@
-package files
-
-import "fmt"
-
-type brokenRefError struct {- name string
- err error
-}
-
-func (err brokenRefError) Error() string {- return fmt.Sprintf("refstore/files: broken reference %q: %v", err.name, err.err)-}
-
-func (err brokenRefError) Unwrap() error {- return err.err
-}
--- a/refstore/files/root_ref_path.go
+++ /dev/null
@@ -1,28 +1,0 @@
-package files
-
-import (
- "fmt"
- "strings"
-)
-
-type refPath struct {- root rootKind
- path string
-}
-
-func (tx *Transaction) targetKey(name refPath) string {- return fmt.Sprintf("%d:%s", name.root, name.path)-}
-
-func refPathFromKey(key string) refPath {- rootValue, pathValue, ok := strings.Cut(key, ":")
- if !ok || rootValue == "" {- return refPath{root: rootCommon, path: key}- }
-
- if rootValue == "0" {- return refPath{root: rootGit, path: pathValue}- }
-
- return refPath{root: rootCommon, path: pathValue}-}
--- a/refstore/files/transaction.go
+++ b/refstore/files/transaction.go
@@ -6,7 +6,7 @@
type Transaction struct {store *Store
- ops []txOp
+ ops []queuedUpdate
}
var _ refstore.Transaction = (*Transaction)(nil)
--- a/refstore/files/transaction_begin.go
+++ b/refstore/files/transaction_begin.go
@@ -8,6 +8,6 @@
func (store *Store) BeginTransaction() (refstore.Transaction, error) { return &Transaction{store: store,
- ops: make([]txOp, 0, 8),
+ ops: make([]queuedUpdate, 0, 8),
}, nil
}
--- a/refstore/files/transaction_cleanup.go
+++ /dev/null
@@ -1,39 +1,0 @@
-package files
-
-import (
- "errors"
- "os"
- "slices"
-)
-
-func (tx *Transaction) cleanup(prepared []preparedTxOp) 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, tx.targetKey(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)
-
- err := root.Remove(lockName)
- if err == nil || errors.Is(err, os.ErrNotExist) {- tx.tryRemoveEmptyParentPaths(lockPath.root, lockName)
-
- continue
- }
-
- if firstErr == nil {- firstErr = err
- }
- }
-
- return firstErr
-}
--- a/refstore/files/transaction_cleanup_parents.go
+++ /dev/null
@@ -1,35 +1,0 @@
-package files
-
-import (
- "errors"
- "os"
- "path"
-)
-
-func (tx *Transaction) tryRemoveEmptyParents(name string) {- loc := tx.store.loosePath(name)
- tx.tryRemoveEmptyParentPaths(loc.root, loc.path)
-}
-
-func (tx *Transaction) tryRemoveEmptyParentPaths(kind rootKind, name string) {- root := tx.store.rootFor(kind)
- dir := path.Dir(name)
-
- for dir != "." && dir != "/" {- err := root.Remove(dir)
- if err != nil {- if errors.Is(err, os.ErrNotExist) {- return
- }
-
- var pathErr *os.PathError
- if errors.As(err, &pathErr) {- return
- }
-
- return
- }
-
- dir = path.Dir(dir)
- }
-}
--- 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()
+ executor := &refUpdateExecutor{store: tx.store}+ prepared, err := executor.prepareUpdates(tx.ops)
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)
- 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)
}
--- a/refstore/files/transaction_dir_tree.go
+++ /dev/null
@@ -1,59 +1,0 @@
-package files
-
-import (
- "errors"
- "fmt"
- "os"
- "path"
-)
-
-func (tx *Transaction) removeEmptyDirTree(name refPath) error {- root := tx.store.rootFor(name.root)
-
- info, err := root.Stat(name.path)
- if err != nil {- if errors.Is(err, os.ErrNotExist) {- return nil
- }
-
- return err
- }
-
- if !info.IsDir() {- return nil
- }
-
- return tx.removeEmptyDirTreeRecursive(name)
-}
-
-func (tx *Transaction) removeEmptyDirTreeRecursive(name refPath) error {- root := tx.store.rootFor(name.root)
-
- dir, err := root.Open(name.path)
- if err != nil {- return err
- }
-
- entries, err := dir.ReadDir(-1)
- _ = dir.Close()
-
- if err != nil {- return err
- }
-
- for _, entry := range entries {- if !entry.IsDir() {- return fmt.Errorf("refstore/files: non-empty directory blocks reference %q", name.path)- }
-
- err = tx.removeEmptyDirTreeRecursive(refPath{- root: name.root,
- path: path.Join(name.path, entry.Name()),
- })
- if err != nil {- return err
- }
- }
-
- return root.Remove(name.path)
-}
--- a/refstore/files/transaction_direct_read.go
+++ /dev/null
@@ -1,76 +1,0 @@
-package files
-
-import (
- "errors"
- "fmt"
-
- "codeberg.org/lindenii/furgit/ref"
- "codeberg.org/lindenii/furgit/ref/refname"
- "codeberg.org/lindenii/furgit/refstore"
-)
-
-func (tx *Transaction) directRead(name string) (directRef, error) {- loc := tx.store.loosePath(name)
- hasPacked := false
-
- if loc.root == rootCommon && refname.ParseWorktree(name).Type == refname.WorktreeShared {- packed, packedErr := tx.store.readPackedRefs()
- if packedErr != nil {- return directRef{}, packedErr- }
-
- _, hasPacked = packed.byName[name]
- }
-
- loose, err := tx.store.readLooseRef(name)
- if err == nil {- switch loose := loose.(type) {- case ref.Detached:
- return directRef{- kind: directDetached,
- name: name,
- id: loose.ID,
- isLoose: true,
- isPacked: hasPacked,
- }, nil
- case ref.Symbolic:
- return directRef{- kind: directSymbolic,
- name: name,
- target: loose.Target,
- isLoose: true,
- isPacked: hasPacked,
- }, nil
- default:
- return directRef{}, 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)
- if statErr != nil || !info.IsDir() {- return directRef{}, err- }
- }
-
- if hasPacked {- packed, packedErr := tx.store.readPackedRefs()
- if packedErr != nil {- return directRef{}, packedErr- }
-
- detached := packed.byName[name]
-
- return directRef{- kind: directDetached,
- name: name,
- id: detached.ID,
- isPacked: true,
- }, nil
- }
-
- return directRef{- kind: directMissing,
- name: name,
- }, nil
-}
--- a/refstore/files/transaction_direct_ref.go
+++ /dev/null
@@ -1,20 +1,0 @@
-package files
-
-import "codeberg.org/lindenii/furgit/objectid"
-
-type directKind uint8
-
-const (
- directMissing directKind = iota
- directDetached
- directSymbolic
-)
-
-type directRef struct {- kind directKind
- name string
- id objectid.ObjectID
- target string
- isLoose bool
- isPacked bool
-}
--- a/refstore/files/transaction_kind.go
+++ /dev/null
@@ -1,14 +1,0 @@
-package files
-
-type txKind uint8
-
-const (
- txCreate txKind = iota
- txUpdate
- txDelete
- txVerify
- txCreateSymbolic
- txUpdateSymbolic
- txDeleteSymbolic
- txVerifySymbolic
-)
--- a/refstore/files/transaction_lock.go
+++ /dev/null
@@ -1,25 +1,0 @@
-package files
-
-import (
- "os"
- "path"
-)
-
-func (tx *Transaction) createLock(name refPath) error {- root := tx.store.rootFor(name.root)
- dir := path.Dir(name.path)
-
- if dir != "." {- err := root.MkdirAll(dir, 0o755)
- if err != nil {- return err
- }
- }
-
- file, err := root.OpenFile(name.path+".lock", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644)
- if err != nil {- return err
- }
-
- return file.Close()
-}
--- a/refstore/files/transaction_lock_packed.go
+++ /dev/null
@@ -1,44 +1,0 @@
-package files
-
-import (
- "errors"
- "os"
- "time"
-)
-
-func (tx *Transaction) createPackedLock(timeout time.Duration) error {- const (
- initialBackoffMs = 1
- backoffMaxMultiplier = 1000
- )
-
- deadline := time.Now().Add(timeout)
- multiplier := 1
- n := 1
-
- for {- file, err := tx.store.commonRoot.OpenFile("packed-refs.lock", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644)- if err == nil {- return file.Close()
- }
-
- if !errors.Is(err, os.ErrExist) {- return err
- }
-
- if timeout == 0 || (timeout > 0 && time.Now().After(deadline)) {- return err
- }
-
- backoffMs := multiplier * initialBackoffMs
- waitMs := (750 + tx.store.lockRand.Intn(500)) * backoffMs / 1000
- time.Sleep(time.Duration(waitMs) * time.Millisecond)
-
- multiplier += 2*n + 1
- if multiplier > backoffMaxMultiplier {- multiplier = backoffMaxMultiplier
- } else {- n++
- }
- }
-}
--- a/refstore/files/transaction_operation.go
+++ /dev/null
@@ -1,23 +1,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
-}
--- a/refstore/files/transaction_prepare.go
+++ /dev/null
@@ -1,102 +1,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
-}
--- 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
}
--- a/refstore/files/transaction_queue_ops.go
+++ b/refstore/files/transaction_queue_ops.go
@@ -3,33 +3,33 @@
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})}
--- a/refstore/files/transaction_resolve_target.go
+++ /dev/null
@@ -1,21 +1,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)- }
-}
--- a/refstore/files/transaction_resolve_target_ordinary.go
+++ /dev/null
@@ -1,46 +1,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)- }
- }
-}
--- a/refstore/files/transaction_validate.go
+++ /dev/null
@@ -1,65 +1,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
-}
--- a/refstore/files/transaction_verify_current.go
+++ /dev/null
@@ -1,53 +1,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)- }
-}
--- a/refstore/files/transaction_verify_refnames.go
+++ /dev/null
@@ -1,40 +1,0 @@
-package files
-
-import (
- "fmt"
- "strings"
-)
-
-func verifyRefnameAvailable(name string, existing map[string]struct{}, writes []string, deleted map[string]struct{}) error {- for existingName := range existing {- if existingName == name {- continue
- }
-
- if _, skip := deleted[existingName]; skip {- continue
- }
-
- if refnamesConflict(name, existingName) {- return fmt.Errorf("refstore/files: reference name conflict between %q and %q", name, existingName)- }
- }
-
- for _, other := range writes {- if other == name {- continue
- }
-
- if refnamesConflict(name, other) {- return fmt.Errorf("refstore/files: reference name conflict between %q and %q", name, other)- }
- }
-
- return nil
-}
-
-func refnamesConflict(left, right string) bool {- return left == right ||
- strings.HasPrefix(left, right+"/") ||
- strings.HasPrefix(right, left+"/")
-}
--- a/refstore/files/transaction_visible_names.go
+++ /dev/null
@@ -1,29 +1,0 @@
-package files
-
-func (tx *Transaction) visibleNames() (map[string]struct{}, error) {- names := make(map[string]struct{})-
- looseNames, err := tx.store.collectLooseRefNames()
- if err != nil {- return nil, err
- }
-
- for _, name := range looseNames {- names[name] = struct{}{}- }
-
- packed, err := tx.store.readPackedRefs()
- if err != nil {- return nil, err
- }
-
- for name := range packed.byName {- if _, exists := names[name]; exists {- continue
- }
-
- names[name] = struct{}{}- }
-
- return names, nil
-}
--- a/refstore/files/transaction_write_loose.go
+++ /dev/null
@@ -1,59 +1,0 @@
-package files
-
-import (
- "fmt"
- "os"
- "path"
- "strings"
-)
-
-func (tx *Transaction) writeLoose(item preparedTxOp) error {- root := tx.store.rootFor(item.target.loc.root)
- lockName := item.target.loc.path + ".lock"
-
- lock, err := root.OpenFile(lockName, os.O_WRONLY|os.O_TRUNC, 0o644)
- if err != nil {- return err
- }
-
- var content string
-
- switch item.op.kind {- case txCreate, txUpdate:
- content = item.op.newID.String() + "\n"
- case txCreateSymbolic, txUpdateSymbolic:
- content = "ref: " + strings.TrimSpace(item.op.newTarget) + "\n"
- case txDelete, txVerify, txDeleteSymbolic, txVerifySymbolic:
- default:
- _ = lock.Close()
-
- return fmt.Errorf("refstore/files: unsupported write operation %d", item.op.kind)- }
-
- _, err = lock.WriteString(content)
- if err != nil {- _ = lock.Close()
-
- return err
- }
-
- err = lock.Close()
- if err != nil {- return err
- }
-
- dir := path.Dir(item.target.loc.path)
- if dir != "." {- err = root.MkdirAll(dir, 0o755)
- if err != nil {- return err
- }
- }
-
- err = tx.removeEmptyDirTree(item.target.loc)
- if err != nil {- return err
- }
-
- return root.Rename(lockName, item.target.loc.path)
-}
--- a/refstore/files/transaction_write_packed_deltas.go
+++ /dev/null
@@ -1,98 +1,0 @@
-package files
-
-import (
- "errors"
- "os"
-)
-
-func (tx *Transaction) applyPackedDeletes(prepared []preparedTxOp) error {- _, err := tx.store.commonRoot.Stat("packed-refs.lock")- if err != nil {- if errors.Is(err, os.ErrNotExist) {- return nil
- }
-
- return err
- }
-
- packed, err := tx.store.readPackedRefs()
- if err != nil {- return err
- }
-
- deleted := make(map[string]struct{})- needed := false
-
- for _, item := range prepared {- if item.op.kind != txDelete && item.op.kind != txDeleteSymbolic {- continue
- }
-
- deleted[item.target.name] = struct{}{}- if item.target.ref.isPacked {- needed = true
- }
- }
-
- if !needed {- return nil
- }
-
- lock, err := tx.store.commonRoot.OpenFile("packed-refs.new", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644)- if err != nil {- return err
- }
-
- createdTemp := true
-
- defer func() {- if !createdTemp {- return
- }
-
- _ = tx.store.commonRoot.Remove("packed-refs.new")- }()
-
- _, err = lock.WriteString("# pack-refs with: peeled fully-peeled sorted\n")- if err != nil {- _ = lock.Close()
-
- return err
- }
-
- for _, entry := range packed.ordered {- if _, skip := deleted[entry.Name()]; skip {- continue
- }
-
- _, err = lock.WriteString(entry.ID.String() + " " + entry.Name() + "\n")
- if err != nil {- _ = lock.Close()
-
- return err
- }
-
- if entry.Peeled != nil {- _, err = lock.WriteString("^" + entry.Peeled.String() + "\n")- if err != nil {- _ = lock.Close()
-
- return err
- }
- }
- }
-
- err = lock.Close()
- if err != nil {- return err
- }
-
- err = tx.store.commonRoot.Rename("packed-refs.new", "packed-refs")- if err != nil {- return err
- }
-
- createdTemp = false
-
- return nil
-}
--- /dev/null
+++ b/refstore/files/update_cleanup.go
@@ -1,0 +1,39 @@
+package files
+
+import (
+ "errors"
+ "os"
+ "slices"
+)
+
+func (executor *refUpdateExecutor) cleanupPreparedUpdates(prepared []preparedUpdate) error {+ var firstErr error
+
+ lockNames := make([]string, 0, len(prepared)+1)
+ for _, item := range prepared {+ lockNames = append(lockNames, updateTargetKey(item.target.loc))
+ }
+
+ 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 := executor.store.rootFor(lockPath.root)
+
+ err := root.Remove(lockName)
+ if err == nil || errors.Is(err, os.ErrNotExist) {+ executor.tryRemoveEmptyParentPaths(lockPath.root, lockName)
+
+ continue
+ }
+
+ if firstErr == nil {+ firstErr = err
+ }
+ }
+
+ return firstErr
+}
--- /dev/null
+++ b/refstore/files/update_cleanup_parents.go
@@ -1,0 +1,35 @@
+package files
+
+import (
+ "errors"
+ "os"
+ "path"
+)
+
+func (executor *refUpdateExecutor) tryRemoveEmptyParents(name string) {+ loc := executor.store.loosePath(name)
+ executor.tryRemoveEmptyParentPaths(loc.root, loc.path)
+}
+
+func (executor *refUpdateExecutor) tryRemoveEmptyParentPaths(kind rootKind, name string) {+ root := executor.store.rootFor(kind)
+ dir := path.Dir(name)
+
+ for dir != "." && dir != "/" {+ err := root.Remove(dir)
+ if err != nil {+ if errors.Is(err, os.ErrNotExist) {+ return
+ }
+
+ var pathErr *os.PathError
+ if errors.As(err, &pathErr) {+ return
+ }
+
+ return
+ }
+
+ dir = path.Dir(dir)
+ }
+}
--- /dev/null
+++ b/refstore/files/update_commit.go
@@ -1,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)
+}
--- /dev/null
+++ b/refstore/files/update_commit_delete.go
@@ -1,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
+}
--- /dev/null
+++ b/refstore/files/update_dir_tree.go
@@ -1,0 +1,59 @@
+package files
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path"
+)
+
+func (executor *refUpdateExecutor) removeEmptyDirTree(name refPath) error {+ root := executor.store.rootFor(name.root)
+
+ info, err := root.Stat(name.path)
+ if err != nil {+ if errors.Is(err, os.ErrNotExist) {+ return nil
+ }
+
+ return err
+ }
+
+ if !info.IsDir() {+ return nil
+ }
+
+ return executor.removeEmptyDirTreeRecursive(name)
+}
+
+func (executor *refUpdateExecutor) removeEmptyDirTreeRecursive(name refPath) error {+ root := executor.store.rootFor(name.root)
+
+ dir, err := root.Open(name.path)
+ if err != nil {+ return err
+ }
+
+ entries, err := dir.ReadDir(-1)
+ _ = dir.Close()
+
+ if err != nil {+ return err
+ }
+
+ for _, entry := range entries {+ if !entry.IsDir() {+ return fmt.Errorf("refstore/files: non-empty directory blocks reference %q", name.path)+ }
+
+ err = executor.removeEmptyDirTreeRecursive(refPath{+ root: name.root,
+ path: path.Join(name.path, entry.Name()),
+ })
+ if err != nil {+ return err
+ }
+ }
+
+ return root.Remove(name.path)
+}
--- /dev/null
+++ b/refstore/files/update_direct_read.go
@@ -1,0 +1,76 @@
+package files
+
+import (
+ "errors"
+ "fmt"
+
+ "codeberg.org/lindenii/furgit/ref"
+ "codeberg.org/lindenii/furgit/ref/refname"
+ "codeberg.org/lindenii/furgit/refstore"
+)
+
+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 := executor.store.readPackedRefs()
+ if packedErr != nil {+ return directRefState{}, packedErr+ }
+
+ _, hasPacked = packed.byName[name]
+ }
+
+ loose, err := executor.store.readLooseRef(name)
+ if err == nil {+ switch loose := loose.(type) {+ case ref.Detached:
+ return directRefState{+ kind: directDetached,
+ name: name,
+ id: loose.ID,
+ isLoose: true,
+ isPacked: hasPacked,
+ }, nil
+ case ref.Symbolic:
+ return directRefState{+ kind: directSymbolic,
+ name: name,
+ target: loose.Target,
+ isLoose: true,
+ isPacked: hasPacked,
+ }, nil
+ default:
+ return directRefState{}, fmt.Errorf("refstore/files: unsupported reference type %T", loose)+ }
+ }
+
+ if !errors.Is(err, refstore.ErrReferenceNotFound) {+ info, statErr := executor.store.rootFor(loc.root).Stat(loc.path)
+ if statErr != nil || !info.IsDir() {+ return directRefState{}, err+ }
+ }
+
+ if hasPacked {+ packed, packedErr := executor.store.readPackedRefs()
+ if packedErr != nil {+ return directRefState{}, packedErr+ }
+
+ detached := packed.byName[name]
+
+ return directRefState{+ kind: directDetached,
+ name: name,
+ id: detached.ID,
+ isPacked: true,
+ }, nil
+ }
+
+ return directRefState{+ kind: directMissing,
+ name: name,
+ }, nil
+}
--- /dev/null
+++ b/refstore/files/update_direct_ref.go
@@ -1,0 +1,20 @@
+package files
+
+import "codeberg.org/lindenii/furgit/objectid"
+
+type directRefKind uint8
+
+const (
+ directMissing directRefKind = iota
+ directDetached
+ directSymbolic
+)
+
+type directRefState struct {+ kind directRefKind
+ name string
+ id objectid.ObjectID
+ target string
+ isLoose bool
+ isPacked bool
+}
--- /dev/null
+++ b/refstore/files/update_error.go
@@ -1,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}+}
--- /dev/null
+++ b/refstore/files/update_executor.go
@@ -1,0 +1,5 @@
+package files
+
+type refUpdateExecutor struct {+ store *Store
+}
--- /dev/null
+++ b/refstore/files/update_kind.go
@@ -1,0 +1,14 @@
+package files
+
+type updateKind uint8
+
+const (
+ updateCreate updateKind = iota
+ updateReplace
+ updateDelete
+ updateVerify
+ updateCreateSymbolic
+ updateReplaceSymbolic
+ updateDeleteSymbolic
+ updateVerifySymbolic
+)
--- /dev/null
+++ b/refstore/files/update_lock.go
@@ -1,0 +1,25 @@
+package files
+
+import (
+ "os"
+ "path"
+)
+
+func (executor *refUpdateExecutor) createUpdateLock(name refPath) error {+ root := executor.store.rootFor(name.root)
+ dir := path.Dir(name.path)
+
+ if dir != "." {+ err := root.MkdirAll(dir, 0o755)
+ if err != nil {+ return err
+ }
+ }
+
+ file, err := root.OpenFile(name.path+".lock", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644)
+ if err != nil {+ return err
+ }
+
+ return file.Close()
+}
--- /dev/null
+++ b/refstore/files/update_lock_packed.go
@@ -1,0 +1,44 @@
+package files
+
+import (
+ "errors"
+ "os"
+ "time"
+)
+
+func (executor *refUpdateExecutor) createPackedRefsLock(timeout time.Duration) error {+ const (
+ initialBackoffMs = 1
+ backoffMaxMultiplier = 1000
+ )
+
+ deadline := time.Now().Add(timeout)
+ multiplier := 1
+ n := 1
+
+ for {+ 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()
+ }
+
+ if !errors.Is(err, os.ErrExist) {+ return err
+ }
+
+ if timeout == 0 || (timeout > 0 && time.Now().After(deadline)) {+ return err
+ }
+
+ backoffMs := multiplier * initialBackoffMs
+ waitMs := (750 + executor.store.lockRand.Intn(500)) * backoffMs / 1000
+ time.Sleep(time.Duration(waitMs) * time.Millisecond)
+
+ multiplier += 2*n + 1
+ if multiplier > backoffMaxMultiplier {+ multiplier = backoffMaxMultiplier
+ } else {+ n++
+ }
+ }
+}
--- /dev/null
+++ b/refstore/files/update_operation_prepared.go
@@ -1,0 +1,6 @@
+package files
+
+type preparedUpdate struct {+ op queuedUpdate
+ target resolvedUpdateTarget
+}
--- /dev/null
+++ b/refstore/files/update_operation_queue.go
@@ -1,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
+}
--- /dev/null
+++ b/refstore/files/update_path.go
@@ -1,0 +1,28 @@
+package files
+
+import (
+ "fmt"
+ "strings"
+)
+
+type refPath struct {+ root rootKind
+ path string
+}
+
+func updateTargetKey(name refPath) string {+ return fmt.Sprintf("%d:%s", name.root, name.path)+}
+
+func refPathFromKey(key string) refPath {+ rootValue, pathValue, ok := strings.Cut(key, ":")
+ if !ok || rootValue == "" {+ return refPath{root: rootCommon, path: key}+ }
+
+ if rootValue == "0" {+ return refPath{root: rootGit, path: pathValue}+ }
+
+ return refPath{root: rootCommon, path: pathValue}+}
--- /dev/null
+++ b/refstore/files/update_prepare.go
@@ -1,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
+}
--- /dev/null
+++ b/refstore/files/update_prepare_lock.go
@@ -1,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
+}
--- /dev/null
+++ b/refstore/files/update_prepare_resolve.go
@@ -1,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
+}
--- /dev/null
+++ b/refstore/files/update_prepare_verify.go
@@ -1,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
+}
--- /dev/null
+++ b/refstore/files/update_resolve_target.go
@@ -1,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)+ }
+}
--- /dev/null
+++ b/refstore/files/update_resolve_target_ordinary.go
@@ -1,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)+ }
+ }
+}
--- /dev/null
+++ b/refstore/files/update_target_resolved.go
@@ -1,0 +1,7 @@
+package files
+
+type resolvedUpdateTarget struct {+ name string
+ loc refPath
+ ref directRefState
+}
--- /dev/null
+++ b/refstore/files/update_validate.go
@@ -1,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
+}
--- /dev/null
+++ b/refstore/files/update_verify_current.go
@@ -1,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
+}
--- /dev/null
+++ b/refstore/files/update_verify_refnames.go
@@ -1,0 +1,41 @@
+package files
+
+import (
+ "strings"
+
+ "codeberg.org/lindenii/furgit/refstore"
+)
+
+func verifyRefnameAvailable(name string, existing map[string]struct{}, writes []string, deleted map[string]struct{}) error {+ for existingName := range existing {+ if existingName == name {+ continue
+ }
+
+ if _, skip := deleted[existingName]; skip {+ continue
+ }
+
+ if refnamesConflict(name, existingName) {+ return wrapUpdateError(name, &refstore.NameConflictError{Other: existingName})+ }
+ }
+
+ for _, other := range writes {+ if other == name {+ continue
+ }
+
+ if refnamesConflict(name, other) {+ return wrapUpdateError(name, &refstore.NameConflictError{Other: other})+ }
+ }
+
+ return nil
+}
+
+func refnamesConflict(left, right string) bool {+ return left == right ||
+ strings.HasPrefix(left, right+"/") ||
+ strings.HasPrefix(right, left+"/")
+}
--- /dev/null
+++ b/refstore/files/update_visible_names.go
@@ -1,0 +1,29 @@
+package files
+
+func (executor *refUpdateExecutor) collectVisibleNames() (map[string]struct{}, error) {+ names := make(map[string]struct{})+
+ looseNames, err := executor.store.collectLooseRefNames()
+ if err != nil {+ return nil, err
+ }
+
+ for _, name := range looseNames {+ names[name] = struct{}{}+ }
+
+ packed, err := executor.store.readPackedRefs()
+ if err != nil {+ return nil, err
+ }
+
+ for name := range packed.byName {+ if _, exists := names[name]; exists {+ continue
+ }
+
+ names[name] = struct{}{}+ }
+
+ return names, nil
+}
--- /dev/null
+++ b/refstore/files/update_write_loose.go
@@ -1,0 +1,59 @@
+package files
+
+import (
+ "fmt"
+ "os"
+ "path"
+ "strings"
+)
+
+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)
+ if err != nil {+ return err
+ }
+
+ var content string
+
+ switch item.op.kind {+ case updateCreate, updateReplace:
+ content = item.op.newID.String() + "\n"
+ case updateCreateSymbolic, updateReplaceSymbolic:
+ content = "ref: " + strings.TrimSpace(item.op.newTarget) + "\n"
+ case updateDelete, updateVerify, updateDeleteSymbolic, updateVerifySymbolic:
+ default:
+ _ = lock.Close()
+
+ return fmt.Errorf("refstore/files: unsupported write operation %d", item.op.kind)+ }
+
+ _, err = lock.WriteString(content)
+ if err != nil {+ _ = lock.Close()
+
+ return err
+ }
+
+ err = lock.Close()
+ if err != nil {+ return err
+ }
+
+ dir := path.Dir(item.target.loc.path)
+ if dir != "." {+ err = root.MkdirAll(dir, 0o755)
+ if err != nil {+ return err
+ }
+ }
+
+ err = executor.removeEmptyDirTree(item.target.loc)
+ if err != nil {+ return err
+ }
+
+ return root.Rename(lockName, item.target.loc.path)
+}
--- /dev/null
+++ b/refstore/files/update_write_packed_refs.go
@@ -1,0 +1,98 @@
+package files
+
+import (
+ "errors"
+ "os"
+)
+
+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
+ }
+
+ return err
+ }
+
+ packed, err := executor.store.readPackedRefs()
+ if err != nil {+ return err
+ }
+
+ deleted := make(map[string]struct{})+ needed := false
+
+ for _, item := range prepared {+ if item.op.kind != updateDelete && item.op.kind != updateDeleteSymbolic {+ continue
+ }
+
+ deleted[item.target.name] = struct{}{}+ if item.target.ref.isPacked {+ needed = true
+ }
+ }
+
+ if !needed {+ return nil
+ }
+
+ lock, err := executor.store.commonRoot.OpenFile("packed-refs.new", os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644)+ if err != nil {+ return err
+ }
+
+ createdTemp := true
+
+ defer func() {+ if !createdTemp {+ return
+ }
+
+ _ = executor.store.commonRoot.Remove("packed-refs.new")+ }()
+
+ _, err = lock.WriteString("# pack-refs with: peeled fully-peeled sorted\n")+ if err != nil {+ _ = lock.Close()
+
+ return err
+ }
+
+ for _, entry := range packed.ordered {+ if _, skip := deleted[entry.Name()]; skip {+ continue
+ }
+
+ _, err = lock.WriteString(entry.ID.String() + " " + entry.Name() + "\n")
+ if err != nil {+ _ = lock.Close()
+
+ return err
+ }
+
+ if entry.Peeled != nil {+ _, err = lock.WriteString("^" + entry.Peeled.String() + "\n")+ if err != nil {+ _ = lock.Close()
+
+ return err
+ }
+ }
+ }
+
+ err = lock.Close()
+ if err != nil {+ return err
+ }
+
+ err = executor.store.commonRoot.Rename("packed-refs.new", "packed-refs")+ if err != nil {+ return err
+ }
+
+ createdTemp = false
+
+ return nil
+}
--- /dev/null
+++ b/refstore/update_errors.go
@@ -1,0 +1,110 @@
+package refstore
+
+import "fmt"
+
+// InvalidNameError indicates that one requested reference name is invalid.
+type InvalidNameError struct {+ Err error
+}
+
+func (err *InvalidNameError) Error() string {+ if err == nil || err.Err == nil {+ return "invalid reference name"
+ }
+
+ return fmt.Sprintf("invalid reference name: %v", err.Err)+}
+
+func (err *InvalidNameError) Unwrap() error {+ if err == nil {+ return nil
+ }
+
+ return err.Err
+}
+
+// InvalidValueError indicates that one requested reference value is invalid.
+type InvalidValueError struct {+ Err error
+}
+
+func (err *InvalidValueError) Error() string {+ if err == nil || err.Err == nil {+ return "invalid reference value"
+ }
+
+ return fmt.Sprintf("invalid reference value: %v", err.Err)+}
+
+func (err *InvalidValueError) Unwrap() error {+ if err == nil {+ return nil
+ }
+
+ return err.Err
+}
+
+// DuplicateUpdateError indicates that one batch or transaction includes a
+// duplicate update target.
+type DuplicateUpdateError struct{}+
+func (err *DuplicateUpdateError) Error() string {+ return "duplicate reference update"
+}
+
+// CreateExistsError indicates that one create operation targeted an existing
+// reference.
+type CreateExistsError struct{}+
+func (err *CreateExistsError) Error() string {+ return "reference already exists"
+}
+
+// IncorrectOldValueError indicates that one operation's expected old value did
+// not match the current reference value.
+type IncorrectOldValueError struct {+ Actual string
+ Expected string
+}
+
+func (err *IncorrectOldValueError) Error() string {+ if err == nil {+ return "incorrect old value provided"
+ }
+
+ if err.Actual == "" && err.Expected == "" {+ return "incorrect old value provided"
+ }
+
+ return fmt.Sprintf("incorrect old value provided: got %q, expected %q", err.Actual, err.Expected)+}
+
+// ExpectedDetachedError indicates that one operation required a detached
+// reference but found a different kind.
+type ExpectedDetachedError struct{}+
+func (err *ExpectedDetachedError) Error() string {+ return "expected detached reference"
+}
+
+// ExpectedSymbolicError indicates that one operation required a symbolic
+// reference but found a different kind.
+type ExpectedSymbolicError struct{}+
+func (err *ExpectedSymbolicError) Error() string {+ return "expected symbolic reference"
+}
+
+// NameConflictError indicates that one reference name conflicts with another
+// visible or queued reference name.
+type NameConflictError struct {+ Other string
+}
+
+func (err *NameConflictError) Error() string {+ if err == nil || err.Other == "" {+ return "reference name conflict"
+ }
+
+ return fmt.Sprintf("reference name conflict with %q", err.Other)+}
--
⑨