Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ and this project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html)

## [Unreleased]

### Added

- **`Promote.preview`:** every file that differs between two projects, as the target has it and as
a promotion writes it (the source's logic, the target's values), for a review that shows both
sides and lets a person merge line by line. DirXMLDevWeb's *Compare with project…* uses it.

## [0.53.0] - 2026-10-26

### Added
Expand Down
88 changes: 74 additions & 14 deletions src/main/java/com/pointblue/dirxml/dev/promote/Promote.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.stream.Stream;

Expand Down Expand Up @@ -248,17 +249,7 @@ static Result run(Path target, Path source, String pairName, Selection sel, bool
r.refusal = "the promoted tree can't be read: " + e.getMessage();
return r;
}
// values: the target's, but for the ones chosen
for (Map.Entry<String, String> e : s.targetValues.entrySet()) {
Overrides.Target t = Overrides.resolve(after, e.getKey());
if (!t.resolved()) {
continue;
}
String want = values.contains(e.getKey()) ? s.sourceValues.get(e.getKey()) : e.getValue();
if (!Objects.equals(t.value(), want)) {
t.set(want == null && t.definition != null ? "" : want);
}
}
restoreValues(after, s, values);

AsCodeWriter.write(after, out);
Map<String, byte[]> produced = Sides.read(out);
Expand Down Expand Up @@ -358,16 +349,85 @@ static Result run(Path target, Path source, String pairName, Selection sel, bool
}
}

/** The target's values back in a promoted model, but for the ones chosen, which take the source's. */
private static void restoreValues(DriverSet after, Sides s, Set<String> chosen) {
for (Map.Entry<String, String> e : s.targetValues.entrySet()) {
Overrides.Target t = Overrides.resolve(after, e.getKey());
if (!t.resolved()) {
continue;
}
String want = chosen.contains(e.getKey()) ? s.sourceValues.get(e.getKey()) : e.getValue();
if (!Objects.equals(t.value(), want)) {
t.set(want == null && t.definition != null ? "" : want);
}
}
}

/** A comparison with, for each differing file, its bytes in the target now and once promoted (null: that side has none). */
public static final class Preview {
public final ProjectCompare compare;
public final Map<String, byte[]> before = new TreeMap<>();
public final Map<String, byte[]> after = new TreeMap<>();

Preview(ProjectCompare compare) {
this.compare = compare;
}
}

/**
* What a review shows: every differing file as the target has it and as a promotion of it writes it, the source's
* logic with the target's values (no value chosen). A file whose promotion depends on its manifest is shown as the
* whole of the source's files promoted together write it.
*/
public static Preview preview(Path target, Path source, String pairName) throws IOException {
Pair pair = Pair.read(target, pairName);
Sides s = Sides.read(target, source, pair.map);
Preview pv = new Preview(ProjectCompare.of(s, pair));
Path sourceWritten = Files.createTempDirectory("idm-promote-src");
Path work = Files.createTempDirectory("idm-promote-work");
try {
AsCodeWriter.write(s.source, sourceWritten);
copyTree(target, work);
for (ProjectCompare.Item i : pv.compare.items) {
if (i.value) {
continue;
}
Path t = target.resolve(i.path);
pv.before.put(i.path, Files.isRegularFile(t) ? Files.readAllBytes(t) : null);
Path from = sourceWritten.resolve(i.path);
Path w = work.resolve(i.path);
if (Files.isRegularFile(from)) {
Files.createDirectories(w.getParent());
Files.copy(from, w, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
} else {
Files.deleteIfExists(w);
}
}
DriverSet after = AsCodeReader.read(work);
restoreValues(after, s, Set.of());
Map<String, byte[]> produced = Sides.written(after);
for (ProjectCompare.Item i : pv.compare.items) {
if (!i.value) {
pv.after.put(i.path, produced.get(i.path));
}
}
return pv;
} finally {
Sides.delete(sourceWritten);
Sides.delete(work);
}
}

/** The manifest that lists a tree file. */
static String manifestOf(String path) {
String[] parts = path.split("/");
if (parts.length >= 3 && parts[0].equals("drivers")) {
if (parts.length >= 3 && parts[0].equals("drivers") && !(parts.length == 3 && parts[2].equals(AsCodeWriter.DRIVER_MANIFEST))) {
return "drivers/" + parts[1] + "/" + AsCodeWriter.DRIVER_MANIFEST;
}
if (parts.length >= 2 && parts[0].equals("library")) {
if (parts.length >= 2 && parts[0].equals("library") && !path.equals("library/" + AsCodeWriter.LIBRARY_MANIFEST)) {
return "library/" + AsCodeWriter.LIBRARY_MANIFEST;
}
return AsCodeWriter.DRIVERSET_MANIFEST;
return AsCodeWriter.DRIVERSET_MANIFEST; // the drivers, the Library and the set's own objects
}

private static List<Finding> newErrors(Report before, Report after) {
Expand Down
31 changes: 31 additions & 0 deletions src/test/java/com/pointblue/dirxml/dev/promote/PromoteTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,37 @@ public void aMergedFileIsRecordedAsTheTargetHasIt() throws Exception {
assertEquals("still differs, but as the promotion left it", ProjectCompare.Bucket.UNCHANGED, compare().item(POLICY).bucket);
}

@Test
public void aNewDriversManifestIsListedByTheDriverSet() {
assertEquals("drivers/AD/driver.xml", Promote.manifestOf("drivers/AD/subscriber/sub-ctp.policy.xml"));
assertEquals("drivers/AD/driver.xml", Promote.manifestOf("drivers/AD/config-values.xml"));
assertEquals("driverset.xml", Promote.manifestOf("drivers/AD/driver.xml"));
assertEquals("library/library.xml", Promote.manifestOf("library/lib-shared.policy.xml"));
assertEquals("driverset.xml", Promote.manifestOf("library/library.xml"));
assertEquals("driverset.xml", Promote.manifestOf("jobs/StatisticsJob.xml"));
}

@Test
public void aPreviewShowsEachFileAsAPromotionWritesIt() throws Exception {
setUp();
edit(qaTree, ds -> {
setPolicy(ds, "AD QA", "sub-ctp", "qa changed it");
Driver d = ds.driver("AD QA");
d.subscriber.policies.add(new Policy("sub-new", Scope.SUBSCRIBER, "AD QA", ValidatorTest.xml(rule("new"))));
d.links.add(new PolicyLink(PolicySet.SUB_COMMAND, "drivers/AD QA/subscriber/sub-new", 1));
});
Promote.Preview pv = Promote.preview(prodTree, qaTree, "acme-qa");
String added = "drivers/AD/subscriber/sub-new.policy.xml";
assertNull("not in the target yet", pv.before.get(added));
assertTrue(new String(pv.after.get(added), StandardCharsets.UTF_8).contains("new"));
assertTrue(new String(pv.before.get(POLICY), StandardCharsets.UTF_8).contains("r1"));
assertTrue(new String(pv.after.get(POLICY), StandardCharsets.UTF_8).contains("qa changed it"));
String manifest = new String(pv.after.get(MANIFEST), StandardCharsets.UTF_8);
assertTrue(manifest, manifest.contains("sub-new") && manifest.contains("cn=AD,cn=dvs,o=system"));
assertFalse("the value stays the target's: config-values.xml doesn't differ", pv.after.containsKey("drivers/AD/config-values.xml"));
assertFalse("a preview writes nothing", Files.exists(prodTree.resolve(added)));
}

@Test
public void aPromotionThatWouldNotValidateIsRefused() throws Exception {
setUp();
Expand Down
Loading