Skip to content

Commit cad02ee

Browse files
committed
fix: allow @, #, and Unicode in branch names
- Allow @ character for common naming patterns (fixes #998) - Allow # character for Azure DevOps integration (fixes #1024) - Allow Unicode characters for internationalization (fixes #1020) - Quote branch name in git push command to prevent # being treated as comment - Add comprehensive unit and e2e tests Security: Quotes are still blocked by validation, making shell interpolation safe.
1 parent eb99fb3 commit cad02ee

4 files changed

Lines changed: 70 additions & 8 deletions

File tree

src/create-prompt/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -448,7 +448,7 @@ function getCommitInstructions(
448448
Bash(git commit -m "<message>\\n\\n${coAuthorLine}")`
449449
: ""
450450
}
451-
- Push to the remote: Bash(git push origin ${branchName})`;
451+
- Push to the remote: Bash(git push origin "${branchName}")`;
452452
}
453453
}
454454
}

src/github/operations/branch.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,19 @@ function extractFirstLabel(githubData: FetchDataResult): string | undefined {
2323
}
2424

2525
/**
26-
* Validates a git branch name against a strict whitelist pattern.
26+
* Validates a git branch name against a whitelist pattern.
2727
* This prevents command injection by ensuring only safe characters are used.
2828
*
2929
* Valid branch names:
30-
* - Start with alphanumeric character (not dash, to prevent option injection)
31-
* - Contain only alphanumeric, forward slash, hyphen, underscore, or period
30+
* - Start with letter or number (not dash, to prevent option injection)
31+
* - Contain only letters, numbers, forward slash, hyphen, underscore, period, @, or #
32+
* - Support Unicode characters (e.g., Japanese branch names)
3233
* - Do not start or end with a period
3334
* - Do not end with a slash
3435
* - Do not contain '..' (path traversal)
3536
* - Do not contain '//' (consecutive slashes)
3637
* - Do not end with '.lock'
37-
* - Do not contain '@{'
38+
* - Do not contain '@{' (git reflog syntax)
3839
* - Do not contain control characters or special git characters (~^:?*[\])
3940
*/
4041
export function validateBranchName(branchName: string): void {
@@ -58,12 +59,15 @@ export function validateBranchName(branchName: string): void {
5859
);
5960
}
6061

61-
// Strict whitelist pattern: alphanumeric start, then alphanumeric/slash/hyphen/underscore/period
62-
const validPattern = /^[a-zA-Z0-9][a-zA-Z0-9/_.-]*$/;
62+
// Whitelist pattern: Unicode letter/number start, then Unicode letter/number/slash/hyphen/underscore/period/@/#
63+
// Allows @ and # for Azure DevOps integration (AB#123) and common naming patterns
64+
// Allows Unicode for internationalization (e.g., Japanese branch names)
65+
// Note: quotes are still blocked for safe shell interpolation
66+
const validPattern = /^[\p{L}\p{N}][\p{L}\p{N}/_@#.-]*$/u;
6367

6468
if (!validPattern.test(branchName)) {
6569
throw new Error(
66-
`Invalid branch name: "${branchName}". Branch names must start with an alphanumeric character and contain only alphanumeric characters, forward slashes, hyphens, underscores, or periods.`,
70+
`Invalid branch name: "${branchName}". Branch names must start with a letter or number and contain only letters, numbers, forward slashes, hyphens, underscores, periods, @, or #.`,
6771
);
6872
}
6973

test/branch-template.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,5 +243,49 @@ describe("branch template utilities", () => {
243243
expect(result).toMatch(/^fix\/pr-456-\d{8}-\d{4}$/);
244244
expect(result.length).toBeLessThanOrEqual(50);
245245
});
246+
247+
it.each([
248+
{
249+
template: "{{prefix}}TICKET@{{entityNumber}}",
250+
prefix: "feat/",
251+
entityNumber: 123,
252+
expected: "feat/TICKET@123",
253+
description: "@ character",
254+
},
255+
{
256+
template: "{{prefix}}AB#{{entityNumber}}-fix",
257+
prefix: "feature/",
258+
entityNumber: 1992,
259+
expected: "feature/AB#1992-fix",
260+
description: "# character",
261+
},
262+
{
263+
template: "{{prefix}}機能-{{entityNumber}}",
264+
prefix: "feat/",
265+
entityNumber: 456,
266+
expected: "feat/機能-456",
267+
description: "Japanese characters",
268+
},
269+
{
270+
template: "{{prefix}}особенность-{{entityNumber}}",
271+
prefix: "fix/",
272+
entityNumber: 789,
273+
expected: "fix/особенность-789",
274+
description: "Russian characters",
275+
},
276+
{
277+
template: "{{prefix}}AB#{{entityNumber}}@新機能",
278+
prefix: "特徴/",
279+
entityNumber: 999,
280+
expected: "特徴/AB#999@新機能",
281+
description: "@, #, and Unicode combined",
282+
},
283+
])("should generate and validate branch name with $description", ({ template, prefix, entityNumber, expected }) => {
284+
const result = generateBranchName(template, prefix, "issue", entityNumber);
285+
expect(result).toBe(expected);
286+
287+
const { validateBranchName } = require("../src/github/operations/branch");
288+
expect(() => validateBranchName(result)).not.toThrow();
289+
});
246290
});
247291
});

test/validate-branch-name.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,20 @@ describe("validateBranchName", () => {
3636
expect(() => validateBranchName("refs/heads/main")).not.toThrow();
3737
expect(() => validateBranchName("bugfix/JIRA-1234")).not.toThrow();
3838
});
39+
40+
it.each([
41+
"TICKET-123@add-feature",
42+
"user@branch",
43+
"feat@test",
44+
"feature/AB#1992-sentry-enhancements",
45+
"AB#123-fix",
46+
"issue#456",
47+
"feat/add-機能追加",
48+
"フィーチャー/新機能",
49+
"особенность/новая",
50+
])("should accept special characters and Unicode: %s", (branchName) => {
51+
expect(() => validateBranchName(branchName)).not.toThrow();
52+
});
3953
});
4054

4155
describe("command injection attempts", () => {

0 commit comments

Comments
 (0)