37 |
} |
} |
38 |
|
|
39 |
function findInPath(executable) { |
function findInPath(executable) { |
40 |
for (const segment of process.env.PATH?.split(process.platform === "win32" ? ";" : ":") ?? []) { |
for (const segment of process.env.PATH?.split( |
41 |
const executablePath = path.join(segment, executable + (process.platform === "win32" ? ".exe" : "")); |
process.platform === "win32" ? ";" : ":" |
42 |
|
) ?? []) { |
43 |
|
const executablePath = path.join( |
44 |
|
segment, |
45 |
|
executable + (process.platform === "win32" ? ".exe" : "") |
46 |
|
); |
47 |
|
|
48 |
if (existsSync(executablePath)) { |
if (existsSync(executablePath)) { |
49 |
return executablePath; |
return executablePath; |
50 |
} |
} |
68 |
function getGitLog(gitPath) { |
function getGitLog(gitPath) { |
69 |
try { |
try { |
70 |
return execSync(gitPath + " --no-pager log", { encoding: "utf8" }); |
return execSync(gitPath + " --no-pager log", { encoding: "utf8" }); |
71 |
} |
} catch { |
|
catch { |
|
72 |
perror("command `git --no-pager log' failed"); |
perror("command `git --no-pager log' failed"); |
73 |
exit(1); |
exit(1); |
74 |
} |
} |
84 |
continue; |
continue; |
85 |
} |
} |
86 |
|
|
87 |
const [, hash] = lines[i++].split(' '); |
const [, hash] = lines[i++].split(" "); |
88 |
const headerProps = {}; |
const headerProps = {}; |
89 |
|
|
90 |
while (i < lines.length && lines[i].trim() !== "" && !/^\s/.test(lines[i])) { |
while ( |
91 |
|
i < lines.length && |
92 |
|
lines[i].trim() !== "" && |
93 |
|
!/^\s/.test(lines[i]) |
94 |
|
) { |
95 |
const colonIndex = lines[i].indexOf(":"); |
const colonIndex = lines[i].indexOf(":"); |
96 |
const name = lines[i].slice(0, colonIndex).toLowerCase(); |
const name = lines[i].slice(0, colonIndex).toLowerCase(); |
97 |
const value = lines[i].slice(colonIndex + 1).trim(); |
const value = lines[i].slice(colonIndex + 1).trim(); |
107 |
if (!lineToPush) { |
if (!lineToPush) { |
108 |
continue; |
continue; |
109 |
} |
} |
110 |
|
|
111 |
messageLines.push(lineToPush); |
messageLines.push(lineToPush); |
112 |
} |
} |
113 |
|
|
114 |
let mindex = messageLines.length - 1; |
let mindex = messageLines.length - 1; |
115 |
const footerProps = {}; |
const footerProps = {}; |
116 |
const validFooterProps = ["signed-off-by", "co-authored-by", "on-behalf-of"]; |
const validFooterProps = [ |
117 |
|
"signed-off-by", |
118 |
while (mindex >= 1 && /^[A-Za-z0-9-]+: /.test(messageLines.at(mindex))) { |
"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--]; |
const messageLine = messageLines[mindex--]; |
127 |
const colonIndex = messageLine.indexOf(":"); |
const colonIndex = messageLine.indexOf(":"); |
128 |
const name = messageLine.slice(0, colonIndex).toLowerCase(); |
const name = messageLine.slice(0, colonIndex).toLowerCase(); |
130 |
if (!validFooterProps.includes(name)) { |
if (!validFooterProps.includes(name)) { |
131 |
continue; |
continue; |
132 |
} |
} |
133 |
|
|
134 |
const value = messageLine.slice(colonIndex + 1).trim(); |
const value = messageLine.slice(colonIndex + 1).trim(); |
135 |
|
|
136 |
if (name in footerProps && !Array.isArray(footerProps[name])) { |
if (name in footerProps && !Array.isArray(footerProps[name])) { |
139 |
|
|
140 |
if (Array.isArray(footerProps[name])) { |
if (Array.isArray(footerProps[name])) { |
141 |
footerProps[name].push(value); |
footerProps[name].push(value); |
142 |
} |
} else { |
|
else { |
|
143 |
footerProps[name] = value; |
footerProps[name] = value; |
144 |
} |
} |
145 |
|
|
147 |
} |
} |
148 |
|
|
149 |
const message = messageLines.join("\n"); |
const message = messageLines.join("\n"); |
150 |
|
|
151 |
commits.push({ |
commits.push({ |
152 |
hash, |
hash, |
153 |
message, |
message, |
156 |
signedOffBy: footerProps["signed-off-by"], |
signedOffBy: footerProps["signed-off-by"], |
157 |
onBehalfOf: footerProps["on-behalf-of"], |
onBehalfOf: footerProps["on-behalf-of"], |
158 |
author: headerProps["author"], |
author: headerProps["author"], |
159 |
createdAt: new Date(headerProps["date"]) |
createdAt: new Date(headerProps["date"]), |
160 |
}); |
}); |
161 |
} |
} |
162 |
|
|
165 |
|
|
166 |
function generateChangelog(commits) { |
function generateChangelog(commits) { |
167 |
let output = ""; |
let output = ""; |
168 |
|
|
169 |
const grouppedCommitsByDate = {}; |
const grouppedCommitsByDate = {}; |
170 |
|
|
171 |
for (const commit of commits) { |
for (const commit of commits) { |
172 |
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}`; |
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] ??= []; |
grouppedCommitsByDate[key] ??= []; |
184 |
grouppedCommitsByDate[key].push(commit); |
grouppedCommitsByDate[key].push(commit); |
185 |
} |
} |
189 |
const date = key.slice(0, separatorPosition); |
const date = key.slice(0, separatorPosition); |
190 |
const commits = grouppedCommitsByDate[key]; |
const commits = grouppedCommitsByDate[key]; |
191 |
|
|
192 |
output += `${date} ${Array.isArray(commits[0].author) ? commits[0].author.join(", ") : commits[0].author}\n\n`; |
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) { |
for (const commit of commits) { |
199 |
output += ` ${commit.message.replaceAll("\n", "\n ")}\n\n`; |
output += ` ${commit.message.replaceAll( |
200 |
|
"\n", |
201 |
|
"\n " |
202 |
|
)}\n\n`; |
203 |
} |
} |
204 |
} |
} |
205 |
|
|
206 |
return output.trim(); |
return output.trim(); |
207 |
} |
} |
208 |
|
|
225 |
|
|
226 |
function printVersion() { |
function printVersion() { |
227 |
console.log("Copyright (C) 2024 OSN, Inc."); |
console.log("Copyright (C) 2024 OSN, Inc."); |
228 |
console.log("License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>."); |
console.log( |
229 |
console.log("This is free software: you are free to change and redistribute it."); |
"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."); |
console.log("There is NO WARRANTY, to the extent permitted by law."); |
235 |
console.log(); |
console.log(); |
236 |
console.log("Written by Ar Rakin."); |
console.log("Written by Ar Rakin."); |
247 |
options: { |
options: { |
248 |
help: { |
help: { |
249 |
type: "boolean", |
type: "boolean", |
250 |
alias: 'h' |
alias: "h", |
251 |
}, |
}, |
252 |
version: { |
version: { |
253 |
type: "boolean", |
type: "boolean", |
254 |
alias: "v" |
alias: "v", |
255 |
}, |
}, |
256 |
output: { |
output: { |
257 |
type: "string", |
type: "string", |
258 |
short: "o" |
short: "o", |
259 |
}, |
}, |
260 |
"no-overwrite": { |
"no-overwrite": { |
261 |
type: "boolean" |
type: "boolean", |
262 |
} |
}, |
263 |
} |
}, |
264 |
}).values; |
}).values; |
265 |
} |
} catch (error) { |
|
catch (error) { |
|
266 |
perror(`${error?.message ?? error}`); |
perror(`${error?.message ?? error}`); |
267 |
exit(1); |
exit(1); |
268 |
} |
} |
278 |
} |
} |
279 |
|
|
280 |
if (!options.output && options["no-overwrite"]) { |
if (!options.output && options["no-overwrite"]) { |
281 |
perror("option `--no-overwrite' without `--output` does not make sense"); |
perror( |
282 |
|
"option `--no-overwrite' without `--output` does not make sense" |
283 |
|
); |
284 |
exit(1); |
exit(1); |
285 |
} |
} |
286 |
|
|
287 |
if (options.output && options["no-overwrite"] && existsSync(options.output)) { |
if ( |
288 |
|
options.output && |
289 |
|
options["no-overwrite"] && |
290 |
|
existsSync(options.output) |
291 |
|
) { |
292 |
perror(`${options.output}: cannot write changelog: File exists`); |
perror(`${options.output}: cannot write changelog: File exists`); |
293 |
exit(1); |
exit(1); |
294 |
} |
} |
295 |
|
|
296 |
const gitPath = checkForGit(); |
const gitPath = checkForGit(); |
297 |
const gitLog = getGitLog(gitPath); |
const gitLog = getGitLog(gitPath); |
298 |
const commits = parseGitLog(gitLog); |
const commits = parseGitLog(gitLog); |
299 |
const changelog = generateChangelog(commits); |
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) { |
if (options.output) { |
308 |
try { |
try { |
309 |
await writeFile(options.output, changelog); |
await writeFile(options.output, changelog); |
310 |
} |
} catch (error) { |
311 |
catch (error) { |
perror( |
312 |
perror(`${options.output}: failed to write changelog: ${error?.message ?? error}`); |
`${options.output}: failed to write changelog: ${ |
313 |
|
error?.message ?? error |
314 |
|
}` |
315 |
|
); |
316 |
exit(1); |
exit(1); |
317 |
} |
} |
318 |
|
|
319 |
print(`wrote generated changelog to ${options.output}`); |
print(`wrote generated changelog to ${options.output}`); |
320 |
} |
} else { |
|
else { |
|
321 |
console.log(changelog); |
console.log(changelog); |
322 |
} |
} |
323 |
} |
} |