package memory
import (
"errors"
"fmt"
"strings"
objectid "codeberg.org/lindenii/furgit/object/id"
refname "codeberg.org/lindenii/furgit/ref/name"
refstore "codeberg.org/lindenii/furgit/ref/store"
)
type updateKind uint8
const (
updateCreate updateKind = iota
updateReplace
updateDelete
updateVerify
updateCreateSymbolic
updateReplaceSymbolic
updateDeleteSymbolic
updateVerifySymbolic
)
type queuedUpdate struct {
name string
kind updateKind
newID objectid.ObjectID
oldID objectid.ObjectID
newTarget string
oldTarget string
}
type resolvedUpdateTarget struct {
name string
ref storedRef
}
type preparedUpdate struct {
op queuedUpdate
target resolvedUpdateTarget
}
type updateContextError struct {
name string
err error
}
func (err *updateContextError) Error() string {
return fmt.Sprintf("refstore/memory: 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}
}
func 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.Algorithm().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.Algorithm().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/memory: 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
}
func prepareUpdates(refs map[string]storedRef, ops []queuedUpdate) ([]preparedUpdate, error) {
prepared, err := resolvePreparedUpdates(refs, ops)
if err != nil {
return prepared, err
}
deleted, written := collectPreparedWrites(prepared)
existing := collectVisibleNames(refs)
for _, name := range written {
err = verifyRefnameAvailable(name, existing, written, deleted)
if err != nil {
return prepared, err
}
}
err = verifyPreparedUpdates(refs, prepared)
if err != nil {
return prepared, err
}
return prepared, nil
}
func resolvePreparedUpdates(refs map[string]storedRef, ops []queuedUpdate) ([]preparedUpdate, error) {
prepared := make([]preparedUpdate, 0, len(ops))
targets := make(map[string]struct{}, len(ops))
for _, op := range ops {
target, err := resolveQueuedUpdateTarget(refs, op)
if err != nil {
return prepared, err
}
if _, exists := targets[target.name]; exists {
return prepared, wrapUpdateError(op.name, &refstore.DuplicateUpdateError{})
}
targets[target.name] = struct{}{}
prepared = append(prepared, preparedUpdate{op: op, target: target})
}
return prepared, nil
}
func resolveQueuedUpdateTarget(refs map[string]storedRef, op queuedUpdate) (resolvedUpdateTarget, error) {
switch op.kind {
case updateCreate:
return resolveOrdinaryTarget(refs, op.name, true)
case updateReplace, updateDelete, updateVerify:
return resolveOrdinaryTarget(refs, op.name, false)
case updateCreateSymbolic, updateReplaceSymbolic, updateDeleteSymbolic, updateVerifySymbolic:
return resolvedUpdateTarget{name: op.name, ref: directRead(refs, op.name)}, nil
default:
return resolvedUpdateTarget{}, fmt.Errorf("refstore/memory: unsupported update operation %d", op.kind)
}
}
func resolveOrdinaryTarget(refs map[string]storedRef, name string, allowMissing bool) (resolvedUpdateTarget, error) {
cur := name
seen := make(map[string]struct{})
for {
if _, ok := seen[cur]; ok {
return resolvedUpdateTarget{}, fmt.Errorf("refstore/memory: symbolic reference cycle at %q", cur)
}
seen[cur] = struct{}{}
refState := directRead(refs, cur)
switch refState.kind {
case storedMissing:
if !allowMissing {
return resolvedUpdateTarget{}, wrapUpdateError(name, refstore.ErrReferenceNotFound)
}
return resolvedUpdateTarget{name: cur, ref: refState}, nil
case storedDetached:
return resolvedUpdateTarget{name: cur, ref: refState}, nil
case storedSymbolic:
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/memory: unsupported stored reference kind %d", refState.kind)
}
}
}
func directRead(refs map[string]storedRef, name string) storedRef {
stored, ok := refs[name]
if !ok {
return storedRef{kind: storedMissing}
}
return cloneStoredRef(stored)
}
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
}
func collectVisibleNames(refs map[string]storedRef) map[string]struct{} {
names := make(map[string]struct{}, len(refs))
for name := range refs {
names[name] = struct{}{}
}
return names
}
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+"/")
}
func verifyPreparedUpdates(refs map[string]storedRef, prepared []preparedUpdate) error {
for i := range prepared {
item := &prepared[i]
item.target.ref = directRead(refs, item.target.name)
err := verifyPreparedUpdateCurrent(*item)
if err != nil {
return err
}
}
return nil
}
func verifyPreparedUpdateCurrent(item preparedUpdate) error {
switch item.op.kind {
case updateCreate:
if item.target.ref.kind != storedMissing {
return wrapUpdateError(item.op.name, &refstore.CreateExistsError{})
}
return nil
case updateReplace, updateDelete, updateVerify:
if item.target.ref.kind == storedMissing {
return wrapUpdateError(item.op.name, refstore.ErrReferenceNotFound)
}
if item.target.ref.kind != storedDetached {
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 != storedMissing {
return wrapUpdateError(item.op.name, &refstore.CreateExistsError{})
}
return nil
case updateReplaceSymbolic, updateDeleteSymbolic, updateVerifySymbolic:
if item.target.ref.kind == storedMissing {
return wrapUpdateError(item.op.name, refstore.ErrReferenceNotFound)
}
if item.target.ref.kind != storedSymbolic {
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
}
func applyPreparedUpdates(refs map[string]storedRef, prepared []preparedUpdate) {
for _, item := range prepared {
switch item.op.kind {
case updateCreate, updateReplace:
refs[item.target.name] = storedRef{kind: storedDetached, id: item.op.newID}
case updateCreateSymbolic, updateReplaceSymbolic:
refs[item.target.name] = storedRef{kind: storedSymbolic, target: strings.TrimSpace(item.op.newTarget)}
case updateDelete, updateDeleteSymbolic:
delete(refs, item.target.name)
case updateVerify, updateVerifySymbolic:
}
}
}
func isBatchRejected(err error) bool {
_, invalidName := errors.AsType[*refstore.InvalidNameError](err)
_, invalidValue := errors.AsType[*refstore.InvalidValueError](err)
_, duplicateUpdate := errors.AsType[*refstore.DuplicateUpdateError](err)
_, createExists := errors.AsType[*refstore.CreateExistsError](err)
_, incorrectOldValue := errors.AsType[*refstore.IncorrectOldValueError](err)
_, expectedDetached := errors.AsType[*refstore.ExpectedDetachedError](err)
_, expectedSymbolic := errors.AsType[*refstore.ExpectedSymbolicError](err)
_, nameConflict := errors.AsType[*refstore.NameConflictError](err)
return errors.Is(err, refstore.ErrReferenceNotFound) ||
invalidName ||
invalidValue ||
duplicateUpdate ||
createExists ||
incorrectOldValue ||
expectedDetached ||
expectedSymbolic ||
nameConflict
}
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
}
// TODO: these really shouldn't be used in the batched path, really...