Skip to content

Commit c04edba

Browse files
committed
테스트 코드 추가
1 parent 0cc9218 commit c04edba

File tree

12 files changed

+763
-4
lines changed

12 files changed

+763
-4
lines changed

dist/chunk-NF65L2JT.js

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
// src/core.ts
2+
import fs2 from "fs";
3+
import cp from "child_process";
4+
5+
// src/config.ts
6+
import fs from "fs";
7+
import path from "path";
8+
var DEFAULTS = {
9+
includePatterns: ["*"],
10+
format: "[${ticket}] ${msg}",
11+
fallbackFormat: "[${seg0}] ${msg}",
12+
exclude: ["merge", "squash", "revert"]
13+
};
14+
function loadConfig(cwd) {
15+
try {
16+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, "package.json"), "utf-8"));
17+
const pkgCfg = pkg.commitFromBranch;
18+
const cfg = pkgCfg ?? {};
19+
const include = cfg.includePattern ?? "*";
20+
return {
21+
includePatterns: Array.isArray(include) ? include : [include],
22+
format: cfg.format ?? DEFAULTS.format,
23+
fallbackFormat: cfg.fallbackFormat ?? DEFAULTS.fallbackFormat,
24+
exclude: (cfg.exclude ?? DEFAULTS.exclude).map(String)
25+
};
26+
} catch {
27+
return { ...DEFAULTS };
28+
}
29+
}
30+
31+
// src/tokens.ts
32+
function renderTemplate(tpl, ctx) {
33+
let out = String(tpl);
34+
out = out.replace(/\$\{prefix:(\d+)\}/g, (_m, n) => {
35+
const k = Math.max(0, parseInt(n, 10) || 0);
36+
return ctx.segs.slice(0, k).join("/") || "";
37+
});
38+
out = out.replace(/\$\{seg(\d+)\}/g, (_m, i) => {
39+
const idx = parseInt(i, 10) || 0;
40+
return ctx.segs[idx] || "";
41+
});
42+
return out.replace(/\$\{ticket\}/g, ctx.ticket || "").replace(/\$\{branch\}/g, ctx.branch || "").replace(/\$\{segments\}/g, ctx.segs.join("/")).replace(/\$\{msg\}/g, ctx.msg || "").replace(/\$\{body\}/g, ctx.body || "");
43+
}
44+
45+
// src/core.ts
46+
var reStar = (pat) => new RegExp("^" + pat.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$", "i");
47+
var includeMatch = (s, pats) => pats.some((p) => reStar(p).test(s));
48+
var isDebug = (env) => /^(1|true|yes)$/i.test(String(env.BRANCH_PREFIX_DEBUG || ""));
49+
var isDryRun = (env) => /^(1|true|yes)$/i.test(String(env.BRANCH_PREFIX_DRYRUN || ""));
50+
function run(opts = {}) {
51+
const argv = opts.argv ?? process.argv;
52+
const env = opts.env ?? process.env;
53+
const cwd = opts.cwd ?? process.cwd();
54+
const [, , COMMIT_MSG_PATH, SOURCE] = argv;
55+
const debug = isDebug(env);
56+
const log = (...a) => {
57+
if (debug) console.log("[cfb]", ...a);
58+
};
59+
if (!COMMIT_MSG_PATH) return 0;
60+
const cfg = loadConfig(cwd);
61+
log("config", cfg);
62+
if (SOURCE && cfg.exclude.some((x) => new RegExp(x, "i").test(String(SOURCE)))) {
63+
log("exit: excluded by source", SOURCE);
64+
return 0;
65+
}
66+
let branch = "";
67+
try {
68+
branch = cp.execSync("git rev-parse --abbrev-ref HEAD", { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
69+
} catch {
70+
}
71+
if (!branch || branch === "HEAD") {
72+
log("exit: no branch or detached HEAD");
73+
return 0;
74+
}
75+
if (!includeMatch(branch, cfg.includePatterns)) {
76+
log("exit: includePattern mismatch", branch);
77+
return 0;
78+
}
79+
const ticket = (branch.match(/([A-Z]+-\d+)/i)?.[1] || "").toUpperCase();
80+
const body = fs2.readFileSync(COMMIT_MSG_PATH, "utf8");
81+
const lines = body.split("\n");
82+
const msg0 = lines[0] ?? "";
83+
const segs = branch.split("/");
84+
const tpl = ticket ? cfg.format : cfg.fallbackFormat;
85+
const ctx = { branch, segs, ticket, msg: msg0, body };
86+
const hasMsgToken = /\$\{msg\}|\$\{body\}/.test(tpl);
87+
const rendered = renderTemplate(tpl, ctx);
88+
log("branch", branch, "ticket", ticket || "(none)");
89+
log("tpl", tpl);
90+
log("rendered", rendered);
91+
log("mode", hasMsgToken ? "replace-line" : "prefix-only");
92+
if (hasMsgToken) {
93+
if (msg0 === rendered) return 0;
94+
lines[0] = rendered;
95+
} else {
96+
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
97+
if (new RegExp("^\\s*" + esc(rendered), "i").test(msg0)) return 0;
98+
if (ticket) {
99+
const ticketRegex = new RegExp(`\\b${esc(ticket)}\\b`, "i");
100+
if (ticketRegex.test(msg0)) {
101+
log("exit: ticket already in message", ticket);
102+
return 0;
103+
}
104+
}
105+
if (segs[0] && segs[0] !== "HEAD") {
106+
const segRegex = new RegExp(`\\b${esc(segs[0])}\\b`, "i");
107+
if (segRegex.test(msg0)) {
108+
log("exit: branch segment already in message", segs[0]);
109+
return 0;
110+
}
111+
}
112+
lines[0] = rendered + msg0;
113+
}
114+
if (isDryRun(env)) {
115+
log("dry-run: not writing");
116+
return 0;
117+
}
118+
fs2.writeFileSync(COMMIT_MSG_PATH, lines.join("\n"), "utf8");
119+
log("write ok");
120+
return 0;
121+
}
122+
123+
export {
124+
run
125+
};

dist/cli.cjs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,20 @@ function run(opts = {}) {
120120
} else {
121121
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
122122
if (new RegExp("^\\s*" + esc(rendered), "i").test(msg0)) return 0;
123+
if (ticket) {
124+
const ticketRegex = new RegExp(`\\b${esc(ticket)}\\b`, "i");
125+
if (ticketRegex.test(msg0)) {
126+
log("exit: ticket already in message", ticket);
127+
return 0;
128+
}
129+
}
130+
if (segs[0] && segs[0] !== "HEAD") {
131+
const segRegex = new RegExp(`\\b${esc(segs[0])}\\b`, "i");
132+
if (segRegex.test(msg0)) {
133+
log("exit: branch segment already in message", segs[0]);
134+
return 0;
135+
}
136+
}
123137
lines[0] = rendered + msg0;
124138
}
125139
if (isDryRun(env)) {

dist/cli.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env node
22
import {
33
run
4-
} from "./chunk-PMXOKYP7.js";
4+
} from "./chunk-NF65L2JT.js";
55
import {
66
initHusky
77
} from "./chunk-4EHHYCRE.js";

dist/core.cjs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,20 @@ function run(opts = {}) {
129129
} else {
130130
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
131131
if (new RegExp("^\\s*" + esc(rendered), "i").test(msg0)) return 0;
132+
if (ticket) {
133+
const ticketRegex = new RegExp(`\\b${esc(ticket)}\\b`, "i");
134+
if (ticketRegex.test(msg0)) {
135+
log("exit: ticket already in message", ticket);
136+
return 0;
137+
}
138+
}
139+
if (segs[0] && segs[0] !== "HEAD") {
140+
const segRegex = new RegExp(`\\b${esc(segs[0])}\\b`, "i");
141+
if (segRegex.test(msg0)) {
142+
log("exit: branch segment already in message", segs[0]);
143+
return 0;
144+
}
145+
}
132146
lines[0] = rendered + msg0;
133147
}
134148
if (isDryRun(env)) {

dist/core.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {
22
run
3-
} from "./chunk-PMXOKYP7.js";
3+
} from "./chunk-NF65L2JT.js";
44
export {
55
run
66
};

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
},
4141
"scripts": {
4242
"build": "tsup src/core.ts src/cli.ts src/init.ts --dts --format cjs,esm --out-dir dist",
43+
"test": "node --test test/*.test.js",
4344
"prepublishOnly": "npm run build"
4445
},
4546
"devDependencies": {

src/core.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,25 @@ export function run(opts: RunOptions = {}) {
6666
} else {
6767
const esc = (s: string) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
6868
if (new RegExp('^\\s*' + esc(rendered), 'i').test(msg0)) return 0;
69+
70+
// 추가 중복 체크: 메시지 안에 이미 티켓이나 브랜치 정보가 있는지 확인
71+
if (ticket) {
72+
const ticketRegex = new RegExp(`\\b${esc(ticket)}\\b`, 'i');
73+
if (ticketRegex.test(msg0)) {
74+
log('exit: ticket already in message', ticket);
75+
return 0;
76+
}
77+
}
78+
79+
// 브랜치의 첫 번째 세그먼트가 이미 메시지에 있는지 확인
80+
if (segs[0] && segs[0] !== 'HEAD') {
81+
const segRegex = new RegExp(`\\b${esc(segs[0])}\\b`, 'i');
82+
if (segRegex.test(msg0)) {
83+
log('exit: branch segment already in message', segs[0]);
84+
return 0;
85+
}
86+
}
87+
6988
lines[0] = rendered + msg0;
7089
}
7190

test/config.test.js

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import { test, describe, beforeEach, afterEach } from 'node:test';
2+
import assert from 'node:assert';
3+
import fs from 'fs';
4+
import path from 'path';
5+
6+
// Helper function that replicates loadConfig logic for testing
7+
function loadConfig(cwd) {
8+
const DEFAULTS = {
9+
includePatterns: ['*'],
10+
format: '[${ticket}] ${msg}',
11+
fallbackFormat: '[${seg0}] ${msg}',
12+
exclude: ['merge', 'squash', 'revert']
13+
};
14+
15+
try {
16+
const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf-8'));
17+
const pkgCfg = pkg.commitFromBranch;
18+
const cfg = (pkgCfg ?? {});
19+
const include = cfg.includePattern ?? '*';
20+
21+
return {
22+
includePatterns: Array.isArray(include) ? include : [include],
23+
format: cfg.format ?? DEFAULTS.format,
24+
fallbackFormat: cfg.fallbackFormat ?? DEFAULTS.fallbackFormat,
25+
exclude: (cfg.exclude ?? DEFAULTS.exclude).map(String)
26+
};
27+
} catch {
28+
return { ...DEFAULTS };
29+
}
30+
}
31+
32+
describe('loadConfig', () => {
33+
let tempDir;
34+
35+
beforeEach(() => {
36+
tempDir = fs.mkdtempSync(path.join(process.cwd(), 'config-test-'));
37+
});
38+
39+
afterEach(() => {
40+
if (tempDir && fs.existsSync(tempDir)) {
41+
fs.rmSync(tempDir, { recursive: true, force: true });
42+
}
43+
});
44+
45+
test('should return defaults when no package.json exists', () => {
46+
const config = loadConfig(tempDir);
47+
48+
assert.deepStrictEqual(config, {
49+
includePatterns: ['*'],
50+
format: '[${ticket}] ${msg}',
51+
fallbackFormat: '[${seg0}] ${msg}',
52+
exclude: ['merge', 'squash', 'revert']
53+
});
54+
});
55+
56+
test('should return defaults when package.json has no config', () => {
57+
const packageJson = {
58+
name: 'test-package',
59+
version: '1.0.0'
60+
};
61+
fs.writeFileSync(
62+
path.join(tempDir, 'package.json'),
63+
JSON.stringify(packageJson, null, 2)
64+
);
65+
66+
const config = loadConfig(tempDir);
67+
68+
assert.deepStrictEqual(config, {
69+
includePatterns: ['*'],
70+
format: '[${ticket}] ${msg}',
71+
fallbackFormat: '[${seg0}] ${msg}',
72+
exclude: ['merge', 'squash', 'revert']
73+
});
74+
});
75+
76+
test('should load custom config from package.json', () => {
77+
const packageJson = {
78+
name: 'test-package',
79+
version: '1.0.0',
80+
commitFromBranch: {
81+
includePattern: ['feature/*', 'hotfix/*'],
82+
format: '${ticket}: ${msg}',
83+
fallbackFormat: '${branch} - ${msg}',
84+
exclude: ['merge']
85+
}
86+
};
87+
fs.writeFileSync(
88+
path.join(tempDir, 'package.json'),
89+
JSON.stringify(packageJson, null, 2)
90+
);
91+
92+
const config = loadConfig(tempDir);
93+
94+
assert.deepStrictEqual(config, {
95+
includePatterns: ['feature/*', 'hotfix/*'],
96+
format: '${ticket}: ${msg}',
97+
fallbackFormat: '${branch} - ${msg}',
98+
exclude: ['merge']
99+
});
100+
});
101+
102+
test('should handle single includePattern as string', () => {
103+
const packageJson = {
104+
name: 'test-package',
105+
version: '1.0.0',
106+
commitFromBranch: {
107+
includePattern: 'feature/*'
108+
}
109+
};
110+
fs.writeFileSync(
111+
path.join(tempDir, 'package.json'),
112+
JSON.stringify(packageJson, null, 2)
113+
);
114+
115+
const config = loadConfig(tempDir);
116+
117+
assert.deepStrictEqual(config.includePatterns, ['feature/*']);
118+
});
119+
120+
test('should merge partial config with defaults', () => {
121+
const packageJson = {
122+
name: 'test-package',
123+
version: '1.0.0',
124+
commitFromBranch: {
125+
format: 'CUSTOM: ${msg}'
126+
}
127+
};
128+
fs.writeFileSync(
129+
path.join(tempDir, 'package.json'),
130+
JSON.stringify(packageJson, null, 2)
131+
);
132+
133+
const config = loadConfig(tempDir);
134+
135+
assert.strictEqual(config.format, 'CUSTOM: ${msg}');
136+
assert.strictEqual(config.fallbackFormat, '[${seg0}] ${msg}'); // should use default
137+
assert.deepStrictEqual(config.exclude, ['merge', 'squash', 'revert']); // should use default
138+
});
139+
140+
test('should handle malformed package.json gracefully', () => {
141+
fs.writeFileSync(
142+
path.join(tempDir, 'package.json'),
143+
'{ invalid json }'
144+
);
145+
146+
const config = loadConfig(tempDir);
147+
148+
assert.deepStrictEqual(config, {
149+
includePatterns: ['*'],
150+
format: '[${ticket}] ${msg}',
151+
fallbackFormat: '[${seg0}] ${msg}',
152+
exclude: ['merge', 'squash', 'revert']
153+
});
154+
});
155+
});

0 commit comments

Comments
 (0)