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...