PushMojo.java
package network.ike.plugin.ws;
import network.ike.plugin.ws.vcs.VcsOperations;
import network.ike.plugin.ws.vcs.VcsState;
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.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* Push with a VCS bridge catch-up preamble.
*
* <p>When run from a workspace root (where {@code workspace.yaml} exists),
* iterates all subproject repositories in topological order and pushes each.
* When run from a single repository, operates on the current directory only.
*
* <p>Usage:
* <pre>{@code
* mvn ws:push
* }</pre>
*/
@Mojo(name = "push", projectRequired = false, aggregator = true)
public class PushMojo extends AbstractWorkspaceMojo {
/** Creates this goal instance. */
public PushMojo() {}
/**
* Remote name to push to.
*/
@Parameter(property = "remote", defaultValue = "origin")
String remote;
/**
* When {@code true}, the first push failure (e.g. non-fast-forward,
* authentication error) halts the goal with a {@link MojoException}
* instead of warning and continuing. Default {@code false} preserves
* the per-subproject best-effort behavior; {@link WsSyncMojo} sets
* this to {@code true} so a non-fast-forward leaves the workspace
* in a known state for the user to resolve, rather than appearing
* to succeed for some subprojects and silently failing for others.
*/
@Parameter(property = "failFast", defaultValue = "false")
boolean failFast;
@Override
protected WorkspaceReportSpec runGoal() throws MojoException {
if (isWorkspaceMode()) {
return executeWorkspace();
} else {
return executeSingleRepo(new File(System.getProperty("user.dir")));
}
}
private WorkspaceReportSpec executeWorkspace() throws MojoException {
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("Push"));
getLog().info("══════════════════════════════════════════════════════════════");
getLog().info("");
int pushed = 0;
int skipped = 0;
int failed = 0;
int uncommittedWarnings = 0;
// Push workspace root if it has a .git directory
if (new File(root, ".git").exists()) {
try {
VcsOperations.catchUp(root, getLog());
String branch = VcsOperations.currentBranch(root);
VcsOperations.push(root, getLog(), remote, branch);
VcsOperations.writeVcsState(root, VcsState.Action.PUSH);
getLog().info(Ansi.green(" ✓ ") + "workspace root → "
+ remote + "/" + branch);
pushed++;
if (!VcsOperations.isClean(root)) {
getLog().warn(Ansi.yellow(" ⚠ ") + "workspace root"
+ " — has uncommitted changes (not pushed)");
uncommittedWarnings++;
}
} catch (MojoException e) {
if (failFast) {
throw new MojoException("push failed at workspace root: "
+ e.getMessage(), e);
}
getLog().warn(Ansi.red(" ✗ ") + "workspace root — "
+ e.getMessage());
failed++;
}
}
for (String name : sorted) {
File dir = new File(root, name);
File gitDir = new File(dir, ".git");
if (!gitDir.exists()) {
getLog().debug(name + " — not cloned, skipping");
skipped++;
continue;
}
try {
VcsOperations.catchUp(dir, getLog());
String branch = VcsOperations.currentBranch(dir);
try {
VcsOperations.push(dir, getLog(), remote, branch);
} catch (MojoException e) {
// Handle missing upstream — retry with -u (#132)
if (e.getMessage() != null
&& e.getMessage().contains("has no upstream")) {
getLog().info(" " + name
+ " — setting upstream and pushing...");
VcsOperations.pushWithUpstream(
dir, getLog(), remote, branch);
} else {
throw e;
}
}
VcsOperations.writeVcsState(dir, VcsState.Action.PUSH);
getLog().info(Ansi.green(" ✓ ") + name + " → "
+ remote + "/" + branch);
pushed++;
// Warn if repo has uncommitted changes (#132)
if (!VcsOperations.isClean(dir)) {
getLog().warn(Ansi.yellow(" ⚠ ") + name
+ " — has uncommitted changes (not pushed)");
uncommittedWarnings++;
}
} catch (MojoException e) {
if (failFast) {
throw new MojoException("push failed at " + name + ": "
+ e.getMessage(), e);
}
getLog().warn(Ansi.red(" ✗ ") + name + " — "
+ e.getMessage());
failed++;
}
}
getLog().info("");
var summary = new StringBuilder();
summary.append(pushed).append(" pushed");
if (skipped > 0) {
summary.append(", ").append(skipped).append(" skipped");
}
if (uncommittedWarnings > 0) {
summary.append(", ").append(uncommittedWarnings)
.append(" with uncommitted changes");
}
if (failed > 0) {
summary.append(", ").append(failed).append(" failed");
}
getLog().info(" Done: " + summary);
getLog().info("");
if (failed > 0) {
getLog().warn(" Some pushes failed — check output above for details.");
}
return new WorkspaceReportSpec(WsGoal.PUSH, summary + "\n");
}
private WorkspaceReportSpec executeSingleRepo(File dir) throws MojoException {
getLog().info("");
getLog().info("IKE VCS Bridge — Push");
getLog().info("══════════════════════════════════════════════════════════════");
VcsOperations.catchUp(dir, getLog());
String branch = VcsOperations.currentBranch(dir);
getLog().info(" Pushing to " + remote + "/" + branch + "...");
VcsOperations.push(dir, getLog(), remote, branch);
VcsOperations.writeVcsState(dir, VcsState.Action.PUSH);
getLog().info("");
getLog().info(" Done.");
getLog().info("");
return new WorkspaceReportSpec(WsGoal.PUSH,
"Pushed `" + dir.getName() + "` to `" + remote + "/"
+ branch + "`.\n");
}
}