Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/config/loadConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@ export interface GitbunConfig {
customPrompt?: string;
format?: string;
model?: string;
qualityCheck?: boolean;
strictQuality?: boolean;
}

/** Loads and returns user config from .gitbunrc or cosmiconfig. */
export async function loadConfig(): Promise<GitbunConfig> {
const explorer = cosmiconfig("smartcommit");
const result = await explorer.search();

return result?.config || {};
return {
qualityCheck: true,
strictQuality: false,
...result?.config,
};
}
24 changes: 23 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ import { analyzeSemanticChanges } from "./analyzer/semanticAnalyzer";
import { SemanticEvent } from "./analyzer/semanticTypes";
import { ValidationError, CancellationError } from "./utils/errors";
import { colorizeCommitMessage } from "./utils/commitColors";
import {
analyzeCommitQuality,
shouldBlockCommitForQuality,
} from "./validators/commitQuality";

interface CliOptions {
ai?: boolean;
Expand Down Expand Up @@ -119,6 +123,7 @@ export async function run(options: CliOptions) {

const spinner = ora();
let commitMessage = "";
const config = await loadConfig();

try {
// --- Build enriched files (from main, but without spinner yet) ---
Expand Down Expand Up @@ -177,7 +182,6 @@ export async function run(options: CliOptions) {

// --- Generate commit message (pass semantic events) ---
spinner.start("Generating commit message...");
const config = await loadConfig();
commitMessage = generateCommitMessage(
type,
scope,
Expand Down Expand Up @@ -230,6 +234,24 @@ export async function run(options: CliOptions) {
process.exit(0);
}

if (config.qualityCheck !== false) {
const quality = analyzeCommitQuality(commitMessage);

if (!options.auto && quality.score < 100) {
console.log(`\nCommit Quality: ${quality.score}/100\n`);
console.log("Warnings:");
for (const warning of quality.warnings) {
console.log(`- ${warning}`);
}
}

if (shouldBlockCommitForQuality(quality, config.strictQuality)) {
throw new ValidationError(
`Commit quality score ${quality.score}/100 is below strict threshold.`
);
}
}

// Confirmation
let finalMessage: string;
if (options.auto) {
Expand Down
47 changes: 47 additions & 0 deletions src/validators/commitQuality.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, expect, it } from "vitest";
import {
analyzeCommitQuality,
shouldBlockCommitForQuality,
} from "./commitQuality";

describe("analyzeCommitQuality", () => {
it("accepts basic Conventional Commit messages", () => {
expect(analyzeCommitQuality("feat: add auth").warnings).not.toContain(
"Invalid Conventional Commit format",
);
expect(analyzeCommitQuality("fix(core): resolve bug").score).toBe(100);
});

it("warns for invalid Conventional Commit messages", () => {
expect(analyzeCommitQuality("update code").warnings).toContain(
"Invalid Conventional Commit format",
);
expect(analyzeCommitQuality("fix stuff").warnings).toContain(
"Invalid Conventional Commit format",
);
});

it("detects generic commit messages case-insensitively", () => {
expect(analyzeCommitQuality("WIP").warnings).toContain(
"Generic commit message",
);
});

it("applies simple score penalties", () => {
expect(analyzeCommitQuality("fix stuff")).toEqual({
score: 0,
warnings: [
"Invalid Conventional Commit format",
"Generic commit message",
"Subject too short",
],
});
});

it("blocks only in strict mode when score is below 60", () => {
const result = analyzeCommitQuality("changes");

expect(shouldBlockCommitForQuality(result, true)).toBe(true);
expect(shouldBlockCommitForQuality(result, false)).toBe(false);
});
});
57 changes: 57 additions & 0 deletions src/validators/commitQuality.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
export interface CommitQualityResult {
score: number;
warnings: string[];
}

const GENERIC_MESSAGES = [
"update code",
"fix stuff",
"misc changes",
"changes",
"update files",
"work in progress",
"wip",
];

const CONVENTIONAL_COMMIT_PATTERN =
/^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-z0-9._-]+\))?: .+/i;
Comment thread
vraj826 marked this conversation as resolved.

function getSubject(message: string): string {
const separatorIndex = message.indexOf(":");
return separatorIndex >= 0
? message.slice(separatorIndex + 1).trim()
: message.trim();
}

export function analyzeCommitQuality(message: string): CommitQualityResult {
const normalizedMessage = message.trim().toLowerCase();
const warnings: string[] = [];
let score = 100;

if (!CONVENTIONAL_COMMIT_PATTERN.test(message.trim())) {
score -= 40;
warnings.push("Invalid Conventional Commit format");
}

if (GENERIC_MESSAGES.includes(normalizedMessage)) {
score -= 40;
warnings.push("Generic commit message");
}

if (getSubject(message).length < 10) {
score -= 20;
warnings.push("Subject too short");
}

return {
score: Math.max(0, Math.min(100, score)),
warnings,
};
}

export function shouldBlockCommitForQuality(
result: CommitQualityResult,
strictQuality = false,
): boolean {
return strictQuality && result.score < 60;
}
Loading