Skip to content

Commit 267c7ce

Browse files
committed
Merge branch 'release/0.2.0'
2 parents 90376c4 + 4aa177f commit 267c7ce

File tree

8 files changed

+118
-60
lines changed

8 files changed

+118
-60
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
## [0.2.0](https://github.com/VChet/git-merged-branches/compare/0.1.2...0.2.0) (2025-04-16)
4+
5+
### Features
6+
7+
* add support for multiple 'issueUrlPrefix', more fine-grained 'issueUrlFormat' ([37b3b74](https://github.com/VChet/git-merged-branches/commit/37b3b74e97f4b5e45a3c3b14eabb403a3fd63d54))
8+
39
## [0.1.2](https://github.com/VChet/git-merged-branches/compare/0.1.1...0.1.2) (2025-04-15)
410

511
### Features

README.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,14 @@ hotfix/urgent-fix
4141
You can configure the utility in your `package.json` under `git-merged-branches`. This allows you to set:
4242

4343
- **issueUrlFormat**: Base URL for your issue tracker (must be a valid URL).
44-
- **issueUrlPrefix**: Prefix for issue identifiers in branch names (must be letters).
44+
- **issueUrlPrefix**: Array of prefixes for issue identifiers in branch names.
4545

4646
Example configuration:
4747

4848
```json
4949
"git-merged-branches": {
50-
"issueUrlFormat": "https://your-jira-instance.net/browse",
51-
"issueUrlPrefix": "TOKEN"
50+
"issueUrlFormat": "https://your-jira-instance.net/browse/{{prefix}}{{id}}",
51+
"issueUrlPrefix": ["TOKEN-", "PROJECT-"]
5252
}
5353
```
5454

@@ -57,8 +57,8 @@ With this setup, `git-merged-branches` will generate links for branches with suc
5757
```bash
5858
$ git-merged-branches
5959
Branches merged into 'master':
60-
TOKEN-800_new-feature <https://your-jira-instance.net/TOKEN-800>
61-
fix/TOKEN-123_some-fix <https://your-jira-instance.net/TOKEN-123>
60+
TOKEN-800_new-feature <https://your-jira-instance.net/browse/TOKEN-800>
61+
fix/TOKEN-123_some-fix <https://your-jira-instance.net/browse/TOKEN-123>
6262
fix/EXTERNAL-391
6363
hotfix
6464
```

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "git-merged-branches",
33
"description": "CLI tool to list all Git branches merged into a base branch with issue link formatting",
44
"type": "module",
5-
"version": "0.1.2",
5+
"version": "0.2.0",
66
"license": "MIT",
77
"author": {
88
"name": "VChet",
@@ -32,11 +32,12 @@
3232
"lint:js:fix": "npm run lint:js -- --fix",
3333
"lint:all": "npm run lint:ts && npm run lint:js",
3434
"test": "vitest --run",
35-
"release": "release-it"
35+
"release": "dotenv release-it"
3636
},
3737
"devDependencies": {
3838
"@release-it/conventional-changelog": "^10.0.0",
3939
"@types/node": "^22.14.1",
40+
"dotenv-cli": "^8.0.0",
4041
"esbuild": "^0.25.2",
4142
"eslint": "^9.24.0",
4243
"neostandard": "^0.12.1",

pnpm-lock.yaml

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

src/output.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,23 @@
11
import { GitMergedConfig } from "./repo.js";
2-
import { isValidIssuePrefix, isValidURL } from "./validate.js";
2+
import { isValidURL } from "./validate.js";
3+
4+
function formatSingleBranch(
5+
branch: string,
6+
issueUrlFormat: NonNullable<GitMergedConfig["issueUrlFormat"]>,
7+
issueUrlPrefix: NonNullable<GitMergedConfig["issueUrlPrefix"]>
8+
): string {
9+
for (const prefix of issueUrlPrefix) {
10+
const prefixRegex = new RegExp(`\\b${prefix}(\\d+)`, "i");
11+
const match = branch.match(prefixRegex);
12+
13+
if (!match) { continue; }
14+
15+
const [_fullToken, id] = match;
16+
const url = issueUrlFormat.replace("{{prefix}}", prefix).replace("{{id}}", id);
17+
return `${branch} <${url}>`;
18+
}
19+
return branch;
20+
}
321

422
export function formatTaskBranches(branches: string[], { issueUrlFormat, issueUrlPrefix }: GitMergedConfig): string[] {
523
if (!issueUrlFormat || !issueUrlPrefix) { return branches; }
@@ -9,20 +27,11 @@ export function formatTaskBranches(branches: string[], { issueUrlFormat, issueUr
927
return branches;
1028
}
1129
// issueUrlPrefix
12-
if (!isValidIssuePrefix(issueUrlPrefix)) {
13-
console.warn(`'${issueUrlPrefix}' is not a valid issue prefix. Skipped formatting.`);
30+
if (!Array.isArray(issueUrlPrefix)) {
31+
console.warn(`'${issueUrlPrefix}' is not an array. Skipped formatting.`);
1432
return branches;
1533
}
16-
const prefixRegex = new RegExp(`\\b(${issueUrlPrefix}-\\d+)`, "i");
17-
18-
return branches.map((branch) => {
19-
const match = branch.match(prefixRegex);
20-
if (match && issueUrlFormat) {
21-
const issueId = match[1].toUpperCase();
22-
return `${branch} <${issueUrlFormat}/${issueId}>`;
23-
}
24-
return branch;
25-
});
34+
return branches.map((branch) => formatSingleBranch(branch, issueUrlFormat, issueUrlPrefix));
2635
}
2736

2837
export function outputMergedBranches(branches: string[], targetBranch: string, config: GitMergedConfig): void {

src/repo.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export function isGitRepo(): boolean {
66
try {
77
execSync("git rev-parse --is-inside-work-tree", { stdio: "ignore" });
88
return true;
9-
} catch (error) {
9+
} catch {
1010
return false;
1111
}
1212
}
@@ -42,15 +42,15 @@ export function getMergedBranches(targetBranch: string): string[] {
4242

4343
export interface GitMergedConfig {
4444
issueUrlFormat?: string;
45-
issueUrlPrefix?: string;
45+
issueUrlPrefix?: string[];
4646
};
4747

4848
export function getConfig(): GitMergedConfig {
4949
try {
5050
const pkgPath = join(process.cwd(), "package.json");
5151
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
5252
return pkg["git-merged-branches"] || {};
53-
} catch (error) {
53+
} catch {
5454
return {};
5555
}
5656
}

src/tests/output.test.ts

Lines changed: 53 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,38 +3,64 @@ import { formatTaskBranches, outputMergedBranches } from "../output";
33
import { GitMergedConfig } from "../repo";
44

55
const DEFAULT_CONFIG: GitMergedConfig = {
6-
issueUrlFormat: "https://test-instance.org",
7-
issueUrlPrefix: "TOKEN"
6+
issueUrlFormat: "https://test-instance.org/browse/{{prefix}}{{id}}",
7+
issueUrlPrefix: ["TOKEN-"]
88
};
99

1010
describe("formatTaskBranches", () => {
1111
it("should format branches with issue URL correctly", () => {
1212
const branches = ["feat/TOKEN-800_new-feature", "fix/TOKEN-123_some-fix"];
1313
const result = formatTaskBranches(branches, DEFAULT_CONFIG);
1414
expect(result).toEqual([
15-
'feat/TOKEN-800_new-feature <https://test-instance.org/TOKEN-800>',
16-
'fix/TOKEN-123_some-fix <https://test-instance.org/TOKEN-123>'
15+
'feat/TOKEN-800_new-feature <https://test-instance.org/browse/TOKEN-800>',
16+
'fix/TOKEN-123_some-fix <https://test-instance.org/browse/TOKEN-123>'
1717
]);
1818
});
1919

20-
it("should not format branches without issue prefix", () => {
21-
const branches = ["fix/hotfix", "feature/no-issue"];
22-
const result = formatTaskBranches(branches, DEFAULT_CONFIG);
23-
expect(result).toEqual(branches); // No change, since no matching prefix
20+
it("should support multiple issue prefixes", () => {
21+
const branches = ["fix/TOKEN-123_fix", "feat/PROJECT-45_add-feature"];
22+
const config: GitMergedConfig = {
23+
issueUrlFormat: "https://example.com/browse/{{prefix}}{{id}}",
24+
issueUrlPrefix: ["TOKEN-", "PROJECT-"]
25+
};
26+
const result = formatTaskBranches(branches, config);
27+
expect(result).toEqual([
28+
"fix/TOKEN-123_fix <https://example.com/browse/TOKEN-123>",
29+
"feat/PROJECT-45_add-feature <https://example.com/browse/PROJECT-45>"
30+
]);
2431
});
2532

26-
it("should return branches as is if URL format is invalid", () => {
33+
it("should not format branches if issueUrlFormat is not provided", () => {
34+
const branches = ["feat/TOKEN-100"];
35+
const config = { issueUrlFormat: DEFAULT_CONFIG.issueUrlFormat };
36+
const result = formatTaskBranches(branches, config);
37+
expect(result).toEqual(branches);
38+
});
39+
40+
it("should not format branches if issueUrlPrefix is not provided", () => {
41+
const branches = ["feat/TOKEN-100"];
42+
const config = { issueUrlPrefix: DEFAULT_CONFIG.issueUrlPrefix };
43+
const result = formatTaskBranches(branches, config);
44+
expect(result).toEqual(branches);
45+
});
46+
47+
it("should not format branches without valid issueUrlFormat", () => {
2748
const branches = ["feat/TOKEN-800_new-feature"];
2849
const config: GitMergedConfig = { ...DEFAULT_CONFIG, issueUrlFormat: "invalid-url" };
2950
const result = formatTaskBranches(branches, config);
3051
expect(result).toEqual(branches); // Invalid URL, no changes
3152
});
3253

33-
it("should return branches as is if issue prefix is invalid", () => {
34-
const branches = ["feat/TOKEN-800_new-feature"];
35-
const config: GitMergedConfig = { ...DEFAULT_CONFIG, issueUrlPrefix: "invalid!prefix" };
36-
const result = formatTaskBranches(branches, config);
37-
expect(result).toEqual(branches); // Invalid prefix, no changes
54+
it("should not format branches without matching prefix", () => {
55+
const branches = ["fix/hotfix", "feature/no-issue"];
56+
const result = formatTaskBranches(branches, DEFAULT_CONFIG);
57+
expect(result).toEqual(branches); // No matching prefix, no changes
58+
});
59+
60+
it("should not format branches when prefix is part of another word", () => {
61+
const branches = ["fix/NOTTOKEN-100_bad-branch"];
62+
const result = formatTaskBranches(branches, DEFAULT_CONFIG);
63+
expect(result).toEqual(branches); // No matching prefix, prefix must be exact, no changes
3864
});
3965
});
4066

@@ -46,33 +72,28 @@ describe("outputMergedBranches", () => {
4672
warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
4773
});
4874

49-
it("should warn when issueUrlFormat is not a valid URL", () => {
50-
const branches = ["feat/TOKEN-100"];
51-
const config = { ...DEFAULT_CONFIG, issueUrlFormat: "invalid-url" };
52-
53-
outputMergedBranches(branches, "master", config);
54-
expect(warnSpy).toHaveBeenCalledWith("'invalid-url' is not a valid URL. Skipped formatting.");
55-
});
56-
57-
it("should warn when issueUrlPrefix is not valid", () => {
58-
const branches = ["feat/TOKEN-100"];
59-
const config = { ...DEFAULT_CONFIG, issueUrlPrefix: "invalid!prefix" };
60-
61-
outputMergedBranches(branches, "master", config);
62-
expect(warnSpy).toHaveBeenCalledWith("'invalid!prefix' is not a valid issue prefix. Skipped formatting.");
63-
});
64-
6575
it("should log the correct branches when there are merged branches", () => {
6676
const branches = ["feat/TOKEN-800_new-feature", "fix/TOKEN-123_some-fix"];
6777

6878
outputMergedBranches(branches, "master", DEFAULT_CONFIG);
6979
expect(infoSpy).toHaveBeenNthCalledWith(1, "Branches merged into 'master':");
70-
expect(infoSpy).toHaveBeenNthCalledWith(2, "feat/TOKEN-800_new-feature <https://test-instance.org/TOKEN-800>\nfix/TOKEN-123_some-fix <https://test-instance.org/TOKEN-123>")
80+
const branchOutput = [
81+
"feat/TOKEN-800_new-feature <https://test-instance.org/browse/TOKEN-800>",
82+
"fix/TOKEN-123_some-fix <https://test-instance.org/browse/TOKEN-123>"
83+
]
84+
expect(infoSpy).toHaveBeenNthCalledWith(2, branchOutput.join("\n"));
7185
});
7286

7387
it("should log a message when no branches are merged", () => {
7488
outputMergedBranches([], "master", DEFAULT_CONFIG);
75-
7689
expect(infoSpy).toHaveBeenCalledWith("No branches merged into 'master'.");
7790
});
91+
92+
it("should warn when issueUrlFormat is not a valid URL", () => {
93+
const branches = ["feat/TOKEN-100"];
94+
const config = { ...DEFAULT_CONFIG, issueUrlFormat: "invalid-url" };
95+
96+
outputMergedBranches(branches, "master", config);
97+
expect(warnSpy).toHaveBeenCalledWith("'invalid-url' is not a valid URL. Skipped formatting.");
98+
});
7899
});

src/validate.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,3 @@ export function isValidURL(url?: string): boolean {
77
return false;
88
}
99
}
10-
11-
export function isValidIssuePrefix(prefix?: string): boolean {
12-
if (!prefix) return false;
13-
return /^[A-Z]{2,10}$/i.test(prefix);
14-
}

0 commit comments

Comments
 (0)