PullWorkspaceMojo.java

package network.ike.plugin.ws;

import network.ike.plugin.ReleaseSupport;
import network.ike.plugin.ws.preflight.Preflight;
import network.ike.plugin.ws.preflight.PreflightCondition;
import network.ike.plugin.ws.preflight.PreflightContext;

import network.ike.workspace.Subproject;
import network.ike.workspace.WorkspaceGraph;
import org.apache.maven.api.plugin.MojoException;
import org.apache.maven.api.plugin.annotations.Mojo;

import java.io.File;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;

/**
 * Pull latest changes across the workspace.
 *
 * <p>When the workspace root is itself a git repository (i.e., it has a
 * {@code .git} directory), it is pulled first so any changes to the root
 * POM or {@code workspace.yaml} land before subproject operations run.
 * Runs {@code git pull --rebase} in each cloned subproject directory in
 * topological order (dependencies first). Uninitialized components are
 * skipped with a warning.
 *
 * <pre>{@code
 * mvn ws:pull
 * }</pre>
 */
@Mojo(name = "pull", projectRequired = false, aggregator = true)
public class PullWorkspaceMojo extends AbstractWorkspaceMojo {

    /** Creates this goal instance. */
    public PullWorkspaceMojo() {}

    @Override
    protected WorkspaceReportSpec runGoal() throws MojoException {
        WorkspaceGraph graph = loadGraph();
        File root = workspaceRoot();

        Set<String> targets = graph.manifest().subprojects().keySet();

        List<String> sorted = graph.topologicalSort(new LinkedHashSet<>(targets));

        // Preflight: all working trees must be clean (#132, #154)
        Preflight.of(
                List.of(PreflightCondition.WORKING_TREE_CLEAN),
                PreflightContext.of(root, graph, sorted))
                .requirePassed(WsGoal.PULL);

        getLog().info("");
        getLog().info(header("Pull"));
        getLog().info("══════════════════════════════════════════════════════════════");
        getLog().info("");

        int pulled = 0;
        int skipped = 0;
        int failed = 0;

        // Pull workspace root if it has a .git directory (#179). Must run
        // before subproject pulls so any update to the root POM or
        // workspace.yaml is observed by downstream steps.
        if (new File(root, ".git").exists()) {
            getLog().info(Ansi.cyan("  ↓ ") + "workspace root");
            try {
                ReleaseSupport.exec(root, getLog(),
                        "git", "pull", "--rebase");
                pulled++;
            } catch (MojoException e) {
                getLog().warn(Ansi.red("  ✗ ") + "workspace root — pull failed: "
                        + e.getMessage());
                failed++;
            }
        }

        for (String name : sorted) {
            File dir = new File(root, name);
            File gitDir = new File(dir, ".git");

            if (!gitDir.exists()) {
                getLog().info(Ansi.yellow("  ⚠ ") + name + " — not cloned, skipping");
                skipped++;
                continue;
            }

            getLog().info(Ansi.cyan("  ↓ ") + name);
            try {
                ReleaseSupport.exec(dir, getLog(),
                        "git", "pull", "--rebase");
                pulled++;
            } catch (MojoException e) {
                getLog().warn(Ansi.red("  ✗ ") + name + " — pull failed: " + e.getMessage());
                failed++;
            }
        }

        getLog().info("");
        getLog().info("  Done: " + pulled + " pulled, " + skipped
                + " skipped, " + failed + " failed");
        getLog().info("");

        if (failed > 0) {
            getLog().warn("  Some pulls failed — check output above for details.");
        }

        // Structured markdown report
        WorkspaceReportSpec spec = new WorkspaceReportSpec(WsGoal.PULL,
                pulled + " pulled, " + skipped
                + " skipped, " + failed + " failed.\n");

        PostMutationSync.refresh(root, getLog());
        return spec;
    }
}