Files
BlitzLLVM/tools/copyright.js
T
Michael Fabian 'Xaymar' Dirks 22c7614e7c Fix CRLF and submodules with auto-generated copyright headers
We no longer add another character to the file every time it is committed, and instead now properly handle CRLF. Additionally submodules are no longer updated when they shouldn't be, without requiring a manual config edit.
2024-07-05 15:19:18 +02:00

640 lines
16 KiB
JavaScript

// AUTOGENERATED COPYRIGHT HEADER START
// Copyright (C) 2024 Michael Fabian 'Xaymar' Dirks <info@xaymar.com>
// AUTOGENERATED COPYRIGHT HEADER END
const ignoreList = [
/^\.git$/gi,
/^cmake\/clang$/gi,
/^cmake\/version$/gi,
/^third-party$/gi,
]
const sectionStart = "AUTOGENERATED COPYRIGHT HEADER START";
const sectionEnd = "AUTOGENERATED COPYRIGHT HEADER END";
const formatStyleList = {
"#": {
files: [
"cmakelists.txt"
], exts: [
".clang-tidy",
".clang-format",
".cmake",
".editorconfig",
".gitignore",
".gitmodules",
".yml",
".sh"
],
prepend: [
`# ${sectionStart}`,
],
append: [
`# ${sectionEnd}`,
],
prefix: "# ",
suffix: "",
},
";": {
files: [
], exts: [
".iss",
".iss.in",
".bb",
".b3d",
".b2d",
".bpl"
],
prepend: [
`; ${sectionStart}`,
],
append: [
`; ${sectionEnd}`,
],
prefix: "; ",
suffix: "",
},
"//": {
files: [
], exts: [
".c",
".c.in",
".cpp",
".cpp.in",
".h",
".h.in",
".hpp",
".hpp.in",
".js",
".rc",
".rc.in",
".effect"
],
prepend: [
`// ${sectionStart}`,
],
append: [
`// ${sectionEnd}`,
],
prefix: "// ",
suffix: "",
},
"<!---->": {
files: [
], exts: [
".htm",
".htm.in",
".html",
".html.in",
".xml",
".xml.in",
".plist",
".plist.in",
".pkgproj",
".pkgproj.in",
".md"
],
prepend: [
`<!-- ${sectionStart} -->`,
],
append: [
`<!-- ${sectionEnd} -->`,
],
prefix: "<!-- ",
suffix: " -->",
}
};
const commitFormat="%aI|%aN <%aE>"
const debug = false;
// --------------------------------------------------------------------------------
// End of easily modified area, best not touch things beyond here.
// --------------------------------------------------------------------------------
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");
const READLINE = require('node:readline');
if (!debug)
console.debug = function() {}
class RateLimiter {
constructor(limit) {
Object.defineProperty(this, "_maximum", {
enumerable: true,
configurable: true,
writable: true,
value: 0
});
Object.defineProperty(this, "_available", {
enumerable: true,
configurable: true,
writable: true,
value: 0
});
Object.defineProperty(this, "_instances", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
if (!limit) {
this._maximum = Math.ceil(Math.max(1, OS.cpus().length / 3 * 2));
}
else {
this._maximum = limit;
}
this._available = this._maximum;
this._instances = [];
}
async queue(executor, ...args) {
while (this._available == 0) {
await Promise.race(this._instances);
}
--this._available;
const data = {};
data.task = new Promise((resolve, reject) => {
try {
if (executor.constructor.name == "Promise") {
executor.then((res) => {
resolve(res);
}, (err) => {
reject(err);
});
} else if (executor.constructor.name == "AsyncFunction") {
executor(...args).then((res) => {
resolve(res);
}, (err) => {
reject(err);
});
}
else {
resolve(executor(...args));
}
}
catch (ex) {
console.error(executor, executor.toString())
reject(ex);
}
});
data.solver = data.task.finally(() => {
const taskIndex = this._instances.indexOf(data.task);
if (taskIndex >= 0) {
this._instances.splice(taskIndex, 1);
}
const solverIndex = this._instances.indexOf(data.solver);
if (solverIndex >= 0) {
this._instances.splice(solverIndex, 1);
}
++this._available;
});
this._instances.push(data.solver);
return await data.solver;
}
}
let abortAllWork = false;
let gitRL = new RateLimiter(3);
let workRL = new RateLimiter();
let gitCurrentFiles;
let gitUserName;
let gitUserMail;
let gitSubmodules;
let gitDate = (new Date()).toISOString();
/** Run a process asynchronously, returning an array of messages.
*
* @param {*} path
* @param {*} args
* @param {*} options
* @returns [ErrorCode, [{type, data, text}, ...]]
*/
async function runProcessAsync(path, args, options) {
console.debug(arguments.callee.name, Array.from(arguments));
return await new Promise((resolve, reject) => {
let messages = [];
try {
let proc = CHILD_PROCESS.spawn(path, args, Object.assign({
"cwd": PROCESS.cwd(),
"encoding": "utf8",
}, options));
let pushOutput = function(type, chunk) {
if (!chunk) return;
let lastMessage = undefined;
if (messages.length == 0 || messages[messages.length - 1].type !== type) {
lastMessage = {
type: type,
chunks: [],
length: 0,
};
messages.push(lastMessage);
} else {
lastMessage = messages[messages.length - 1];
}
lastMessage.length += chunk.length;
lastMessage.chunks.push(chunk);
};
proc.stderr.on('data', (chunk) => { pushOutput("error", chunk); });
proc.stderr.on('close', () => { pushOutput("output", proc.stderr.read()); });
proc.stdout.on('data', (chunk) => { pushOutput("output", chunk); });
proc.stdout.on('close', () => { pushOutput("output", proc.stdout.read()); });
proc.on('error', (err) => {
reject(err);
})
proc.on('exit', (code) => {
// Merge chunks of messages into a single message.
for (let message of messages) {
message.data = Buffer.alloc(message.length);
let offset = 0;
for (let chunk of message.chunks) {
Buffer.from(chunk).copy(message.data, offset, 0);
offset += chunk.byteLength;
}
message.chunks = undefined;
message.length = undefined;
message.text = message.data.toString();
}
resolve([code, messages]);
});
} catch (ex) {
reject(ex);
}
});
}
async function git_isIgnored(path) {
console.debug(arguments.callee.name, Array.from(arguments));
let rpath = PATH.relative(process.cwd(), path).replaceAll(PATH.sep, PATH.posix.sep);
// Check manual ignore list.
for (let ignore of ignoreList) {
if (ignore instanceof RegExp) {
if (ignore.global) {
let matches = rpath.matchAll(ignore);
for (let match of matches) {
return true;
}
} else {
if (rpath.match(ignore) !== null) {
return true;
}
}
} else if (rpath.startsWith(ignore)) {
return true;
}
}
// Check if this happens to be (in) a submodule.
let modules = await git_subModules()
for (let module of modules) {
if (rpath.startsWith(module)) {
return true;
}
}
// Finally check if git ignores this file.
let result = await gitRL.queue(runProcessAsync, "git", ["check-ignore", path], {});
return (result[0] == 0)
}
async function git_getCurrentAuthor() {
console.debug(arguments.callee.name, Array.from(arguments));
if (!gitUserName) {
let result = await gitRL.queue(runProcessAsync, "git", ["config", "user.name"], {});
gitUserName = result[1][0].text.split('\n')[0];
}
if (!gitUserMail) {
let result = await gitRL.queue(runProcessAsync, "git", ["config", "user.email"], {});
gitUserMail = result[1][0].text.split('\n')[0];
}
return commitFormat.replace("%aI", gitDate).replace("%aN", gitUserName).replace("%aE", gitUserMail);
}
async function git_subModules() {
if (!gitSubmodules) {
gitSubmodules = new Set();
let gitmodules = PATH.join(process.cwd(), ".gitmodules");
if ((await FSPROMISES.stat(gitmodules)).isFile()) {
let gmfs = FS.createReadStream(gitmodules);
const rl = READLINE.createInterface({
input: gmfs,
crlfDelay: Infinity
});
for await(const line of rl) {
let parts = line.trim().split("=");
if (parts.length > 1) {
if(parts[0].trim() == "path") {
gitSubmodules.add(parts[1].trim());
}
}
}
}
}
return gitSubmodules;
}
async function git_isInCurrentCommit(file) {
console.debug(arguments.callee.name, Array.from(arguments));
if (!gitCurrentFiles) {
let files = [];
let result = await gitRL.queue(runProcessAsync, "git", ["diff", "--name-only", "--cached"], {});
if (result[0] != 0) {
let output = "";
for (let message of result[1]) {
output += message.text;
}
throw new Error(output);
} else {
lines = "";
for (let message of result[1]) {
if (message.type == "output")
lines += message.text;
}
files = lines.split(lines.indexOf("\r\n") >= 0 ? "\r\n" : "\n");
}
gitCurrentFiles = new Set(files);
for (let file of files) {
if (file.length == 0) continue;
file = PATH.relative(PROCESS.cwd(), PATH.resolve(file));
gitCurrentFiles.add(file);
}
}
// Normalize file given to us.
file = PATH.relative(PROCESS.cwd(), PATH.resolve(file));
// Check and return if it's in here.
return gitCurrentFiles.has(file);
}
async function git_retrieveAuthors(file) {
console.debug(arguments.callee.name, Array.from(arguments));
// git --no-pager log --date-order --reverse "--format=format:%aI|%aN <%aE>" -- file
let lines = [];
let result = await gitRL.queue(runProcessAsync, "git", ["--no-pager", "log", "--follow", `--format=format:${commitFormat}`, "--", file], {});
if (result[0] != 0) {
let output = "";
for (let message of result[1]) {
output += message.text;
}
throw new Error(output);
} else {
let buffered = "";
for (let message of result[1]) {
if (message.type == "output")
buffered += message.text;
}
lines = buffered.split(buffered.indexOf("\r\n") >= 0 ? "\r\n" : "\n");
}
if (await git_isInCurrentCommit(file)) {
lines.push(await git_getCurrentAuthor());
}
let authors = new Map();
for (let line of lines) {
let [date, name] = line.split("|");
let author = authors.get(name);
if (author) {
let dt = new Date(date)
if (author.from > dt)
author.from = dt;
if (author.to < dt)
author.to = dt;
} else {
authors.set(name, {
from: new Date(date),
to: new Date(date),
})
}
}
return authors;
}
async function generateCopyright(file) {
console.debug(arguments.callee.name, Array.from(arguments));
let authors = await git_retrieveAuthors(file)
authors = new Map([...authors].sort((a, b) => {
if (a[1].from < b[1].from) {
return -1;
} else if (a[1].from > b[1].from) {
return 1;
}
return 0;
}));
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) {
console.debug(arguments.callee.name, Array.from(arguments));
let file_name = PATH.basename(file).toLocaleLowerCase();
let file_exts = file_name.substring(file_name.indexOf("."));
let styles = formatStyleList;
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 updateFile(file) {
console.debug(arguments.callee.name, Array.from(arguments));
await workRL.queue(async () => {
try {
if (abortAllWork) {
return;
}
// Copyright information.
let copyright = await generateCopyright(file);
let header = undefined;
try {
header = makeHeader(file, copyright);
} catch (ex) {
console.log(`Skipping file '${file}': ${ex.message}`);
return;
}
console.log(`Updating file '${file}'...`);
// ToDo: Do we actually need to read the file first, or can we use the same rw stream?
// File contents.
let contentBuf = await FSPROMISES.readFile(file);
let eol = contentBuf.indexOf("\r\n") != -1 ? "\r\n" : "\n";
let headerBuf = Buffer.from(header.join(eol) + eol, "utf8");
// Find the starting point.
let startHeader = contentBuf.indexOf(Buffer.from(header[0], "utf8"));
if (startHeader != -1) {
//startHeader = contentBuf.lastIndexOf(eolBuf, startHeader);
//startHeader += eolb.byteLength;
}
console.log(sectionStart, startHeader);
// Find the ending point.
let endHeader = contentBuf.lastIndexOf(Buffer.from(header[header.length - 1], "utf8"));
if (endHeader != -1) {
endHeader += Buffer.from(header[header.length - 1], "utf8").byteLength;
endHeader += Buffer.byteLength(eol, "utf8");
}
console.log(sectionEnd, endHeader);
// Last check for early-exit here.
if (abortAllWork) {
return;
}
let fd = await FSPROMISES.open(file, "w");
if (startHeader == -1 || (endHeader < startHeader)) {
fd.write(headerBuf, 0, null, 0);
fd.write(contentBuf, 0, null, headerBuf.byteLength);
} else {
let pos = 0;
if (startHeader > 0) {
fd.write(contentBuf, 0, startHeader - Buffer.byteLength(eol, "utf8").byteLength, 0);
pos += startHeader;
}
fd.write(headerBuf, 0, null, pos); pos += headerBuf.byteLength;
fd.write(contentBuf, endHeader, null, pos);
}
await fd.close();
} catch (ex) {
console.error(`Error processing '${file}'!: ${ex}`);
abortAllWork = true;
PROCESS.exitCode = 1;
return;
}
});
}
async function scanPath(path) {
console.debug(arguments.callee.name, Array.from(arguments));
// Abort here if the user aborted the process, or if the path is ignored.
if (abortAllWork) {
return;
}
console.log(`Scanning path '${path}'...`);
let promises = [];
await workRL.queue(async () => {
let files = await FSPROMISES.readdir(path, { "withFileTypes": true });
for (let file of files) {
if (abortAllWork) {
break;
}
let fullname = PATH.join(path, file.name);
if (await git_isIgnored(fullname)) {
console.log(`Ignoring path '${fullname}'...`);
continue;
}
if (file.isDirectory()) {
promises.push(scanPath(fullname));
} else {
promises.push(updateFile(fullname));
}
}
});
await Promise.all(promises);
}
(async function () {
PROCESS.on("SIGINT", () => {
abortAllWork = true;
PROCESS.exitCode = 1;
console.log("Sanely aborting all pending work...");
})
const root_path = PROCESS.cwd();
let is_git_directory = false;
console.debug(root_path, PROCESS.argv, PROCESS.execArgv);
var args = PROCESS.argv.slice(2);
let promises = new Array();
while (args.length > 0) {
// Try to place ourselves where git actually is.
while (!is_git_directory) {
if (abortAllWork) {
return;
}
let entries = await FSPROMISES.readdir(PROCESS.cwd());
if (entries.includes(".git") && ((await FSPROMISES.stat(PATH.join(PROCESS.cwd(), ".git"))).isDirectory())) {
console.log(`Found .git at '${process.cwd()}'.`);
is_git_directory = true;
} else {
PROCESS.chdir(PATH.resolve(PATH.join(PROCESS.cwd(), "..")));
}
}
// If that failed, we just exit now.
if (!is_git_directory) {
console.error("Failed to figure out where git is, is this even a repository?");
PROCESS.exitCode = 1;
return;
}
if (abortAllWork) {
return;
}
git_getCurrentAuthor();
// Then proceed with normal work.
let path = PATH.normalize(PATH.relative(process.cwd(), PATH.resolve(root_path, args[0])));
// If we're handed a path, scan said path, otherwise update the file itself.
let pathStat = await FSPROMISES.stat(path);
if (pathStat) {
if (await git_isIgnored(path)) {
console.log(`Ignoring path '${path}'...`);
} else if(pathStat.isDirectory()) {
//await scanPath(path);
promises.push(scanPath(path));
} else {
//await updateFile(path);
promises.push(updateFile(path));
}
}
// Slice off the first argument and continue.
args = args.slice(1);
}
await Promise.all(promises);
console.log("Done");
})();