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