diff --git a/src/commands/default.ts b/src/commands/default.ts index 745e42f..a4ed77b 100644 --- a/src/commands/default.ts +++ b/src/commands/default.ts @@ -9,6 +9,7 @@ import { parseCommits, bumpVersion, generateMarkDown, + filterCommits, BumpVersionOptions, } from ".."; import { npmPublish, renamePackage } from "../package"; @@ -37,6 +38,7 @@ export default async function defaultMain(args: Argv) { config.types[c.type] && !(c.type === "chore" && c.scope === "deps" && !c.isBreaking) ); + const filteredCommits = filterCommits(commits, config); // Shortcut for canary releases if (args.canary) { @@ -59,7 +61,7 @@ export default async function defaultMain(args: Argv) { // Bump version optionally if (args.bump || args.release) { const bumpOptions = _getBumpVersionOptions(args); - const newVersion = await bumpVersion(commits, config, bumpOptions); + const newVersion = await bumpVersion(filteredCommits, config, bumpOptions); if (!newVersion) { consola.error("Unable to bump version based on changes."); process.exit(1); @@ -68,7 +70,7 @@ export default async function defaultMain(args: Argv) { } // Generate markdown - const markdown = await generateMarkDown(commits, config); + const markdown = await generateMarkDown(filteredCommits, config); // Show changelog in CLI unless bumping or releasing const displayOnly = !args.bump && !args.release; diff --git a/src/git.ts b/src/git.ts index ef3b4b4..d92c36e 100644 --- a/src/git.ts +++ b/src/git.ts @@ -25,6 +25,12 @@ export interface GitCommit extends RawGitCommit { references: Reference[]; authors: GitCommitAuthor[]; isBreaking: boolean; + revertedHashes: string[]; +} + +export interface RevertPair { + shortRevertingHash: string; + revertedHash: string; } export async function getLastGitTag() { @@ -100,6 +106,7 @@ const ConventionalCommitRegex = const CoAuthoredByRegex = /co-authored-by:\s*(?.+)(<(?.+)>)/gim; const PullRequestRE = /\([ a-z]*(#\d+)\s*\)/gm; const IssueRE = /(#\d+)/gm; +const RevertHashRE = /This reverts commit (?[\da-f]{40})./gm; export function parseGitCommit( commit: RawGitCommit, @@ -133,6 +140,13 @@ export function parseGitCommit( // Remove references and normalize description = description.replace(PullRequestRE, "").trim(); + // Extract the reverted hashes. + const revertedHashes = []; + const matchedHashes = commit.body.matchAll(RevertHashRE); + for (const matchedHash of matchedHashes) { + revertedHashes.push(matchedHash.groups.hash); + } + // Find all authors const authors: GitCommitAuthor[] = [commit.author]; for (const match of commit.body.matchAll(CoAuthoredByRegex)) { @@ -150,5 +164,58 @@ export function parseGitCommit( scope, references, isBreaking, + revertedHashes, }; } + +export function filterCommits( + commits: GitCommit[], + config: ChangelogConfig +): GitCommit[] { + const commitsWithNoDeps = commits.filter( + (c) => + config.types[c.type] && + !(c.type === "chore" && c.scope === "deps" && !c.isBreaking) + ); + + let resolvedCommits: GitCommit[] = []; + let revertWatchList: RevertPair[] = []; + for (const commit of commitsWithNoDeps) { + // Include the reverted hashes in the watch list + if (commit.revertedHashes.length > 0) { + revertWatchList.push( + ...commit.revertedHashes.map( + (revertedHash) => + ({ + revertedHash, + shortRevertingHash: commit.shortHash, + } as RevertPair) + ) + ); + } + + // Find the commits which revert the current commit being evaluated + const shortRevertingHashes = revertWatchList + .filter((pair) => pair.revertedHash.startsWith(commit.shortHash)) + .map((pair) => pair.shortRevertingHash); + + if (shortRevertingHashes.length > 0) { + // Remove commits that reverts this current commit + resolvedCommits = resolvedCommits.filter( + (resolvedCommit) => + !shortRevertingHashes.includes(resolvedCommit.shortHash) + ); + + // Unwatch reverting hashes that has been resolved + revertWatchList = revertWatchList.filter( + (watchedRevert) => + !shortRevertingHashes.includes(watchedRevert.shortRevertingHash) + ); + } else { + // If the current commit is known not to have been reverted, put it to resolved commits. + resolvedCommits = [...resolvedCommits, commit]; + } + } + + return resolvedCommits; +} diff --git a/test/git.test.ts b/test/git.test.ts index 66ae596..d13e30b 100644 --- a/test/git.test.ts +++ b/test/git.test.ts @@ -1,9 +1,12 @@ import { describe, expect, test } from "vitest"; +import { GitCommit } from "../src/git"; +import type { ChangelogConfig } from "../src/config"; import { generateMarkDown, getGitDiff, loadChangelogConfig, parseCommits, + filterCommits, getRepoConfig, formatReference, } from "../src"; @@ -133,6 +136,7 @@ describe("git", () => { "value": "3828bda", }, ], + "revertedHashes": [], "scope": "release", "shortHash": "3828bda", "type": "chore", @@ -155,6 +159,7 @@ describe("git", () => { "value": "20e622e", }, ], + "revertedHashes": [], "scope": "scope", "shortHash": "20e622e", "type": "fix", @@ -169,6 +174,7 @@ describe("git", () => { "value": "6fc5087", }, ], + "revertedHashes": [], "scope": "release", "shortHash": "6fc5087", "type": "chore", @@ -187,6 +193,7 @@ describe("git", () => { "value": "c0febf1", }, ], + "revertedHashes": [], "scope": "", "shortHash": "c0febf1", "type": "feat", @@ -201,6 +208,7 @@ describe("git", () => { "value": "f4f42a3", }, ], + "revertedHashes": [], "scope": "release", "shortHash": "f4f42a3", "type": "chore", @@ -215,6 +223,7 @@ describe("git", () => { "value": "648ccf1", }, ], + "revertedHashes": [], "scope": "", "shortHash": "648ccf1", "type": "fix", @@ -229,6 +238,7 @@ describe("git", () => { "value": "5451f18", }, ], + "revertedHashes": [], "scope": "", "shortHash": "5451f18", "type": "feat", @@ -243,6 +253,7 @@ describe("git", () => { "value": "8796cf1", }, ], + "revertedHashes": [], "scope": "", "shortHash": "8796cf1", "type": "chore", @@ -257,6 +268,7 @@ describe("git", () => { "value": "c210976", }, ], + "revertedHashes": [], "scope": "", "shortHash": "c210976", "type": "chore", @@ -275,6 +287,7 @@ describe("git", () => { "value": "a80e372", }, ], + "revertedHashes": [], "scope": "deps", "shortHash": "a80e372", "type": "chore", @@ -318,6 +331,94 @@ describe("git", () => { `); }); + test("filterCommits should retain reverts from previous version", () => { + const inputLog = [ + { + type: "example", + scope: "", + shortHash: "a12345", + revertedHashes: ["b12345"], + } as unknown as GitCommit, + { + type: "example", + scope: "", + shortHash: "c12345", + revertedHashes: ["d12345"], + } as unknown as GitCommit, + ]; + const config: ChangelogConfig = { + types: { + example: { title: "Example" }, + }, + scopeMap: undefined, + github: "", + from: "", + to: "", + cwd: "", + output: "", + }; + + const resolvedLog = filterCommits(inputLog, config); + expect(resolvedLog).toStrictEqual([ + { + type: "example", + scope: "", + shortHash: "a12345", + revertedHashes: ["b12345"], + } as unknown as GitCommit, + { + type: "example", + scope: "", + shortHash: "c12345", + revertedHashes: ["d12345"], + } as unknown as GitCommit, + ]); + }); + + test("filterCommits should remove reverts from upcoming version", () => { + const inputLog = [ + { + type: "example", + scope: "", + shortHash: "a12345", + revertedHashes: ["b12345"], + } as unknown as GitCommit, + { + type: "example", + scope: "", + shortHash: "b12345", + revertedHashes: [], + } as unknown as GitCommit, + { + type: "example", + scope: "", + shortHash: "c12345", + revertedHashes: [], + } as unknown as GitCommit, + ]; + const config: ChangelogConfig = { + types: { + example: { title: "Example" }, + }, + scopeMap: undefined, + github: "", + from: "", + to: "", + cwd: "", + output: "", + }; + + const resolvedLog = filterCommits(inputLog, config); + expect(resolvedLog).toStrictEqual([ + { + type: "example", + scope: "", + shortHash: "c12345", + revertedHashes: [], + } as unknown as GitCommit, + ]); + }); + test("parse host config", () => { expect(getRepoConfig(undefined)).toMatchObject({}); expect(getRepoConfig("")).toMatchObject({});