ParkSupport.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.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Comparator;
import java.util.stream.Stream;
/**
* Work-preserving park and per-user auto-stash primitives shared by the
* branch-scoped workspace goals (ike-issues#573, ike-issues#575).
*
* <p>"Parking" a subproject sets its clone aside without losing work: the
* subproject's branch is first pushed to {@value #STASH_REMOTE} (origin is the
* park), and only then is the local clone removed. If the push fails the park
* <b>aborts in place</b> — the clone is never reduced below what is on origin.
* "Stashing" moves uncommitted work (including untracked files) to a pushable
* per-user custom ref ({@code refs/ws-stash/<user-slug>/<branch>}) on origin so
* it follows the developer across branches and machines, and re-applies it on
* return.
*
* <p>These primitives were extracted verbatim from {@code WsSwitchDraftMojo}
* (where {@code ws:switch} introduced them in ike-issues#573) so {@code ws:remove}
* can reuse them, taking an explicit {@link Log} rather than reaching for a
* mojo's {@code getLog()}. The extraction is behavior-preserving for
* {@code ws:switch}.
*/
final class ParkSupport {
/** Full ref prefix for auto-stash refs (see ike-issues#153). */
static final String STASH_REF_PREFIX = "refs/ws-stash/";
/** Remote name for auto-stash and park push/fetch/delete. */
static final String STASH_REMOTE = "origin";
private ParkSupport() {
}
/**
* Build the auto-stash ref path for a given user slug and branch.
* Branches with {@code /} in their name (e.g. {@code feature/A})
* become multi-segment ref paths, which git supports.
*
* @param slug user slug from {@link VcsOperations#userSlug(String)}
* @param branch the branch name
* @return full ref path, e.g.
* {@code "refs/ws-stash/kec--knowledge-design/feature/A"}
*/
static String stashRef(String slug, String branch) {
return STASH_REF_PREFIX + slug + "/" + branch;
}
/**
* Park a subproject that is not a member of the target branch (#573):
* push its branch to {@value #STASH_REMOTE} so no work is lost, then
* remove the local clone. Aborts in place (no deletion) if the push
* fails — the working tree is never reduced below what is on origin.
*
* @param dir the subproject directory
* @param log Maven logger
* @param name the subproject name
* @param compBranch the branch the subproject is currently on
* @throws MojoException if the branch cannot be pushed (park aborts)
*/
static void parkSubproject(File dir, Log log, String name, String compBranch)
throws MojoException {
try {
VcsOperations.push(dir, log, STASH_REMOTE, compBranch);
} catch (MojoException e) {
throw new MojoException("Park aborted for '" + name + "': could not "
+ "push '" + compBranch + "' to " + STASH_REMOTE
+ " — refusing to remove a clone whose work is not on "
+ "origin. " + e.getMessage(), e);
}
log.info(" " + Ansi.cyan("⇲ ") + name + " — parked ("
+ compBranch + " → " + STASH_REMOTE + ", clone removed)");
deleteDirectory(dir.toPath());
}
/**
* Execute the leave flow on a subproject with uncommitted work:
* stash (including untracked), move stash to custom ref, drop local
* stash entry, push ref to origin. A collision on the source ref is
* detected at preflight; hitting it here means state changed between
* preflight and execute (racy), so fail loudly.
*
* @param dir the subproject directory
* @param log Maven logger
* @param slug user slug
* @param sourceBranch the branch we're leaving
* @throws MojoException if any step fails
*/
static void stashLeave(File dir, Log log, String slug, String sourceBranch)
throws MojoException {
String ref = stashRef(slug, sourceBranch);
String message = "ws-auto/" + sourceBranch;
VcsOperations.stashPushUntracked(dir, log, message);
VcsOperations.updateRef(dir, log, ref, "refs/stash");
// Push the per-user ref to origin BEFORE dropping the local stash, so a
// push failure (offline / no origin write) leaves the work recoverable
// as a normal local stash entry rather than stranded in a local-only
// ref (IKE-Network/ike-issues#781).
VcsOperations.pushRef(dir, log, STASH_REMOTE, ref);
VcsOperations.stashDrop(dir, log);
log.info(" " + Ansi.yellow("↟ ") + "stashed → " + ref);
}
/**
* Execute the arrive flow on a subproject that's just checked out
* the target branch: probe for a remote stash ref for this
* user/branch; if present, fetch it, apply it, and delete the ref
* locally and remotely.
*
* @param dir the subproject directory
* @param log Maven logger
* @param slug user slug
* @param targetBranch the branch we just switched to
* @return {@code true} if a stash was applied, {@code false} if no
* stash was present
* @throws MojoException if the apply or cleanup fails
*/
static boolean stashArrive(File dir, Log log, String slug, String targetBranch)
throws MojoException {
String ref = stashRef(slug, targetBranch);
boolean present;
try {
present = VcsOperations.remoteRefExists(dir, STASH_REMOTE, ref);
} catch (MojoException e) {
log.warn(" " + Ansi.yellow("⚠ ") + "could not probe "
+ STASH_REMOTE + " for " + ref + " — " + e.getMessage());
return false;
}
if (!present) return false;
VcsOperations.fetchRef(dir, log, STASH_REMOTE, ref);
VcsOperations.stashApply(dir, log, ref);
VcsOperations.deleteLocalRef(dir, log, ref);
VcsOperations.deleteRemoteRef(dir, log, STASH_REMOTE, ref);
log.info(" " + Ansi.green("↡ ") + "stash applied from " + ref);
return true;
}
/**
* Recursively delete a directory tree (removes a parked clone).
*
* @param dir the directory to delete
* @throws MojoException if deletion fails
*/
static void deleteDirectory(Path dir) throws MojoException {
try (Stream<Path> paths = Files.walk(dir)) {
paths.sorted(Comparator.reverseOrder())
.forEach(p -> {
try {
Files.delete(p);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
} catch (IOException | RuntimeException e) {
throw new MojoException(
"Failed to delete " + dir + ": " + e.getMessage(), e);
}
}
}