fork-version 1.8.0 → 2.0.2

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.
@@ -1,16 +1,15 @@
1
1
  import { z } from 'zod';
2
- import { resolve, parse } from 'node:path';
2
+ import { execFile } from 'child_process';
3
+ import semver from 'semver';
4
+ import { resolve, parse } from 'path';
3
5
  import { glob } from 'glob';
4
6
  import conventionalChangelogConfigSpec from 'conventional-changelog-config-spec';
5
- import { execFile } from 'node:child_process';
6
- import { writeFileSync, readFileSync, lstatSync } from 'node:fs';
7
+ import { writeFileSync, readFileSync, lstatSync } from 'fs';
7
8
  import JoyCon from 'joycon';
8
9
  import { bundleRequire } from 'bundle-require';
9
10
  import { modify, applyEdits, parse as parse$1 } from 'jsonc-parser';
10
11
  import { parse as parse$2, parseDocument } from 'yaml';
11
12
  import * as cheerio from 'cheerio/slim';
12
- import semver from 'semver';
13
- import conventionalRecommendedBump from 'conventional-recommended-bump';
14
13
  import conventionalChangelog from 'conventional-changelog';
15
14
 
16
15
  // src/config/schema.js
@@ -267,6 +266,303 @@ var ForkConfigSchema = z.object({
267
266
  */
268
267
  releaseMessageSuffix: z.string().optional().describe("Add a suffix to the release commit message.")
269
268
  });
269
+ var Git = class {
270
+ constructor(config) {
271
+ this.config = config;
272
+ this.add = this.add.bind(this);
273
+ this.commit = this.commit.bind(this);
274
+ this.tag = this.tag.bind(this);
275
+ this.log = this.log.bind(this);
276
+ this.isIgnored = this.isIgnored.bind(this);
277
+ this.getBranchName = this.getBranchName.bind(this);
278
+ this.getRemoteUrl = this.getRemoteUrl.bind(this);
279
+ this.getTags = this.getTags.bind(this);
280
+ this.getMostRecentTag = this.getMostRecentTag.bind(this);
281
+ this.getCleanedTags = this.getCleanedTags.bind(this);
282
+ this.getHighestSemverVersionFromTags = this.getHighestSemverVersionFromTags.bind(this);
283
+ this.getCommits = this.getCommits.bind(this);
284
+ }
285
+ async #execGit(command, args) {
286
+ return new Promise((onResolve, onReject) => {
287
+ execFile(
288
+ "git",
289
+ [command, ...args],
290
+ {
291
+ cwd: this.config.path,
292
+ maxBuffer: Infinity
293
+ },
294
+ (error, stdout, stderr) => {
295
+ if (error) {
296
+ onReject(error);
297
+ } else {
298
+ onResolve(stdout ? stdout : stderr);
299
+ }
300
+ }
301
+ );
302
+ });
303
+ }
304
+ /**
305
+ * Add file contents to the index
306
+ *
307
+ * [git-add Documentation](https://git-scm.com/docs/git-add)
308
+ *
309
+ * @example
310
+ * ```ts
311
+ * await git.add("CHANGELOG.md");
312
+ * ```
313
+ */
314
+ async add(...args) {
315
+ if (this.config.dryRun) {
316
+ return "";
317
+ }
318
+ return this.#execGit("add", args.filter(Boolean));
319
+ }
320
+ /**
321
+ * Record changes to the repository
322
+ *
323
+ * [git-commit Documentation](https://git-scm.com/docs/git-commit)
324
+ *
325
+ * @example
326
+ * ```ts
327
+ * await git.commit("--message", "chore(release): 1.2.3");
328
+ * ```
329
+ */
330
+ async commit(...args) {
331
+ if (this.config.dryRun) {
332
+ return "";
333
+ }
334
+ return this.#execGit("commit", args.filter(Boolean));
335
+ }
336
+ /**
337
+ * Create, list, delete or verify a tag object
338
+ *
339
+ * [git-tag Documentation](https://git-scm.com/docs/git-tag)
340
+ *
341
+ * @example
342
+ * ```ts
343
+ * await git.tag("--annotate", "v1.2.3", "--message", "chore(release): 1.2.3");
344
+ * ```
345
+ */
346
+ async tag(...args) {
347
+ if (this.config.dryRun) {
348
+ return "";
349
+ }
350
+ return this.#execGit("tag", args.filter(Boolean));
351
+ }
352
+ /**
353
+ * Show commit logs
354
+ *
355
+ * - [git-log Documentation](https://git-scm.com/docs/git-log)
356
+ * - [pretty-formats Documentation](https://git-scm.com/docs/pretty-formats)
357
+ *
358
+ * @example
359
+ * ```ts
360
+ * await git.log("--oneline");
361
+ * ```
362
+ */
363
+ async log(...args) {
364
+ try {
365
+ return await this.#execGit("log", args.filter(Boolean));
366
+ } catch {
367
+ return "";
368
+ }
369
+ }
370
+ /**
371
+ * Check if a file is ignored by git
372
+ *
373
+ * [git-check-ignore Documentation](https://git-scm.com/docs/git-check-ignore)
374
+ *
375
+ * @example
376
+ * ```ts
377
+ * await git.isIgnored("src/my-file.txt");
378
+ * ```
379
+ */
380
+ async isIgnored(file) {
381
+ try {
382
+ await this.#execGit("check-ignore", ["--no-index", file]);
383
+ return true;
384
+ } catch (_error) {
385
+ return false;
386
+ }
387
+ }
388
+ /**
389
+ * Get the name of the current branch
390
+ *
391
+ * [git-rev-parse Documentation](https://git-scm.com/docs/git-rev-parse)
392
+ *
393
+ * @example
394
+ * ```ts
395
+ * await git.getBranchName(); // "main"
396
+ * ```
397
+ */
398
+ async getBranchName() {
399
+ try {
400
+ const branchName = await this.#execGit("rev-parse", ["--abbrev-ref", "HEAD"]);
401
+ return branchName.trim();
402
+ } catch {
403
+ return "";
404
+ }
405
+ }
406
+ /**
407
+ * Get the URL of the remote repository
408
+ *
409
+ * [git-config Documentation](https://git-scm.com/docs/git-config)
410
+ *
411
+ * @example
412
+ * ```ts
413
+ * await git.getRemoteUrl(); // "https://github.com/eglavin/fork-version"
414
+ * ```
415
+ */
416
+ async getRemoteUrl() {
417
+ try {
418
+ const remoteUrl = await this.#execGit("config", ["--get", "remote.origin.url"]);
419
+ return remoteUrl.trim();
420
+ } catch (_error) {
421
+ return "";
422
+ }
423
+ }
424
+ /**
425
+ * `getTags` returns valid semver version tags in order of the commit history
426
+ *
427
+ * Using `git log` to get the commit history, we then parse the tags from the
428
+ * commit details which is expected to be in the following format:
429
+ * ```txt
430
+ * commit 3841b1d05750d42197fe958e3d8e06df378a842d (HEAD -> main, tag: v1.0.2, tag: v1.0.1, tag: v1.0.0)
431
+ * Author: Username <username@example.com>
432
+ * Date: Sat Nov 9 15:00:00 2024 +0000
433
+ *
434
+ * chore(release): v1.0.0
435
+ * ```
436
+ *
437
+ * - [Functionality extracted from the conventional-changelog - git-semver-tags project](https://github.com/conventional-changelog/conventional-changelog/blob/fac8045242099c016f5f3905e54e02b7d466bd7b/packages/git-semver-tags/index.js)
438
+ * - [conventional-changelog git-semver-tags MIT Licence](https://github.com/conventional-changelog/conventional-changelog/blob/fac8045242099c016f5f3905e54e02b7d466bd7b/packages/git-semver-tags/LICENSE.md)
439
+ *
440
+ * @example
441
+ * ```ts
442
+ * await git.getTags("v"); // ["v1.0.2", "v1.0.1", "v1.0.0"]
443
+ * ```
444
+ */
445
+ async getTags(tagPrefix) {
446
+ const logOutput = await this.log("--decorate", "--no-color", "--date-order");
447
+ const TAG_REGEX = /tag:\s*(?<tag>.+?)[,)]/gi;
448
+ const tags = [];
449
+ let tagMatch = null;
450
+ while (tagMatch = TAG_REGEX.exec(logOutput)) {
451
+ const { tag = "" } = tagMatch.groups ?? {};
452
+ if (tagPrefix) {
453
+ if (tag.startsWith(tagPrefix)) {
454
+ const tagWithoutPrefix = tag.replace(new RegExp(`^${tagPrefix}`), "");
455
+ if (semver.valid(tagWithoutPrefix)) {
456
+ tags.push(tag);
457
+ }
458
+ }
459
+ } else if (/^\d/.test(tag) && semver.valid(tag)) {
460
+ tags.push(tag);
461
+ }
462
+ }
463
+ return tags;
464
+ }
465
+ /**
466
+ * Returns the latest git tag based on commit date
467
+ *
468
+ * @example
469
+ * ```ts
470
+ * await git.getMostRecentTag("v"); // "1.2.3"
471
+ * ```
472
+ */
473
+ async getMostRecentTag(tagPrefix) {
474
+ const tags = await this.getTags(tagPrefix);
475
+ return tags[0] || void 0;
476
+ }
477
+ /**
478
+ * Get cleaned semver tags, with any tag prefix's removed
479
+ *
480
+ * @example
481
+ * ```ts
482
+ * await git.getCleanedTags("v"); // ["1.2.3", "1.2.2", "1.2.1"]
483
+ * ```
484
+ */
485
+ async getCleanedTags(tagPrefix) {
486
+ const tags = await this.getTags(tagPrefix);
487
+ const cleanedTags = [];
488
+ for (const tag of tags) {
489
+ const tagWithoutPrefix = tag.replace(new RegExp(`^${tagPrefix}`), "");
490
+ const cleanedTag = semver.clean(tagWithoutPrefix);
491
+ if (cleanedTag) {
492
+ cleanedTags.push(cleanedTag);
493
+ }
494
+ }
495
+ return cleanedTags;
496
+ }
497
+ /**
498
+ * Get the highest semver version from git tags. This will return the highest
499
+ * semver version found for the given tag prefix, regardless of the commit date.
500
+ *
501
+ * @example
502
+ * ```ts
503
+ * await git.getHighestSemverVersionFromTags("v"); // "1.2.3"
504
+ * ```
505
+ */
506
+ async getHighestSemverVersionFromTags(tagPrefix) {
507
+ const cleanedTags = await this.getCleanedTags(tagPrefix);
508
+ return cleanedTags.sort(semver.rcompare)[0] || void 0;
509
+ }
510
+ /**
511
+ * Get commit history in a parsable format
512
+ *
513
+ * An array of strings with commit details is returned in the following format:
514
+ * ```txt
515
+ * subject
516
+ * body
517
+ * hash
518
+ * committer date
519
+ * committer name
520
+ * committer email
521
+ * ```
522
+ *
523
+ * @example
524
+ * ```ts
525
+ * await git.getCommits("v1.0.0", "HEAD", "src/utils");
526
+ * ```
527
+ */
528
+ async getCommits(from = "", to = "HEAD", ...paths) {
529
+ const SCISSOR = "^----------- FORK VERSION -----------^";
530
+ const LOG_FORMAT = [
531
+ "%s",
532
+ // subject
533
+ "%b",
534
+ // body
535
+ "%H",
536
+ // hash
537
+ "%cI",
538
+ // committer date
539
+ "%cN",
540
+ // committer name
541
+ "%cE",
542
+ // committer email
543
+ SCISSOR
544
+ ].join("%n");
545
+ const commits = await this.log(
546
+ `--format=${LOG_FORMAT}`,
547
+ [from, to].filter(Boolean).join(".."),
548
+ paths.length ? "--" : "",
549
+ ...paths
550
+ );
551
+ const splitCommits = commits.split(`
552
+ ${SCISSOR}
553
+ `);
554
+ if (splitCommits.length === 0) {
555
+ return splitCommits;
556
+ }
557
+ if (splitCommits[0] === SCISSOR) {
558
+ splitCommits.shift();
559
+ }
560
+ if (splitCommits[splitCommits.length - 1] === "") {
561
+ splitCommits.pop();
562
+ }
563
+ return splitCommits;
564
+ }
565
+ };
270
566
  function getChangelogPresetConfig(mergedConfig, cliArguments, detectedGitHost) {
271
567
  const preset = {
272
568
  name: "conventionalcommits"
@@ -370,12 +666,13 @@ All notable changes to this project will be documented in this file. See [fork-v
370
666
  skipTag: false,
371
667
  changelogPresetConfig: {}
372
668
  };
669
+
670
+ // src/config/detect-git-host.ts
373
671
  async function detectGitHost(cwd) {
374
- const remoteUrl = await new Promise((onResolve) => {
375
- execFile("git", ["config", "--get", "remote.origin.url"], { cwd }, (_error, stdout) => {
376
- onResolve(stdout ? stdout.trim() : "");
377
- });
378
- });
672
+ const remoteUrl = await new Git({
673
+ path: cwd,
674
+ dryRun: false
675
+ }).getRemoteUrl();
379
676
  if (remoteUrl.startsWith("https://") && remoteUrl.includes("@dev.azure.com/")) {
380
677
  const match = /^https:\/\/(?<atorganisation>.*?)@dev.azure.com\/(?<organisation>.*?)\/(?<project>.*?)\/_git\/(?<repository>.*?)(?:\.git)?$/.exec(
381
678
  remoteUrl
@@ -844,131 +1141,481 @@ var FileManager = class {
844
1141
  this.logger.error(`[File Manager] Unsupported file: ${fileState.path}`);
845
1142
  }
846
1143
  };
847
- var Git = class {
848
- constructor(config) {
849
- this.config = config;
850
- this.add = this.add.bind(this);
851
- this.commit = this.commit.bind(this);
852
- this.tag = this.tag.bind(this);
853
- this.isIgnored = this.isIgnored.bind(this);
854
- this.getCurrentBranchName = this.getCurrentBranchName.bind(this);
855
- this.getTags = this.getTags.bind(this);
856
- this.getLatestTag = this.getLatestTag.bind(this);
1144
+
1145
+ // src/utils/trim-string-array.ts
1146
+ function trimStringArray(array) {
1147
+ const items = [];
1148
+ if (Array.isArray(array)) {
1149
+ for (const item of array) {
1150
+ const _item = item.trim();
1151
+ if (_item) {
1152
+ items.push(_item);
1153
+ }
1154
+ }
857
1155
  }
858
- async execGit(command, args) {
859
- return new Promise((onResolve, onReject) => {
860
- execFile(
861
- "git",
862
- [command, ...args],
863
- {
864
- cwd: this.config.path,
865
- maxBuffer: Infinity
866
- },
867
- (error, stdout, stderr) => {
868
- if (error) {
869
- onReject(error);
870
- } else {
871
- onResolve(stdout ? stdout : stderr);
872
- }
873
- }
874
- );
875
- });
1156
+ if (items.length === 0) {
1157
+ return void 0;
1158
+ }
1159
+ return items;
1160
+ }
1161
+
1162
+ // src/commit-parser/options.ts
1163
+ function createParserOptions(userOptions) {
1164
+ const referenceActions = trimStringArray(userOptions?.referenceActions) ?? [
1165
+ "close",
1166
+ "closes",
1167
+ "closed",
1168
+ "fix",
1169
+ "fixes",
1170
+ "fixed",
1171
+ "resolve",
1172
+ "resolves",
1173
+ "resolved"
1174
+ ];
1175
+ const joinedReferenceActions = referenceActions.join("|");
1176
+ const issuePrefixes = trimStringArray(userOptions?.issuePrefixes) ?? ["#"];
1177
+ const joinedIssuePrefixes = issuePrefixes.join("|");
1178
+ const noteKeywords = trimStringArray(userOptions?.noteKeywords) ?? [
1179
+ "BREAKING CHANGE",
1180
+ "BREAKING-CHANGE"
1181
+ ];
1182
+ const joinedNoteKeywords = noteKeywords.join("|");
1183
+ return {
1184
+ subjectPattern: /^(?<type>\w+)(?:\((?<scope>.*)\))?(?<breakingChange>!)?:\s+(?<title>.*)/,
1185
+ mergePattern: /^Merge pull request #(?<id>\d*) from (?<source>.*)/,
1186
+ revertPattern: /^[Rr]evert "(?<subject>.*)"(\s*This reverts commit (?<hash>[a-zA-Z0-9]*)\.)?/,
1187
+ commentPattern: /^#(?!\d+\s)/,
1188
+ mentionPattern: /(?<!\w)@(?<username>[\w-]+)/,
1189
+ referenceActions,
1190
+ referenceActionPattern: joinedReferenceActions ? new RegExp(
1191
+ `(?<action>${joinedReferenceActions})(?:\\s+(?<reference>.*?))(?=(?:${joinedReferenceActions})|$)`
1192
+ ) : void 0,
1193
+ issuePrefixes,
1194
+ issuePattern: joinedIssuePrefixes ? new RegExp(
1195
+ `(?:.*?)??\\s*(?<repository>[\\w-\\.\\/]*?)??(?<prefix>${joinedIssuePrefixes})(?<issue>[\\w-]*\\d+)`
1196
+ ) : void 0,
1197
+ noteKeywords,
1198
+ notePattern: joinedNoteKeywords ? new RegExp(`^(?<title>${joinedNoteKeywords}):(\\s*(?<text>.*))`) : void 0,
1199
+ // Override defaults with user options
1200
+ ...userOptions
1201
+ };
1202
+ }
1203
+
1204
+ // src/commit-parser/parser-error.ts
1205
+ var ParserError = class extends Error {
1206
+ detail;
1207
+ constructor(message, detail) {
1208
+ super(message);
1209
+ this.name = "ParserError";
1210
+ this.detail = detail;
1211
+ }
1212
+ };
1213
+
1214
+ // src/commit-parser/commit-parser.ts
1215
+ var CommitParser = class {
1216
+ #options;
1217
+ #logger;
1218
+ constructor(userOptions) {
1219
+ this.#options = createParserOptions(userOptions);
1220
+ this.setLogger = this.setLogger.bind(this);
1221
+ this.createCommit = this.createCommit.bind(this);
1222
+ this.parseRawCommit = this.parseRawCommit.bind(this);
1223
+ this.parseSubject = this.parseSubject.bind(this);
1224
+ this.parseMerge = this.parseMerge.bind(this);
1225
+ this.parseRevert = this.parseRevert.bind(this);
1226
+ this.parseMentions = this.parseMentions.bind(this);
1227
+ this.parseReferenceParts = this.parseReferenceParts.bind(this);
1228
+ this.parseReferences = this.parseReferences.bind(this);
1229
+ this.parseNotes = this.parseNotes.bind(this);
1230
+ this.parseRawLines = this.parseRawLines.bind(this);
1231
+ this.parse = this.parse.bind(this);
1232
+ }
1233
+ setLogger(logger) {
1234
+ this.#logger = logger;
1235
+ return this;
1236
+ }
1237
+ createCommit() {
1238
+ return {
1239
+ raw: "",
1240
+ subject: "",
1241
+ body: "",
1242
+ hash: "",
1243
+ date: "",
1244
+ name: "",
1245
+ email: "",
1246
+ type: "",
1247
+ scope: "",
1248
+ breakingChange: "",
1249
+ title: "",
1250
+ merge: null,
1251
+ revert: null,
1252
+ notes: [],
1253
+ mentions: [],
1254
+ references: []
1255
+ };
876
1256
  }
877
1257
  /**
878
- * - [git-add Documentation](https://git-scm.com/docs/git-add)
1258
+ * Parse the raw commit message into its expected parts
1259
+ * - subject
1260
+ * - body
1261
+ * - hash
1262
+ * - date
1263
+ * - name
1264
+ * - email
1265
+ *
1266
+ * @throws {ParserError}
879
1267
  */
880
- async add(...args) {
881
- if (this.config.dryRun) {
882
- return "";
1268
+ parseRawCommit(rawCommit) {
1269
+ const parsedCommit = this.createCommit();
1270
+ const parts = rawCommit.split(/\r?\n/);
1271
+ if (parts.length < 6) {
1272
+ throw new ParserError("Commit doesn't contain enough parts", rawCommit);
1273
+ }
1274
+ const email = parts.pop();
1275
+ const name = parts.pop();
1276
+ const date = parts.pop();
1277
+ const hash = parts.pop();
1278
+ if (email) parsedCommit.email = email.trim();
1279
+ if (name) parsedCommit.name = name.trim();
1280
+ if (date) {
1281
+ parsedCommit.date = date.trim();
1282
+ if (Number.isNaN(Date.parse(parsedCommit.date))) {
1283
+ throw new ParserError("Unable to parse commit date", rawCommit);
1284
+ }
1285
+ }
1286
+ if (hash) parsedCommit.hash = hash.trim();
1287
+ const subject = parts.shift()?.trimStart();
1288
+ if (subject) {
1289
+ parsedCommit.subject = subject;
1290
+ parsedCommit.raw = subject;
883
1291
  }
884
- return this.execGit("add", args.filter(Boolean));
1292
+ parsedCommit.body = parts.filter((line) => {
1293
+ if (this.#options.commentPattern) {
1294
+ return !this.#options.commentPattern.test(line.trim());
1295
+ }
1296
+ return true;
1297
+ }).join("\n").trim();
1298
+ const raw = parts.join("\n").trim();
1299
+ if (raw) parsedCommit.raw += "\n" + raw;
1300
+ return parsedCommit;
885
1301
  }
886
1302
  /**
887
- * - [git-commit Documentation](https://git-scm.com/docs/git-commit)
1303
+ * Parse the commit subject into its expected parts
1304
+ * - type
1305
+ * - scope (optional)
1306
+ * - breaking change (optional)
1307
+ * - title
1308
+ *
1309
+ * @throws {ParserError}
888
1310
  */
889
- async commit(...args) {
890
- if (this.config.dryRun) {
891
- return "";
1311
+ parseSubject(commit) {
1312
+ if (!this.#options.subjectPattern) return false;
1313
+ const subjectMatch = new RegExp(this.#options.subjectPattern, "i").exec(commit.subject);
1314
+ if (subjectMatch?.groups) {
1315
+ const { type = "", scope = "", breakingChange = "", title = "" } = subjectMatch.groups;
1316
+ if (!type || !title) {
1317
+ throw new ParserError("Unable to parse commit subject", commit);
1318
+ }
1319
+ commit.type = type;
1320
+ commit.scope = scope;
1321
+ if (breakingChange) commit.breakingChange = breakingChange;
1322
+ commit.title = title;
1323
+ return true;
892
1324
  }
893
- return this.execGit("commit", args.filter(Boolean));
1325
+ return false;
894
1326
  }
895
1327
  /**
896
- * - [git-tag Documentation](https://git-scm.com/docs/git-tag)
1328
+ * Parse merge information from the commit subject
1329
+ * @example
1330
+ * ```txt
1331
+ * "Merge pull request #123 from fork-version/feature"
1332
+ * ```
897
1333
  */
898
- async tag(...args) {
899
- if (this.config.dryRun) {
900
- return "";
1334
+ parseMerge(commit) {
1335
+ if (!this.#options.mergePattern) return false;
1336
+ const mergeMatch = new RegExp(this.#options.mergePattern).exec(commit.subject);
1337
+ if (mergeMatch?.groups) {
1338
+ const { id = "", source = "" } = mergeMatch.groups;
1339
+ commit.merge = {
1340
+ id,
1341
+ source
1342
+ };
1343
+ return true;
901
1344
  }
902
- return this.execGit("tag", args.filter(Boolean));
1345
+ return false;
903
1346
  }
904
1347
  /**
905
- * - [git-check-ignore Documentation](https://git-scm.com/docs/git-check-ignore)
1348
+ * Parse revert information from the commit body
1349
+ * @example
1350
+ * ```txt
1351
+ * "Revert "feat: initial commit"
1352
+ *
1353
+ * This reverts commit 4a79e9e546b4020d2882b7810dc549fa71960f4f."
1354
+ * ```
906
1355
  */
907
- async isIgnored(file) {
908
- try {
909
- await this.execGit("check-ignore", ["--no-index", file]);
1356
+ parseRevert(commit) {
1357
+ if (!this.#options.revertPattern) return false;
1358
+ const revertMatch = new RegExp(this.#options.revertPattern).exec(commit.raw);
1359
+ if (revertMatch?.groups) {
1360
+ const { hash = "", subject = "" } = revertMatch.groups;
1361
+ commit.revert = {
1362
+ hash,
1363
+ subject
1364
+ };
910
1365
  return true;
911
- } catch (_error) {
912
- return false;
913
1366
  }
1367
+ return false;
914
1368
  }
915
- async getCurrentBranchName() {
916
- return (await this.execGit("rev-parse", ["--abbrev-ref", "HEAD"])).trim();
1369
+ /**
1370
+ * Search for mentions from the commit line
1371
+ * @example
1372
+ * ```txt
1373
+ * "@fork-version"
1374
+ * ```
1375
+ */
1376
+ parseMentions(line, outMentions) {
1377
+ if (!this.#options.mentionPattern) return false;
1378
+ const mentionRegex = new RegExp(this.#options.mentionPattern, "g");
1379
+ let foundMention = false;
1380
+ let mentionMatch;
1381
+ while (mentionMatch = mentionRegex.exec(line)) {
1382
+ if (!mentionMatch) {
1383
+ break;
1384
+ }
1385
+ const { username = "" } = mentionMatch.groups ?? {};
1386
+ outMentions.add(username);
1387
+ foundMention = true;
1388
+ }
1389
+ return foundMention;
917
1390
  }
918
1391
  /**
919
- * `getTags` returns valid semver version tags in order of the commit history.
920
- *
921
- * Using `git log` to get the commit history, we then parse the tags from the
922
- * commit details which is expected to be in the following format:
1392
+ * Search for references from the commit line
923
1393
  * @example
924
1394
  * ```txt
925
- * commit 3841b1d05750d42197fe958e3d8e06df378a842d (HEAD -> main, tag: 1.0.2)
926
- * Author: Username <username@example.com>
927
- * Date: Sat Nov 9 15:00:00 2024 +0000
928
- *
929
- * chore(release): 1.2.3
1395
+ * "#1234"
1396
+ * "owner/repo#1234"
930
1397
  * ```
931
- *
932
- * - [Functionality extracted from the conventional-changelog - git-semver-tags project](https://github.com/conventional-changelog/conventional-changelog/blob/fac8045242099c016f5f3905e54e02b7d466bd7b/packages/git-semver-tags/index.js)
933
- * - [conventional-changelog git-semver-tags MIT Licence](https://github.com/conventional-changelog/conventional-changelog/blob/fac8045242099c016f5f3905e54e02b7d466bd7b/packages/git-semver-tags/LICENSE.md)
934
1398
  */
935
- async getTags(tagPrefix) {
936
- const logOutput = await this.execGit("log", ["--decorate", "--no-color", "--date-order"]);
937
- const TAG_REGEX = /tag:\s*(.+?)[,)]/gi;
938
- const tags = [];
939
- let match = null;
940
- let tag;
941
- let tagWithoutPrefix;
942
- for (const logOutputLine of logOutput.split("\n")) {
943
- while (match = TAG_REGEX.exec(logOutputLine)) {
944
- tag = match[1];
945
- if (tagPrefix) {
946
- if (tag.startsWith(tagPrefix)) {
947
- tagWithoutPrefix = tag.replace(tagPrefix, "");
948
- if (semver.valid(tagWithoutPrefix)) {
949
- tags.push(tag);
950
- }
951
- }
952
- } else if (semver.valid(tag)) {
953
- tags.push(tag);
1399
+ parseReferenceParts(referenceText, action) {
1400
+ if (!this.#options.issuePattern) return void 0;
1401
+ const references = [];
1402
+ const issueRegex = new RegExp(this.#options.issuePattern, "gi");
1403
+ let issueMatch;
1404
+ while (issueMatch = issueRegex.exec(referenceText)) {
1405
+ if (!issueMatch) {
1406
+ break;
1407
+ }
1408
+ const { repository = "", prefix = "", issue = "" } = issueMatch.groups ?? {};
1409
+ const reference = {
1410
+ prefix,
1411
+ issue,
1412
+ action,
1413
+ owner: null,
1414
+ repository: null
1415
+ };
1416
+ if (repository) {
1417
+ const slashIndex = repository.indexOf("/");
1418
+ if (slashIndex !== -1) {
1419
+ reference.owner = repository.slice(0, slashIndex);
1420
+ reference.repository = repository.slice(slashIndex + 1);
1421
+ } else {
1422
+ reference.repository = repository;
954
1423
  }
955
1424
  }
1425
+ references.push(reference);
956
1426
  }
957
- return tags;
1427
+ if (references.length > 0) {
1428
+ return references;
1429
+ }
1430
+ return void 0;
958
1431
  }
959
- async getLatestTag(tagPrefix) {
960
- const tags = await this.getTags(tagPrefix);
961
- if (!tags.length) return "";
962
- const cleanedTags = [];
963
- for (const tag of tags) {
964
- const cleanedTag = semver.clean(tag.replace(new RegExp(`^${tagPrefix}`), ""));
965
- if (cleanedTag) {
966
- cleanedTags.push(cleanedTag);
1432
+ /**
1433
+ * Search for actions and references from the commit line
1434
+ * @example
1435
+ * ```txt
1436
+ * "Closes #1234"
1437
+ * "fixes owner/repo#1234"
1438
+ * ```
1439
+ */
1440
+ parseReferences(line, outReferences) {
1441
+ if (!this.#options.referenceActionPattern || !this.#options.issuePattern) return false;
1442
+ const referenceActionRegex = new RegExp(this.#options.referenceActionPattern, "gi").test(line) ? new RegExp(this.#options.referenceActionPattern, "gi") : /(?<reference>.*)/g;
1443
+ let foundReference = false;
1444
+ let referenceActionMatch;
1445
+ while (referenceActionMatch = referenceActionRegex.exec(line)) {
1446
+ if (!referenceActionMatch) {
1447
+ break;
1448
+ }
1449
+ const { action = "", reference = "" } = referenceActionMatch.groups ?? {};
1450
+ const parsedReferences = this.parseReferenceParts(reference, action || null);
1451
+ if (!parsedReferences) {
1452
+ break;
1453
+ }
1454
+ for (const ref of parsedReferences) {
1455
+ if (!outReferences.some((r) => r.prefix === ref.prefix && r.issue === ref.issue)) {
1456
+ outReferences.push(ref);
1457
+ }
967
1458
  }
1459
+ foundReference = true;
1460
+ }
1461
+ return foundReference;
1462
+ }
1463
+ /**
1464
+ * Search for notes from the commit line
1465
+ * @example
1466
+ * ```txt
1467
+ * "BREAKING CHANGE: this is a breaking change"
1468
+ * ```
1469
+ */
1470
+ parseNotes(line, outNotes) {
1471
+ if (!this.#options.notePattern) return false;
1472
+ const noteMatch = new RegExp(this.#options.notePattern, "ig").exec(line);
1473
+ if (noteMatch?.groups) {
1474
+ const { title = "", text = "" } = noteMatch.groups;
1475
+ outNotes.push({
1476
+ title,
1477
+ text
1478
+ });
1479
+ return true;
1480
+ }
1481
+ return false;
1482
+ }
1483
+ /**
1484
+ * Parse the raw commit for mentions, references and notes
1485
+ */
1486
+ parseRawLines(commit) {
1487
+ const mentions = /* @__PURE__ */ new Set();
1488
+ const references = [];
1489
+ const notes = [];
1490
+ let lastNoteLine = -1;
1491
+ const splitMessage = commit.raw.split("\n");
1492
+ for (let index = 0; index < splitMessage.length; index++) {
1493
+ const line = splitMessage[index];
1494
+ const trimmedLine = line.trim();
1495
+ if (this.#options.commentPattern?.test(trimmedLine)) {
1496
+ continue;
1497
+ }
1498
+ this.parseMentions(trimmedLine, mentions);
1499
+ const foundReference = this.parseReferences(trimmedLine, references);
1500
+ if (foundReference) {
1501
+ lastNoteLine = -1;
1502
+ continue;
1503
+ }
1504
+ if (this.parseNotes(trimmedLine, notes)) {
1505
+ lastNoteLine = index;
1506
+ } else if (lastNoteLine !== -1) {
1507
+ notes[notes.length - 1].text += `
1508
+ ${line}`;
1509
+ lastNoteLine = index;
1510
+ }
1511
+ }
1512
+ if (mentions.size > 0) {
1513
+ commit.mentions = Array.from(mentions);
1514
+ }
1515
+ if (references.length > 0) {
1516
+ commit.references = references;
1517
+ }
1518
+ if (notes.length > 0) {
1519
+ commit.notes = notes.map((note) => ({
1520
+ ...note,
1521
+ text: note.text.trim()
1522
+ }));
1523
+ }
1524
+ }
1525
+ /**
1526
+ * Parse a commit log with the following format separated by new line characters:
1527
+ * ```txt
1528
+ * refactor: add test file
1529
+ * Add a test file to the project
1530
+ * 4ef2c86d393a9660aa9f753144256b1f200c16bd
1531
+ * 2024-12-22T17:36:50Z
1532
+ * Fork Version
1533
+ * fork-version@example.com
1534
+ * ```
1535
+ *
1536
+ * @example
1537
+ * ```ts
1538
+ * parse("refactor: add test file\nAdd a test file to the project\n4ef2c86d393a9660aa9f753144256b1f200c16bd\n2024-12-22T17:36:50Z\nFork Version\nfork-version@example.com");
1539
+ * ```
1540
+ *
1541
+ * The expected input value can be generated by running the following command:
1542
+ * ```sh
1543
+ * git log --format="%s%n%b%n%H%n%cI%n%cN%n%cE%n"
1544
+ * ```
1545
+ * @see {@link https://git-scm.com/docs/pretty-formats|Git Pretty Format Documentation}
1546
+ */
1547
+ parse(rawCommit) {
1548
+ try {
1549
+ const commit = this.parseRawCommit(rawCommit);
1550
+ this.parseSubject(commit);
1551
+ this.parseMerge(commit);
1552
+ this.parseRevert(commit);
1553
+ this.parseRawLines(commit);
1554
+ return commit;
1555
+ } catch (error) {
1556
+ if (this.#logger) {
1557
+ this.#logger.debug("[Commit Parser] Failed to parse commit", { error });
1558
+ }
1559
+ return void 0;
968
1560
  }
969
- return cleanedTags.sort(semver.rcompare)[0];
970
1561
  }
971
1562
  };
1563
+
1564
+ // src/commit-parser/filter-reverted-commits.ts
1565
+ function filterRevertedCommits(parsedCommits) {
1566
+ const revertedCommits = [];
1567
+ for (const commit of parsedCommits) {
1568
+ if (!commit.revert) continue;
1569
+ if (revertedCommits.some(
1570
+ (r) => r.revert?.hash === commit.hash || r.revert?.subject === commit.subject
1571
+ )) {
1572
+ continue;
1573
+ }
1574
+ revertedCommits.push(commit);
1575
+ }
1576
+ if (revertedCommits.length === 0) {
1577
+ return parsedCommits;
1578
+ }
1579
+ const commitsWithoutReverts = [];
1580
+ for (const commit of parsedCommits) {
1581
+ if (commit.revert) continue;
1582
+ const revertedIndex = revertedCommits.findIndex(
1583
+ (r) => r.revert?.hash === commit.hash || r.revert?.subject === commit.subject
1584
+ );
1585
+ if (revertedIndex !== -1) {
1586
+ revertedCommits.splice(revertedIndex, 1);
1587
+ continue;
1588
+ }
1589
+ commitsWithoutReverts.push(commit);
1590
+ }
1591
+ return commitsWithoutReverts;
1592
+ }
1593
+
1594
+ // src/process/get-commits.ts
1595
+ async function getCommitsSinceTag(config, logger, git) {
1596
+ const commitParser = new CommitParser();
1597
+ if (config.debug) commitParser.setLogger(logger);
1598
+ const latestTag = await git.getMostRecentTag(config.tagPrefix);
1599
+ if (!latestTag) {
1600
+ logger.warn("No previous tag found, using all commits");
1601
+ }
1602
+ const foundCommits = await git.getCommits(latestTag, "HEAD");
1603
+ logger.debug(`Found ${foundCommits.length} commits since last tag (${latestTag ?? "none"})`);
1604
+ const commits = foundCommits.reduce((acc, commit) => {
1605
+ const parsed = commitParser.parse(commit);
1606
+ if (parsed) {
1607
+ acc.push(parsed);
1608
+ }
1609
+ return acc;
1610
+ }, []);
1611
+ logger.debug(`Parsed ${commits.length} commits after applying commit parser`);
1612
+ const filteredCommits = filterRevertedCommits(commits);
1613
+ logger.debug(`Filtered to ${filteredCommits.length} commits after removing reverts`);
1614
+ return {
1615
+ latestTag,
1616
+ commits: filteredCommits
1617
+ };
1618
+ }
972
1619
  function getPriority(type) {
973
1620
  return ["patch", "minor", "major"].indexOf(type ?? "");
974
1621
  }
@@ -1018,7 +1665,7 @@ async function getCurrentVersion(config, logger, git, fileManager, filesToUpdate
1018
1665
  versions.add(config.currentVersion);
1019
1666
  }
1020
1667
  if (versions.size === 0 && config.gitTagFallback) {
1021
- const version = await git.getLatestTag(config.tagPrefix);
1668
+ const version = await git.getHighestSemverVersionFromTags(config.tagPrefix);
1022
1669
  if (version) {
1023
1670
  logger.warn(`Using latest git tag as fallback`);
1024
1671
  versions.add(version);
@@ -1045,65 +1692,77 @@ async function getCurrentVersion(config, logger, git, fileManager, filesToUpdate
1045
1692
  version: currentVersion
1046
1693
  };
1047
1694
  }
1048
- async function getNextVersion(config, logger, currentVersion) {
1695
+ async function getNextVersion(config, logger, commits, currentVersion) {
1049
1696
  if (config.skipBump) {
1050
1697
  logger.warn(`Skip bump, using ${currentVersion} as the next version`);
1051
1698
  return {
1052
1699
  version: currentVersion
1053
1700
  };
1054
1701
  }
1055
- if (config.nextVersion && semver.valid(config.nextVersion)) {
1702
+ if (config.nextVersion) {
1703
+ if (!semver.valid(config.nextVersion)) {
1704
+ throw new Error(`Invalid Version: ${config.nextVersion}`);
1705
+ }
1056
1706
  logger.log(`Next version: ${config.nextVersion}`);
1057
1707
  return {
1058
1708
  version: config.nextVersion
1059
1709
  };
1060
1710
  }
1061
1711
  const isPreMajor = semver.lt(currentVersion, "1.0.0");
1062
- let recommendedBump;
1712
+ let releaseType = "patch";
1713
+ const changes = { major: 0, minor: 0, patch: 0 };
1063
1714
  if (config.releaseAs) {
1064
- recommendedBump = {
1065
- releaseType: config.releaseAs,
1066
- level: -1,
1067
- reason: "User defined"
1068
- };
1715
+ releaseType = config.releaseAs;
1069
1716
  } else {
1070
- try {
1071
- recommendedBump = await conventionalRecommendedBump({
1072
- preset: {
1073
- name: "conventionalcommits",
1074
- ...config.changelogPresetConfig,
1075
- preMajor: isPreMajor
1076
- },
1077
- path: config.path,
1078
- tagPrefix: config.tagPrefix,
1079
- cwd: config.path
1080
- });
1081
- } catch (cause) {
1082
- throw new Error(`[conventional-recommended-bump] Unable to determine next version`, {
1083
- cause
1084
- });
1717
+ let level = 2;
1718
+ const MINOR_TYPES = ["feat", "feature"];
1719
+ for (const commit of commits) {
1720
+ if (commit.notes.length > 0 || commit.breakingChange) {
1721
+ changes.major += commit.notes.length + (commit.breakingChange ? 1 : 0);
1722
+ level = 0;
1723
+ } else if (MINOR_TYPES.includes(commit.type.toLowerCase())) {
1724
+ changes.minor += 1;
1725
+ if (level === 2) {
1726
+ level = 1;
1727
+ }
1728
+ } else {
1729
+ changes.patch += 1;
1730
+ }
1731
+ }
1732
+ if (isPreMajor && level < 2) {
1733
+ level++;
1734
+ changes.patch += changes.minor;
1735
+ changes.minor = changes.major;
1736
+ changes.major = 0;
1737
+ }
1738
+ if (level === 0) {
1739
+ releaseType = "major";
1740
+ } else if (level === 1) {
1741
+ releaseType = "minor";
1742
+ } else {
1743
+ releaseType = "patch";
1085
1744
  }
1086
1745
  }
1087
- if (recommendedBump.releaseType) {
1088
- const releaseType = getReleaseType(
1089
- recommendedBump.releaseType,
1090
- currentVersion,
1091
- config.preRelease
1746
+ const releaseTypeOrPreRelease = getReleaseType(releaseType, currentVersion, config.preRelease);
1747
+ const nextVersion = semver.inc(
1748
+ currentVersion,
1749
+ releaseTypeOrPreRelease,
1750
+ typeof config.preRelease === "string" ? config.preRelease : ""
1751
+ ) ?? "";
1752
+ logger.log(`Next version: ${nextVersion} (${releaseTypeOrPreRelease})`);
1753
+ if (commits.length > 0) {
1754
+ logger.log(
1755
+ ` - Commits: ${commits.length}` + (changes.major > 0 ? `, Breaking Changes: ${changes.major}` : "") + (changes.minor > 0 ? `, New Features: ${changes.minor}` : "") + (changes.patch > 0 ? `, Bug Fixes: ${changes.patch}` : "")
1092
1756
  );
1093
- const nextVersion = semver.inc(
1094
- currentVersion,
1095
- releaseType,
1096
- typeof config.preRelease === "string" ? config.preRelease : void 0
1097
- ) ?? "";
1098
- logger.log(`Next version: ${nextVersion} (${releaseType})`);
1099
- return {
1100
- ...recommendedBump,
1101
- preMajor: isPreMajor,
1102
- releaseType,
1103
- version: nextVersion
1104
- };
1757
+ } else {
1758
+ logger.log(" - No commits found.");
1105
1759
  }
1106
- throw new Error("Unable to find next version");
1760
+ return {
1761
+ version: nextVersion,
1762
+ releaseType: releaseTypeOrPreRelease,
1763
+ preMajor: isPreMajor,
1764
+ changes
1765
+ };
1107
1766
  }
1108
1767
  var RELEASE_PATTERN = /(^#+ \[?[0-9]+\.[0-9]+\.[0-9]+|<a name=)/m;
1109
1768
  function getOldReleaseContent(filePath, exists) {
@@ -1232,6 +1891,6 @@ async function tagChanges(config, logger, git, nextVersion) {
1232
1891
  );
1233
1892
  }
1234
1893
 
1235
- export { FileManager, ForkConfigSchema, Git, Logger, commitChanges, getCurrentVersion, getNextVersion, getUserConfig, tagChanges, updateChangelog };
1236
- //# sourceMappingURL=chunk-RLGF46N7.js.map
1237
- //# sourceMappingURL=chunk-RLGF46N7.js.map
1894
+ export { CommitParser, FileManager, ForkConfigSchema, Git, Logger, commitChanges, createParserOptions, filterRevertedCommits, getCommitsSinceTag, getCurrentVersion, getNextVersion, getUserConfig, tagChanges, updateChangelog };
1895
+ //# sourceMappingURL=chunk-D2PQT6ZM.js.map
1896
+ //# sourceMappingURL=chunk-D2PQT6ZM.js.map