SiblingRemoveDraftMojo.java

package network.ike.plugin.ws;

import network.ike.plugin.support.GoalReportBuilder;
import network.ike.workspace.WorkingSet;
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.List;

/**
 * Preview removing a sibling working set — the read-only counterpart of
 * {@link SiblingRemovePublishMojo} (IKE-Network/ike-issues#600).
 *
 * <p>The documented cleanup used to be {@code rm -rf}: correct, but
 * unguarded against exactly the things that die silently with a deleted
 * tree. This preview names them per member repository — uncommitted
 * changes, stashes, unlanded branches (squash-aware), bare member trees —
 * and reads the sibling's lease without acquiring anything. Every finding
 * comes with its remediation; {@code rm -rf} remains valid for the
 * I-know-what-I-am-doing case, and {@code -Dforce=true} is its
 * goal-shaped equivalent.
 *
 * <pre>{@code
 * mvn ws:sibling-remove-draft   -Dfeature=jira-456
 * mvn ws:sibling-remove-publish -Dfeature=jira-456
 * }</pre>
 *
 * @see SiblingRemovePublishMojo for the executing counterpart
 * @see SiblingListMojo for the inventory these previews start from
 */
@Mojo(name = "sibling-remove-draft", projectRequired = false, aggregator = true)
public class SiblingRemoveDraftMojo extends AbstractWorkspaceMojo {

    /** Feature whose sibling ({@code <primary>꞉<feature>}) to remove. */
    @Parameter(property = "feature")
    String feature;

    /** Explicit sibling directory name; alternative to {@code -Dfeature}. */
    @Parameter(property = "name")
    String name;

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

    @Override
    protected WorkspaceReportSpec runGoal() throws MojoException {
        WorkingSet workingSet = resolveWorkingSet();
        File primaryRoot = isWorkspaceMode()
                ? workspaceRoot()
                : workingSet.members().getFirst().directory().toFile();
        SiblingRemoval.Target target = SiblingRemoval.resolve(
                primaryRoot, workingSet.baseName(), feature, name);

        getLog().info("");
        getLog().info(header("Sibling Remove — DRAFT"));
        getLog().info("══════════════════════════════════════════════════════════════");
        getLog().info("  Sibling: " + target.sibling().root().getAbsolutePath());
        getLog().info("");

        boolean clean = SiblingRemoval.clean(target);
        String lease = WorkingSetLease.status(target.sibling().root().toPath())
                .map(status -> status.line()
                        + (status.liveElsewhere()
                                ? " — **removal will refuse; force does not "
                                        + "override a live lease**" : ""))
                .orElse("no lease machinery on this machine");

        GoalReportBuilder report = new GoalReportBuilder();
        report.paragraph("**Preview — nothing is deleted.**");
        SiblingRemoval.describeTarget(report, target);
        report.paragraph("**Lease:** " + lease);
        report.section("Preflight")
                .table(List.of("", "Member", "Finding"),
                        SiblingRemoval.preflightRows(target));
        report.paragraph(clean
                ? "Every member is clean and landed — `"
                        + WsGoal.SIBLING_REMOVE_PUBLISH.qualified()
                        + invocationSuffix() + "` will remove the sibling "
                        + "and garbage-collect its lease record."
                : "**Resolve the ✗ rows above** — finish the work "
                        + "(`ws:feature-finish-squash-publish` in the "
                        + "sibling), commit or drop stashes — **or pass "
                        + "`-Dforce=true`** to `"
                        + WsGoal.SIBLING_REMOVE_PUBLISH.qualified()
                        + "` to discard them deliberately. Force never "
                        + "overrides a lease held live elsewhere.");
        report.section("What removal does")
                .paragraph("Deletes the sibling directory (whole-working-set "
                        + "removal is the supported operation — the sync "
                        + "layer propagates it, staggered file versioning is "
                        + "the net) and deletes `leases/"
                        + target.sibling().name() + ".lease`, the record a "
                        + "working set leaves behind (ike-issues#1006's GC, "
                        + "done eagerly).");
        return new WorkspaceReportSpec(WsGoal.SIBLING_REMOVE_DRAFT,
                report.build());
    }

    private String invocationSuffix() {
        return name != null && !name.isBlank()
                ? " -Dname=" + name : " -Dfeature=" + feature;
    }
}