obs-StreamFX/tools/copyright.js
Michael Fabian 'Xaymar' Dirks 4339a5f853 Update copyright.js tool
It will now properly sort authors by date, and follow renames, which should give a much better coverage of copyright information.
2023-09-30 09:25:30 +02:00

469 lines
10 KiB
JavaScript

// 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");
const SECTION_START = "AUTOGENERATED COPYRIGHT HEADER START";
const SECTION_END = "AUTOGENERATED COPYRIGHT HEADER END";
const IGNORED = [
/^\.git$/gi,
/^cmake\/clang$/gi,
/^cmake\/version$/gi,
/^third-party$/gi,
]
let abortAllWork = false;
class RateLimiter {
constructor(limit = undefined) {
const OS = require("node:os");
this._limit = limit;
if (!this._limit) {
this._limit = Math.ceil(Math.max(2, OS.cpus().length / 3 * 2));
}
this._cur = this._limit;
this._pend = 0;
this._locks = [];
}
async run(runner) {
// Use Promises to spin-lock this execution path until there is a free slot.
this._pend += 1;
while (true) {
if (this._cur > 0) {
this._cur -= 1;
break;
} else {
await Promise.race(this._locks);
}
}
this._pend -= 1;
let data = {};
data.pri = new Promise((resolve, reject) => {
try {
if (runner.constructor.name == "AsyncFunction") {
runner().then((res) => {
resolve(res);
}, (err) => {
reject(err);
})
} else {
resolve(runner());
}
} catch (ex) {
reject(ex);
}
});
data.sec = data.pri.finally(() => {
// Remove this promise from the locks list.
let idx = this._locks.indexOf(data.pri);
if (idx >= 0) {
this._locks.splice(idx, 1);
}
let idx2 = this._locks.indexOf(data.sec);
if (idx2 >= 0) {
this._locks.splice(idx2, 1);
}
this._cur += 1;
//console.log(`Avail: ${this._cur} / ${this._limit}; Pending: ${this._pend}`)
});
this._locks.push(data.sec);
return await data.sec;
}
}
let gitRL = new RateLimiter(1);
let workRL = new RateLimiter();
async function isIgnored(path) {
let rpath = PATH.relative(process.cwd(), path).replaceAll(PATH.sep, PATH.posix.sep);
for (let ignore of IGNORED) {
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;
}
}
return await gitRL.run(async () => {
return 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);
}
});
});
}
async function git_retrieveAuthors(file) {
// git --no-pager log --date-order --reverse "--format=format:%aI|%aN <%aE>" -- file
let lines = await gitRL.run(async () => {
return await new Promise((resolve, reject) => {
try {
let chunks = [];
let proc = CHILD_PROCESS.spawn("git", [
"--no-pager",
"log",
"--follow",
"--format=format:%aI|%aN <%aE>",
"--",
file
], {
"cwd": PROCESS.cwd(),
"encoding": "utf8",
});
proc.stdout.on('data', (chunk) => {
chunks.push(chunk);
});
proc.stdout.on('close', () => {
let chunk = proc.stdout.read();
if (chunk) {
chunks.push(chunk);
}
});
proc.on('exit', (code) => {
// Merge all data into one buffer.
let length = 0;
for (let chunk of chunks) {
length += chunk.byteLength;
}
let buf = Buffer.alloc(length);
length = 0;
for (let chunk of chunks) {
if (!(chunk instanceof Buffer)) {
chunk = Buffer.from(chunk);
}
chunk.copy(buf, length, 0);
length += chunk.byteLength;
}
if (code == 0) {
if (buf) {
resolve(buf.toString());
} else {
reject(code);
}
} else {
reject(code);
}
});
} 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) {
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) {
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) {
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: [
`# ${SECTION_START}`,
],
append: [
`# ${SECTION_END}`,
],
prefix: "# ",
suffix: "",
},
";": {
files: [
""
], exts: [
".iss",
".iss.in",
],
prepend: [
`; ${SECTION_START}`,
],
append: [
`; ${SECTION_END}`,
],
prefix: "; ",
suffix: "",
},
"//": {
files: [
], exts: [
".c",
".c.in",
".cpp",
".cpp.in",
".h",
".h.in",
".hpp",
".hpp.in",
".js",
".rc",
".rc.in",
".effect"
],
prepend: [
`// ${SECTION_START}`,
],
append: [
`// ${SECTION_END}`,
],
prefix: "// ",
suffix: "",
},
"<!---->": {
files: [
], exts: [
".htm",
".htm.in",
".html",
".html.in",
".xml",
".xml.in",
".plist",
".plist.in",
".pkgproj",
".pkgproj.in",
],
prepend: [
`<!-- ${SECTION_START} -->`,
],
append: [
`<!-- ${SECTION_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 updateFile(file) {
await workRL.run(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}'...`);
return;
}
console.log(`Updating file '${file}'...`);
// File contents.
let content = await FSPROMISES.readFile(file);
let eol = (content.indexOf("\r\n") != -1 ? OS.EOL : "\n");
let insert = Buffer.from(header.join(eol) + eol);
// Find the starting point.
let startHeader = content.indexOf(SECTION_START);
startHeader = content.lastIndexOf(eol, startHeader);
startHeader += Buffer.from(eol).byteLength;
// Find the ending point.
let endHeader = content.indexOf(SECTION_END);
endHeader = content.indexOf(eol, endHeader);
endHeader += Buffer.from(eol).byteLength;
if (abortAllWork) {
return;
}
let fd = await FSPROMISES.open(file, "w");
let fp = [];
if ((startHeader >= 0) && (endHeader > startHeader)) {
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();
} catch (ex) {
console.error(`Error processing '${file}'!: ${ex}`);
abortAllWork = true;
PROCESS.exitCode = 1;
return;
}
});
}
async function scanPath(path) {
// Abort here if the user aborted the process, or if the path is ignored.
if (abortAllWork) {
return;
}
let promises = [];
await workRL.run(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 isIgnored(fullname)) {
console.log(`Ignoring path '${fullname}'...`);
continue;
}
if (file.isDirectory()) {
console.log(`Scanning path '${fullname}'...`);
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...");
})
let path = PATH.resolve(PROCESS.argv[2]);
{ // Bootstrap to actually be in the directory where '.git' is.
let is_git_directory = false;
while (!is_git_directory) {
if (abortAllWork) {
return;
}
let entries = await FSPROMISES.readdir(PROCESS.cwd());
if (entries.includes(".git")) {
console.log(`Found .git at '${process.cwd()}'.`);
is_git_directory = true;
} else {
PROCESS.chdir(PATH.resolve(PATH.join(PROCESS.cwd(), "..")));
}
}
path = PATH.normalize(PATH.relative(process.cwd(), path));
}
if (!await isIgnored(path)) {
if ((await FSPROMISES.stat(path)).isDirectory()) {
console.log(`Scanning path '${path}'...`);
await scanPath(path);
} else {
await updateFile(path);
}
} else {
console.log(`Ignoring path '${path}'...`);
}
console.log("Done");
})();