WsRefreshMainMojo.java

package network.ike.plugin.ws;

import network.ike.plugin.support.GoalReportBuilder;
import network.ike.workspace.WorkspaceGraph;
import org.apache.maven.api.plugin.MojoException;
import org.apache.maven.api.plugin.annotations.Mojo;
import org.apache.maven.api.plugin.annotations.Parameter;

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

/**
 * Refresh local main from {@code origin/main} across the workspace.
 *
 * <p>For each subproject, this goal fetches origin and reconciles local
 * main with {@code origin/main}: fast-forward when behind, leave alone
 * when purely ahead (unpushed work), auto-resolve via merge when
 * diverged. The auto-resolve merge stays local until the user pushes
 * via {@code ws:push} or {@code ws:sync}.
 *
 * <p>Use this when you want main current across the workspace without
 * doing anything else &mdash; before review, before starting a feature,
 * after returning to a machine. The same logic runs as a precondition
 * inside {@code ws:update-feature-*}, {@code ws:feature-finish-*},
 * {@code ws:feature-start-*}, and {@code ws:sync}, so this goal is
 * mostly a convenience wrapper.
 *
 * <p>If the auto-resolve merge would produce file conflicts (the rare
 * "two machines edited the same file on main without push/pull" case),
 * the goal hard-errors with the conflict list. The working tree is not
 * touched.
 *
 * <pre>{@code
 * mvn ws:refresh-main                       # refresh "main" from origin
 * mvn ws:refresh-main -DmainBranch=trunk    # alternative branch name
 * mvn ws:refresh-main -Dremote=upstream     # alternative remote name
 * }</pre>
 *
 * <p>See ike-issues#284.
 */
@Mojo(name = "refresh-main", projectRequired = false, aggregator = true)
public class WsRefreshMainMojo extends AbstractWorkspaceMojo {

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

    /** The conceptual main branch to refresh. */
    @Parameter(property = "mainBranch", defaultValue = "main")
    String mainBranch;

    /** The remote to refresh from. */
    @Parameter(property = "remote",
            defaultValue = RefreshMainSupport.DEFAULT_REMOTE)
    String remote;

    @Override
    protected WorkspaceReportSpec runGoal() throws MojoException {
        if (!isWorkspaceMode()) {
            throw new MojoException(
                    "ws:refresh-main requires a workspace (workspace.yaml).");
        }

        WorkspaceGraph graph = loadGraph();
        File root = workspaceRoot();
        Set<String> targets = graph.manifest().subprojects().keySet();
        List<String> sorted = graph.topologicalSort(new LinkedHashSet<>(targets));

        getLog().info("");
        getLog().info(header("Refresh Main"));
        getLog().info("══════════════════════════════════════════════════════════════");
        getLog().info("  Branch:  " + mainBranch);
        getLog().info("  Remote:  " + remote);
        getLog().info("  Scope:   " + sorted.size() + " components");
        getLog().info("");

        List<RefreshMainSupport.Outcome> outcomes =
                RefreshMainSupport.refreshOrThrow(root, sorted, mainBranch, getLog());

        WorkspaceReportSpec spec = new WorkspaceReportSpec(
                WsGoal.REFRESH_MAIN, buildReport(outcomes));

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

    private String buildReport(List<RefreshMainSupport.Outcome> outcomes) {
        List<String[]> rows = new ArrayList<>();
        for (RefreshMainSupport.Outcome o : outcomes) {
            rows.add(new String[]{o.component(), outcomeLabel(o)});
        }
        GoalReportBuilder report = new GoalReportBuilder();
        report.paragraph("**Branch:** `" + mainBranch + "` ← `"
                        + remote + "/" + mainBranch + "`")
                .table(List.of("Subproject", "Outcome"), rows)
                .paragraph("**" + outcomes.size()
                        + "** subproject(s) refreshed.");
        return report.build();
    }

    private static String outcomeLabel(RefreshMainSupport.Outcome o) {
        return switch (o) {
            case RefreshMainSupport.Skipped(var c, var r) -> "skipped (" + r + ")";
            case RefreshMainSupport.UpToDate(var c) -> "up to date";
            case RefreshMainSupport.FastForwarded(var c, var n) ->
                    "fast-forwarded " + n + " commit" + (n == 1 ? "" : "s");
            case RefreshMainSupport.CreatedFromRemote(var c) -> "created from remote";
            case RefreshMainSupport.AheadOnly(var c, var n) ->
                    n + " unpushed commit" + (n == 1 ? "" : "s") + ", left as-is";
            case RefreshMainSupport.AutoResolved(var c, var local, var remoteCount) ->
                    "auto-resolved (kept " + local + " local, merged "
                            + remoteCount + " from remote)";
            case RefreshMainSupport.Conflicts(var c, var files) ->
                    files.size() + " conflict(s)";
        };
    }
}