package memory import ( objectid "codeberg.org/lindenii/furgit/object/id" refstore "codeberg.org/lindenii/furgit/ref/store" ) // Batch stages in-memory updates for one subset commit. type Batch struct { store *Store ops []queuedUpdate } var _ refstore.Batch = (*Batch)(nil) // BeginBatch creates one new in-memory batch. // //nolint:ireturn func (store *Store) BeginBatch() (refstore.Batch, error) { return &Batch{ store: store, ops: make([]queuedUpdate, 0, 8), }, nil } // Create queues a detached reference creation. func (batch *Batch) Create(name string, newID objectid.ObjectID) error { return batch.queue(queuedUpdate{name: name, kind: updateCreate, newID: newID}) } // Update queues a detached reference update. func (batch *Batch) Update(name string, newID, oldID objectid.ObjectID) error { return batch.queue(queuedUpdate{name: name, kind: updateReplace, newID: newID, oldID: oldID}) } // Delete queues a detached reference deletion. func (batch *Batch) Delete(name string, oldID objectid.ObjectID) error { return batch.queue(queuedUpdate{name: name, kind: updateDelete, oldID: oldID}) } // Verify queues a detached reference verification. func (batch *Batch) Verify(name string, oldID objectid.ObjectID) error { return batch.queue(queuedUpdate{name: name, kind: updateVerify, oldID: oldID}) } // CreateSymbolic queues a symbolic reference creation. func (batch *Batch) CreateSymbolic(name, newTarget string) error { return batch.queue(queuedUpdate{name: name, kind: updateCreateSymbolic, newTarget: newTarget}) } // UpdateSymbolic queues a symbolic reference update. func (batch *Batch) UpdateSymbolic(name, newTarget, oldTarget string) error { return batch.queue(queuedUpdate{name: name, kind: updateReplaceSymbolic, newTarget: newTarget, oldTarget: oldTarget}) } // DeleteSymbolic queues a symbolic reference deletion. func (batch *Batch) DeleteSymbolic(name, oldTarget string) error { return batch.queue(queuedUpdate{name: name, kind: updateDeleteSymbolic, oldTarget: oldTarget}) } // VerifySymbolic queues a symbolic reference verification. func (batch *Batch) VerifySymbolic(name, oldTarget string) error { return batch.queue(queuedUpdate{name: name, kind: updateVerifySymbolic, oldTarget: oldTarget}) } // Apply validates queued operations, // drops rejected operations, // and applies the remaining compatible set. // Concurrent readers observe // either the pre-Apply state // or the post-Apply state. func (batch *Batch) Apply() ([]refstore.BatchResult, error) { results := make([]refstore.BatchResult, 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)) batch.store.mu.Lock() defer batch.store.mu.Unlock() for i, op := range batch.ops { results[i].Name = op.name target, err := resolveQueuedUpdateTarget(batch.store.refs, 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].Status = refstore.BatchStatusNotAttempted results[j].Error = batchResultError(err) } return results, err } if _, exists := seenTargets[target.name]; exists { results[i].Status = refstore.BatchStatusRejected results[i].Error = &refstore.DuplicateUpdateError{} continue } seenTargets[target.name] = struct{}{} remainingIdx = append(remainingIdx, i) remainingOps = append(remainingOps, op) } for len(remainingOps) > 0 { prepared, err := prepareUpdates(batch.store.refs, remainingOps) if err == nil { next := cloneRefs(batch.store.refs) applyPreparedUpdates(next, prepared) batch.store.refs = next for _, idx := range remainingIdx { results[idx].Status = refstore.BatchStatusApplied } return results, nil } if !isBatchRejected(err) { 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 } name := batchResultName(err) rejectedAt := -1 for i, op := range remainingOps { if op.name == name { rejectedAt = i break } } if rejectedAt < 0 { for _, idx := range remainingIdx { results[idx].Status = refstore.BatchStatusNotAttempted results[idx].Error = batchResultError(err) } 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:]...) } return results, nil } // Abort abandons the batch. func (batch *Batch) Abort() error { return nil } func (batch *Batch) queue(op queuedUpdate) error { err := validateQueuedUpdate(op) if err != nil { return err } batch.ops = append(batch.ops, op) return nil }