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

Annotation of /trunk/git/genchangelog

Parent Directory Parent Directory | Revision Log Revision Log


Revision 11 - (hide annotations)
Sat Aug 3 15:50:38 2024 UTC (7 months, 4 weeks ago) by rakinar2
File size: 8434 byte(s)
chore: add project files back to trunk
1 rakinar2 11 #!/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