AutoStashGuard.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;
/**
* Auto-stash a working tree's uncommitted changes before a VCS-state sync
* (pull / sync / refresh-main) and restore them after — so the SYNC goals run
* against a clean tree instead of refusing on in-flight work (#780). Reuses the
* per-user pushable-stash machinery {@link ParkSupport} extracted from
* {@code ws:switch}: WIP is stashed to {@code refs/ws-stash/<slug>/<branch>} on
* origin, then re-applied — so even a hard failure mid-sync never loses work.
*
* <p>Because the branch does not change across a pull/refresh, the stash is
* keyed on the current branch and restored onto that same (now-advanced)
* branch — a 3-way re-apply, the intended "keep my edits on top of what I
* pulled" outcome. This is what makes a Syncthing-bridged workspace's normal
* state (uncommitted edits in flight) compatible with a routine pull.
*
* <p>The slug is resolved lazily — only when a directory is actually dirty —
* so a clean sync never requires a configured {@code git user.email}.
*/
final class AutoStashGuard {
private AutoStashGuard() {}
/**
* Stash {@code dir}'s uncommitted changes (to the per-user ref for its
* current branch) when it is dirty, leaving the tree clean for the sync.
*
* @param dir the repository directory
* @param log Maven logger
* @return {@code true} if a stash was left — pass it to
* {@link #restoreIfStashed} after the sync
* @throws MojoException if the tree is dirty but {@code git user.email} is
* unset (the stash ref can't be keyed), or a stash
* step fails
*/
static boolean stashIfDirty(File dir, Log log) throws MojoException {
if (VcsOperations.isClean(dir)) {
return false;
}
String branch = VcsOperations.currentBranch(dir);
if (branch.isBlank()) {
// Detached HEAD (release-tag checkout, mid-bisect): there is no
// branch to key the per-user stash ref on, and the work is not on a
// shareable branch anyway. Skip auto-stash rather than build a
// malformed (trailing-slash) ref; leave the tree for the caller's
// own handling instead of failing the whole sync.
log.warn(" " + dir.getName() + ": detached HEAD — skipping "
+ "auto-stash (commit or branch the work first)");
return false;
}
ParkSupport.stashLeave(dir, log, slug(dir), branch);
return true;
}
/**
* Re-apply the stash left by {@link #stashIfDirty} onto the current branch
* (now advanced by the sync). A no-op when {@code stashed} is false.
*
* @param dir the repository directory
* @param log Maven logger
* @param stashed whether {@link #stashIfDirty} left a stash
* @throws MojoException if the re-apply or ref cleanup fails
*/
static void restoreIfStashed(File dir, Log log, boolean stashed)
throws MojoException {
if (!stashed) {
return;
}
ParkSupport.stashArrive(dir, log, slug(dir),
VcsOperations.currentBranch(dir));
}
/**
* Resolve the per-user stash slug for {@code dir} from its
* {@code git user.email}, failing loud with a friendly message rather than
* silently dropping WIP when the email is unset.
*/
private static String slug(File dir) throws MojoException {
try {
return VcsOperations.userSlug(VcsOperations.userEmail(dir));
} catch (MojoException e) {
throw new MojoException("Auto-stash needs a git user.email to key "
+ "the per-user stash ref so in-flight work is never lost. "
+ "Set it (git config user.email <you@example>), or commit "
+ "your changes first. Cause: " + e.getMessage(), e);
}
}
}