better-commits 1.19.0 → 1.20.0-cli-flags
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.better-commits.json +4 -0
- package/.github/workflows/test.yml +27 -0
- package/.opencode/package-lock.json +115 -0
- package/.opencode/plans/cli-args.md +182 -0
- package/.svelte-kit/ambient.d.ts +289 -0
- package/.svelte-kit/generated/client/app.js +28 -0
- package/.svelte-kit/generated/client/matchers.js +1 -0
- package/.svelte-kit/generated/client/nodes/0.js +1 -0
- package/.svelte-kit/generated/client/nodes/1.js +1 -0
- package/.svelte-kit/tsconfig.json +49 -0
- package/0001-feat-branch-124-update-worktrees-feature.patch +316 -0
- package/dist/branch.js +27 -1
- package/dist/chunk-OFJCRS3N.js +4 -0
- package/dist/chunk-SIF4LZUS.js +1 -0
- package/dist/index.js +44 -19
- package/dist/init.js +1 -1
- package/docs/ai-skills.yaml +48 -0
- package/docs/clack.md +143 -0
- package/docs/valibot.md +228 -0
- package/package.json +12 -9
- package/readme.md +18 -2
- package/src/args.test.ts +102 -0
- package/src/args.ts +101 -7
- package/src/branch-args.test.ts +72 -0
- package/src/branch-args.ts +106 -0
- package/src/branch-help.ts +114 -0
- package/src/branch.ts +67 -238
- package/src/help.ts +131 -0
- package/src/index.test.ts +7 -0
- package/src/index.ts +73 -492
- package/src/prompts/branch-checkout.prompt.ts +36 -0
- package/src/prompts/branch-confirm.prompt.ts +134 -0
- package/src/prompts/branch-description.prompt.ts +37 -0
- package/src/prompts/branch-runnable.ts +13 -0
- package/src/prompts/branch-ticket.prompt.ts +41 -0
- package/src/prompts/branch-type.prompt.ts +43 -0
- package/src/prompts/branch-user.prompt.ts +50 -0
- package/src/prompts/branch-version.prompt.ts +41 -0
- package/src/prompts/commit-body.prompt.ts +57 -0
- package/src/prompts/commit-confirm.prompt.ts +119 -0
- package/src/prompts/commit-footer.prompt.ts +195 -0
- package/src/prompts/commit-scope.prompt.ts +73 -0
- package/src/prompts/commit-status.prompt.ts +75 -0
- package/src/prompts/commit-ticket.prompt.ts +82 -0
- package/src/prompts/commit-title.prompt.ts +98 -0
- package/src/prompts/commit-type.prompt.ts +93 -0
- package/src/prompts/runnable.ts +13 -0
- package/src/utils/build-branch.test.ts +141 -0
- package/src/utils/build-branch.ts +46 -0
- package/src/utils/build-commit-string.test.ts +253 -0
- package/src/utils/build-commit-string.ts +158 -0
- package/src/utils/commit-title-size.ts +24 -0
- package/src/utils/infer.test.ts +83 -0
- package/src/utils/infer.ts +114 -0
- package/src/utils/messages.ts +25 -0
- package/src/utils/no-interactive-branch-validation.test.ts +170 -0
- package/src/utils/no-interactive-validation.test.ts +174 -0
- package/src/utils/no-interactive-validation.ts +190 -0
- package/src/utils.ts +59 -66
- package/src/valibot-consts.ts +2 -2
- package/src/valibot-state.test.ts +48 -0
- package/src/valibot-state.ts +133 -130
- package/tsconfig.json +3 -2
- package/vitest.config.ts +8 -0
- package/dist/chunk-K2RPF2JY.js +0 -4
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { parse } from "valibot";
|
|
3
|
+
import { BranchState, Config } from "../valibot-state";
|
|
4
|
+
import { build_branch, build_worktree_path } from "./build-branch";
|
|
5
|
+
|
|
6
|
+
function make_config(input: Record<string, unknown> = {}) {
|
|
7
|
+
return parse(Config, input);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function make_branch(input: Record<string, unknown>) {
|
|
11
|
+
return parse(BranchState, input);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("build_branch", () => {
|
|
15
|
+
it("builds branch with default order and separators", () => {
|
|
16
|
+
const branch = make_branch({
|
|
17
|
+
user: "erik",
|
|
18
|
+
version: "1.2.0",
|
|
19
|
+
type: "feat",
|
|
20
|
+
ticket: "ABC-1",
|
|
21
|
+
description: "add-parser",
|
|
22
|
+
});
|
|
23
|
+
const config = make_config();
|
|
24
|
+
|
|
25
|
+
expect(build_branch(branch, config)).toBe(
|
|
26
|
+
"erik/1.2.0/feat/ABC-1-add-parser",
|
|
27
|
+
);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("omits empty fields without leaving trailing separators", () => {
|
|
31
|
+
const branch = make_branch({
|
|
32
|
+
user: "erik",
|
|
33
|
+
type: "fix",
|
|
34
|
+
description: "repair-parser",
|
|
35
|
+
});
|
|
36
|
+
const config = make_config();
|
|
37
|
+
|
|
38
|
+
expect(build_branch(branch, config)).toBe("erik/fix/repair-parser");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("respects custom branch order", () => {
|
|
42
|
+
const branch = make_branch({
|
|
43
|
+
type: "feat",
|
|
44
|
+
ticket: "ABC-1",
|
|
45
|
+
description: "add-parser",
|
|
46
|
+
user: "erik",
|
|
47
|
+
});
|
|
48
|
+
const config = make_config({
|
|
49
|
+
branch_order: ["type", "ticket", "description", "user"],
|
|
50
|
+
branch_description: { separator: "/" },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(build_branch(branch, config)).toBe("feat/ABC-1-add-parser/erik");
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it("uses per-field separator config", () => {
|
|
57
|
+
const branch = make_branch({
|
|
58
|
+
user: "erik",
|
|
59
|
+
type: "feat",
|
|
60
|
+
ticket: "ABC-1",
|
|
61
|
+
description: "parser",
|
|
62
|
+
});
|
|
63
|
+
const config = make_config({
|
|
64
|
+
branch_user: { separator: "_" },
|
|
65
|
+
branch_type: { separator: "-" },
|
|
66
|
+
branch_ticket: { separator: "_" },
|
|
67
|
+
branch_description: { separator: "" },
|
|
68
|
+
branch_order: ["user", "type", "ticket", "description"],
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(build_branch(branch, config)).toBe("erik_feat-ABC-1_parser");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("build_worktree_path", () => {
|
|
76
|
+
it("builds path from template tokens", () => {
|
|
77
|
+
const branch = make_branch({
|
|
78
|
+
user: "erik",
|
|
79
|
+
type: "feat",
|
|
80
|
+
ticket: "ABC-1",
|
|
81
|
+
description: "add-parser",
|
|
82
|
+
version: "1.2.0",
|
|
83
|
+
});
|
|
84
|
+
const config = make_config({
|
|
85
|
+
worktrees: {
|
|
86
|
+
base_path: "../worktrees",
|
|
87
|
+
folder_template: "{{repo_name}}-{{ticket}}-{{branch_description}}",
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
const result = build_worktree_path(branch, config, "/tmp/my-repo");
|
|
92
|
+
expect(result).toBe("../worktrees/my-repo-ABC-1-add-parser");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("handles base_path with trailing slash", () => {
|
|
96
|
+
const branch = make_branch({
|
|
97
|
+
description: "add-parser",
|
|
98
|
+
});
|
|
99
|
+
const config = make_config({
|
|
100
|
+
worktrees: {
|
|
101
|
+
base_path: "../wt/",
|
|
102
|
+
folder_template: "{{repo_name}}-{{branch_description}}",
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const result = build_worktree_path(branch, config, "/tmp/repo");
|
|
107
|
+
expect(result).toBe("../wt/repo-add-parser");
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("normalizes repeated dashes and trims edges", () => {
|
|
111
|
+
const branch = make_branch({
|
|
112
|
+
ticket: "",
|
|
113
|
+
description: "parser",
|
|
114
|
+
});
|
|
115
|
+
const config = make_config({
|
|
116
|
+
worktrees: {
|
|
117
|
+
base_path: "../wt",
|
|
118
|
+
folder_template: "-{{repo_name}}--{{ticket}}--{{branch_description}}-",
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const result = build_worktree_path(branch, config, "/tmp/repo");
|
|
123
|
+
expect(result).toBe("../wt/repo-parser");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("removes whitespace from rendered worktree name", () => {
|
|
127
|
+
const branch = make_branch({
|
|
128
|
+
user: "erik",
|
|
129
|
+
description: "new feature",
|
|
130
|
+
});
|
|
131
|
+
const config = make_config({
|
|
132
|
+
worktrees: {
|
|
133
|
+
base_path: "../wt",
|
|
134
|
+
folder_template: "{{repo_name}}-{{user}}-{{branch_description}}",
|
|
135
|
+
},
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
const result = build_worktree_path(branch, config, "/tmp/repo");
|
|
139
|
+
expect(result).toBe("../wt/repo-erik-newfeature");
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { InferOutput } from "valibot";
|
|
2
|
+
import { V_BRANCH_CONFIG_FIELDS, V_BRANCH_FIELDS } from "../valibot-consts";
|
|
3
|
+
import { BranchState, Config } from "../valibot-state";
|
|
4
|
+
|
|
5
|
+
export function build_branch(
|
|
6
|
+
branch: InferOutput<typeof BranchState>,
|
|
7
|
+
config: InferOutput<typeof Config>,
|
|
8
|
+
): string {
|
|
9
|
+
let res = "";
|
|
10
|
+
config.branch_order.forEach((b: InferOutput<typeof V_BRANCH_FIELDS>) => {
|
|
11
|
+
const config_key: InferOutput<typeof V_BRANCH_CONFIG_FIELDS> = `branch_${b}`;
|
|
12
|
+
if (branch[b]) res += branch[b] + config[config_key].separator;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
if (res.endsWith("-") || res.endsWith("/") || res.endsWith("_")) {
|
|
16
|
+
return res.slice(0, -1).trim();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return res.trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function build_worktree_path(
|
|
23
|
+
branch_state: InferOutput<typeof BranchState>,
|
|
24
|
+
config: InferOutput<typeof Config>,
|
|
25
|
+
git_root: string,
|
|
26
|
+
): string {
|
|
27
|
+
const repo_name = git_root.split("/").pop() || "repo";
|
|
28
|
+
|
|
29
|
+
let worktree_name = config.worktrees.folder_template;
|
|
30
|
+
|
|
31
|
+
worktree_name = worktree_name
|
|
32
|
+
.replace("{{repo_name}}", repo_name)
|
|
33
|
+
.replace("{{branch_description}}", branch_state.description)
|
|
34
|
+
.replace("{{user}}", branch_state.user || "")
|
|
35
|
+
.replace("{{type}}", branch_state.type || "")
|
|
36
|
+
.replace("{{ticket}}", branch_state.ticket || "")
|
|
37
|
+
.replace("{{version}}", branch_state.version || "");
|
|
38
|
+
|
|
39
|
+
worktree_name = worktree_name
|
|
40
|
+
.replace(/\s/g, "")
|
|
41
|
+
.replace(/--+/g, "-")
|
|
42
|
+
.replace(/^-+|-+$/g, "");
|
|
43
|
+
|
|
44
|
+
const base_path = config.worktrees.base_path;
|
|
45
|
+
return `${base_path}${base_path.endsWith("/") ? "" : "/"}${worktree_name}`;
|
|
46
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { InferOutput, parse } from "valibot";
|
|
3
|
+
import { CommitState, Config } from "../valibot-state";
|
|
4
|
+
import { build_commit_string } from "./build-commit-string";
|
|
5
|
+
|
|
6
|
+
type ConfigOutput = InferOutput<typeof Config>;
|
|
7
|
+
type ConfigOverrides = Partial<
|
|
8
|
+
Omit<ConfigOutput, "check_ticket" | "breaking_change">
|
|
9
|
+
> & {
|
|
10
|
+
check_ticket?: Partial<ConfigOutput["check_ticket"]>;
|
|
11
|
+
breaking_change?: Partial<ConfigOutput["breaking_change"]>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function make_config(overrides?: ConfigOverrides): ConfigOutput {
|
|
15
|
+
const base = parse_config({});
|
|
16
|
+
if (!overrides) return base;
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
...base,
|
|
20
|
+
...overrides,
|
|
21
|
+
check_ticket: {
|
|
22
|
+
...base.check_ticket,
|
|
23
|
+
...(overrides.check_ticket ?? {}),
|
|
24
|
+
},
|
|
25
|
+
breaking_change: {
|
|
26
|
+
...base.breaking_change,
|
|
27
|
+
...(overrides.breaking_change ?? {}),
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function parse_config(input: Record<string, unknown>) {
|
|
33
|
+
return parse(Config, input);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function make_state(input: Record<string, unknown>) {
|
|
37
|
+
return parse(CommitState, input);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe("build_commit_string", () => {
|
|
41
|
+
it("builds a basic conventional commit title", () => {
|
|
42
|
+
const result = build_commit_string({
|
|
43
|
+
commit_state: make_state({
|
|
44
|
+
type: "feat",
|
|
45
|
+
scope: "api",
|
|
46
|
+
title: "add endpoint",
|
|
47
|
+
}),
|
|
48
|
+
config: make_config(),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(result).toBe("feat(api): add endpoint");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("adds ticket at start of title by default", () => {
|
|
55
|
+
const result = build_commit_string({
|
|
56
|
+
commit_state: make_state({
|
|
57
|
+
type: "fix",
|
|
58
|
+
title: "handle null values",
|
|
59
|
+
ticket: "ABC-123",
|
|
60
|
+
}),
|
|
61
|
+
config: make_config(),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
expect(result).toBe("fix: ABC-123 handle null values");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("supports ticket before colon", () => {
|
|
68
|
+
const result = build_commit_string({
|
|
69
|
+
commit_state: make_state({
|
|
70
|
+
type: "feat",
|
|
71
|
+
scope: "core",
|
|
72
|
+
title: "ship parser",
|
|
73
|
+
ticket: "PROJ-9",
|
|
74
|
+
}),
|
|
75
|
+
config: make_config({
|
|
76
|
+
check_ticket: {
|
|
77
|
+
title_position: "before-colon",
|
|
78
|
+
},
|
|
79
|
+
}),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(result).toBe("feat(core) PROJ-9: ship parser");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("supports ticket wrapping when surround is configured", () => {
|
|
86
|
+
const result = build_commit_string({
|
|
87
|
+
commit_state: make_state({
|
|
88
|
+
type: "chore",
|
|
89
|
+
title: "update docs",
|
|
90
|
+
ticket: "42",
|
|
91
|
+
}),
|
|
92
|
+
config: make_config({
|
|
93
|
+
check_ticket: {
|
|
94
|
+
surround: "[]",
|
|
95
|
+
},
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(result).toBe("chore: [42] update docs");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("supports ticket at end of title", () => {
|
|
103
|
+
const result = build_commit_string({
|
|
104
|
+
commit_state: make_state({
|
|
105
|
+
type: "chore",
|
|
106
|
+
title: "cleanup docs",
|
|
107
|
+
ticket: "DOC-7",
|
|
108
|
+
}),
|
|
109
|
+
config: make_config({
|
|
110
|
+
check_ticket: {
|
|
111
|
+
title_position: "end",
|
|
112
|
+
},
|
|
113
|
+
}),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(result).toBe("chore: cleanup docs DOC-7");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("supports ticket at beginning of commit header", () => {
|
|
120
|
+
const result = build_commit_string({
|
|
121
|
+
commit_state: make_state({
|
|
122
|
+
type: "feat",
|
|
123
|
+
scope: "api",
|
|
124
|
+
title: "add endpoint",
|
|
125
|
+
ticket: "ABC-1",
|
|
126
|
+
}),
|
|
127
|
+
config: make_config({
|
|
128
|
+
check_ticket: {
|
|
129
|
+
title_position: "beginning",
|
|
130
|
+
},
|
|
131
|
+
}),
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
expect(result).toBe("ABC-1 feat(api): add endpoint");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("appends body and footer blocks with spacing", () => {
|
|
138
|
+
const result = build_commit_string({
|
|
139
|
+
commit_state: make_state({
|
|
140
|
+
type: "feat",
|
|
141
|
+
title: "add cli",
|
|
142
|
+
body: "line 1\\nline 2",
|
|
143
|
+
custom_footer: "Refs: ABC-1",
|
|
144
|
+
}),
|
|
145
|
+
config: make_config(),
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
expect(result).toBe("feat: add cli\n\nline 1\nline 2\n\nRefs: ABC-1");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("includes trailer only when include_trailer is true", () => {
|
|
152
|
+
const state = make_state({
|
|
153
|
+
type: "feat",
|
|
154
|
+
title: "add cli",
|
|
155
|
+
trailer: "Signed-off-by: Erik",
|
|
156
|
+
});
|
|
157
|
+
const config = make_config();
|
|
158
|
+
|
|
159
|
+
const no_trailer = build_commit_string({
|
|
160
|
+
commit_state: state,
|
|
161
|
+
config,
|
|
162
|
+
include_trailer: false,
|
|
163
|
+
});
|
|
164
|
+
const with_trailer = build_commit_string({
|
|
165
|
+
commit_state: state,
|
|
166
|
+
config,
|
|
167
|
+
include_trailer: true,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
expect(no_trailer).toBe("feat: add cli");
|
|
171
|
+
expect(with_trailer).toBe("feat: add cli\n\nSigned-off-by: Erik");
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it("skips title ticket when add_to_title is false but keeps closes footer", () => {
|
|
175
|
+
const result = build_commit_string({
|
|
176
|
+
commit_state: make_state({
|
|
177
|
+
type: "fix",
|
|
178
|
+
title: "repair parser",
|
|
179
|
+
ticket: "BUG-99",
|
|
180
|
+
closes: "closes",
|
|
181
|
+
}),
|
|
182
|
+
config: make_config({
|
|
183
|
+
check_ticket: {
|
|
184
|
+
add_to_title: false,
|
|
185
|
+
},
|
|
186
|
+
}),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
expect(result).toBe("fix: repair parser\n\ncloses BUG-99");
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("adds breaking exclamation when enabled", () => {
|
|
193
|
+
const result = build_commit_string({
|
|
194
|
+
commit_state: make_state({
|
|
195
|
+
type: "feat",
|
|
196
|
+
title: "replace api",
|
|
197
|
+
breaking_title: "v1 removed",
|
|
198
|
+
}),
|
|
199
|
+
config: make_config({
|
|
200
|
+
breaking_change: {
|
|
201
|
+
add_exclamation_to_title: true,
|
|
202
|
+
},
|
|
203
|
+
}),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
expect(result).toBe("feat!: replace api\n\nBREAKING CHANGE: v1 removed");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it("renders deprecation title and body blocks", () => {
|
|
210
|
+
const result = build_commit_string({
|
|
211
|
+
commit_state: make_state({
|
|
212
|
+
type: "chore",
|
|
213
|
+
title: "remove legacy helper",
|
|
214
|
+
deprecates_title: "legacy helper",
|
|
215
|
+
deprecates_body: "use modern helper",
|
|
216
|
+
}),
|
|
217
|
+
config: make_config(),
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
expect(result).toBe(
|
|
221
|
+
"chore: remove legacy helper\n\nDEPRECATED: legacy helper\n\nuse modern helper",
|
|
222
|
+
);
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
it("formats before-colon ticket without type and scope", () => {
|
|
226
|
+
const result = build_commit_string({
|
|
227
|
+
commit_state: make_state({
|
|
228
|
+
title: "ship parser",
|
|
229
|
+
ticket: "PROJ-9",
|
|
230
|
+
}),
|
|
231
|
+
config: make_config({
|
|
232
|
+
check_ticket: {
|
|
233
|
+
title_position: "before-colon",
|
|
234
|
+
},
|
|
235
|
+
}),
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
expect(result).toBe("PROJ-9: ship parser");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("escapes double quotes and backticks for shell-safe command composition", () => {
|
|
242
|
+
const result = build_commit_string({
|
|
243
|
+
commit_state: make_state({
|
|
244
|
+
type: "fix",
|
|
245
|
+
title: 'handle "quoted" `inline` value',
|
|
246
|
+
}),
|
|
247
|
+
config: make_config(),
|
|
248
|
+
escape_quotes: true,
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
expect(result).toBe('fix: handle \\"quoted\\" \\`inline\\` value');
|
|
252
|
+
});
|
|
253
|
+
});
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import color from "picocolors";
|
|
2
|
+
import { InferOutput } from "valibot";
|
|
3
|
+
import { CommitState, Config } from "../valibot-state";
|
|
4
|
+
|
|
5
|
+
type BuildCommitStringInput = {
|
|
6
|
+
commit_state: InferOutput<typeof CommitState>;
|
|
7
|
+
config: InferOutput<typeof Config>;
|
|
8
|
+
colorize?: boolean;
|
|
9
|
+
escape_quotes?: boolean;
|
|
10
|
+
include_trailer?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function build_commit_string({
|
|
14
|
+
commit_state,
|
|
15
|
+
config,
|
|
16
|
+
colorize = false,
|
|
17
|
+
escape_quotes = false,
|
|
18
|
+
include_trailer = false,
|
|
19
|
+
}: BuildCommitStringInput): string {
|
|
20
|
+
let commit_string = "";
|
|
21
|
+
if (commit_state.type) {
|
|
22
|
+
commit_string += colorize
|
|
23
|
+
? color.blue(commit_state.type)
|
|
24
|
+
: commit_state.type;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (commit_state.scope) {
|
|
28
|
+
const scope = colorize
|
|
29
|
+
? color.cyan(commit_state.scope)
|
|
30
|
+
: commit_state.scope;
|
|
31
|
+
commit_string += `(${scope})`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let title_ticket = commit_state.ticket;
|
|
35
|
+
const surround = config.check_ticket.surround;
|
|
36
|
+
if (commit_state.ticket && surround) {
|
|
37
|
+
const open_token = surround.charAt(0);
|
|
38
|
+
const close_token = surround.charAt(1);
|
|
39
|
+
title_ticket = `${open_token}${commit_state.ticket}${close_token}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const position_beginning = config.check_ticket.title_position === "beginning";
|
|
43
|
+
if (title_ticket && config.check_ticket.add_to_title && position_beginning) {
|
|
44
|
+
commit_string = `${colorize ? color.magenta(title_ticket) : title_ticket} ${commit_string}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const position_before_colon =
|
|
48
|
+
config.check_ticket.title_position === "before-colon";
|
|
49
|
+
if (
|
|
50
|
+
title_ticket &&
|
|
51
|
+
config.check_ticket.add_to_title &&
|
|
52
|
+
position_before_colon
|
|
53
|
+
) {
|
|
54
|
+
const spacing =
|
|
55
|
+
commit_state.scope || (commit_state.type && !config.check_ticket.surround)
|
|
56
|
+
? " "
|
|
57
|
+
: "";
|
|
58
|
+
commit_string += colorize
|
|
59
|
+
? color.magenta(spacing + title_ticket)
|
|
60
|
+
: spacing + title_ticket;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (
|
|
64
|
+
commit_state.breaking_title &&
|
|
65
|
+
config.breaking_change.add_exclamation_to_title
|
|
66
|
+
) {
|
|
67
|
+
commit_string += colorize ? color.red("!") : "!";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (
|
|
71
|
+
commit_state.scope ||
|
|
72
|
+
commit_state.type ||
|
|
73
|
+
(title_ticket && position_before_colon)
|
|
74
|
+
) {
|
|
75
|
+
commit_string += ": ";
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const position_start = config.check_ticket.title_position === "start";
|
|
79
|
+
const position_end = config.check_ticket.title_position === "end";
|
|
80
|
+
if (title_ticket && config.check_ticket.add_to_title && position_start) {
|
|
81
|
+
commit_string += colorize
|
|
82
|
+
? color.magenta(title_ticket) + " "
|
|
83
|
+
: title_ticket + " ";
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (commit_state.title) {
|
|
87
|
+
commit_string += colorize
|
|
88
|
+
? color.reset(commit_state.title)
|
|
89
|
+
: commit_state.title;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (title_ticket && config.check_ticket.add_to_title && position_end) {
|
|
93
|
+
commit_string +=
|
|
94
|
+
" " + (colorize ? color.magenta(title_ticket) : title_ticket);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (commit_state.body) {
|
|
98
|
+
const temp = commit_state.body.split("\\n");
|
|
99
|
+
const res = temp
|
|
100
|
+
.map((value) => (colorize ? color.reset(value.trim()) : value.trim()))
|
|
101
|
+
.join("\n");
|
|
102
|
+
commit_string += `\n\n${res}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (commit_state.breaking_title) {
|
|
106
|
+
const title = colorize
|
|
107
|
+
? color.red(`BREAKING CHANGE: ${commit_state.breaking_title}`)
|
|
108
|
+
: `BREAKING CHANGE: ${commit_state.breaking_title}`;
|
|
109
|
+
commit_string += `\n\n${title}`;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (commit_state.breaking_body) {
|
|
113
|
+
const body = colorize
|
|
114
|
+
? color.red(commit_state.breaking_body)
|
|
115
|
+
: commit_state.breaking_body;
|
|
116
|
+
commit_string += `\n\n${body}`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (commit_state.deprecates_title) {
|
|
120
|
+
const title = colorize
|
|
121
|
+
? color.yellow(`DEPRECATED: ${commit_state.deprecates_title}`)
|
|
122
|
+
: `DEPRECATED: ${commit_state.deprecates_title}`;
|
|
123
|
+
commit_string += `\n\n${title}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (commit_state.deprecates_body) {
|
|
127
|
+
const body = colorize
|
|
128
|
+
? color.yellow(commit_state.deprecates_body)
|
|
129
|
+
: commit_state.deprecates_body;
|
|
130
|
+
commit_string += `\n\n${body}`;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (commit_state.custom_footer) {
|
|
134
|
+
const temp = commit_state.custom_footer.split("\\n");
|
|
135
|
+
const res = temp
|
|
136
|
+
.map((value) => (colorize ? color.reset(value.trim()) : value.trim()))
|
|
137
|
+
.join("\n");
|
|
138
|
+
commit_string += `\n\n${res}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (commit_state.closes && commit_state.ticket) {
|
|
142
|
+
commit_string += colorize
|
|
143
|
+
? `\n\n${color.reset(commit_state.closes)} ${color.magenta(commit_state.ticket)}`
|
|
144
|
+
: `\n\n${commit_state.closes} ${commit_state.ticket}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (include_trailer && commit_state.trailer) {
|
|
148
|
+
commit_string += colorize
|
|
149
|
+
? `\n\n${color.dim(commit_state.trailer)}`
|
|
150
|
+
: `\n\n${commit_state.trailer}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (escape_quotes) {
|
|
154
|
+
commit_string = commit_string.replaceAll('"', '\\"').replaceAll("`", "\\`");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return commit_string;
|
|
158
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
type CommitTitleSizeInput = {
|
|
2
|
+
type?: string;
|
|
3
|
+
scope?: string;
|
|
4
|
+
ticket?: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
type CommitTitleSizeOptions = {
|
|
9
|
+
include_ticket: boolean;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function get_commit_title_size(
|
|
13
|
+
val: CommitTitleSizeInput,
|
|
14
|
+
options: CommitTitleSizeOptions,
|
|
15
|
+
): number {
|
|
16
|
+
const commit_scope_size = val.scope ? val.scope.length + 2 : 0;
|
|
17
|
+
const commit_type_size = val.type?.length ?? 0;
|
|
18
|
+
const commit_ticket_size = options.include_ticket
|
|
19
|
+
? (val.ticket?.length ?? 0)
|
|
20
|
+
: 0;
|
|
21
|
+
const title_size = val.title?.length ?? 0;
|
|
22
|
+
|
|
23
|
+
return commit_scope_size + commit_type_size + commit_ticket_size + title_size;
|
|
24
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("child_process", () => ({
|
|
4
|
+
execSync: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
|
|
7
|
+
vi.mock("../args", () => ({
|
|
8
|
+
flags: {
|
|
9
|
+
interactive: false,
|
|
10
|
+
git_args: "--git-dir=/tmp/repo/.git --work-tree=/tmp/repo",
|
|
11
|
+
},
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
import { execSync } from "child_process";
|
|
15
|
+
import { parse } from "valibot";
|
|
16
|
+
import { Config } from "../valibot-state";
|
|
17
|
+
import {
|
|
18
|
+
infer_not_interactive,
|
|
19
|
+
infer_ticket_from_git,
|
|
20
|
+
infer_type_from_git,
|
|
21
|
+
} from "./infer";
|
|
22
|
+
|
|
23
|
+
const execSyncMock = vi.mocked(execSync);
|
|
24
|
+
|
|
25
|
+
describe("infer", () => {
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
execSyncMock.mockReset();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('prepends hashtags for inferred tickets when prepend_hashtag is "Always"', () => {
|
|
31
|
+
execSyncMock.mockReturnValue(Buffer.from("feat/127-cli-bombshell-args\n"));
|
|
32
|
+
|
|
33
|
+
const result = infer_ticket_from_git(
|
|
34
|
+
{
|
|
35
|
+
append_hashtag: false,
|
|
36
|
+
prepend_hashtag: "Always",
|
|
37
|
+
},
|
|
38
|
+
"--git-dir=/tmp/repo/.git --work-tree=/tmp/repo",
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
expect(result).toBe("#127");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('returns plain inferred tickets when prepend_hashtag is "Never"', () => {
|
|
45
|
+
execSyncMock.mockReturnValue(Buffer.from("feat/ABC-123-add-parser\n"));
|
|
46
|
+
|
|
47
|
+
const result = infer_ticket_from_git(
|
|
48
|
+
{
|
|
49
|
+
append_hashtag: false,
|
|
50
|
+
prepend_hashtag: "Never",
|
|
51
|
+
},
|
|
52
|
+
"--git-dir=/tmp/repo/.git --work-tree=/tmp/repo",
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
expect(result).toBe("ABC-123");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("infers commit types from the current branch", () => {
|
|
59
|
+
execSyncMock.mockReturnValue(Buffer.from("everduin94/feat/ABC-123-add-parser\n"));
|
|
60
|
+
|
|
61
|
+
const result = infer_type_from_git(
|
|
62
|
+
[{ value: "feat" }, { value: "fix" }],
|
|
63
|
+
"--git-dir=/tmp/repo/.git --work-tree=/tmp/repo",
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
expect(result).toBe("feat");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("builds no-interactive inferred state using current config settings", () => {
|
|
70
|
+
execSyncMock.mockReturnValue(Buffer.from("feat/127-cli-bombshell-args\n"));
|
|
71
|
+
|
|
72
|
+
const config = parse(Config, {
|
|
73
|
+
check_ticket: {
|
|
74
|
+
prepend_hashtag: "Always",
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(infer_not_interactive(config)).toEqual({
|
|
79
|
+
ticket: "#127",
|
|
80
|
+
type: "feat",
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|