keepchanges 0.0.2 → 1.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -147
- package/README.zh-CN.md +181 -0
- package/dist/cli.d.mts +1 -14
- package/dist/cli.mjs +245 -233
- package/dist/commit-FX77u6Sj.mjs +190 -0
- package/dist/index.d.mts +152 -0
- package/dist/index.mjs +2 -0
- package/package.json +21 -18
package/dist/cli.mjs
CHANGED
|
@@ -1,107 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { a as insertRelease, c as defaultConfig, i as hasRelease, n as parseCommits, o as readChangelog, r as generateChangelog, s as writeChangelog } from "./commit-FX77u6Sj.mjs";
|
|
3
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
4
|
+
import { normalizeFull } from "verkit";
|
|
3
5
|
import process from "node:process";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
6
|
import { cac } from "cac";
|
|
6
7
|
import { resolve } from "node:path";
|
|
7
8
|
import ansis from "ansis";
|
|
8
|
-
import { readFile, writeFile } from "node:fs/promises";
|
|
9
|
-
import { normalizeFull } from "verkit";
|
|
10
9
|
import { x } from "tinyexec";
|
|
11
10
|
//#region package.json
|
|
12
|
-
var version = "0.0.
|
|
13
|
-
//#endregion
|
|
14
|
-
//#region src/changelog.ts
|
|
15
|
-
function createChangelog(version, commits, repository, comparisonFrom) {
|
|
16
|
-
const body = [
|
|
17
|
-
...renderSection(commits.filter((commit) => commit.isBreaking), "🚨 Breaking Changes", repository),
|
|
18
|
-
...renderSection(commits.filter((commit) => !commit.isBreaking && commit.type === "feat"), "🚀 Features", repository),
|
|
19
|
-
...renderSection(commits.filter((commit) => !commit.isBreaking && commit.type === "fix"), "🐞 Bug Fixes", repository),
|
|
20
|
-
...repository && comparisonFrom ? ["", `##### [View changes on ${repository.provider.name}](${repository.provider.compareUrl(repository, comparisonFrom, `v${version}`)})`] : []
|
|
21
|
-
].join("\n").trim();
|
|
22
|
-
return {
|
|
23
|
-
body,
|
|
24
|
-
release: [
|
|
25
|
-
`## v${version}`,
|
|
26
|
-
"",
|
|
27
|
-
body,
|
|
28
|
-
""
|
|
29
|
-
].join("\n")
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
async function readChangelog(path) {
|
|
33
|
-
return readFile(path, "utf8").catch((error) => {
|
|
34
|
-
if (error.code === "ENOENT") return "# Changelog\n";
|
|
35
|
-
throw error;
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
function writeChangelog(path, changelog) {
|
|
39
|
-
return writeFile(path, changelog);
|
|
40
|
-
}
|
|
41
|
-
function hasRelease(changelog, version) {
|
|
42
|
-
const releaseVersion = normalizeFull(version);
|
|
43
|
-
return releaseHeadings(changelog).some((heading) => heading.version === releaseVersion);
|
|
44
|
-
}
|
|
45
|
-
function insertRelease(changelog, release) {
|
|
46
|
-
const releaseVersion = releaseHeadings(release)[0]?.version;
|
|
47
|
-
const headings = releaseHeadings(changelog);
|
|
48
|
-
const existingReleaseIndex = headings.findIndex((heading) => heading.version === releaseVersion);
|
|
49
|
-
if (existingReleaseIndex !== -1) {
|
|
50
|
-
const start = headings[existingReleaseIndex].index;
|
|
51
|
-
const end = headings[existingReleaseIndex + 1]?.index ?? changelog.length;
|
|
52
|
-
changelog = changelog.slice(0, start) + changelog.slice(end);
|
|
53
|
-
}
|
|
54
|
-
const firstRelease = releaseHeadings(changelog)[0];
|
|
55
|
-
if (!firstRelease) return `${changelog.trimEnd()}\n\n${release.trim()}\n`;
|
|
56
|
-
const preamble = changelog.slice(0, firstRelease.index).trimEnd();
|
|
57
|
-
const history = changelog.slice(firstRelease.index).trim();
|
|
58
|
-
return `${preamble}\n\n${release.trim()}\n\n${history}\n`;
|
|
59
|
-
}
|
|
60
|
-
function releaseHeadings(changelog) {
|
|
61
|
-
return [...changelog.matchAll(/^##\s+(\S+).*$/gm)].flatMap((heading) => {
|
|
62
|
-
const version = normalizeFull(heading[1]);
|
|
63
|
-
return version ? [{
|
|
64
|
-
index: heading.index,
|
|
65
|
-
version
|
|
66
|
-
}] : [];
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
function renderSection(commits, title, repository) {
|
|
70
|
-
const lines = commits.map((commit) => {
|
|
71
|
-
const scope = commit.scope ? `**${escapeHtml(commit.scope)}**: ` : "";
|
|
72
|
-
const reference = repository ? `[<samp>(${commit.hash.slice(0, 5)})</samp>](${repository.provider.commitUrl(repository, commit.hash)})` : "";
|
|
73
|
-
const authorNames = commit.authors.map((author) => author.login ? `@${author.login}` : `**${escapeHtml(author.name)}**`);
|
|
74
|
-
const authors = authorNames.length > 1 ? `${authorNames.slice(0, -1).join(", ")} and ${authorNames.at(-1)}` : authorNames[0] || "";
|
|
75
|
-
const pullRequests = commit.pullRequests.map((pullRequest) => repository ? `[${pullRequest}](${repository.provider.pullRequestUrl(repository, pullRequest)})` : pullRequest).join(", ");
|
|
76
|
-
const details = [
|
|
77
|
-
authors ? `by ${authors}` : "",
|
|
78
|
-
pullRequests ? `in ${pullRequests}` : "",
|
|
79
|
-
reference
|
|
80
|
-
].filter(Boolean).join(" ");
|
|
81
|
-
const suffix = details ? ` - ${details}` : "";
|
|
82
|
-
return `${scope}${escapeHtml(capitalize$1(commit.description))}${suffix}`;
|
|
83
|
-
}).reverse();
|
|
84
|
-
if (!lines.length) return [];
|
|
85
|
-
return [
|
|
86
|
-
"",
|
|
87
|
-
`### ${title}`,
|
|
88
|
-
"",
|
|
89
|
-
...lines.map((line) => `- ${line}`)
|
|
90
|
-
];
|
|
91
|
-
}
|
|
92
|
-
function capitalize$1(value) {
|
|
93
|
-
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
94
|
-
}
|
|
95
|
-
function escapeHtml(value) {
|
|
96
|
-
const htmlEntities = {
|
|
97
|
-
"&": "&",
|
|
98
|
-
"<": "<",
|
|
99
|
-
">": ">",
|
|
100
|
-
"\"": """,
|
|
101
|
-
"'": "'"
|
|
102
|
-
};
|
|
103
|
-
return value.replace(/[&<>"']/g, (character) => htmlEntities[character]);
|
|
104
|
-
}
|
|
11
|
+
var version = "1.0.0-beta.1";
|
|
105
12
|
//#endregion
|
|
106
13
|
//#region src/git.ts
|
|
107
14
|
async function git(cwd, ...args) {
|
|
@@ -130,11 +37,8 @@ async function getRemoteTagCommit(cwd, tag) {
|
|
|
130
37
|
const refs = (await git(cwd, "ls-remote", "--tags", "origin", `refs/tags/${tag}`, `refs/tags/${tag}^{}`)).trim().split("\n").filter(Boolean);
|
|
131
38
|
return (refs.find((line) => line.endsWith("^{}")) || refs[0])?.split(/\s+/)[0];
|
|
132
39
|
}
|
|
133
|
-
async function
|
|
134
|
-
|
|
135
|
-
}
|
|
136
|
-
function parseGitLog(log) {
|
|
137
|
-
const fields = log.split("\0");
|
|
40
|
+
async function readGitCommits(cwd, from, to) {
|
|
41
|
+
const fields = (await git(cwd, "log", from ? `${from}..${to}` : to, "--format=%h%x00%an%x00%ae%x00%s%x00%b%x00")).split("\0");
|
|
138
42
|
const commits = [];
|
|
139
43
|
for (let index = 0; index + 4 < fields.length; index += 5) {
|
|
140
44
|
const subject = fields[index + 3].trim();
|
|
@@ -150,38 +54,100 @@ function parseGitLog(log) {
|
|
|
150
54
|
}
|
|
151
55
|
return commits;
|
|
152
56
|
}
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/repositories/gitea.ts
|
|
59
|
+
const giteaRepository = {
|
|
60
|
+
name: "Gitea",
|
|
61
|
+
parse(source) {
|
|
62
|
+
try {
|
|
63
|
+
const url = new URL(source.replace(/^git\+/, ""));
|
|
64
|
+
if (!/^https?:$/.test(url.protocol)) return;
|
|
65
|
+
const path = url.pathname.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
|
|
66
|
+
if (!path.includes("/")) return;
|
|
67
|
+
return {
|
|
68
|
+
provider: giteaRepository,
|
|
69
|
+
path,
|
|
70
|
+
webUrl: `${url.origin}/${path}`
|
|
71
|
+
};
|
|
72
|
+
} catch {}
|
|
73
|
+
},
|
|
74
|
+
token(explicit, env) {
|
|
75
|
+
return explicit || env.GITEA_TOKEN;
|
|
76
|
+
},
|
|
77
|
+
commitUrl(repository, hash) {
|
|
78
|
+
return `${repository.webUrl}/commit/${hash}`;
|
|
79
|
+
},
|
|
80
|
+
pullRequestUrl(repository, reference) {
|
|
81
|
+
return `${repository.webUrl}/pulls/${reference.replace(/^#/, "")}`;
|
|
82
|
+
},
|
|
83
|
+
compareUrl(repository, from, to) {
|
|
84
|
+
return `${repository.webUrl}/compare/${from}...${to}`;
|
|
85
|
+
},
|
|
86
|
+
manualReleaseUrl(repository, release) {
|
|
87
|
+
const url = new URL(`${repository.webUrl}/releases/new`);
|
|
88
|
+
url.search = new URLSearchParams({ tag: release.tag }).toString();
|
|
89
|
+
return url.toString();
|
|
90
|
+
},
|
|
91
|
+
async resolveAuthors(commits, repository, token, fetch) {
|
|
92
|
+
const headers = {
|
|
93
|
+
accept: "application/json",
|
|
94
|
+
authorization: `token ${token}`
|
|
95
|
+
};
|
|
96
|
+
const commitsByEmail = new Map(commits.flatMap((commit) => commit.authors[0] ? [[commit.authors[0].email, commit]] : []));
|
|
97
|
+
const loginsByEmail = /* @__PURE__ */ new Map();
|
|
98
|
+
await Promise.all([...commitsByEmail].map(async ([email, commit]) => {
|
|
99
|
+
try {
|
|
100
|
+
const response = await fetch(`${new URL(repository.webUrl).origin}/api/v1/repos/${repository.path}/git/commits/${commit.hash}`, { headers });
|
|
101
|
+
if (!response.ok) return;
|
|
102
|
+
const data = await response.json();
|
|
103
|
+
if (data.author?.login) loginsByEmail.set(email, data.author.login);
|
|
104
|
+
} catch {}
|
|
105
|
+
}));
|
|
106
|
+
for (const commit of commits) for (const author of commit.authors) author.login = loginsByEmail.get(author.email);
|
|
107
|
+
},
|
|
108
|
+
async publishRelease(repository, release, token, fetch) {
|
|
109
|
+
const headers = {
|
|
110
|
+
"accept": "application/json",
|
|
111
|
+
"authorization": `token ${token}`,
|
|
112
|
+
"content-type": "application/json"
|
|
113
|
+
};
|
|
114
|
+
const releasesUrl = `${new URL(repository.webUrl).origin}/api/v1/repos/${repository.path}/releases`;
|
|
115
|
+
const existing = await fetch(`${releasesUrl}/tags/${encodeURIComponent(release.tag)}`, { headers });
|
|
116
|
+
let url = releasesUrl;
|
|
117
|
+
let method = "POST";
|
|
118
|
+
let action = "created";
|
|
119
|
+
if (existing.ok) {
|
|
120
|
+
url = `${releasesUrl}/${(await existing.json()).id}`;
|
|
121
|
+
method = "PATCH";
|
|
122
|
+
action = "updated";
|
|
123
|
+
} else if (existing.status !== 404) throw new Error(`Gitea release lookup failed (${existing.status})`);
|
|
124
|
+
const response = await fetch(url, {
|
|
125
|
+
method,
|
|
126
|
+
headers,
|
|
127
|
+
body: JSON.stringify({
|
|
128
|
+
tag_name: release.tag,
|
|
129
|
+
name: release.name,
|
|
130
|
+
body: release.body,
|
|
131
|
+
prerelease: release.prerelease,
|
|
132
|
+
draft: release.draft
|
|
133
|
+
})
|
|
164
134
|
});
|
|
135
|
+
if (!response.ok) throw new Error(`Gitea release publishing failed (${response.status})`);
|
|
136
|
+
return {
|
|
137
|
+
url: (await response.json()).html_url,
|
|
138
|
+
action
|
|
139
|
+
};
|
|
165
140
|
}
|
|
166
|
-
|
|
167
|
-
hash,
|
|
168
|
-
authors: authors.filter((author) => !/\[bot\]|dependabot|\(bot\)/i.test(author.name)),
|
|
169
|
-
type: match.groups.type.toLowerCase(),
|
|
170
|
-
scope: match.groups.scope || "",
|
|
171
|
-
description: match.groups.description.replace(pullRequestPattern, "").trim(),
|
|
172
|
-
pullRequests,
|
|
173
|
-
isBreaking: Boolean(match.groups.breaking) || /^BREAKING(?: |-)CHANGE:/im.test(body)
|
|
174
|
-
};
|
|
175
|
-
}
|
|
141
|
+
};
|
|
176
142
|
//#endregion
|
|
177
|
-
//#region src/
|
|
178
|
-
const
|
|
143
|
+
//#region src/repositories/github.ts
|
|
144
|
+
const githubRepository = {
|
|
179
145
|
name: "GitHub",
|
|
180
146
|
parse(source) {
|
|
181
147
|
const match = /github\.com[:/]([^/]+\/[^/]+?)(?:\.git)?$/.exec(source);
|
|
182
148
|
if (!match) return;
|
|
183
149
|
return {
|
|
184
|
-
provider:
|
|
150
|
+
provider: githubRepository,
|
|
185
151
|
path: match[1],
|
|
186
152
|
webUrl: `https://github.com/${match[1]}`
|
|
187
153
|
};
|
|
@@ -225,8 +191,8 @@ const githubProvider = {
|
|
|
225
191
|
},
|
|
226
192
|
async publishRelease(repository, release, token, fetch) {
|
|
227
193
|
const headers = {
|
|
228
|
-
accept: "application/vnd.github+json",
|
|
229
|
-
authorization: `Bearer ${token}`,
|
|
194
|
+
"accept": "application/vnd.github+json",
|
|
195
|
+
"authorization": `Bearer ${token}`,
|
|
230
196
|
"content-type": "application/json"
|
|
231
197
|
};
|
|
232
198
|
const releasesUrl = `https://api.github.com/repos/${repository.path}/releases`;
|
|
@@ -246,7 +212,8 @@ const githubProvider = {
|
|
|
246
212
|
tag_name: release.tag,
|
|
247
213
|
name: release.name,
|
|
248
214
|
body: release.body,
|
|
249
|
-
prerelease: release.prerelease
|
|
215
|
+
prerelease: release.prerelease,
|
|
216
|
+
draft: release.draft
|
|
250
217
|
})
|
|
251
218
|
});
|
|
252
219
|
if (!response.ok) throw new Error(`GitHub release publishing failed (${response.status})`);
|
|
@@ -258,18 +225,28 @@ const githubProvider = {
|
|
|
258
225
|
};
|
|
259
226
|
//#endregion
|
|
260
227
|
//#region src/repository.ts
|
|
261
|
-
const
|
|
262
|
-
|
|
228
|
+
const repositoryProviders = {
|
|
229
|
+
github: githubRepository,
|
|
230
|
+
gitea: giteaRepository
|
|
231
|
+
};
|
|
232
|
+
async function resolveRepository(cwd, explicit) {
|
|
233
|
+
if (explicit) {
|
|
234
|
+
const repository = parseRepository(/^[^/:]+\/[^/]+$/.test(explicit) ? `https://github.com/${explicit}` : explicit, Object.values(repositoryProviders));
|
|
235
|
+
if (!repository) throw new Error(`Unsupported repository: ${explicit}`);
|
|
236
|
+
return repository;
|
|
237
|
+
}
|
|
238
|
+
let providerName = "";
|
|
263
239
|
let source = await readFile(resolve(cwd, "package.json"), "utf8").then((contents) => {
|
|
264
240
|
const repository = JSON.parse(contents).repository;
|
|
241
|
+
if (typeof repository === "object") providerName = repository?.provider?.toLowerCase() || "";
|
|
265
242
|
return typeof repository === "string" ? repository : repository?.url || "";
|
|
266
243
|
}).catch(() => "");
|
|
267
|
-
if (!source) source =
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
for (const provider of providers
|
|
244
|
+
if (!source) source = await git(cwd, "remote", "get-url", "origin").then((output) => output.trim()).catch(() => "");
|
|
245
|
+
if (providerName) return repositoryProviders[providerName]?.parse(source);
|
|
246
|
+
return parseRepository(source, [githubRepository]);
|
|
247
|
+
}
|
|
248
|
+
function parseRepository(source, providers) {
|
|
249
|
+
for (const provider of providers) {
|
|
273
250
|
const repository = provider.parse(source);
|
|
274
251
|
if (repository) return repository;
|
|
275
252
|
}
|
|
@@ -298,22 +275,41 @@ async function updateVersion(cwd, version) {
|
|
|
298
275
|
}
|
|
299
276
|
}
|
|
300
277
|
//#endregion
|
|
301
|
-
//#region src/
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
278
|
+
//#region src/cli/output.ts
|
|
279
|
+
function printChangesPreview(preview, stdout, colors) {
|
|
280
|
+
stdout([
|
|
281
|
+
colors.dim(`keep${colors.bold("changes")} v${version}`),
|
|
282
|
+
`${colors.cyan(preview.from)}${colors.dim(" -> ")}${colors.blue(preview.tag)}${colors.dim(` (${preview.commitCount} commits)`)}`,
|
|
283
|
+
colors.dim("--------------"),
|
|
284
|
+
"",
|
|
285
|
+
preview.body.replaceAll(" ", ""),
|
|
286
|
+
"",
|
|
287
|
+
colors.dim("--------------"),
|
|
288
|
+
""
|
|
289
|
+
].join("\n"));
|
|
290
|
+
}
|
|
291
|
+
function printManualReleaseUrl(repository, release, stdout, colors) {
|
|
292
|
+
const url = repository.provider.manualReleaseUrl?.(repository, release);
|
|
293
|
+
if (url) stdout(`${colors.yellow("Using the following link to create it manually:")}\n${colors.yellow(url)}\n`);
|
|
294
|
+
}
|
|
295
|
+
function printPublishedRelease(provider, result, stdout, colors) {
|
|
296
|
+
const action = result.action.charAt(0).toUpperCase() + result.action.slice(1);
|
|
297
|
+
stdout(`${colors.green(`${action} ${provider} release: ${result.url}`)}\n`);
|
|
298
|
+
}
|
|
299
|
+
//#endregion
|
|
300
|
+
//#region src/cli/createChanges.ts
|
|
301
|
+
async function createChanges(options, environment) {
|
|
302
|
+
if (options.commit && options.to !== defaultConfig.cli.to) {
|
|
303
|
+
const [toCommit, headCommit] = await Promise.all([git(environment.cwd, "rev-parse", options.to).then((value) => value.trim()), git(environment.cwd, "rev-parse", "HEAD").then((value) => value.trim())]);
|
|
304
|
+
if (toCommit !== headCommit) throw new Error("--to must resolve to HEAD when used with --commit");
|
|
305
|
+
}
|
|
307
306
|
const env = environment.env ?? process.env;
|
|
308
307
|
const stdout = environment.stdout ?? ((value) => process.stdout.write(value));
|
|
309
308
|
const colors = environment.colors ?? ansis;
|
|
310
|
-
const
|
|
311
|
-
const repository = await resolveRepository(environment.cwd);
|
|
309
|
+
const tag = `v${options.version}`;
|
|
310
|
+
const repository = await resolveRepository(environment.cwd, options.repository);
|
|
312
311
|
const token = repository?.provider.token(options.token, env);
|
|
313
|
-
|
|
314
|
-
if (!repository) throw new Error("A supported repository is required to release");
|
|
315
|
-
if (!repository.provider.publishRelease && !repository.provider.manualReleaseUrl) throw new Error(`${repository.provider.name} does not support releases`);
|
|
316
|
-
}
|
|
312
|
+
validateReleaseSupport(options, repository);
|
|
317
313
|
let taggedCommit = options.release ? await getTagCommit(environment.cwd, tag) : void 0;
|
|
318
314
|
let releaseRef = taggedCommit ? tag : void 0;
|
|
319
315
|
const remoteTaggedCommit = options.release ? await getRemoteTagCommit(environment.cwd, tag) : void 0;
|
|
@@ -326,85 +322,89 @@ async function runChangelog(options, environment) {
|
|
|
326
322
|
taggedCommit = await getTagCommit(environment.cwd, tag);
|
|
327
323
|
releaseRef = tag;
|
|
328
324
|
}
|
|
329
|
-
const from = taggedCommit ? await getPreviousTag(environment.cwd, tag, releaseRef) :
|
|
330
|
-
const to = releaseRef ||
|
|
331
|
-
const comparisonFrom = from || (repository ?
|
|
332
|
-
const commits = await
|
|
325
|
+
const from = options.from ?? (taggedCommit ? await getPreviousTag(environment.cwd, tag, releaseRef) : await getLatestTag(environment.cwd));
|
|
326
|
+
const to = releaseRef || options.to;
|
|
327
|
+
const comparisonFrom = from || (repository ? await git(environment.cwd, "rev-list", "--max-parents=0", to).then((value) => value.trim()) : "");
|
|
328
|
+
const commits = parseCommits(await readGitCommits(environment.cwd, from, to));
|
|
333
329
|
if (token && repository && !(options.release && options.dry)) await repository.provider.resolveAuthors?.(commits, repository, token, environment.fetch ?? globalThis.fetch);
|
|
334
|
-
const
|
|
330
|
+
const style = {
|
|
331
|
+
emoji: options.emoji,
|
|
332
|
+
capitalize: options.capitalize,
|
|
333
|
+
group: options.group
|
|
334
|
+
};
|
|
335
|
+
const { body, release } = generateChangelog({
|
|
336
|
+
version: options.version,
|
|
337
|
+
commits,
|
|
338
|
+
repository,
|
|
339
|
+
comparisonFrom
|
|
340
|
+
}, style);
|
|
335
341
|
const repositoryRelease = {
|
|
336
342
|
tag,
|
|
337
|
-
name: tag,
|
|
338
|
-
body
|
|
339
|
-
prerelease: version
|
|
340
|
-
|
|
341
|
-
const printReleasePreview = () => {
|
|
342
|
-
stdout([
|
|
343
|
-
colors.dim(`keep${colors.bold("changes")} v${version}`),
|
|
344
|
-
`${colors.cyan(from || comparisonFrom)}${colors.dim(" -> ")}${colors.blue(tag)}${colors.dim(` (${commits.length} commits)`)}`,
|
|
345
|
-
colors.dim("--------------"),
|
|
346
|
-
"",
|
|
347
|
-
releaseBody.replaceAll(" ", ""),
|
|
348
|
-
"",
|
|
349
|
-
colors.dim("--------------"),
|
|
350
|
-
""
|
|
351
|
-
].join("\n"));
|
|
352
|
-
};
|
|
353
|
-
const printManualReleaseUrl = () => {
|
|
354
|
-
const url = repository.provider.manualReleaseUrl?.(repository, repositoryRelease);
|
|
355
|
-
if (url) stdout(`${colors.yellow("Using the following link to create it manually:")}\n${colors.yellow(url)}\n`);
|
|
356
|
-
return url;
|
|
343
|
+
name: options.name ?? tag,
|
|
344
|
+
body,
|
|
345
|
+
prerelease: options.prerelease ?? options.version.includes("-"),
|
|
346
|
+
draft: options.draft
|
|
357
347
|
};
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
printManualReleaseUrl();
|
|
364
|
-
return;
|
|
365
|
-
}
|
|
366
|
-
const result = await repository.provider.publishRelease(repository, repositoryRelease, token, environment.fetch ?? globalThis.fetch);
|
|
367
|
-
stdout(`${colors.green(`${capitalize(result.action)} ${repository.provider.name} release: ${result.url}`)}\n`);
|
|
348
|
+
const preview = {
|
|
349
|
+
from: from || comparisonFrom,
|
|
350
|
+
tag,
|
|
351
|
+
commitCount: commits.length,
|
|
352
|
+
body
|
|
368
353
|
};
|
|
369
|
-
const outputPath = resolve(environment.cwd, options.output || "CHANGELOG.md");
|
|
370
|
-
const currentChangelog = await readChangelog(outputPath);
|
|
371
|
-
const releaseExists = hasRelease(currentChangelog, version$1);
|
|
372
|
-
const changelog = insertRelease(currentChangelog, release);
|
|
373
354
|
if (options.dry) {
|
|
355
|
+
printChangesPreview(preview, stdout, colors);
|
|
374
356
|
if (options.release) {
|
|
375
|
-
printReleasePreview();
|
|
376
357
|
stdout(`${colors.yellow("Dry run. Release skipped.")}\n\n`);
|
|
377
|
-
printManualReleaseUrl();
|
|
378
|
-
}
|
|
358
|
+
printManualReleaseUrl(repository, repositoryRelease, stdout, colors);
|
|
359
|
+
}
|
|
379
360
|
return;
|
|
380
361
|
}
|
|
381
362
|
if (options.release && taggedCommit) {
|
|
382
363
|
if (!remoteTaggedCommit) await git(environment.cwd, "push", "origin", `refs/tags/${tag}`);
|
|
383
|
-
await
|
|
364
|
+
await publishRelease(repository, repositoryRelease, token, preview, environment);
|
|
384
365
|
return;
|
|
385
366
|
}
|
|
386
|
-
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
const versionChanges = versionPath ? await git(environment.cwd, "status", "--porcelain", "--", versionPath) : "";
|
|
393
|
-
const commitMessage = versionPath && !versionChanges.trim() ? `docs(changelog): ${releaseExists ? "update" : "add"} v${version$1} release notes` : `chore(release): v${version$1}`;
|
|
394
|
-
await git(environment.cwd, ...gitIdentity, "commit", "-m", commitMessage, ...options.author ? ["--author", options.author] : [], "--only", "--", ...releasePaths);
|
|
395
|
-
}
|
|
396
|
-
}
|
|
367
|
+
const outputPath = resolve(environment.cwd, options.output);
|
|
368
|
+
const currentChangelog = await readChangelog(outputPath);
|
|
369
|
+
const releaseExists = hasRelease(currentChangelog, options.version);
|
|
370
|
+
await writeChangelog(outputPath, insertRelease(currentChangelog, release));
|
|
371
|
+
const versionPath = await updateVersion(environment.cwd, options.version);
|
|
372
|
+
if (options.commit || options.release) await commitReleaseFiles(options, environment.cwd, outputPath, versionPath, releaseExists);
|
|
397
373
|
if (options.release) {
|
|
374
|
+
const gitIdentity = resolveGitIdentity(options.author);
|
|
398
375
|
await git(environment.cwd, ...gitIdentity, "tag", "-a", tag, "-m", tag);
|
|
399
376
|
await git(environment.cwd, "push", "origin", "HEAD", `refs/tags/${tag}`);
|
|
400
|
-
await
|
|
377
|
+
await publishRelease(repository, repositoryRelease, token, preview, environment);
|
|
401
378
|
}
|
|
402
379
|
}
|
|
403
|
-
function
|
|
404
|
-
|
|
380
|
+
function validateReleaseSupport(options, repository) {
|
|
381
|
+
if (!options.release) return;
|
|
382
|
+
if (!repository) throw new Error("A supported repository is required to release");
|
|
383
|
+
if (!repository.provider.publishRelease && !repository.provider.manualReleaseUrl) throw new Error(`${repository.provider.name} does not support releases`);
|
|
384
|
+
}
|
|
385
|
+
async function commitReleaseFiles(options, cwd, outputPath, versionPath, releaseExists) {
|
|
386
|
+
const releasePaths = [outputPath, versionPath].filter((path) => path !== void 0);
|
|
387
|
+
if (!(await git(cwd, "status", "--porcelain", "--", ...releasePaths)).trim()) return;
|
|
388
|
+
await git(cwd, "add", "--", ...releasePaths);
|
|
389
|
+
const versionChanges = versionPath ? await git(cwd, "status", "--porcelain", "--", versionPath) : "";
|
|
390
|
+
const commitMessage = versionPath && !versionChanges.trim() ? `docs(changelog): ${releaseExists ? "update" : "add"} v${options.version} release notes` : `chore(release): v${options.version}`;
|
|
391
|
+
await git(cwd, ...resolveGitIdentity(options.author), "commit", "-m", commitMessage, ...options.author ? ["--author", options.author] : [], "--only", "--", ...releasePaths);
|
|
392
|
+
}
|
|
393
|
+
async function publishRelease(repository, release, token, preview, environment) {
|
|
394
|
+
const stdout = environment.stdout ?? ((value) => process.stdout.write(value));
|
|
395
|
+
const colors = environment.colors ?? ansis;
|
|
396
|
+
printChangesPreview(preview, stdout, colors);
|
|
397
|
+
if (!token || !repository.provider.publishRelease) {
|
|
398
|
+
if (!repository.provider.manualReleaseUrl) throw new Error("A repository token is required to release");
|
|
399
|
+
stdout(`${colors.red(`No ${repository.provider.name} token found, specify it via --token or environment variable. Release skipped.`)}\n\n`);
|
|
400
|
+
printManualReleaseUrl(repository, release, stdout, colors);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const result = await repository.provider.publishRelease(repository, release, token, environment.fetch ?? globalThis.fetch);
|
|
404
|
+
printPublishedRelease(repository.provider.name, result, stdout, colors);
|
|
405
405
|
}
|
|
406
|
-
function resolveGitIdentity(author
|
|
407
|
-
const match = /^(
|
|
406
|
+
function resolveGitIdentity(author) {
|
|
407
|
+
const match = /^([^<>\r\n]+)<([^<>\r\n]+)>$/.exec(author);
|
|
408
408
|
if (!match) throw new Error("Author must use the \"Name <email>\" format");
|
|
409
409
|
return [
|
|
410
410
|
"-c",
|
|
@@ -414,31 +414,43 @@ function resolveGitIdentity(author = defaultAuthor) {
|
|
|
414
414
|
];
|
|
415
415
|
}
|
|
416
416
|
//#endregion
|
|
417
|
-
//#region src/cli.ts
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
417
|
+
//#region src/cli/options.ts
|
|
418
|
+
function resolveOptions(versionArgument, options) {
|
|
419
|
+
if (!versionArgument) throw new Error("A release version is required");
|
|
420
|
+
const version = normalizeFull(versionArgument);
|
|
421
|
+
if (!version) throw new Error(`Invalid release version: ${versionArgument}`);
|
|
422
|
+
const commit = options.commit ?? defaultConfig.cli.commit;
|
|
423
|
+
const release = options.release ?? defaultConfig.cli.release;
|
|
424
|
+
if (options.to !== void 0 && release) throw new Error("--to cannot be used with --release");
|
|
425
|
+
if (options.author !== void 0 && !commit && !release) throw new Error("--author requires --commit or --release");
|
|
426
|
+
if (!release && (options.name !== void 0 || options.draft !== void 0 || options.prerelease !== void 0)) throw new Error("--name, --draft, and --prerelease require --release");
|
|
427
|
+
return {
|
|
428
|
+
version,
|
|
429
|
+
from: options.from,
|
|
430
|
+
to: options.to ?? defaultConfig.cli.to,
|
|
431
|
+
repository: options.repository,
|
|
432
|
+
output: options.output ?? defaultConfig.cli.output,
|
|
433
|
+
dry: options.dry ?? defaultConfig.cli.dry,
|
|
434
|
+
commit,
|
|
435
|
+
release,
|
|
436
|
+
author: options.author ?? defaultConfig.cli.author,
|
|
437
|
+
token: options.token,
|
|
438
|
+
name: options.name,
|
|
439
|
+
draft: options.draft ?? defaultConfig.cli.draft,
|
|
440
|
+
prerelease: options.prerelease,
|
|
441
|
+
emoji: options.emoji ?? defaultConfig.changelog.emoji,
|
|
442
|
+
capitalize: options.capitalize ?? defaultConfig.changelog.capitalize,
|
|
443
|
+
group: options.group ?? defaultConfig.changelog.group
|
|
444
|
+
};
|
|
438
445
|
}
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
446
|
+
//#endregion
|
|
447
|
+
//#region src/cli.ts
|
|
448
|
+
const cli = cac("keepchanges").option("--from <ref>", "Start Git reference").option("--to <ref>", "End Git reference").option("--repository <source>", "Repository slug or URL").option("--output <path>", "Changelog file path").option("--dry", "Preview without modifying files or remotes").option("--commit", "Commit the changelog and version update").option("--release", "Publish a repository release").option("--author <author>", "Commit author in \"Name <email>\" format").option("-t, --token <token>", "Repository token").option("--name <name>", "Repository release name").option("-d, --draft", "Create a draft repository release").option("--prerelease", "Mark the repository release as prerelease").option("--emoji", "Use emojis in changelog section titles").option("--capitalize", "Capitalize changelog entries").option("--group", "Group repeated commit scopes");
|
|
449
|
+
cli.command("[version]").usage("<version> [options]").action(async (versionArgument, options) => {
|
|
450
|
+
await createChanges(resolveOptions(versionArgument, options), { cwd: process.cwd() });
|
|
442
451
|
});
|
|
452
|
+
cli.help();
|
|
453
|
+
cli.version(version);
|
|
454
|
+
cli.parse();
|
|
443
455
|
//#endregion
|
|
444
|
-
export {
|
|
456
|
+
export {};
|