FinishLanding.java
package network.ike.plugin.ws;
import org.apache.maven.api.plugin.Log;
import java.io.File;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
/**
* Where a finished feature <em>lands</em> once the working set has
* squash- or merge-landed it on its local target branch — the one phase
* that differs between a remote-origin working set and a local-origin
* sibling (IKE-Network/ike-issues#992).
*
* <ul>
* <li><strong>Remote origin</strong> (a primary, or a sibling created
* before #992): verified pushes of the target branch to
* {@code origin}, the {@code #858} contract — no feature branch is
* deleted until every member's push is confirmed.</li>
* <li><strong>Local origin</strong> (a #992 sibling, whose origin is
* its parent's member path): the parent absorbs the landing by
* fast-forward, and nothing is pushed anywhere. Externalization is
* the parent's own explicit {@code ws:push}.</li>
* </ul>
*
* <p>Either way the caller sees one uniform result: whether the landing
* succeeded, and the message to fail with when it did not. Feature-branch
* deletion gates on {@link Landing#failed()} in both models, so the
* no-stranded-landing invariant holds with the parent playing the role
* the remote plays for a primary.
*/
final class FinishLanding {
private FinishLanding() {}
/**
* The outcome of the landing phase.
*
* @param pushed per-member push results (remote-origin model;
* empty under the local-origin model)
* @param absorbed per-member parent results (local-origin model;
* empty under the remote-origin model)
* @param localParent the parent absorbed into, when this working
* set is a local-origin sibling
* @param failureMessage the message to fail the goal with, or
* {@code null} when the landing succeeded
*/
record Landing(List<FeatureFinishSupport.PushResult> pushed,
List<SiblingFinish.Absorbed> absorbed,
Optional<File> localParent,
String failureMessage) {
/** Whether the landing left work stranded. */
boolean failed() {
return failureMessage != null;
}
/** Whether this working set landed into a local parent (#992). */
boolean toLocalParent() {
return localParent.isPresent();
}
/** A landing that did not run at all (draft, or nothing landed). */
static Landing none(File root) {
return new Landing(List.of(), List.of(),
SiblingFinish.localParent(root), null);
}
}
/**
* Run the landing phase for {@code members} plus the workspace root.
*
* @param root the working set root the finish ran in
* @param members member names whose target branch must land
* @param targetBranch the branch finished into (e.g. {@code main})
* @param push the {@code -Dpush} flag: {@code false} keeps the
* landing local — no push, and no parent absorb —
* which in both models also keeps every feature
* branch
* @param log Maven logger
* @return the uniform landing result
*/
static Landing land(File root, List<String> members, String targetBranch,
boolean push, Log log) {
Optional<File> localParent = SiblingFinish.localParent(root);
if (!push) {
return new Landing(List.of(), List.of(), localParent, null);
}
if (localParent.isPresent()) {
File parent = localParent.get();
List<SiblingFinish.Absorbed> absorbed =
SiblingFinish.absorbIntoParent(root, parent, members,
targetBranch, log);
List<SiblingFinish.Absorbed> failures =
absorbed.stream().filter(a -> !a.ok()).toList();
return new Landing(List.of(), absorbed, localParent,
failures.isEmpty() ? null
: SiblingFinish.absorbFailureMessage(
failures, targetBranch, parent));
}
log.info("");
log.info(" " + Ansi.cyan("→ ") + "Pushing " + targetBranch
+ " for every member (verified)...");
Map<String, File> memberDirs = new LinkedHashMap<>();
for (String name : members) {
memberDirs.put(name, new File(root, name));
}
memberDirs.put(RefreshMainSupport.ROOT_LABEL, root);
List<FeatureFinishSupport.PushResult> pushed =
FeatureFinishSupport.pushTargetsVerified(
memberDirs, targetBranch, log);
List<FeatureFinishSupport.PushResult> failures =
pushed.stream().filter(r -> !r.pushed()).toList();
return new Landing(pushed, List.of(), localParent,
failures.isEmpty() ? null
: FeatureFinishSupport.pushPhaseFailureMessage(
failures, targetBranch));
}
/**
* Preview line for the draft goals: what the landing phase would do.
*
* @param root the working set root
* @param targetBranch the branch that would be finished into
* @param log Maven logger
*/
static void previewLanding(File root, String targetBranch, Log log) {
Optional<File> localParent = SiblingFinish.localParent(root);
if (localParent.isPresent()) {
log.info(" Landing: " + targetBranch + " fast-forwards into the"
+ " parent working set '" + localParent.get().getName()
+ "' — nothing is pushed (ike-issues#992). Publish from"
+ " the parent with " + WsGoal.PUSH.qualified() + ".");
return;
}
FeatureFinishSupport.deriveParentWorkspace(root).ifPresent(
parent -> log.info(" Parent workspace '" + parent.getName()
+ "' will be fast-forwarded after pushes confirm"
+ " (-DsyncParent=false to skip)."));
}
}