Skip to content

Instantly share code, notes, and snippets.

@danyill
Last active April 4, 2022 19:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save danyill/b050f04258b12967b47a8f2156caf5b3 to your computer and use it in GitHub Desktop.
Save danyill/b050f04258b12967b47a8f2156caf5b3 to your computer and use it in GitHub Desktop.
Recent Changes Antora Extension
"use strict";
const fs = require("fs");
const git = requireGit();
const http = requireHttp();
function requireGit() {
return require(require.resolve("isomorphic-git", {
paths: [
require.resolve("@antora/content-aggregator", { paths: module.paths }) +
"/..",
],
}));
}
function requireHttp() {
return require(require.resolve("isomorphic-git/http/node", {
paths: [
require.resolve("@antora/content-aggregator", { paths: module.paths }) +
"/..",
],
}));
}
function date_str(date) {
return date.toLocaleString("en-NZ", {
day: "numeric",
month: "short",
year: "numeric",
hour: "numeric",
minute: "2-digit",
});
}
function convertUTCDateToLocalDate(date) {
var newDate = new Date(date.getTime());
return newDate;
}
/* from Antora in content-catalog.js */
function generateResourceSpec(
{ component, version, module: module_, family, relative },
shorthand = true
) {
return (
`${version}@${component}:${
shorthand && module_ === "ROOT" ? "" : module_
}:` +
(family === "page" || family === "alias" ? "" : `${family}$`) +
relative
);
}
// based on https://github.com/isomorphic-git/isomorphic-git/issues/732
async function commitDiff(gitdir, oidA, oidB) {
// Use git log to get the SHA-1 object ids of the previous two commits
const commits = await git.log({ fs, gitdir, depth: 5, ref: "HEAD" });
const oids = commits.map((commit) => commit.oid);
// Make TREE objects for the first and last commits
const A = git.TREE({ fs, gitdir, ref: oidA });
const B = git.TREE({ fs, gitdir, ref: oidB });
// Get a list of the files that changed
let results = await git.walk({
fs,
gitdir,
trees: [A, B],
map: async function (filepath, [A, B]) {
// ignore directories
if (filepath === ".") {
return;
}
// this appears to be required but it is unclear exactly why -- the initial commit?
if (A == null || B == null) {
return;
}
if (
(A !== null && (await A.type()) === "tree") ||
(B !== null && (await B.type())) === "tree"
) {
return;
}
// generate ids
const Aoid = await A.oid();
const Boid = await B.oid();
// determine modification type
let type = "equal";
if (Aoid !== Boid) {
type = "modify";
}
if (Aoid === undefined) {
type = "add";
}
if (Boid === undefined) {
type = "remove";
}
if (Aoid === undefined && Boid === undefined) {
console.log("Something weird happened:");
}
return {
path: `/${filepath}`,
type: type,
};
},
});
return results;
}
class GitCommitHistoryExtension {
static register({ config }) {
console.log("Registering an extension to build recent changes page");
new GitCommitHistoryExtension(this, config);
}
constructor(generatorContext, config) {
this.config = config;
this.commitData = [];
(this.context = generatorContext)
.on("contentAggregated", this.onContentAggregated.bind(this))
.on("contentClassified", this.onContentClassified.bind(this));
this.logger = this.context.require("@antora/logger")(
"source-list-extension"
);
}
async onContentAggregated({ contentAggregate }) {
this.logger.info("onContentAggregated: Building commit history data");
for (const componentVersionData of contentAggregate) {
const { name, version, files, nav } = componentVersionData;
const referenceFilePath = nav ? nav[0] : "modules/ROOT/pages/index.adoc";
let referenceFile = files.find(({ path }) => path === referenceFilePath);
referenceFile = referenceFile ? referenceFile : files[0];
const { gitdir, refhash } = referenceFile.src.origin;
let result = await git.fetch({
fs,
http,
gitdir: gitdir + '/.git',
depth: 10,
ref: referenceFile.src.origin.branch,
singleBranch: true,
tags: false
})
// url: referenceFile.src.origin.url,
// ref: referenceFile.src.origin.branch,
console.log(result)
const commitData = [];
if (gitdir) {
const commits = await git.log({
fs,
gitdir,
depth: 10,
ref: referenceFile.src.origin.branch,
});
await Promise.all(
commits.map(async (commit) => {
const lastCommit = commit["commit"];
const lastHash = commit["oid"];
let res = []
// try {
res = await commitDiff(
gitdir,
commit.oid,
commit.commit.parent[0]
);
// } catch (e) {
// // TODO: If it is the initial HEAD commit we don't want a diff, we just want everything in that commit.
// // if (e instanceof NotFoundError) {
// // // probably no commit parent, this is OK
// // } else {
// // throw e; // re-throw the error unchanged
// // }
// }
const commitSummary = {
name: `${version ? version + "@" : ""}${name}`,
shortHash: lastHash.substr(0, 7),
// subject: lastCommit.message.split(/$/m)[0],
message: lastCommit.message,
date: convertUTCDateToLocalDate(
new Date(lastCommit.author.timestamp * 1000)
),
author: lastCommit.author.name,
files: res
.filter((file) => file.type !== "equal")
.map((file) => file.path),
};
commitData.push(commitSummary);
})
);
this.commitData.push({
name: name,
version: version,
commitData: commitData.sort((a, b) => b.date - a.date),
});
}
}
}
getGitlabCommitUrlPrefix(contentCatalog, name, version, path) {
const fileData = contentCatalog.getByPath({
component: name,
version: version,
path: path.slice(1),
});
return `${fileData.src.origin.webUrl}/-/commit/`;
}
async onContentClassified({
_playbook,
_siteAsciiDocConfig,
_siteCatalog,
_uiCatalog,
contentCatalog,
}) {
this.logger.info("onContentClassified: Creating and adding new page");
this.commitData.forEach((componentVersion) => {
componentVersion.commitData.forEach((commit) => {
commit["fileIds"] = [];
commit.files.forEach((file) => {
// this does not follow path changes but those will be a bit rare.
const fileData = contentCatalog.getByPath({
component: componentVersion.name,
version: componentVersion.version,
path: file.slice(1),
});
if (fileData !== undefined) {
commit["fileIds"].push(generateResourceSpec(fileData.src));
commit["url"] = this.getGitlabCommitUrlPrefix(
contentCatalog,
componentVersion.name,
componentVersion.version,
file
);
}
});
});
let rows = [];
componentVersion.commitData.forEach((cData) => {
if (cData["fileIds"].length !== 0) {
rows.push(
`| ${date_str(cData["date"])}
| ${cData["author"]}
| ${cData["url"]}${cData["shortHash"]}[${cData["shortHash"]}]
${cData["message"]}
| ${cData["fileIds"].map((id) => `* xref:${id}[]`).join("\n")}`
);
}
});
const contents = Buffer.from(
`= Recent Changes
[.small]#Generated at ${date_str(convertUTCDateToLocalDate(new Date()))}#
[cols="2,3,6,8a",options="header,unbreakable"]
|====
| Date | Author | Commit | Changed Files
${rows.join("\n")}
|====
`.trim()
);
contentCatalog.addFile({
path: "modules/ROOT/pages/recent-changes.adoc",
src: {
component: componentVersion.name,
version: componentVersion.version,
module: "ROOT",
family: "page",
relative: "recent-changes.adoc",
},
contents: contents,
});
});
}
}
module.exports = GitCommitHistoryExtension;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment