From d9f4244e3ef9a8982b74f0926957da0e6b330b4d Mon Sep 17 00:00:00 2001 From: Yanick Champoux Date: Thu, 18 May 2023 11:09:33 -0400 Subject: [PATCH] add 'git-gather' command --- CHANGELOG.yml | 3 +++ README.md | 25 +++++++++++++++++ src/changelord.js | 35 +++++++++++++++++++++--- src/command/add.js | 21 +-------------- src/command/git-gather.js | 51 +++++++++++++++++++++++++++++++++++ src/command/latest-version.js | 38 +++++++++++++------------- src/command/upcoming.js | 34 ++++++++++++++--------- 7 files changed, 153 insertions(+), 54 deletions(-) create mode 100644 src/command/git-gather.js diff --git a/CHANGELOG.yml b/CHANGELOG.yml index 0419e87..8ed0c56 100644 --- a/CHANGELOG.yml +++ b/CHANGELOG.yml @@ -4,6 +4,9 @@ project: with_stats: true ticket_url: null releases: + - version: NEXT + changes: + - add `git-gather` command - version: 0.1.0 changes: - port the core of the Perl changelord to JavaScript. diff --git a/README.md b/README.md index 0773a06..64a828d 100644 --- a/README.md +++ b/README.md @@ -82,5 +82,30 @@ Outputs the latest non-NEXT release. $ changelord latest-version 3.2.0 +### `changelord git-gather` + +Gathers change entries from git commits. If any are found, they are +added to the changelog. + +#### Lower bound of the git log + +`git-gather` inspects the git log from the most recent of those +three points: + +- The last change in the NEXT release having a `commit` property. +- The last tagged version. +- The beginning of time. + +#### Change-like git message + +Git messages are compared to the regular expression +configured at `project.commit_regex`. If none is found, it +defaults to + + ^(?[^: ]+):(?.*?)(\[(?[^\]]+)\])?$ + +The regular expression must capture a `desc` field, and may +capture a `type` and `ticket` as well. + [blog]: https://techblog.babyl.ca/entry/changelord/ [original]: https://metacpan.org/dist/App-Changelord/view/bin/changelord diff --git a/src/changelord.js b/src/changelord.js index d35da24..37fabea 100755 --- a/src/changelord.js +++ b/src/changelord.js @@ -6,20 +6,46 @@ import { join } from "path"; import yaml from "yaml"; import fs from "fs-extra"; import consola from "consola"; +import u from "@yanick/updeep-remeda"; import print from "./command/print.js"; import init from "./command/init.js"; import schema from "./command/schema.js"; import add from "./command/add.js"; import cut from "./command/cut.js"; -import upcoming from "./command/upcoming.js"; -import latest from "./command/latest-version.js"; +import upcoming, { next_release } from "./command/upcoming.js"; +import latest, { latest_version } from "./command/latest-version.js"; +import git_gather from "./command/git-gather.js"; consola.raw = (...args) => console.log(...args); yargs(hideBin(process.argv)) - .config({ - consola, + .middleware((config) => { + config.consola = consola; + config.changelog = () => + fs.readFile(config.source, "utf-8").then(yaml.parse); + config.next_release = next_release.bind(config); + config.latest_version = latest_version.bind(config); + return config; + }) + .middleware((argv) => { + argv.add_to_next = async (entry) => { + const changelog = yaml.parse(await fs.readFile(argv.source, "utf-8")); + + let next = changelog.releases.find(u.matches({ version: "NEXT" })); + if (!next) { + next = { version: "NEXT", changes: [] }; + changelog.releases.unshift(next); + } + + if (Object.keys(entry).length === 1) { + entry = entry.desc; + } + + next.changes.push(entry); + + return fs.writeFile(argv.source, yaml.stringify(changelog)); + }; }) .default("source", join(process.cwd(), "CHANGELOG.yml")) .describe("source", "changelog source") @@ -30,6 +56,7 @@ yargs(hideBin(process.argv)) .command(schema) .command(upcoming) .command(latest) + .command(git_gather) .strictCommands() .help() .parse(); diff --git a/src/command/add.js b/src/command/add.js index e04ae40..c4a2b24 100644 --- a/src/command/add.js +++ b/src/command/add.js @@ -28,26 +28,7 @@ export default { .string("ticket") .describe("ticket", "ticket associated with the change") .string("type") - .describe("type", "type of change") - .middleware((argv) => { - argv.add_to_next = async (entry) => { - const changelog = yaml.parse(await fs.readFile(argv.source, "utf-8")); - - let next = changelog.releases.find(u.matches({ version: "NEXT" })); - if (!next) { - next = { version: "NEXT", changes: [] }; - changelog.releases.unshift(next); - } - - if (Object.keys(entry).length === 1) { - entry = entry.desc; - } - - next.changes.push(entry); - - return fs.writeFile(argv.source, yaml.stringify(changelog)); - }; - }); + .describe("type", "type of change"); }, handler, }; diff --git a/src/command/git-gather.js b/src/command/git-gather.js new file mode 100644 index 0000000..45b792e --- /dev/null +++ b/src/command/git-gather.js @@ -0,0 +1,51 @@ +import simpleGit from "simple-git"; +import { prop, compact, pickBy } from "remeda"; + +const handler = async (config) => { + const next = await config.next_release(); + const seen_sha1s = compact(next?.changes.map(prop("commit"))); + + const git = simpleGit(); + + const { version } = await config.latest_version(); + + let { all: commits } = await git.log({ + from: "v" + version, + }); + + config.consola.start(`gathering changes since v${version}`); + + commits = commits.filter(({ hash }) => !seen_sha1s.includes(hash)); + + const regex = new RegExp( + (await config.changelog().project?.commit_regex) ?? + /^(?[^: ]+):(?.*?)(\[(?[^\]]+)\])?$/ + ); + + const changes = commits + .map((commit) => [commit, commit.message.match(regex)]) + .filter((x) => x[1]) + .map(([commit, res]) => + pickBy({ ...res.groups, commit: commit.hash }, (x) => x) + ); + + if (changes.length === 0) { + config.consola.success("no changes detected"); + return; + } + + for (const change of changes) { + config.consola.info(`${change.type}: ${change.desc}`); + config.add_to_next(change); + } + config.consola.success("done"); +}; + +export default { + command: "git-gather", + desc: "gather change entries from git commits", + builder: (yargs) => { + yargs; + }, + handler, +}; diff --git a/src/command/latest-version.js b/src/command/latest-version.js index 4519ed2..88e0a4e 100644 --- a/src/command/latest-version.js +++ b/src/command/latest-version.js @@ -1,27 +1,29 @@ import yaml from "yaml"; import fs from "fs-extra"; +export async function latest_version() { + const changelog = await this.changelog(); + + return changelog.releases.find(({ version }) => version !== "NEXT"); +} + const handler = async (config) => { - const changelog = yaml.parse(await fs.readFile(config.source, "utf-8")); + const latest_version = config.latest_version()?.version ?? "0.0.0"; - const latest_version = changelog.releases.find( - ({ version }) => version !== "NEXT" - ); - - if (config.json) { - config.consola.raw(latest_version); - } else { - config.consola.raw(latest_version.version); - } + if (config.json) { + config.consola.raw(latest_version); + } else { + config.consola.raw(latest_version.version); + } }; export default { - command: "latest-version", - desc: "output the latest version present in the changelog", - builder: (yargs) => { - yargs - .boolean("json") - .describe("json", "output latest version entry as json"); - }, - handler, + command: "latest-version", + desc: "output the latest version present in the changelog", + builder: (yargs) => { + yargs + .boolean("json") + .describe("json", "output latest version entry as json"); + }, + handler, }; diff --git a/src/command/upcoming.js b/src/command/upcoming.js index 9899ab4..0bb1d9a 100644 --- a/src/command/upcoming.js +++ b/src/command/upcoming.js @@ -3,22 +3,32 @@ import fs from "fs-extra"; import yaml from "yaml"; import { render_release } from "./print.js"; +export async function next_release() { + const source = await fs.readFile(this.source, "utf-8").then(yaml.parse); + return ( + source.releases.find(u.matches({ version: "NEXT" })) ?? { + version: "NEXT", + changes: [], + } + ); +} + const handler = async (config) => { - const source = await fs.readFile(config.source, "utf-8").then(yaml.parse); + const source = await fs.readFile(config.source, "utf-8").then(yaml.parse); - const res = render_release( - { ...config, next: true }, - source - )(source.releases.find(u.matches({ version: "NEXT" }))); + const res = render_release( + { ...config, next: true }, + source + )(source.releases.find(u.matches({ version: "NEXT" }))); - config.consola.raw("\n" + res.body); + config.consola.raw("\n" + res.body); }; export default { - command: "upcoming", - desc: "output the changes in NEXT", - builder: (yargs) => { - yargs; - }, - handler, + command: "upcoming", + desc: "output the changes in NEXT", + builder: (yargs) => { + yargs; + }, + handler, };