aboutsummaryrefslogtreecommitdiff
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
}