Skip to content

Commit

Permalink
Update build-diff script to also accept a URL (#1264)
Browse files Browse the repository at this point in the history
This update makes the `src/build-diff.js` script accept the canonical URL of a
spec as parameter, and build the updates to `index.json` that this URL would
trigger if it were added to `specs.json` (or if it would replace the
corresponding entry in `specs.json`).

The script still accepts commit references, although the syntax was slightly
updated to get back to one parameter and better align with `git diff`, from
`node src/build-diff HEAD HEAD~2` to `node src/build-diff HEAD..HEAD~2`.

The script is now also available as a module and the exported function also
accepts additional spec properties on top of the URL. That is not yet used but
will be useful for later.

Next step is to create a more proper CLI that makes use of the module, can
also run tests on the updates, and propose a human-readable report that could
be added to a pull request and/or used as commit message.
  • Loading branch information
tidoust authored Mar 19, 2024
1 parent 660669e commit 1cd3e2f
Showing 1 changed file with 147 additions and 18 deletions.
165 changes: 147 additions & 18 deletions src/build-diff.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
/**
* Script that builds the diff that the provided change(s) to `specs.json`
* would entail to `index.json`.
*
* The script takes the canonical URL of a spec as input, or a "git diff"-like
* reference to named commit(s), in which case it compiles the list of changes
* from the differences in `specs.json` between both commits. It computes the
* updates that the update(s) would trigger to `index.json` and reports the
* diff in a JSON structure with `add`, `update`, `delete`, and `seriesUpdate`
* properties.
*
* For named commits, "working" is the equivalent of Git's "--cached" option
* and means "use the working copy of specs.json".
*
* Examples:
* node src/build-diff https://www.w3.org/TR/webrtc/
* node src/build-diff working
* node src/build-diff HEAD
* node src/build-diff HEAD..HEAD~3
*/

const assert = require("assert");
const path = require("path");
const { execSync } = require("child_process");
Expand Down Expand Up @@ -72,12 +93,68 @@ function areIdentical(s1, s2) {
}
}


/**
* Build the diff for the given spec or list of changes. The first parameter
* may be the canonical URL of a spec, a named Git commit, or two named Git
* commits separated by two dots. Named Git commit are used to compile the
* changes in specs.json that need to be built.
*
* For instance:
* - https://w3c.github.io/example-spec/
* - HEAD
* - HEAD..HEAD~3
*
* Internally, the function branches to `buildCommits` or `buildSpec`
* depending on what needs to be built.
*
* If what to build is the canonical URL of a spec and the options contain a
* `custom` property, that property is used to complete the initial info for
* the spec.
*
* The function throws in case of errors.
*/
async function build(what, options) {
if (!what) {
throw new Error('Nothing to build');
}
const reCommit = /^([\w~\^]+)(?:\.\.([\w~\^]+))?$/;
const commitMatch = what.match(reCommit);
if (commitMatch) {
// We seem to have received a named <commit>
let from = commitMatch[2];
const to = commitMatch[1];
if (!from) {
from = (to.toLowerCase() === 'working') ? 'HEAD' : 'HEAD~1';
}
return buildCommits(to, from, options);
}
else {
// We seem to have received a URL
let url;
try {
url = new URL(what);
}
catch (err) {
throw new Error('Invalid what argument received. Should be a URL, a named commit, or a couple of named commit separated by two dots (..)');
}
const custom = options?.custom ?? {};
const spec = Object.assign({}, custom, { url: url.toString() });
return buildSpec(spec, options);
}
}


/**
* Generate the new index file from the given initial list file.
* Build the diff for changes made to `specs.json` between the provided named
* Git commits.
*
* Internally, the function compiles the diff and then hands it over to
* `buildDiff`.
*
* The function throws in case of errors.
*/
async function compareIndex(newRef, baseRef, { diffType = "diff", log = console.log }) {
async function buildCommits(newRef, baseRef, { diffType = "diff", log = console.log }) {
log(`Retrieve specs.json at "${newRef}"...`);
let newSpecs;
if (newRef.toLowerCase() === "working") {
Expand Down Expand Up @@ -119,6 +196,45 @@ async function compareIndex(newRef, baseRef, { diffType = "diff", log = console.
}
log(`Compute specs.json diff... done`);

return buildDiff(diff, baseSpecs, baseIndex, { diffType, log });
}


/**
* Build the diff for the given spec.
*
* The spec object must have a `url` property. It may have other properties.
*
* Internally, the function turns the parameter into a diff and hands it over
* to `buildDiff`.
*
* The function throws in case of errors.
*/
async function buildSpec(spec, { diffType = "diff", log = console.log }) {
log(`Retrieve specs.json...`);
const baseSpecs = require(path.resolve(__dirname, "..", "specs.json"));
log(`Retrieve specs.json... done`);

log(`Retrieve index.json...`);
const baseIndex = require(path.resolve(__dirname, "..", "index.json"));
log(`Retrieve index.json... done`);

log(`Prepare diff...`);
const isNew = !baseSpecs.find(s => haveSameUrl(s, spec));
log(isNew ? `- spec is new` : `- spec is already in specs.json`);
const diff = {
add: isNew ? [spec] : [],
update: isNew ? [] : [spec],
delete: []
};
log(`Prepare diff... done`);

return buildDiff(diff, baseSpecs, baseIndex, { diffType, log });
}


async function buildDiff(diff, baseSpecs, baseIndex, { diffType = "diff", log = console.log }) {
diff = Object.assign({}, diff);
log(`Delete specs that were dropped...`);
diff.delete = baseIndex.filter(spec => diff.delete.find(s => haveSameUrl(s, spec)));
let newIndex = baseIndex.filter(spec => !diff.delete.find(s => haveSameUrl(s, spec)));
Expand All @@ -136,7 +252,9 @@ async function compareIndex(newRef, baseRef, { diffType = "diff", log = console.
previousIndex: newIndex,
log: function(...msg) { log(' ', ...msg); } });
diff.add = diff.add.map(spec => built.find(s => haveSameUrl(s, spec)));
diff.update = diff.update.map(spec => built.find(s => haveSameUrl(s, spec)));
diff.update = diff.update
.map(spec => built.find(s => haveSameUrl(s, spec)))
.filter(spec => !areIdentical(spec, baseIndex.find(s => s.url === spec.url)));
diff.seriesUpdate = built
.filter(spec =>
!diff.add.find(s => haveSameUrl(s, spec)) &&
Expand All @@ -159,21 +277,32 @@ async function compareIndex(newRef, baseRef, { diffType = "diff", log = console.
}


/*******************************************************************************
Export main function for use as module
*******************************************************************************/
module.exports = {
build,
buildCommits,
buildSpec
};


/*******************************************************************************
Main loop
*******************************************************************************/
const newRef = process.argv[2] ?? "working";
const baseRef = process.argv[3] ?? "HEAD";
const diffType = process.argv[4] ?? "diff";

compareIndex(newRef, baseRef, { diffType, log: console.warn })
.then(diff => {
// Note: using process.stdout.write to avoid creating a final newline in
// "full" diff mode. This makes it easier to compare the result with the
// index.json file in the repo (which does not have a final newline).
process.stdout.write(JSON.stringify(diff, null, 2));
})
.catch(err => {
console.error(err);
process.exit(1);
});
if (require.main === module) {
const what = process.argv[2] ?? "working";
const diffType = process.argv[3] ?? "diff";

build(what, { diffType, log: console.warn })
.then(diff => {
// Note: using process.stdout.write to avoid creating a final newline in
// "full" diff mode. This makes it easier to compare the result with the
// index.json file in the repo (which does not have a final newline).
process.stdout.write(JSON.stringify(diff, null, 2));
})
.catch(err => {
console.error(err);
process.exit(1);
});
}

0 comments on commit 1cd3e2f

Please sign in to comment.