Merge branch 'git-gather'

main
Yanick Champoux 2023-05-18 11:15:55 -04:00
commit 0d34b92358
7 changed files with 153 additions and 54 deletions

View File

@ -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.

View File

@ -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
^(?<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/
[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 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();

View File

@ -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,
};

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 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,
};

View File

@ -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,
};