GoalAuthoredChanges.java

package network.ike.plugin.ws;

import network.ike.plugin.ws.vcs.VcsOperations;
import org.apache.maven.api.plugin.Log;
import org.apache.maven.api.plugin.MojoException;

import java.io.File;
import java.util.ArrayList;
import java.util.List;

/**
 * Commit only the paths a goal itself authored, safely — the reusable
 * primitive behind the {@link AuthoredCommit#IN_ISOLATION} contract
 * (IKE-Network/ike-issues#780). Generalized from {@link PostMutationSync}'s
 * {@code workspace.yaml} self-commit (IKE-Network/ike-issues#774).
 *
 * <p>Usage: {@link #snapshot snapshot} the paths the goal is about to write,
 * <em>before</em> it writes them; let the goal run; then
 * {@link #commitAuthored commitAuthored}. Only paths that were unmodified
 * before the goal ran <em>and</em> were modified by it are committed — so a
 * caller's concurrent WIP, a {@code -DstagedOnly} subset, work syncing in over
 * Syncthing, or a not-yet-bootstrapped sibling's untracked state is never
 * swept in. The helper never runs {@code git add -A}; it commits exact paths
 * via {@link VcsOperations#commitPaths}.
 */
public final class GoalAuthoredChanges {

    private final File root;
    private final Log log;
    private final List<String> unmodifiedBefore;

    private GoalAuthoredChanges(File root, Log log,
                               List<String> unmodifiedBefore) {
        this.root = root;
        this.log = log;
        this.unmodifiedBefore = unmodifiedBefore;
    }

    /**
     * Snapshot, before the goal runs, which of {@code paths} are currently
     * unmodified in the working tree. Only those become eligible to be
     * committed in isolation later — a path that already carries a user or
     * caller edit is excluded, so a later commit can never sweep that edit in.
     *
     * @param root  the repository root directory
     * @param log   Maven logger
     * @param paths the paths the goal intends to author, relative to
     *              {@code root}
     * @return a snapshot to {@link #commitAuthored} after the goal has run
     */
    public static GoalAuthoredChanges snapshot(File root, Log log,
                                               String... paths) {
        List<String> unmodified = new ArrayList<>();
        for (String path : paths) {
            if (VcsOperations.isPathClean(root, path)) {
                unmodified.add(path);
            }
        }
        return new GoalAuthoredChanges(root, log, unmodified);
    }

    /**
     * Commit, in isolation, exactly the snapshotted paths that were unmodified
     * before the goal ran and have since been modified by it. A no-op
     * (returns {@code false}) when no such path exists. A commit failure is
     * logged at WARN and treated as a no-op rather than aborting the caller.
     *
     * @param message the commit message — a {@code <type>: <summary>} line
     *                plus a {@code Refs:} trailer; must not be {@code null} or
     *                blank
     * @return {@code true} if a commit was made
     */
    public boolean commitAuthored(String message) {
        List<String> authored = new ArrayList<>();
        for (String path : unmodifiedBefore) {
            if (!VcsOperations.isPathClean(root, path)) {
                authored.add(path);
            }
        }
        if (authored.isEmpty()) {
            return false;
        }
        try {
            VcsOperations.commitPaths(root, log, message,
                    authored.toArray(new String[0]));
            return true;
        } catch (MojoException e) {
            log.warn("  could not commit goal-authored change(s) " + authored
                    + " — " + e.getMessage());
            return false;
        }
    }
}