/[sudobot]/trunk/scripts/genchangelog
ViewVC logotype

Contents of /trunk/scripts/genchangelog

Parent Directory Parent Directory | Revision Log Revision Log


Revision 575 - (show annotations)
Mon Jul 29 17:59:26 2024 UTC (8 months ago) by rakinar2
File size: 8429 byte(s)
chore: add trunk
1 #!/usr/bin/env node
2
3 /**
4 * Copyright (C) 2024 OSN, Inc.
5 *
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
20 "use strict";
21
22 const path = require("path");
23 const { existsSync } = require("fs");
24 const { exit } = require("process");
25 const { execSync } = require("child_process");
26 const { writeFile } = require("fs/promises");
27 const { parseArgs } = require("util");
28
29 const ME = path.basename(process.argv[1]);
30
31 function print(...args) {
32 console.log(`${ME}: ${args.join(" ")}`);
33 }
34
35 function perror(...args) {
36 console.error(`\x1b[0m${ME}: error: ${args.join(" ")}`);
37 }
38
39 function findInPath(executable) {
40 for (const segment of process.env.PATH?.split(process.platform === "win32" ? ";" : ":") ?? []) {
41 const executablePath = path.join(segment, executable + (process.platform === "win32" ? ".exe" : ""));
42
43 if (existsSync(executablePath)) {
44 return executablePath;
45 }
46 }
47
48 return null;
49 }
50
51 function checkForGit() {
52 const gitPath = findInPath("git");
53
54 if (!gitPath) {
55 perror("could not find git in $PATH");
56 perror("please make sure git is installed and available in $PATH");
57 exit(1);
58 }
59
60 return gitPath;
61 }
62
63 function getGitLog(gitPath) {
64 try {
65 return execSync(gitPath + " --no-pager log", { encoding: "utf8" });
66 }
67 catch {
68 perror("command `git --no-pager log' failed");
69 exit(1);
70 }
71 }
72
73 function parseGitLog(gitLog) {
74 const lines = gitLog.split("\n");
75 const commits = [];
76
77 for (let i = 0; i < lines.length; ) {
78 if (!lines[i].startsWith("commit")) {
79 i++;
80 continue;
81 }
82
83 const [, hash] = lines[i++].split(' ');
84 const headerProps = {};
85
86 while (i < lines.length && lines[i].trim() !== "" && !/^\s/.test(lines[i])) {
87 const colonIndex = lines[i].indexOf(":");
88 const name = lines[i].slice(0, colonIndex).toLowerCase();
89 const value = lines[i].slice(colonIndex + 1).trim();
90 headerProps[name] = value;
91 i++;
92 }
93
94 const messageLines = [];
95
96 while (i < lines.length && !lines[i].startsWith("commit")) {
97 const lineToPush = lines[i++].replace(/^ /, "");
98
99 if (!lineToPush) {
100 continue;
101 }
102
103 messageLines.push(lineToPush);
104 }
105
106 let mindex = messageLines.length - 1;
107 const footerProps = {};
108 const validFooterProps = ["signed-off-by", "co-authored-by", "on-behalf-of"];
109
110 while (mindex >= 1 && /^[A-Za-z0-9-]+: /.test(messageLines.at(mindex))) {
111 const messageLine = messageLines[mindex--];
112 const colonIndex = messageLine.indexOf(":");
113 const name = messageLine.slice(0, colonIndex).toLowerCase();
114
115 if (!validFooterProps.includes(name)) {
116 continue;
117 }
118
119 const value = messageLine.slice(colonIndex + 1).trim();
120
121 if (name in footerProps && !Array.isArray(footerProps[name])) {
122 footerProps[name] = [footerProps[name]];
123 }
124
125 if (Array.isArray(footerProps[name])) {
126 footerProps[name].push(value);
127 }
128 else {
129 footerProps[name] = value;
130 }
131
132 messageLines.splice(mindex - 1, 1);
133 }
134
135 const message = messageLines.join("\n");
136
137 commits.push({
138 hash,
139 message,
140 headerProps,
141 footerProps,
142 signedOffBy: footerProps["signed-off-by"],
143 onBehalfOf: footerProps["on-behalf-of"],
144 author: headerProps["author"],
145 createdAt: new Date(headerProps["date"])
146 });
147 }
148
149 return commits;
150 }
151
152 function generateChangelog(commits) {
153 let output = "";
154
155 const grouppedCommitsByDate = {};
156
157 for (const commit of commits) {
158 const key = `${commit.createdAt.getUTCDate().toString().padStart(2, 0)}-${commit.createdAt.getUTCMonth().toString().padStart(2, '0')}-${commit.createdAt.getUTCFullYear()}::${Array.isArray(commit.author) ? commit.author.join(':') : commit.author}`;
159 grouppedCommitsByDate[key] ??= [];
160 grouppedCommitsByDate[key].push(commit);
161 }
162
163 for (const key in grouppedCommitsByDate) {
164 const separatorPosition = key.indexOf("::");
165 const date = key.slice(0, separatorPosition);
166 const commits = grouppedCommitsByDate[key];
167
168 output += `${date} ${Array.isArray(commits[0].author) ? commits[0].author.join(", ") : commits[0].author}\n\n`;
169
170 for (const commit of commits) {
171 output += ` ${commit.message.replaceAll("\n", "\n ")}\n\n`;
172 }
173 }
174
175 return output.trim();
176 }
177
178 function printHelp() {
179 console.log("Usage:");
180 console.log(` ${ME} [OPTION]...`);
181 console.log("Generate a formatted ChangeLog from Git commit logs.");
182 console.log();
183 console.log("Options:");
184 console.log(" -h, --help Show this help and exit.");
185 console.log(" -v, --version Show this script's version.");
186 console.log(" -o, --output=[FILE] Write the generated changelog to");
187 console.log(" a file instead of standard output.");
188 console.log(" --no-overwrite Disallow overwriting of the output");
189 console.log(" file if it exists already.");
190 console.log();
191 console.log("Send general inquiries, questions and bug reports");
192 console.log("to <[email protected]>.");
193 }
194
195 function printVersion() {
196 console.log("Copyright (C) 2024 OSN, Inc.");
197 console.log("License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.");
198 console.log("This is free software: you are free to change and redistribute it.");
199 console.log("There is NO WARRANTY, to the extent permitted by law.");
200 console.log();
201 console.log("Written by Ar Rakin.");
202 }
203
204 async function main() {
205 let options;
206
207 try {
208 options = parseArgs({
209 argv: process.argv.slice(1),
210 strict: true,
211 allowPositionals: false,
212 options: {
213 help: {
214 type: "boolean",
215 alias: 'h'
216 },
217 version: {
218 type: "boolean",
219 alias: "v"
220 },
221 output: {
222 type: "string",
223 short: "o"
224 },
225 "no-overwrite": {
226 type: "boolean"
227 }
228 }
229 }).values;
230 }
231 catch (error) {
232 perror(`${error?.message ?? error}`);
233 exit(1);
234 }
235
236 if (options.help) {
237 printHelp();
238 exit(0);
239 }
240
241 if (options.version) {
242 printVersion();
243 exit(0);
244 }
245
246 if (!options.output && options["no-overwrite"]) {
247 perror("option `--no-overwrite' without `--output` does not make sense");
248 exit(1);
249 }
250
251 if (options.output && options["no-overwrite"] && existsSync(options.output)) {
252 perror(`${options.output}: cannot write changelog: File exists`);
253 exit(1);
254 }
255
256 const gitPath = checkForGit();
257 const gitLog = getGitLog(gitPath);
258 const commits = parseGitLog(gitLog);
259 const changelog = generateChangelog(commits);
260
261 if (options.output) {
262 try {
263 await writeFile(options.output, changelog);
264 }
265 catch (error) {
266 perror(`${options.output}: failed to write changelog: ${error?.message ?? error}`);
267 exit(1);
268 }
269
270 print(`wrote generated changelog to ${options.output}`);
271 }
272 else {
273 console.log(changelog);
274 }
275 }
276
277 main();

Properties

Name Value
svn:executable *

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26