YamlDepsSync.java
package network.ike.plugin.ws;
import network.ike.workspace.Manifest;
import network.ike.workspace.ManifestException;
import network.ike.workspace.ManifestReader;
import network.ike.workspace.Subproject;
import org.apache.maven.api.plugin.Log;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Re-derive each subproject's {@code depends-on} edges from POM
* contents and rewrite {@code workspace.yaml} when they have drifted.
*
* <p><b>Why.</b> {@code ws:add} derives {@code depends-on} once at
* add time. POMs change every commit; without a periodic re-derive the
* YAML graph drifts from POM reality, and {@code ws:overview},
* {@code ws:release} topo-sort, and {@code ws:cascade} all use the
* stale graph silently. This sync runs as part of the post-mutation
* hook so any goal that touches the workspace also leaves the YAML
* matching the POMs.
*
* <p><b>Idempotent.</b> Same POMs in → same YAML out. Re-running
* back-to-back produces no further change.
*
* <p><b>Safety.</b> Only the {@code depends-on:} block is rewritten,
* one subproject at a time, via
* {@link WsAddMojo#rewriteDependsOnBlock}. All other YAML content
* (comments, defaults, branch fields, version fields) is preserved
* verbatim.
*
* <p>Subprojects that aren't cloned on disk are left untouched —
* we can't read the POM that drives the derivation.
*
* <p>See {@code IKE-Network/ike-issues#279}.
*/
final class YamlDepsSync {
private YamlDepsSync() {}
/**
* Refresh {@code depends-on} edges for the workspace at
* {@code workspaceRoot}.
*
* @param workspaceRoot the workspace root directory
* @param log plugin log for the per-subproject summary
*/
static void run(File workspaceRoot, Log log) {
Path manifestPath = workspaceRoot.toPath().resolve("workspace.yaml");
if (!Files.isRegularFile(manifestPath)) {
log.debug("yaml-deps-sync: no workspace.yaml — skipping");
return;
}
try {
Manifest manifest = ManifestReader.read(manifestPath);
String yaml = Files.readString(manifestPath, StandardCharsets.UTF_8);
String updated = yaml;
int totalAdded = 0;
int totalRemoved = 0;
for (Map.Entry<String, Subproject> entry
: manifest.subprojects().entrySet()) {
String name = entry.getKey();
Subproject sub = entry.getValue();
Path subDir = workspaceRoot.toPath().resolve(name);
if (!Files.exists(subDir.resolve("pom.xml"))) {
// Not cloned — leave existing depends-on alone
continue;
}
List<WsAddMojo.DerivedDep> derived =
WsAddMojo.deriveDependencies(
workspaceRoot.toPath(), manifestPath,
subDir, name);
if (derived == null) {
derived = List.of();
}
Set<String> currentDepNames = currentDependsOnNames(sub);
Set<String> newDepNames = new HashSet<>();
for (WsAddMojo.DerivedDep d : derived) {
newDepNames.add(d.subproject());
}
if (currentDepNames.equals(newDepNames)) continue;
int added = countOnlyIn(newDepNames, currentDepNames);
int removed = countOnlyIn(currentDepNames, newDepNames);
totalAdded += added;
totalRemoved += removed;
String before = updated;
updated = WsAddMojo.rewriteDependsOnBlock(
updated, name, derived);
if (before.equals(updated)) {
// Subproject not present in YAML in a recognizable
// form — likely a freshly added entry without a
// depends-on block yet. Skip rather than guess.
log.debug("yaml-deps-sync: " + name
+ " — could not locate depends-on block");
continue;
}
List<String> addedNames = new ArrayList<>(newDepNames);
addedNames.removeAll(currentDepNames);
List<String> removedNames = new ArrayList<>(currentDepNames);
removedNames.removeAll(newDepNames);
log.info(" workspace.yaml: " + name + " depends-on (+"
+ added + ", -" + removed + ")"
+ (addedNames.isEmpty() ? ""
: " added " + addedNames)
+ (removedNames.isEmpty() ? ""
: " removed " + removedNames));
}
if (!updated.equals(yaml)) {
Files.writeString(manifestPath, updated, StandardCharsets.UTF_8);
log.info(" yaml-deps-sync: " + totalAdded + " edge(s) added, "
+ totalRemoved + " edge(s) removed");
} else {
log.debug("yaml-deps-sync: workspace.yaml is up to date");
}
} catch (IOException | ManifestException e) {
log.warn("yaml-deps-sync: cannot update workspace.yaml — "
+ e.getMessage());
}
}
private static Set<String> currentDependsOnNames(Subproject sub) {
if (sub.dependsOn() == null) return Set.of();
Set<String> names = new HashSet<>();
for (var dep : sub.dependsOn()) {
names.add(dep.subproject());
}
return names;
}
private static int countOnlyIn(Set<String> a, Set<String> b) {
int count = 0;
for (String s : a) if (!b.contains(s)) count++;
return count;
}
}