Skip to content

Commit

Permalink
feat: add isFixupCommit and isMergeCommit properties
Browse files Browse the repository at this point in the history
This commit adds the `isFixupCommit` and `isMergeCommit`
properties to both Commit and ConventionalCommit classes.

Current implementation is straightforward and only concerns the
commit message subject:

- Fixup: subject starts with `fixup!`
- Merge: support for default merge commit message patterns of
         GitHub, BitBucket and GitLab

These properties can be used to (for example) ignore Conventional
Commit message validation for these types of commits.

Implements #33
  • Loading branch information
Kevin-de-Jong committed Jan 17, 2024
1 parent dc079b8 commit 94579fc
Show file tree
Hide file tree
Showing 8 changed files with 285 additions and 135 deletions.
212 changes: 108 additions & 104 deletions package-lock.json

Large diffs are not rendered by default.

53 changes: 49 additions & 4 deletions src/commit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ export interface IStringDataSourceOptions {
message: string;
}

/**
* Name and Date type
*/
export type NameAndDateType = { name: string; date: Date };

/**
* Commit information
* @interface ICommit
Expand All @@ -32,13 +37,17 @@ export interface IStringDataSourceOptions {
* @internal
*/
export interface ICommit {
author?: { name: string; date: Date };
committer?: { name: string; date: Date };
author?: NameAndDateType;
committer?: NameAndDateType;
hash: string;
raw: string;
subject: string;
body?: string;
footer?: Record<string, string>;
attributes: {
isFixup: boolean;
isMerge: boolean;
};
}

const TRAILER_REGEX = /^((BREAKING CHANGE:)|([\w-]+(:| #))|([ \t]+)\w*)/i;
Expand Down Expand Up @@ -120,6 +129,12 @@ export class Commit {
get raw(): string {
return this._commit.raw;
}
get isFixupCommit(): boolean {
return this._commit.attributes.isFixup;
}
get isMergeCommit(): boolean {
return this._commit.attributes.isMerge;
}

toJSON(): ICommit {
return this._commit;
Expand Down Expand Up @@ -176,6 +191,24 @@ export function getFooterElementsFromParagraph(
return Object.keys(result).length > 0 ? result : undefined;
}

/**
* Checks if the provided subject is a common (default) merge pattern.
* Currently supported:
* - GitHub
* - BitBucket
* - GitLab
*
* @param subject The subject to check
* @returns True if the subject is a common merge pattern, false otherwise
*/
function subjectIsMergePattern(subject: string): boolean {
const githubMergeRegex = /^Merge pull request #(\d+) from (.*)$/;
const bitbucketMergeRegex = /^Merged in (.*) \(pull request #(\d+)\)$/;
const gitlabMergeRegex = /^Merge branch '(.*?)' into '(.*?)'$/;

return githubMergeRegex.test(subject) || bitbucketMergeRegex.test(subject) || gitlabMergeRegex.test(subject);
}

/**
* Parses the provided commit message (full message, not just the subject) into
* a Commit object.
Expand All @@ -187,6 +220,10 @@ export function parseCommitMessage(message: string): {
subject: string;
body?: string;
footer?: Record<string, string>;
attributes: {
isFixup: boolean;
isMerge: boolean;
};
} {
const isTrailerOnly = (message: string): boolean =>
message.split(/\r?\n/).every(line => {
Expand All @@ -208,12 +245,20 @@ export function parseCommitMessage(message: string): {
if (body === "") body = undefined;
}

const subject = paragraphs[0].trim();
const isFixup = subject.toLowerCase().startsWith("fixup!");
const isMerge = subjectIsMergePattern(subject);

return {
subject: paragraphs[0].trim(),
body: body,
subject,
body,
footer: getFooterElementsFromParagraph(footer ?? "")?.reduce((acc, cur) => {
acc[cur.key] = cur.value;
return acc;
}, {} as Record<string, string>),
attributes: {
isFixup,
isMerge,
},
};
}
12 changes: 12 additions & 0 deletions src/conventionalCommit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,14 @@ export class ConventionalCommit {
);
}

// Attributes
get isFixupCommit(): boolean {
return this._raw.commit.isFixupCommit;
}
get isMergeCommit(): boolean {
return this._raw.commit.isMergeCommit;
}

// Raw
get raw(): string {
return this._raw.commit.raw;
Expand Down Expand Up @@ -201,6 +209,10 @@ export class ConventionalCommit {
errors: this.errors,
warnings: this.warnings,
},
attributes: {
isFixup: this.isFixupCommit,
isMerge: this.isMergeCommit,
},
};
}

Expand Down
32 changes: 7 additions & 25 deletions src/git.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,7 @@ import * as path from "path";
import * as zlib from "zlib";

import * as ccommit from "./commit";

/** @internal */
type NameAndDateType = { name: string; date: Date };

/** @internal */
type GitCommitType = {
hash: string;
raw: string;
author?: NameAndDateType;
committer?: NameAndDateType;
subject: string;
body?: string;
footer?: Record<string, string>;
};
import { ICommit, NameAndDateType } from "./commit";

/** @internal */
export const gitObjectFolder = ".git/objects";
Expand Down Expand Up @@ -71,30 +58,25 @@ function getValueFromKey(commit: string, key: string): string | undefined {
* @param hash Commit hash
* @returns Commit object
*/
function parseCommitMessage(commit: string, hash: string): GitCommitType {
function parseCommitMessage(commit: string, hash: string): ICommit {
const author = extractNameAndDate(getValueFromKey(commit, "author"));
const committer = extractNameAndDate(getValueFromKey(commit, "committer"));

const raw = commit
.split(/^[\r\n]+/m)
.splice(1)
.join("\n")
.trim();

return {
raw,
hash: hash,
author: author,
committer: committer,
...ccommit.parseCommitMessage(raw),
};
return { raw, hash, author, committer, ...ccommit.parseCommitMessage(raw) };
}

/**
* Reads a (local) commit message from the .git/objects folder
* @param hash Hash of the commit to read
* @returns Commit object or undefined if the commit could not be found in the local repository
*/
function getCommitFromLocalObjects(hash: string, rootPath: string): ccommit.ICommit | undefined {
function getCommitFromLocalObjects(hash: string, rootPath: string): ICommit | undefined {
const objectPath = path.join(rootPath, gitObjectFolder, hash.substring(0, 2), "/", hash.substring(2));
if (!fs.existsSync(objectPath)) return undefined;

Expand All @@ -108,7 +90,7 @@ function getCommitFromLocalObjects(hash: string, rootPath: string): ccommit.ICom
* @param rootPath Path to the git repository
* @returns Commit object or undefined if the commit could not be found in the pack files
*/
function getCommitFromPackFile(hash: string, rootPath: string): ccommit.ICommit | undefined {
function getCommitFromPackFile(hash: string, rootPath: string): ICommit | undefined {
for (const entry of fs.readdirSync(path.join(rootPath, gitObjectFolder, "pack"))) {
const filePath = path.join(rootPath, gitObjectFolder, "pack", entry);
if (path.extname(filePath) !== ".idx") continue;
Expand All @@ -127,7 +109,7 @@ function getCommitFromPackFile(hash: string, rootPath: string): ccommit.ICommit
* @returns Commit object
* @internal
*/
export function getCommitFromHash(hash: string, rootPath: string): ccommit.ICommit {
export function getCommitFromHash(hash: string, rootPath: string): ICommit {
if (!fs.existsSync(path.join(rootPath, gitObjectFolder)))
throw new Error(`Invalid git folder specified (${path.join(rootPath, gitObjectFolder)})`);

Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@
*/

export { Commit } from "./commit";
export type { NameAndDateType } from "./commit";

export { ConventionalCommit } from "./conventionalCommit";
export type { IConventionalCommitOptions } from "./conventionalCommit";
Original file line number Diff line number Diff line change
Expand Up @@ -285,3 +285,29 @@ describe("Commit message ends at first comment (#)", () => {
expect(commit.breaking).toBe(test.breaking);
});
});

describe("Fixup commits", () => {
const tests = [{ message: "fixup! add feat: some feature" }, { message: "fixup! fixup! add feat: some feature" }];

it.each(tests)("$message", test => {
const commit = ConventionalCommit.fromString({ hash: "01ab2cd3", message: test.message });

expect(commit.isValid).toBe(false);
expect(commit.isFixupCommit).toBe(true);
});
});

describe("Merge commits", () => {
const tests = [
{ message: "Merge pull request #123 from some-branch/feature/branch" },
{ message: "Merged in ci/some-branch (pull request #123)" },
{ message: "Merge branch 'ci/some-branch' into 'main'" },
];

it.each(tests)("$message", test => {
const commit = ConventionalCommit.fromString({ hash: "01ab2cd3", message: test.message });

expect(commit.isValid).toBe(false);
expect(commit.isMergeCommit).toBe(true);
});
});
7 changes: 5 additions & 2 deletions test/git.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* SPDX-License-Identifier: MIT
*/

import { ICommit } from "../src/commit";
import * as git from "../src/git";
import { Commit } from "../src/index";

Expand Down Expand Up @@ -38,7 +39,8 @@ in .git/objects. Rationale: remove any need for external dependencies
Calling \`getCommitMessage(...)\` will return a standard ICommit object`,
footer: undefined,
} as Commit,
attributes: { isFixup: false, isMerge: false },
} as ICommit,
});
});

Expand All @@ -59,7 +61,8 @@ Calling \`getCommitMessage(...)\` will return a standard ICommit object`,
subject: "Initial commit",
body: undefined,
footer: undefined,
} as Commit,
attributes: { isFixup: false, isMerge: false },
} as ICommit,
});
});

Expand Down
Loading

0 comments on commit 94579fc

Please sign in to comment.