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

Contents of /trunk/git/genchangelog

Parent Directory Parent Directory | Revision Log Revision Log


Revision 34 - (show annotations)
Thu Aug 8 18:23:21 2024 UTC (7 months, 3 weeks ago) by rakinar2
File size: 9074 byte(s)
fix(genchangelog): unexpected zero based month numbers
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.getUTCMonth() + 1)
176 .toString()
177 .padStart(2, "0")}-${commit.createdAt.getUTCFullYear()}::${
178 Array.isArray(commit.author)
179 ? commit.author.join(":")
180 : commit.author
181 }`;
182 grouppedCommitsByDate[key] ??= [];
183 grouppedCommitsByDate[key].push(commit);
184 }
185
186 for (const key in grouppedCommitsByDate) {
187 const separatorPosition = key.indexOf("::");
188 const date = key.slice(0, separatorPosition);
189 const commits = grouppedCommitsByDate[key];
190
191 output += `${date} ${
192 Array.isArray(commits[0].author)
193 ? commits[0].author.join(", ")
194 : commits[0].author
195 }\n\n`;
196
197 for (const commit of commits) {
198 output += ` ${commit.message.replaceAll(
199 "\n",
200 "\n "
201 )}\n\n`;
202 }
203 }
204
205 return output.trim();
206 }
207
208 function printHelp() {
209 console.log("Usage:");
210 console.log(` ${ME} [OPTION]...`);
211 console.log("Generate a formatted ChangeLog from Git commit logs.");
212 console.log();
213 console.log("Options:");
214 console.log(" -h, --help Show this help and exit.");
215 console.log(" -v, --version Show this script's version.");
216 console.log(" -o, --output=[FILE] Write the generated changelog to");
217 console.log(" a file instead of standard output.");
218 console.log(" --no-overwrite Disallow overwriting of the output");
219 console.log(" file if it exists already.");
220 console.log();
221 console.log("Send general inquiries, questions and bug reports");
222 console.log("to <[email protected]>.");
223 }
224
225 function printVersion() {
226 console.log("Copyright (C) 2024 OSN, Inc.");
227 console.log(
228 "License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>."
229 );
230 console.log(
231 "This is free software: you are free to change and redistribute it."
232 );
233 console.log("There is NO WARRANTY, to the extent permitted by law.");
234 console.log();
235 console.log("Written by Ar Rakin.");
236 }
237
238 async function main() {
239 let options;
240
241 try {
242 options = parseArgs({
243 argv: process.argv.slice(1),
244 strict: true,
245 allowPositionals: false,
246 options: {
247 help: {
248 type: "boolean",
249 alias: "h",
250 },
251 version: {
252 type: "boolean",
253 alias: "v",
254 },
255 output: {
256 type: "string",
257 short: "o",
258 },
259 "no-overwrite": {
260 type: "boolean",
261 },
262 },
263 }).values;
264 } catch (error) {
265 perror(`${error?.message ?? error}`);
266 exit(1);
267 }
268
269 if (options.help) {
270 printHelp();
271 exit(0);
272 }
273
274 if (options.version) {
275 printVersion();
276 exit(0);
277 }
278
279 if (!options.output && options["no-overwrite"]) {
280 perror(
281 "option `--no-overwrite' without `--output` does not make sense"
282 );
283 exit(1);
284 }
285
286 if (
287 options.output &&
288 options["no-overwrite"] &&
289 existsSync(options.output)
290 ) {
291 perror(`${options.output}: cannot write changelog: File exists`);
292 exit(1);
293 }
294
295 const gitPath = checkForGit();
296 const gitLog = getGitLog(gitPath);
297 const commits = parseGitLog(gitLog);
298 const filteredCommits = commits.filter(
299 (commit) =>
300 !/Merge pull request #\d+ from|Merge branch '\S+' of/.test(
301 commit.message
302 )
303 );
304 const changelog = generateChangelog(filteredCommits);
305
306 if (options.output) {
307 try {
308 await writeFile(options.output, changelog);
309 } catch (error) {
310 perror(
311 `${options.output}: failed to write changelog: ${
312 error?.message ?? error
313 }`
314 );
315 exit(1);
316 }
317
318 print(`wrote generated changelog to ${options.output}`);
319 } else {
320 console.log(changelog);
321 }
322 }
323
324 main();

Properties

Name Value
svn:executable *

[email protected]
ViewVC Help
Powered by ViewVC 1.1.26