Compare commits

...

4 Commits

7 changed files with 157 additions and 54 deletions

View File

@ -4,6 +4,11 @@ project:
with_stats: true with_stats: true
ticket_url: null ticket_url: null
releases: releases:
- version: NEXT
changes:
- add `git-gather` command
- type: feat
desc: add 'next' to alias to 'upcoming'
- version: 0.1.0 - version: 0.1.0
changes: changes:
- port the core of the Perl changelord to JavaScript. - port the core of the Perl changelord to JavaScript.

View File

@ -82,5 +82,30 @@ Outputs the latest non-NEXT release.
$ changelord latest-version $ changelord latest-version
3.2.0 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
^(?<type>[^: ]+):(?<desc>.*?)(\[(?<ticket>[^\]]+)\])?$
The regular expression must capture a `desc` field, and may
capture a `type` and `ticket` as well.
[blog]: https://techblog.babyl.ca/entry/changelord/ [blog]: https://techblog.babyl.ca/entry/changelord/
[original]: https://metacpan.org/dist/App-Changelord/view/bin/changelord [original]: https://metacpan.org/dist/App-Changelord/view/bin/changelord

View File

@ -6,20 +6,46 @@ import { join } from "path";
import yaml from "yaml"; import yaml from "yaml";
import fs from "fs-extra"; import fs from "fs-extra";
import consola from "consola"; import consola from "consola";
import u from "@yanick/updeep-remeda";
import print from "./command/print.js"; import print from "./command/print.js";
import init from "./command/init.js"; import init from "./command/init.js";
import schema from "./command/schema.js"; import schema from "./command/schema.js";
import add from "./command/add.js"; import add from "./command/add.js";
import cut from "./command/cut.js"; import cut from "./command/cut.js";
import upcoming from "./command/upcoming.js"; import upcoming, { next_release } from "./command/upcoming.js";
import latest from "./command/latest-version.js"; import latest, { latest_version } from "./command/latest-version.js";
import git_gather from "./command/git-gather.js";
consola.raw = (...args) => console.log(...args); consola.raw = (...args) => console.log(...args);
yargs(hideBin(process.argv)) yargs(hideBin(process.argv))
.config({ .middleware((config) => {
consola, 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")) .default("source", join(process.cwd(), "CHANGELOG.yml"))
.describe("source", "changelog source") .describe("source", "changelog source")
@ -29,7 +55,13 @@ yargs(hideBin(process.argv))
.command(cut) .command(cut)
.command(schema) .command(schema)
.command(upcoming) .command(upcoming)
.command({
...upcoming,
command: "next",
desc: 'alias for "upcoming"',
})
.command(latest) .command(latest)
.command(git_gather)
.strictCommands() .strictCommands()
.help() .help()
.parse(); .parse();

View File

@ -28,26 +28,7 @@ export default {
.string("ticket") .string("ticket")
.describe("ticket", "ticket associated with the change") .describe("ticket", "ticket associated with the change")
.string("type") .string("type")
.describe("type", "type of change") .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));
};
});
}, },
handler, handler,
}; };

51
src/command/git-gather.js Normal file
View File

@ -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) ??
/^(?<type>[^: ]+):(?<desc>.*?)(\[(?<ticket>[^\]]+)\])?$/
);
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,
};

View File

@ -1,27 +1,29 @@
import yaml from "yaml"; import yaml from "yaml";
import fs from "fs-extra"; 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 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( if (config.json) {
({ version }) => version !== "NEXT" 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 { export default {
command: "latest-version", command: "latest-version",
desc: "output the latest version present in the changelog", desc: "output the latest version present in the changelog",
builder: (yargs) => { builder: (yargs) => {
yargs yargs
.boolean("json") .boolean("json")
.describe("json", "output latest version entry as json"); .describe("json", "output latest version entry as json");
}, },
handler, handler,
}; };

View File

@ -3,22 +3,29 @@ import fs from "fs-extra";
import yaml from "yaml"; import yaml from "yaml";
import { render_release } from "./print.js"; 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 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( const res = render_release(
{ ...config, next: true }, { ...config, next: true },
source source
)(source.releases.find(u.matches({ version: "NEXT" }))); )(source.releases.find(u.matches({ version: "NEXT" })));
config.consola.raw("\n" + res.body); config.consola.raw("\n" + res.body);
}; };
export default { export default {
command: "upcoming", command: "upcoming",
desc: "output the changes in NEXT", desc: "output the changes in NEXT",
builder: (yargs) => { handler,
yargs;
},
handler,
}; };