tools: Add tool to generate and update copyright headers

This tool aids in the proper upkeep of copyright headers for changes contained within this repository. It will generate a new header, or replace the original one if one exists. As this task has often been forgotten by both developers and contributors, having a tool manage this will hopefully improve the situation.

The choice of Node.JS for this tool was deliberate, as many developers and CI solutions already have Node.JS in a reasonably up to date version installed. Additionally the versatility of Node.JS eliminates the need to create custom or platform specific solutions for tasks that are relatively simple. While the performance is not ideal, it still completes its task relatively quickly.
This commit is contained in:
Michael Fabian 'Xaymar' Dirks 2023-02-28 01:27:31 +01:00
parent c80a19ae3c
commit d1e3b6d0d1

346
tools/copyright.js Normal file
View file

@ -0,0 +1,346 @@
/* AUTOGENERATED COPYRIGHT HEADER START
* Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks <info@xaymar.com>
* AUTOGENERATED COPYRIGHT HEADER END */
const CHILD_PROCESS = require("node:child_process");
const PROCESS = require("node:process");
const PATH = require("node:path");
const FS = require("node:fs");
const FSPROMISES = require("node:fs/promises");
const OS = require("os");
async function git_isIgnored(path) {
await new Promise((resolve, reject) => {
try {
let proc = CHILD_PROCESS.spawn("git", [
"check-ignore",
path
], {
"cwd": PROCESS.cwd(),
"encoding": "utf8",
});
proc.stdout.on('data', (data) => {
})
proc.on('close', (code) => {
resolve(code == 0);
});
proc.on('exit', (code) => {
resolve(code == 0);
});
} catch (ex) {
reject(ex);
}
});
/* Sync alternative
try {
return CHILD_PROCESS.spawnSync("git", [
"check-ignore",
path
], {
"cwd": PROCESS.cwd(),
"encoding": "utf8"
}).status == 0;
} catch (ex) {
return true;
}
*/
}
async function git_retrieveAuthors(file) {
// git --no-pager log --date-order --reverse "--format=format:%aI|%aN <%aE>" -- file
let lines = await new Promise((resolve, reject) => {
try {
let lines = "";
let proc = CHILD_PROCESS.spawn("git", [
"--no-pager",
"log",
"--date-order",
"--reverse",
"--format=format:%aI|%aN <%aE>",
"--",
file
], {
"cwd": PROCESS.cwd(),
"encoding": "utf8",
});
proc.stdout.on('data', (data) => {
lines += data.toString();
})
proc.on('close', (code) => {
resolve(lines);
});
proc.on('exit', (code) => {
resolve(lines);
});
} catch (ex) {
reject(ex);
}
});
lines = lines.split(lines.indexOf("\r\n") >= 0 ? "\r\n" : "\n");
let authors = new Map();
for (let line of lines) {
let [date, name] = line.split("|");
let author = authors.get(name);
if (author) {
author.to = new Date(date)
} else {
authors.set(name, {
from: new Date(date),
to: new Date(date),
})
}
}
return authors;
/* Sync Variant
try {
let data = await CHILD_PROCESS
let lines = data.stdout.toString().split("\n");
let authors = new Map();
for (let line of lines) {
let [date, name] = line.split("|");
let author = authors.get(name);
if (author) {
author.to = new Date(date)
} else {
authors.set(name, {
from: new Date(date),
to: new Date(date),
})
}
}
return authors;
} catch (ex) {
console.error(ex);
throw ex;
}
*/
}
async function generateCopyright(file) {
let authors = await git_retrieveAuthors(file)
let lines = [];
for (let entry of authors) {
let from = entry[1].from.getUTCFullYear();
let to = entry[1].to.getUTCFullYear();
lines.push(`Copyright (C) ${from != to ? `${from}-${to}` : to} ${entry[0]}`);
}
return lines;
}
function makeHeader(file, copyright) {
let file_name = PATH.basename(file).toLocaleLowerCase();
let file_exts = file_name.substring(file_name.indexOf("."));
let styles = {
"#": {
files: [
"cmakelists.txt"
], exts: [
".clang-tidy",
".clang-format",
".cmake",
".editorconfig",
".gitignore",
".gitmodules",
".yml",
],
prepend: [
"#\u0020AUTOGENERATED COPYRIGHT HEADER START",
],
append: [
"#\u0020AUTOGENERATED COPYRIGHT HEADER END",
],
prefix: "# ",
suffix: "",
},
";": {
files: [
""
], exts: [
".iss",
".iss.in",
],
prepend: [
";\u0020AUTOGENERATED COPYRIGHT HEADER START",
],
append: [
";\u0020AUTOGENERATED COPYRIGHT HEADER END",
],
prefix: "; ",
suffix: "",
},
"/**/": {
files: [
], exts: [
".c",
".c.in",
".cpp",
".cpp.in",
".h",
".h.in",
".hpp",
".hpp.in",
".js",
".rc",
".rc.in",
".effect"
],
prepend: [
"/*\u0020AUTOGENERATED COPYRIGHT HEADER START",
],
append: [
" *\u0020AUTOGENERATED COPYRIGHT HEADER END */",
],
prefix: " * ",
suffix: "",
},
"<!---->": {
files: [
], exts: [
".htm",
".htm.in",
".html",
".html.in",
".xml",
".xml.in",
".plist",
".plist.in",
".pkgproj",
".pkgproj.in",
],
prepend: [
"<!--\u0020AUTOGENERATED COPYRIGHT HEADER START",
],
append: [
" --\u0020AUTOGENERATED COPYRIGHT HEADER END -->",
],
prefix: " --",
suffix: "",
}
};
for (let key in styles) {
let style = [key, styles[key]];
if (style[1].files.includes(file_name)
|| style[1].files.includes(file)
|| style[1].exts.includes(file_exts)) {
let header = [];
header.push(...style[1].prepend);
for (let line of copyright) {
header.push(`${style[1].prefix}${line}${style[1].suffix}`);
}
header.push(...style[1].append);
return header;
}
}
throw new Error("Unrecognized file format.")
}
async function addCopyright(file) {
try {
// Async/Promises
let content = await FSPROMISES.readFile(file);
let eol = (content.indexOf("\r\n") != -1 ? OS.EOL : "\n");
let copyright = await generateCopyright(file);
let header = makeHeader(file, copyright);
let insert = Buffer.from(header.join(eol) + eol);
let startHeader = content.indexOf(header[0]);
let endHeader = content.indexOf(header[header.length - 1], startHeader + 1);
endHeader += header[header.length - 1].length + eol.length;
let fd = await FSPROMISES.open(file, "w+");
let fp = [];
if ((startHeader >= 0) && (endHeader >= 0)) {
let pos = 0;
if (startHeader > 0) {
fd.write(content, 0, startHeader, 0);
pos += startHeader;
}
fd.write(insert, 0, undefined, pos);
pos += insert.byteLength;
fd.write(content, endHeader, undefined, pos);
} else {
fd.write(insert, 0, undefined, 0);
fd.write(content, 0, undefined, insert.byteLength);
}
await fd.close();
/* Sync variant (slow!)
let content = FS.readFileSync(file);
let eol = (content.indexOf("\r\n") != -1 ? OS.EOL : "\n");
let copyright = await generateCopyright(file);
let header = makeHeader(file, copyright);
let insert = Buffer.from(header.join(eol) + eol);
let startHeader = content.indexOf(header[0]);
let endHeader = content.indexOf(header[header.length - 1], startHeader + 1);
endHeader += header[header.length - 1].length + eol.length;
let fd = FS.openSync(file, "w+");
if ((startHeader >= 0) && (endHeader >= 0)) {
let pos = 0;
if (startHeader > 0) {
FS.writeSync(fd, content, 0, startHeader, 0);
pos += startHeader;
}
FS.writeSync(fd, insert, 0, undefined, pos);
pos += insert.byteLength;
FS.writeSync(fd, content, endHeader, undefined, pos);
} else {
FS.writeSync(fd, insert, 0, undefined, 0);
FS.writeSync(fd, content, 0, undefined, insert.byteLength);
}
FS.close(fd, (err) => {
if (err)
throw err;
})*/
} catch (ex) {
console.error(`Error processing '${file}'!: ${ex}`);
return;
}
}
async function addCopyrights(path) {
if (await git_isIgnored(path)) {
return;
}
let promises = [];
let files = await FSPROMISES.readdir(path, { "withFileTypes": true });
for (let file of files) {
let fullname = PATH.join(path, file.name);
if (await git_isIgnored(fullname)) {
console.log(`Ignoring path '${fullname}'...`);
continue;
}
if (file.isDirectory()) {
console.log(`Scanning path '${fullname}'...`);
promises.push(addCopyrights(fullname));
} else {
console.log(`Updating file '${fullname}'...`);
promises.push(addCopyright(fullname));
}
}
await Promise.all(promises);
}
(async function () {
let file = PROCESS.argv[2];
let pathStat = await FSPROMISES.stat(file);
if (pathStat.isDirectory()) {
await addCopyrights(PATH.resolve(file));
} else {
await addCopyright(PATH.resolve(file));
}
console.log("Done");
})();