From c0c01ef8acb27efd034da630549e17d93e528a5d Mon Sep 17 00:00:00 2001 From: djk01281 Date: Wed, 1 Jul 2026 17:40:44 +0900 Subject: [PATCH 1/2] :sparkles: feat: port passthrough + Jigit key recognition to commit-helper Reflect the passthrough / verbatim-key rules from @naverpay/commithelper-go into the original @naverpay/commit-helper. - passthrough: verbatim Jira/Linear keys (feature/PROJ-1871 -> [PROJ-1871]), Jigit key recognition (-<1-7 digit number>, anywhere in the branch), allowlist-only so tokens like UTF-8 are never mistaken for issues. - reference-specific idempotency (alreadyHasRef) replacing the old any-tag ISSUE_TAGGING_REGEX gate; safe on git commit --amend / hook re-run, including verbatim keys like [PROJ-1871]. - hybrid casing: preserve branch case so uppercase keys are recognized; rules lookup and protect matching stay case-insensitive. BREAKING: idempotency is now reference-specific -- a tag for a different issue no longer suppresses tagging ([#123] [#999] ...). Major bump via changeset. Co-Authored-By: Claude Opus 4.8 --- .changeset/commit-helper-passthrough.md | 9 ++ packages/commit-helper/README.ko.md | 36 ++++- packages/commit-helper/README.md | 36 ++++- packages/commit-helper/__test__/cli.test.js | 120 +++++++++++--- packages/commit-helper/bin/cli.ts | 171 ++++++++++++++------ packages/commit-helper/src/constant.ts | 2 +- 6 files changed, 293 insertions(+), 81 deletions(-) create mode 100644 .changeset/commit-helper-passthrough.md diff --git a/.changeset/commit-helper-passthrough.md b/.changeset/commit-helper-passthrough.md new file mode 100644 index 0000000..00fc85b --- /dev/null +++ b/.changeset/commit-helper-passthrough.md @@ -0,0 +1,9 @@ +--- +"@naverpay/commit-helper": major +--- + +Add `passthrough` for Jira/Linear-style issue keys, and make re-tagging match only your branch's own tag. + +**`passthrough`** — list your project keys, and branches that already contain the full key are tagged as-is. With `{ "passthrough": ["PROJ"] }`, branch `feature/PROJ-1871` becomes `[PROJ-1871]`. Only the keys you list are tagged, so unrelated text like `UTF-8` is never mistaken for an issue. Key detection matches [Jigit](https://marketplace.atlassian.com/apps/1217129), so a branch links the same way in Jira and here. + +**Breaking change** — commit-helper skips a commit that is already tagged, and what counts as "already tagged" changed. Before, *any* `[#…]` tag in the message stopped it. Now, only *your current branch's own* tag does. For example, on branch `feature/123`, a message you wrote as `[#999] fix` used to be left alone, but now becomes `[#123] [#999] fix`. Re-running the hook or `git commit --amend` still never adds your tag twice — this now includes verbatim keys like `[PROJ-1871]`, which the old check could not detect. diff --git a/packages/commit-helper/README.ko.md b/packages/commit-helper/README.ko.md index 42f9f73..d738a5f 100644 --- a/packages/commit-helper/README.ko.md +++ b/packages/commit-helper/README.ko.md @@ -46,6 +46,7 @@ git commit -m "새로운 기능 추가" - `feature/123` → `[#123] 메시지` - `qa/456` → `[your-org/your-repo#456] 메시지` - `hotfix/789-urgent` → `[#789] 메시지` +- `feature/PROJ-1871` → `[PROJ-1871] 메시지` (passthrough 로 Jira/Linear 키 지원) ### 🛡️ 브랜치 보호 @@ -90,6 +91,27 @@ git commit -m "새로운 기능 추가" - 키: 브랜치 접두사 (예: `"feature"`) - 값: 저장소 이름 또는 현재 저장소인 경우 `null` +#### `passthrough` (배열) + +이슈 키에 이미 프로젝트가 포함된 트래커(Jira/Linear 스타일 `PROJ-1871`)를 위한 프로젝트 키 목록. `rules` 가 브랜치 접두사를 저장소 참조로 변환하는 것과 달리, 등록된 키는 커밋 메시지에 **그대로** 복사됩니다. + +```json +{ "passthrough": ["PROJ", "OPS"] } +``` + +`feature/PROJ-1871` 브랜치는 `[PROJ-1871]` 로 태깅됩니다. 등록된 프로젝트만 인식하므로 `UTF-8` 같은 무관한 `대문자-숫자` 토큰이 이슈로 오인되지 않습니다. + +키 인식은 [Jigit](https://marketplace.atlassian.com/apps/1217129)(Jira↔Git 연동)과 동일합니다. 키는 브랜치 어디에나 올 수 있는 `-` 형태이며, `PROJECT` 는 대문자로 시작해 대문자/숫자가 이어지고(2자 이상), `NUMBER` 는 1~7자리 숫자입니다. + +| 브랜치 (`["PROJ"]` 기준) | 결과 | +| ----------------------------- | ------------------------- | +| `feature/PROJ-1871` | `[PROJ-1871]` | +| `feature/PROJ-1871-add-login` | `[PROJ-1871]` | +| `feature/OPS-42` | 태깅 안 됨 (`OPS` 미등록) | +| `feature/PROJ-12345678` | 태깅 안 됨 (8자리 이상) | + +브랜치가 둘 다에 매칭되면 `rules` 가 `passthrough` 보다 우선합니다. `feature` 와 `repo` 는 빌트인 prefix 규칙(`rules` 에 없어도 항상 활성)이라, `feature/311` 같은 `/` 브랜치는 passthrough 키가 아니라 prefix 경로로 `[#311]` 태깅됩니다. `feature/PROJ-311` 처럼 슬래시 뒤에 숫자가 아닌 글자가 오는 verbatim 키는 영향받지 않습니다. + #### `extends` (문자열) 설정을 상속받을 URL: @@ -105,11 +127,21 @@ git commit -m "새로운 기능 추가" ### 기본 기능 브랜치 ```bash -git checkout -b feature/NP-1234-결제-통합 +git checkout -b feature/1234-결제-통합 git commit -m "결제 게이트웨이 구현" # 결과: [#1234] 결제 게이트웨이 구현 ``` +### Jira/Linear 이슈 키 (passthrough) + +`{ "passthrough": ["PROJ"] }` 설정 시: + +```bash +git checkout -b feature/PROJ-1871-결제-통합 +git commit -m "결제 게이트웨이 구현" +# 결과: [PROJ-1871] 결제 게이트웨이 구현 +``` + ### 외부 저장소 참조 설정 파일: @@ -183,7 +215,7 @@ npx @naverpay/commit-helper ## 자주 묻는 질문 **Q: 이미 이슈 태그가 있는 경우에도 작동하나요?** -A: 네, 커밋 메시지에 이미 `[#123]` 같은 태그가 있으면 commit-helper는 건너뜁니다. +A: 메시지에 **현재 브랜치의** 참조(예: `feature/123` 의 `[#123]`)가 이미 있으면 그대로 둡니다 — 재실행/`git commit --amend` 에 안전합니다. 단, _다른_ 이슈 태그(예: 수동으로 넣은 `[#999]`)는 브랜치 참조 추가를 막지 않습니다. **Q: 여러 이슈 번호를 사용할 수 있나요?** A: 브랜치 이름에서는 하나의 이슈 번호만 지원하지만, 커밋 메시지에 수동으로 추가할 수 있습니다. diff --git a/packages/commit-helper/README.md b/packages/commit-helper/README.md index dc21cae..a637b33 100644 --- a/packages/commit-helper/README.md +++ b/packages/commit-helper/README.md @@ -46,6 +46,7 @@ Extracts issue numbers from branch names and adds them to commit messages: - `feature/123` → `[#123] your message` - `qa/456` → `[your-org/your-repo#456] your message` - `hotfix/789-urgent` → `[#789] your message` +- `feature/PROJ-1871` → `[PROJ-1871] your message` (Jira/Linear keys via `passthrough`) ### 🛡️ Branch Protection @@ -90,6 +91,27 @@ Mapping of branch prefixes to repository names. - Key: Branch prefix (e.g., `"feature"`) - Value: Repository name or `null` for current repo +#### `passthrough` (array) + +Project keys for trackers whose issue key already contains the project (Jira/Linear-style `PROJ-1871`). Listed keys are copied **verbatim** into the commit message — unlike `rules`, which translate a branch prefix into a repo reference. + +```json +{ "passthrough": ["PROJ", "OPS"] } +``` + +A branch like `feature/PROJ-1871` is tagged `[PROJ-1871]`. Only listed projects are recognized, so unrelated `UPPERCASE-NUMBER` tokens (e.g. `UTF-8`) are never mistaken for issues. + +Key recognition matches [Jigit](https://marketplace.atlassian.com/apps/1217129) (the Jira↔Git integration): a key is `-` found anywhere in the branch, where `PROJECT` is an uppercase letter followed by uppercase letters/digits (≥2 chars) and `NUMBER` is 1–7 digits. + +| Branch (with `["PROJ"]`) | Result | +| ----------------------------- | ----------------------------- | +| `feature/PROJ-1871` | `[PROJ-1871]` | +| `feature/PROJ-1871-add-login` | `[PROJ-1871]` | +| `feature/OPS-42` | not tagged (`OPS` not listed) | +| `feature/PROJ-12345678` | not tagged (8+ digits) | + +`rules` take precedence over `passthrough` when a branch matches both. Note that `feature` and `repo` are built-in prefix rules (active even without a `rules` entry), so a `/` branch like `feature/311` is tagged `[#311]` by the prefix path rather than as a passthrough key. Verbatim keys such as `feature/PROJ-311` are unaffected — a letter, not a digit, follows the slash. + #### `extends` (string) URL to inherit configuration from: @@ -105,11 +127,21 @@ URL to inherit configuration from: ### Basic Feature Branch ```bash -git checkout -b feature/NP-1234-payment-integration +git checkout -b feature/1234-payment-integration git commit -m "Implement payment gateway" # Result: [#1234] Implement payment gateway ``` +### Jira/Linear Issue Key (passthrough) + +With `{ "passthrough": ["PROJ"] }`: + +```bash +git checkout -b feature/PROJ-1871-payment-integration +git commit -m "Implement payment gateway" +# Result: [PROJ-1871] Implement payment gateway +``` + ### External Repository Reference With configuration: @@ -183,7 +215,7 @@ npx @naverpay/commit-helper ## FAQ **Q: Does it work with existing issue tags?** -A: Yes, if your commit message already contains a tag like `[#123]`, commit-helper will skip it. +A: If the message already contains **your current branch's** reference (e.g. `[#123]` on `feature/123`), it is left unchanged — safe on re-run and `git commit --amend`. A tag for a _different_ issue (e.g. a hand-written `[#999]`) does not stop your branch's reference from being added. **Q: Can I use multiple issue numbers?** A: The branch name supports one issue number, but you can manually add more in your commit message. diff --git a/packages/commit-helper/__test__/cli.test.js b/packages/commit-helper/__test__/cli.test.js index a0a04bc..d339867 100644 --- a/packages/commit-helper/__test__/cli.test.js +++ b/packages/commit-helper/__test__/cli.test.js @@ -1,26 +1,5 @@ -import {getCommitMessageByBranchName, isStringMatchingPatterns} from '../bin/cli.js' -import {ISSUE_TAGGING_REGEX, BRANCH_ISSUE_TAGGING_REGEX} from '../src/constant.js' - -describe('커밋 메시지내 이슈를 찾는 정규식 테스트', () => { - it.each([ - ['일반적인 메시지', false], - ['naverpay/cli#1', false], - ['naverpay/cli#1]', false], - ['naverpay/cli#1] 결제는 네이버페이로', false], - ['[naverpay/cli#1', false], - ['[naverpay/cli#1 결제는 네이버페이로', false], - ['[naverpay/cli#1]', true], - ['[naverpay/cli#1] 결제는 네이버페이로', true], - ['[naverpay/cli#12345] 결제는 네이버페이로', true], - ['[naverpay/cli#123456] 결제는 네이버페이로', false], - ['[#12345] 결제는 네이버페이로', true], - ['[#123] 결제는 네이버페이로', true], - ['[#123456] 결제는 네이버페이로', false], - ])('커밋 메시지 "%s"는 내부에 이슈 태깅이 있다 => (%s)', (message, result) => { - const output = message.match(ISSUE_TAGGING_REGEX) !== null - expect(output).toBe(result) - }) -}) +import {getCommitMessageByBranchName, isStringMatchingPatterns, resolveKey, alreadyHasRef} from '../bin/cli.js' +import {BRANCH_ISSUE_TAGGING_REGEX} from '../src/constant.js' describe('브랜치가 commithelper 형식에 맞는지 확인하는 정규식', () => { it.each([ @@ -84,3 +63,98 @@ describe('', () => { expect(output).toBe(result) }) }) + +/** + * @description passthrough 키 인식 (commithelper-go 의 resolveKey 와 동일 규칙). + */ +describe('resolveKey: passthrough 키 인식', () => { + it.each([ + [['PROJ'], 'feature/PROJ-1871', 'PROJ-1871'], // 등록된 키 + [['PROJ'], 'PROJ-1871', 'PROJ-1871'], // prefix 없는 순수 키 + [['PROJ'], 'feature/PROJ-1871-add-login', 'PROJ-1871'], // 설명 suffix 무시 + [['PROJ'], 'feature/PROJ-1871-20260101', 'PROJ-1871'], // 날짜 suffix 인식 (Jigit 규칙) + [['PROJ'], 'feature/PROJ-1871_wip', 'PROJ-1871'], // underscore 인식 (Jigit 규칙) + [['PROJ'], 'feature/OPS-42', null], // 미등록 프로젝트 + [['PROJ'], 'feature/PROJ-12345678', null], // 8자리 숫자 거부 + [['PROJ'], 'XPROJ-123', null], // 더 긴 키 안에서는 프로젝트가 매칭되지 않음 + [['PROJ'], 'chore/UTF-8-PROJ-123', 'PROJ-123'], // 잡토큰 건너뛰고 등록된 키 선택 + [['proj'], 'feature/PROJ-1', null], // 소문자 목록은 매칭되지 않음 + [undefined, 'feature/PROJ-1871', null], // passthrough 없으면 비활성 + [[], 'feature/PROJ-1871', null], // 빈 목록도 비활성 + ])('resolveKey("%s", %o) === %s', (passthrough, branch, expected) => { + expect(resolveKey(branch, passthrough)).toBe(expected) + }) +}) + +/** + * @description Jigit 문서에 나오는 키 인식 케이스와 동일하게 동작하는지 (["ABC"] 기준). + */ +describe('resolveKey: Jigit 문서 케이스 패리티', () => { + it.each([ + ['ABC-123', 'ABC-123'], + ['feature/ABC-123', 'ABC-123'], + ['feature_ABC-123', 'ABC-123'], + ['feature/ABC-123-modal', 'ABC-123'], + ['ABC-123_modal', 'ABC-123'], + ['[ABC-123] feature', 'ABC-123'], + ['release/test/ABC-123/fix', 'ABC-123'], + ['ABC-123-4', 'ABC-123'], + ['ABC-12345678', null], + ['abc-123', null], + ['Abc-123', null], + ['ABC_123', null], + ['ABC123', null], + ['ABC-abc', null], + ])('resolveKey("%s", ["ABC"]) === %s', (branch, expected) => { + expect(resolveKey(branch, ['ABC'])).toBe(expected) + }) +}) + +/** + * @description 멱등 태깅 경계 (commithelper-go 의 alreadyHasRef 와 동일). + */ +describe('alreadyHasRef: 멱등 태깅 경계', () => { + it.each([ + ['[#123] fix', '#123', true], // 앞쪽 기본 태그 + ['[#1234] fix', '#123', false], // 더 긴 숫자는 매칭 아님 + ['fix\n\nRef. [#123]', '#123', true], // 본문 하단 참조 (template) + ['[PROJ-1871] fix', 'PROJ-1871', true], // verbatim 키 존재 + ['[org/repo#123] fix', 'org/repo#123', true], // cross-repo 참조 존재 + ['fix login', '#123', false], // 없음 + ['see AB-1abc here', 'AB-1', false], // 참조 뒤에 글자가 오면 매칭 아님 + ['[MY-PROJ-1871] earlier', 'PROJ-1871', false], // 더 긴 하이픈 키 안에서는 매칭 아님 + ])('alreadyHasRef("%s", "%s") === %s', (message, ref, expected) => { + expect(alreadyHasRef(message, ref)).toBe(expected) + }) +}) + +/** + * @description resolve 우선순위(prefix > passthrough)와 멱등성 end-to-end. + */ +describe('getCommitMessageByBranchName: 우선순위와 멱등', () => { + const rules = {feature: null} + const passthrough = ['ABC', 'PROJ'] + + it.each([ + // prefix 해석 (JS 정규식 기준) + ['my-team/11', '메시지', {'my-team': 'my-org/my-repo'}, undefined, '[my-org/my-repo#11] 메시지'], + ['feature/42', '메시지', {feature: null}, undefined, '[#42] 메시지'], + ['main', '메시지', {feature: null}, undefined, '메시지'], + ['unknown/99', '메시지', {feature: null}, undefined, '메시지'], + ['feature/PROJ-1', '메시지', {feature: null}, undefined, '메시지'], // prefix 모양 아님 + passthrough 없음 + // 우선순위 + ['feature/123', '메시지', rules, passthrough, '[#123] 메시지'], // github prefix + ['feature/ABC-99', '메시지', rules, passthrough, '[ABC-99] 메시지'], // verbatim 키 + ['feature/12-ABC-34', '메시지', rules, passthrough, '[#12] 메시지'], // 둘 다 매칭 → prefix 우선 + ['wip', '메시지', rules, passthrough, '메시지'], // 둘 다 아님 + // 멱등 / verbatim + ['feature/PROJ-1871', 'fix', {}, ['PROJ'], '[PROJ-1871] fix'], // verbatim 태깅 + ['feature/123', '[#123] fix', {feature: null}, undefined, '[#123] fix'], // 재실행 멱등 + ['feature/PROJ-1871', '[MY-PROJ-1871] earlier', {}, ['PROJ'], '[PROJ-1871] [MY-PROJ-1871] earlier'], // 임베디드 키는 태깅됨으로 보지 않음 + ['feature/PROJ-1', 'work on PROJ-1abc', {}, ['PROJ'], '[PROJ-1] work on PROJ-1abc'], // 뒤에 글자 오는 건 태깅됨 아님 + // behavior-change lock (major 사유): 다른 이슈 태그는 브랜치 참조 추가를 막지 않는다 + ['feature/123', '[#999] fix', {feature: null}, undefined, '[#123] [#999] fix'], + ])('[%s] "%s" → "%s"', (branch, message, rulesMap, pass, expected) => { + expect(getCommitMessageByBranchName(branch, message, rulesMap, pass)).toBe(expected) + }) +}) diff --git a/packages/commit-helper/bin/cli.ts b/packages/commit-helper/bin/cli.ts index 9d3916e..0823c6d 100644 --- a/packages/commit-helper/bin/cli.ts +++ b/packages/commit-helper/bin/cli.ts @@ -10,8 +10,8 @@ import { ISSUE_TAGGING_MAP, BRANCH_ISSUE_TAGGING_REGEX, PURE_BRANCH_ISSUE_TAGGING_REGEX, + KEY_PATTERN, DEFAULT_PROTECTED_BRANCHES, - ISSUE_TAGGING_REGEX, } from '../src/constant.js' async function getCurrentBranchName(): Promise { @@ -30,71 +30,126 @@ async function getCurrentBranchName(): Promise { }) } -export function getCommitMessageByBranchName( - branchName: string, - originCommitMessage: string, - externalConfig?: Record, -) { - let finalCommitMessage = '' +// resolvePrefix: "/" 브랜치를 rules 로 참조 문자열로 변환한다. +// null 값 → "#N", repo 값 → "repo#N", 매칭/미등록이면 null. +function resolvePrefix(branchName: string, issueMap: Record): string | null { + const foundedIssueTagging = branchName.match(BRANCH_ISSUE_TAGGING_REGEX) - // 커밋 메시지 내부에 이슈 태깅이 되어 있을 경우, 브랜치명을 확인하지 않고 그냥 보낸다. - if (ISSUE_TAGGING_REGEX.test(originCommitMessage)) { - finalCommitMessage = originCommitMessage + if (!foundedIssueTagging || foundedIssueTagging.length === 0) { + return null } - // 현재 브랜치명에서 이슈 태깅을 찾아서 커밋 메시지에 추가한다. - else { - const foundedIssueTagging = branchName.match(BRANCH_ISSUE_TAGGING_REGEX) - // 브랜치 명이 정규식과 일치한다면 - if (foundedIssueTagging && foundedIssueTagging.length > 0) { - // 브랜치명에는 _fx 와 같은 추가정보가 있을 수 있으므로, 서비스 명을 추가로 뽑아낸다. - const foundBranch = foundedIssueTagging[0].match(PURE_BRANCH_ISSUE_TAGGING_REGEX) + // 브랜치명에는 _fix 와 같은 추가정보가 있을 수 있으므로, 서비스 명을 추가로 뽑아낸다. + const foundBranch = foundedIssueTagging[0].match(PURE_BRANCH_ISSUE_TAGGING_REGEX) - if (!foundBranch) { - // rebase 등으로 브랜치 없이 작업할 수 있음 - return originCommitMessage - } + if (!foundBranch) { + // rebase 등으로 브랜치 없이 작업할 수 있음 + return null + } + + const branchInfo = foundBranch[0] + const serviceName = branchInfo.split('/')[0].toLowerCase() + const issueNumber = branchInfo.split('/')[1] - const branchInfo = foundBranch[0] + const repoName = issueMap[serviceName] - const serviceName = branchInfo.split('/')[0].toLowerCase() - const issueNumber = branchInfo.split('/')[1] + // 사용자 설정에 있거나 내부상수에 정의된 경우 + if (repoName) { + return `${repoName}#${issueNumber}` + } + // null 로 명시되었다는 것은 자기자신 (#123) 으로 태깅하겠다는 뜻 + if (repoName === null) { + return `#${issueNumber}` + } + // undefined 는 못찾았다는 뜻 + return null +} - /** - * @description 내부 상수를 후순위. 사용자 설정이 덮어쓴다. - */ - const issueMap = { - ...ISSUE_TAGGING_MAP, - ...externalConfig, - } as Record +// resolveKey: Jira/Linear 스타일 키(PROJ-1871)를 브랜치에서 그대로 가져온다. +// passthrough 에 등록된 프로젝트만, Jigit 규칙(-<1~7자리 숫자>)으로 인식한다. +export function resolveKey(branchName: string, passthrough?: string[]): string | null { + if (!passthrough || passthrough.length === 0) { + return null + } - // 태깅 맵 객체에 맞는게 있는지 확인한다. - const repoName = issueMap[serviceName] + const allowed = new Set(passthrough) - // 사용자 설정에 있거나 내부상수에 정의된 경우 - if (repoName) { - finalCommitMessage = `[${repoName}#${issueNumber}] ${originCommitMessage}` - } - // null 로 명시되었다는 것은 자기자신 (#123) 으로 태깅하겠다는 뜻 - else if (repoName === null) { - finalCommitMessage = `[#${issueNumber}] ${originCommitMessage}` - } - // undefined 는 못찾았다는 뜻. 커밋메시지를 그냥 돌려보낸다. - else { - finalCommitMessage = originCommitMessage - } + for (const match of branchName.matchAll(KEY_PATTERN)) { + const [full, project, issueNumber] = match + if (issueNumber.length <= 7 && allowed.has(project)) { + return full } - // 맞는게 없다면 그냥 기존 메시지로 보낸다 - else { - finalCommitMessage = originCommitMessage + } + + return null +} + +function isKeyByte(char: string): boolean { + return /[-\w]/.test(char) +} + +// alreadyHasRef: 해결된 참조가 이미 온전한 토큰으로 존재하는지 확인한다 +// ("#123" 이 "#1234" 안에서 매칭되지 않도록). 재실행/`git commit --amend` 멱등성. +export function alreadyHasRef(message: string, ref: string): boolean { + if (ref === '') { + return false + } + + let from = 0 + while (from <= message.length) { + const index = message.indexOf(ref, from) + if (index < 0) { + return false + } + + const start = index + const end = index + ref.length + const beforeOK = start === 0 || !isKeyByte(message[start - 1]) + const afterOK = end >= message.length || !isKeyByte(message[end]) + + if (beforeOK && afterOK) { + return true } + + from = start + 1 } - return finalCommitMessage + + return false +} + +export function getCommitMessageByBranchName( + branchName: string, + originCommitMessage: string, + externalConfig?: Record, + passthrough?: string[], +) { + /** + * @description 내부 상수를 후순위. 사용자 설정이 덮어쓴다. + */ + const issueMap = { + ...ISSUE_TAGGING_MAP, + ...externalConfig, + } as Record + + // GitHub 스타일 prefix 규칙을 먼저, 없으면 passthrough 키를 그대로 사용한다. + const ref = resolvePrefix(branchName, issueMap) ?? resolveKey(branchName, passthrough) + + if (!ref) { + return originCommitMessage + } + + // 이미 이 브랜치의 참조가 있으면 중복 태깅하지 않는다. + if (alreadyHasRef(originCommitMessage, ref)) { + return originCommitMessage + } + + return `[${ref}] ${originCommitMessage}` } interface Config { rules: Record protect?: string[] + passthrough?: string[] } export async function readExternalConfig(): Promise { @@ -109,6 +164,7 @@ export async function readExternalConfig(): Promise { const mergedRules: Record = {...(localConfig.rules || {})} let mergedProtect: string[] = Array.isArray(localConfig.protect) ? [...localConfig.protect] : [] + let mergedPassthrough: string[] = Array.isArray(localConfig.passthrough) ? [...localConfig.passthrough] : [] const extendsUrl = localConfig.extends if (typeof extendsUrl === 'string' && /^(http|https):\/\//.test(extendsUrl)) { @@ -127,6 +183,10 @@ export async function readExternalConfig(): Promise { if (Array.isArray(extendsConfig.protect)) { mergedProtect = [...extendsConfig.protect, ...mergedProtect] } + + if (Array.isArray(extendsConfig.passthrough)) { + mergedPassthrough = [...extendsConfig.passthrough, ...mergedPassthrough] + } } catch (e) { throw new Error(`Failed to load external config from "${extendsUrl}": ${(e as Error).message}`) } @@ -140,6 +200,10 @@ export async function readExternalConfig(): Promise { result.protect = [...new Set(mergedProtect)] } + if (mergedPassthrough.length > 0) { + result.passthrough = [...new Set(mergedPassthrough)] + } + return result } @@ -163,9 +227,9 @@ export async function run() { flags: {help: {type: 'boolean', shortFlag: 'h'}, show: {type: 'boolean', shortFlag: 's'}}, }) - const currentBranchName = (await getCurrentBranchName()).toLowerCase() + const currentBranchName = await getCurrentBranchName() - const {rules = {}, protect} = await readExternalConfig() + const {rules = {}, protect, passthrough} = await readExternalConfig() if (cli.flags.show) { /** @@ -189,13 +253,14 @@ export async function run() { throw new Error('Commit message is required.') } - const isProtectedBranch = isStringMatchingPatterns(currentBranchName, protect) + // protect 매칭은 소문자화한 브랜치로(기존 동작 유지), 태깅은 원본 브랜치로(대문자 키 인식). + const isProtectedBranch = isStringMatchingPatterns(currentBranchName.toLowerCase(), protect) if (isProtectedBranch) { throw new Error(`You can't commit on this branch: ${currentBranchName}`) } - const result = getCommitMessageByBranchName(currentBranchName, commitMessage, rules) + const result = getCommitMessageByBranchName(currentBranchName, commitMessage, rules, passthrough) await fs.writeFile(commitFilePath, result, {encoding: 'utf8'}) } diff --git a/packages/commit-helper/src/constant.ts b/packages/commit-helper/src/constant.ts index ef7ddc7..7da2ebc 100644 --- a/packages/commit-helper/src/constant.ts +++ b/packages/commit-helper/src/constant.ts @@ -3,7 +3,7 @@ export const ISSUE_TAGGING_MAP = { feature: null, } as const -export const ISSUE_TAGGING_REGEX = /\[(?:[A-Za-z-_]+\/)?[A-Za-z-_]*#\d{1,5}\]/ export const BRANCH_ISSUE_TAGGING_REGEX = /[A-Za-z-]+\/\d{1,5}([-_a-zA-Z]+[0-9]*)*$/ export const PURE_BRANCH_ISSUE_TAGGING_REGEX = /[A-Za-z-]+\/\d{1,5}/ +export const KEY_PATTERN = /([A-Z][A-Z0-9]+)-([0-9]+)/g export const DEFAULT_PROTECTED_BRANCHES: Readonly = ['main'] From 31cb20a21bea002fd98feea04341ffebd6fbefb5 Mon Sep 17 00:00:00 2001 From: djk01281 Date: Wed, 1 Jul 2026 22:36:16 +0900 Subject: [PATCH 2/2] =?UTF-8?q?:memo:=20docs:=20commit-helper=20=EC=A3=BC?= =?UTF-8?q?=EC=84=9D=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/commit-helper/bin/cli.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/commit-helper/bin/cli.ts b/packages/commit-helper/bin/cli.ts index 0823c6d..de2b57e 100644 --- a/packages/commit-helper/bin/cli.ts +++ b/packages/commit-helper/bin/cli.ts @@ -30,8 +30,6 @@ async function getCurrentBranchName(): Promise { }) } -// resolvePrefix: "/" 브랜치를 rules 로 참조 문자열로 변환한다. -// null 값 → "#N", repo 값 → "repo#N", 매칭/미등록이면 null. function resolvePrefix(branchName: string, issueMap: Record): string | null { const foundedIssueTagging = branchName.match(BRANCH_ISSUE_TAGGING_REGEX) @@ -65,8 +63,7 @@ function resolvePrefix(branchName: string, issueMap: Record-<1~7자리 숫자>)으로 인식한다. +// passthrough 에 등록된 프로젝트 키만 인식한다 (Jigit 규칙: -<1~7자리 숫자>). export function resolveKey(branchName: string, passthrough?: string[]): string | null { if (!passthrough || passthrough.length === 0) { return null @@ -88,8 +85,7 @@ function isKeyByte(char: string): boolean { return /[-\w]/.test(char) } -// alreadyHasRef: 해결된 참조가 이미 온전한 토큰으로 존재하는지 확인한다 -// ("#123" 이 "#1234" 안에서 매칭되지 않도록). 재실행/`git commit --amend` 멱등성. +// 재실행·amend 때 같은 태그가 중복으로 붙지 않도록, 이 참조가 메시지에 그대로 있는지 확인한다 ("#123" 은 "#1234" 에 매칭되지 않음). export function alreadyHasRef(message: string, ref: string): boolean { if (ref === '') { return false @@ -131,14 +127,12 @@ export function getCommitMessageByBranchName( ...externalConfig, } as Record - // GitHub 스타일 prefix 규칙을 먼저, 없으면 passthrough 키를 그대로 사용한다. const ref = resolvePrefix(branchName, issueMap) ?? resolveKey(branchName, passthrough) if (!ref) { return originCommitMessage } - // 이미 이 브랜치의 참조가 있으면 중복 태깅하지 않는다. if (alreadyHasRef(originCommitMessage, ref)) { return originCommitMessage }