StampRegistry.java
package network.ike.docs.plugin.diff;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* The packet's stamp set (ike-issues#656): one STAMP per (range commit,
* status) pair, plus the uncommitted pair for working-tree marks. Emits
* reusable AsciiDoc footnotes — every emission carries the full text,
* so document order never matters; Asciidoctor reuses the first
* definition per id — and accumulates the used stamps for the packet's
* Stamp Register table.
*
* <p>Status is stated redundantly in the endnote for STAMP consistency:
* <em>Active</em> for insertions and replacements (a replacement is a
* new Active version; its predecessor is covered by the successor's
* stamp), <em>Inactive</em> for unpaired deletions. The remaining
* coordinates map to the git era as: Time = author date, Author =
* commit author, Module = the topic's domain, Path = the branch.
*/
public final class StampRegistry {
private static final Pattern TRAILER = Pattern.compile(
"^(?:Refs|Fixes):\\s*(\\S+#\\d+)\\s*$", Pattern.MULTILINE);
/**
* Marked-region status, stated redundantly in each endnote. The
* vocabulary is the STAMP status vocabulary — closed. Insertions
* and replacements are Active (a replacement is a new Active
* version; the struck predecessor is covered by its successor's
* stamp); unpaired deletions are Inactive.
*/
public enum Status {
/** A new or replacing version of the text. */
ACTIVE("Active"),
/** An unpaired deletion — an inactivation. */
INACTIVE("Inactive");
private final String label;
Status(String label) {
this.label = label;
}
/**
* The label as printed in endnotes and the register.
*
* @return the human label
*/
public String label() {
return label;
}
}
/**
* One stamp as used somewhere in the packet.
*
* @param footnoteId the reusable footnote id
* @param status the redundant S coordinate
* @param time the T coordinate (author date or render note)
* @param author the A coordinate
* @param module the M coordinate (topic domain)
* @param branch the P coordinate (branch)
* @param commitId the abbreviated commit id, or {@code "worktree"}
* @param refs issue refs from the commit trailer, or empty
*/
public record Stamp(String footnoteId, Status status, String time, String author,
String module, String branch, String commitId, String refs) {
}
private final Map<String, GitSource.CommitMeta> commitsById = new LinkedHashMap<>();
private final Map<String, Stamp> used = new LinkedHashMap<>();
private final String branch;
private final String worktreeAuthor;
private final String renderDate;
private final String rangeLabel;
/**
* Create the registry for one packet.
*
* @param rangeCommits the compared range's commits
* @param branch the branch (STAMP path coordinate)
* @param worktreeAuthor author for uncommitted stamps
* @param renderDate date used for uncommitted stamps' time
* @param rangeLabel label for coarse attribution of unpaired
* deletions in committed ranges, e.g.
* {@code "9a958f7..HEAD"}
*/
public StampRegistry(List<GitSource.CommitMeta> rangeCommits, String branch,
String worktreeAuthor, String renderDate, String rangeLabel) {
rangeCommits.forEach(c -> commitsById.put(c.id(), c));
this.branch = branch;
this.worktreeAuthor = worktreeAuthor;
this.renderDate = renderDate;
this.rangeLabel = rangeLabel;
}
/**
* Whether the packet can carry more than one distinct stamp; when
* not, in-flow refs are suppressed and the cover states the single
* stamp once.
*
* @param hasUncommitted whether any compared content is uncommitted
* @return whether multiple stamps are possible
*/
public boolean multipleStampsPossible(boolean hasUncommitted) {
return commitsById.size() + (hasUncommitted ? 1 : 0) > 1;
}
/**
* Render the single stamp's coordinate line for the cover when
* in-flow refs are suppressed.
*
* @param module the module label to print
* @return the one-line stamp statement
*/
public String singleStampLine(String module) {
Stamp s = commitsById.isEmpty()
? stamp(GitSource.UNCOMMITTED, Status.ACTIVE, module)
: stamp(commitsById.keySet().iterator().next(), Status.ACTIVE, module);
return text(s);
}
/**
* The reusable footnote macro for one attribution + status. Every
* call emits the full text; Asciidoctor keys reuse on the id.
*
* @param attribution an abbreviated commit id or
* {@link GitSource#UNCOMMITTED}
* @param status the region's status
* @param module the topic's domain
* @return the {@code footnote:id[text]} macro
*/
public String footnote(String attribution, Status status, String module) {
Stamp s = stamp(attribution, status, module);
used.putIfAbsent(s.footnoteId(), s);
return "footnote:" + s.footnoteId() + "[" + text(s) + "]";
}
/**
* The stamps actually used in this packet, in first-use order.
*
* @return the used stamps
*/
public List<Stamp> usedStamps() {
return List.copyOf(used.values());
}
private Stamp stamp(String attribution, Status status, String module) {
if (attribution == null) {
// Unpaired deletion in a committed range: attribution is
// deliberately coarse in v1 (pickaxe search deferred, #656).
return new Stamp("stamp-range-" + suffix(status), status,
"within " + rangeLabel, "(range)", module, branch, "range", "");
}
if (GitSource.UNCOMMITTED.equals(attribution)) {
return new Stamp("stamp-wt-" + suffix(status), status,
"uncommitted, as of " + renderDate, worktreeAuthor,
module, branch, "worktree", "");
}
GitSource.CommitMeta c = commitsById.get(attribution);
String refs = "";
if (c != null) {
Matcher m = TRAILER.matcher(c.fullMessage());
refs = m.find() ? m.group(1) : "";
}
return new Stamp("stamp-" + attribution + "-" + suffix(status), status,
c == null ? "in range" : c.date(),
c == null ? "(range)" : c.author(),
module, branch, attribution, refs);
}
private static String suffix(Status status) {
return status == Status.ACTIVE ? "a" : "i";
}
private static String text(Stamp s) {
StringBuilder sb = new StringBuilder("STAMP ")
.append(s.status().label())
.append(" · ").append(s.time())
.append(" · ").append(s.author())
.append(" · ").append(s.module())
.append(" · ").append(s.branch())
.append(" · ").append(s.commitId());
if (!s.refs().isEmpty()) {
sb.append(" · ").append(s.refs());
}
return sb.toString();
}
}