diff --git a/tools/copyright.js b/tools/copyright.js new file mode 100644 index 0000000..afeeada --- /dev/null +++ b/tools/copyright.js @@ -0,0 +1,346 @@ +/* AUTOGENERATED COPYRIGHT HEADER START + * Copyright (C) 2023 Michael Fabian 'Xaymar' Dirks + * 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: [ + "", + ], + 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"); +})();