/[osn-commons]/trunk/git/genchangelog
ViewVC logotype

Contents of /trunk/git/genchangelog

Parent Directory Parent Directory | Revision Log Revision Log


Revision 20 - (show annotations)
Sun Aug 4 13:06:22 2024 UTC (7 months, 3 weeks ago) by rakinar2
File size: 9081 byte(s)
chore: correct mailing list
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(
41 process.platform === "win32" ? ";" : ":"
42 ) ?? []) {
43 const executablePath = path.join(
44 segment,
45 executable + (process.platform === "win32" ? ".exe" : "")
46 );
47
48 if (existsSync(executablePath)) {
49 return executablePath;
50 }
51 }
52
53 return null;
54 }
55
56 function checkForGit() {
57 const gitPath = findInPath("git");
58
59 if (!gitPath) {
60 perror("could not find git in $PATH");
61 perror("please make sure git is installed and available in $PATH");
62 exit(1);
63 }
64
65 return gitPath;
66 }
67
68 function getGitLog(gitPath) {
69 try {
70 return execSync(gitPath + " --no-pager log", { encoding: "utf8" });
71 } catch {
72 perror("command `git --no-pager log' failed");
73 exit(1);
74 }
75 }
76
77 function parseGitLog(gitLog) {
78 const lines = gitLog.split("\n");
79 const commits = [];
80
81 for (let i = 0; i < lines.length; ) {
82 if (!lines[i].startsWith("commit")) {
83 i++;
84 continue;
85 }
86
87 const [, hash] = lines[i++].split(" ");
88 const headerProps = {};
89
90 while (
91 i < lines.length &&
92 lines[i].trim() !== "" &&
93 !/^\s/.test(lines[i])
94 ) {
95 const colonIndex = lines[i].indexOf(":");
96 const name = lines[i].slice(0, colonIndex).toLowerCase();
97 const value = lines[i].slice(colonIndex + 1).trim();
98 headerProps[name] = value;
99 i++;
100 }
101
102 const messageLines = [];
103
104 while (i < lines.length && !lines[i].startsWith("commit")) {
105 const lineToPush = lines[i++].replace(/^ /, "");
106
107 if (!lineToPush) {
108 continue;
109 }
110
111 messageLines.push(lineToPush);
112 }
113
114 let mindex = messageLines.length - 1;
115 const footerProps = {};
116 const validFooterProps = [
117 "signed-off-by",
118 "co-authored-by",
119 "on-behalf-of",
120 ];
121
122 while (
123 mindex >= 1 &&
124 /^[A-Za-z0-9-]+: /.test(messageLines.at(mindex))
125 ) {
126 const messageLine = messageLines[mindex--];
127 const colonIndex = messageLine.indexOf(":");
128 const name = messageLine.slice(0, colonIndex).toLowerCase();
129
130 if (!validFooterProps.includes(name)) {
131 continue;
132 }
133
134 const value = messageLine.slice(colonIndex + 1).trim();
135
136 if (name in footerProps && !Array.isArray(footerProps[name])) {
137 footerProps[name] = [footerProps[name]];
138 }
139
140 if (Array.isArray(footerProps[name])) {
141 footerProps[name].push(value);
142 } else {
143 footerProps[name] = value;
144 }
145
146 messageLines.splice(mindex - 1, 1);
147 }
148
149 const message = messageLines.join("\n");
150
151 commits.push({
152 hash,
153 message,
154 headerProps,
155 footerProps,
156 signedOffBy: footerProps["signed-off-by"],
157 onBehalfOf: footerProps["on-behalf-of"],
158 author: headerProps["author"],
159 createdAt: new Date(headerProps["date"]),
160 });
161 }
162
163 return commits;
164 }
165
166 function generateChangelog(commits) {
167 let output = "";
168
169 const grouppedCommitsByDate = {};
170
171 for (const commit of commits) {
172 const key = `${commit.createdAt
173 .getUTCDate()
174 .toString()
175 .padStart(2, 0)}-${commit.createdAt
176 .getUTCMonth()
177 .toString()
178 .padStart(2, "0")}-${commit.createdAt.getUTCFullYear()}::${
179 Array.isArray(commit.author)
180 ? commit.author.join(":")
181 : commit.author
182 }`;
183 grouppedCommitsByDate[key] ??= [];
184 grouppedCommitsByDate[key].push(commit);
185 }
186
187 for (const key in grouppedCommitsByDate) {
188 const separatorPosition = key.indexOf("::");
189 const date = key.slice(0, separatorPosition);
190 const commits = grouppedCommitsByDate[key];
191
192 output += `${date} ${
193 Array.isArray(commits[0].author)
194 ? commits[0].author.join(", ")
195 : commits[0].author
196 }\n\n`;
197
198 for (const commit of commits) {
199 output += ` ${commit.message.replaceAll(
200 "\n",
201 "\n "
202 )}\n\n`;
203 }
204 }
205
206 return output.trim();
207 }
208
209 function printHelp() {
210 console.log("Usage:");
211 console.log(` ${ME} [OPTION]...`);
212 console.log("Generate a formatted ChangeLog from Git commit logs.");
213 console.log();
214 console.log("Options:");
215 console.log(" -h, --help Show this help and exit.");
216 console.log(" -v, --version Show this script's version.");
217 console.log(" -o, --output=[FILE] Write the generated changelog to");
218 console.log(" a file instead of standard output.");
219 console.log(" --no-overwrite Disallow overwriting of the output");
220 console.log(" file if it exists already.");
221 console.log();
222 console.log("Send general inquiries, questions and bug reports");
223 console.log("to <[email protected]>.");
224 }
225
226 function printVersion() {
227 console.log("Copyright (C) 2024 OSN, Inc.");
228 console.log(
229 "License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>."
230 );
231 console.log(
232 "This is free software: you are free to change and redistribute it."
233 );
234 console.log("There is NO WARRANTY, to the extent permitted by law.");
235 console.log();
236 console.log("Written by Ar Rakin.");
237 }
238
239 async function main() {
240 let options;
241
242 try {
243 options = parseArgs({
244 argv: process.argv.slice(1),
245 strict: true,
246 allowPositionals: false,
247 options: {
248 help: {
249 type: "boolean",
250 alias: "h",
251 },
252 version: {
253 type: "boolean",
254 alias: "v",
255 },
256 output: {
257 type: "string",
258 short: "o",
259 },
260 "no-overwrite": {
261 type: "boolean",
262 },
263 },
264 }).values;
265 } catch (error) {
266 perror(`${error?.message ?? error}`);
267 exit(1);
268 }
269
270 if (options.help) {
271 printHelp();
272 exit(0);
273 }
274
275 if (options.version) {
276 printVersion();
277 exit(0);
278 }
279
280 if (!options.output && options["no-overwrite"]) {
281 perror(
282 "option `--no-overwrite' without `--output` does not make sense"
283 );
284 exit(1);
285 }
286
287 if (
288 options.output &&
289 options["no-overwrite"] &&
290 existsSync(options.output)
291 ) {
292 perror(`${options.output}: cannot write changelog: File exists`);
293 exit(1);
294 }
295
296 const gitPath = checkForGit();
297 const gitLog = getGitLog(gitPath);
298 const commits = parseGitLog(gitLog);
299 const filteredCommits = commits.filter(
300 (commit) =>
301 !/Merge pull request #\d+ from|Merge branch '\S+' of/.test(
302 commit.message
303 )
304 );
305 const changelog = generateChangelog(filteredCommits);
306
307 if (options.output) {
308 try {
309 await writeFile(options.output, changelog);
310 } catch (error) {
311 perror(
312 `${options.output}: failed to write changelog: ${
313 error?.message ?? error
314 }`
315 );
316 exit(1);
317 }
318
319 print(`wrote generated changelog to ${options.output}`);
320 } else {
321 console.log(changelog);
322 }
323 }
324
325 main();

Properties

Name Value
svn:executable *

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26