TrackedTierHandler.java
package network.ike.plugin.scaffold;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.Arrays;
/**
* Tier handler for {@link ScaffoldTier#TRACKED}.
*
* <p>Policy: checksum-guarded whole-file management. Publish refreshes
* the file only when the on-disk content matches the lockfile's
* {@code applied-sha} from the last publish. If the user has edited
* the file, publish skips it and draft output surfaces a textual diff
* so the user can merge manually.
*
* <p>Produces:
* <ul>
* <li>{@link TierAction.UpToDate} when bytes equal the template;</li>
* <li>{@link TierAction.Write} with {@code INSTALL} when the file is
* absent and no prior entry exists;</li>
* <li>{@link TierAction.Write} with {@code UPDATE} when the file
* matches the prior {@code applied-sha} (safe refresh);</li>
* <li>{@link TierAction.Skip} when the file diverges from both the
* prior {@code applied-sha} and the new template.</li>
* </ul>
*/
public final class TrackedTierHandler implements TierHandler {
/**
* Construct a stateless tracked tier handler. Instances are safe
* to share across planning calls; all per-invocation state lives
* on method parameters.
*/
public TrackedTierHandler() {
}
@Override
public ScaffoldTier tier() {
return ScaffoldTier.TRACKED;
}
@Override
public TierAction plan(
ManifestEntry entry,
Path resolvedDest,
byte[] currentContent,
byte[] templateContent,
LockfileEntry priorEntry) {
if (templateContent == null) {
throw new ScaffoldException(
"tracked entry '" + entry.dest()
+ "' has no template content");
}
String templateSha = Sha256.of(templateContent);
// Absent on disk: install, unless we had a prior entry and the
// user has deliberately removed the file — but in that case we
// still install (publish is idempotent) and let the user delete
// again if they really meant it.
if (currentContent == null) {
return new TierAction.Write(
entry, resolvedDest, templateContent,
templateSha, templateSha,
TierAction.Write.Kind.INSTALL, "install");
}
// Already matches the template — nothing to do.
if (Arrays.equals(currentContent, templateContent)) {
return new TierAction.UpToDate(
entry, resolvedDest, templateSha, templateSha,
"up to date");
}
String currentSha = Sha256.of(currentContent);
// First ever publish (no prior entry): treat the existing file
// as user content and skip.
if (priorEntry == null) {
return skipDiverged(
entry, resolvedDest, currentContent, templateContent,
"no prior lockfile entry");
}
// Safe refresh: disk still matches what we wrote last time.
if (priorEntry.appliedSha() != null
&& priorEntry.appliedSha().equals(currentSha)) {
LineDiff.Counts c =
LineDiff.counts(str(currentContent),
str(templateContent));
return new TierAction.Write(
entry, resolvedDest, templateContent,
templateSha, templateSha,
TierAction.Write.Kind.UPDATE,
"refresh (" + c.shortForm() + ")");
}
// User has edited the file since last publish — skip with diff.
return skipDiverged(
entry, resolvedDest, currentContent, templateContent,
"user-edited");
}
private static TierAction.Skip skipDiverged(
ManifestEntry entry,
Path resolvedDest,
byte[] currentContent,
byte[] templateContent,
String why) {
String from = str(currentContent);
String to = str(templateContent);
LineDiff.Counts c = LineDiff.counts(from, to);
String diff = LineDiff.unified(from, to);
return new TierAction.Skip(
entry, resolvedDest,
why + "; " + c.shortForm(),
diff);
}
private static String str(byte[] bytes) {
return bytes == null
? ""
: new String(bytes, StandardCharsets.UTF_8);
}
}