RepositoryStateGuard.java

package com.taxonomy.workspace.service;

import com.taxonomy.dto.ProjectionState;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Component;

import java.util.ArrayList;
import java.util.List;
import com.taxonomy.versioning.service.ContextNavigationService;
import com.taxonomy.versioning.service.RepositoryStateService;

/**
 * Guards write operations by checking the current repository state.
 *
 * <p>Before committing, materializing, cherry-picking, or merging, this
 * component checks whether the operation is safe and returns warnings
 * or blocks. Also blocks writes when the current context is read-only.
 *
 * <p>All checks are workspace-aware: each user's operation state and
 * read-only status are independent.
 */
@Component
public class RepositoryStateGuard {

    private final RepositoryStateService stateService;
    private final @Nullable ContextNavigationService contextNavigationService;

    public RepositoryStateGuard(RepositoryStateService stateService,
                                @Nullable ContextNavigationService contextNavigationService) {
        this.stateService = stateService;
        this.contextNavigationService = contextNavigationService;
    }

    /**
     * Result of a write-operation safety check.
     *
     * @param allowed  whether the operation may proceed
     * @param warnings non-fatal issues the user should be aware of
     * @param blocks   fatal issues that prevent the operation
     */
    public record OperationCheck(
            boolean allowed,
            List<String> warnings,
            List<String> blocks
    ) {}

    /**
     * Check whether a write operation is safe for a specific user.
     *
     * @param username      the user attempting the operation
     * @param branch        the target branch
     * @param operationType the type of operation (e.g. "commit", "materialize", "cherry-pick", "merge")
     * @return the operation check result
     */
    public OperationCheck checkWriteOperation(String username, String branch, String operationType) {
        List<String> warnings = new ArrayList<>();
        List<String> blocks = new ArrayList<>();

        // Block if current context is read-only (per-user check)
        if (contextNavigationService != null && contextNavigationService.isReadOnly(username)) {
            blocks.add("Current context is read-only. Switch to an editable context before making changes.");
        }

        var state = stateService.getState(username, branch);

        // Block if an operation is already in progress (per-user check)
        if (state.operationInProgress()) {
            blocks.add("A " + state.operationKind() + " operation is already in progress. " +
                    "Complete or cancel it before starting a new operation.");
        }

        // Block if branch doesn't exist (for operations that require existing branch)
        if (!"commit".equals(operationType) && state.headCommit() == null) {
            blocks.add("Branch '" + branch + "' does not exist or has no commits.");
        }

        // Warn if projection is stale (per-user check)
        ProjectionState ps = stateService.getProjectionState(username, branch);
        if (ps.projectionStale()) {
            warnings.add("DB projection is stale — it was built from a different commit than HEAD. " +
                    "Consider re-materializing before this operation.");
        }

        // Warn if index is stale (per-user check)
        if (ps.indexStale()) {
            warnings.add("Search index is stale — search results may not reflect latest changes.");
        }

        boolean allowed = blocks.isEmpty();
        return new OperationCheck(allowed, warnings, blocks);
    }
}