bunset 0.0.2
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/LICENSE +21 -0
- package/README.md +110 -0
- package/package.json +25 -0
- package/src/changelog.test.ts +152 -0
- package/src/changelog.ts +96 -0
- package/src/cli.ts +199 -0
- package/src/commits.test.ts +265 -0
- package/src/commits.ts +87 -0
- package/src/config.ts +64 -0
- package/src/deps.ts +47 -0
- package/src/git.ts +65 -0
- package/src/index.ts +197 -0
- package/src/types.ts +50 -0
- package/src/version.test.ts +40 -0
- package/src/version.ts +32 -0
- package/src/workspace.ts +68 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { test, expect, describe } from "bun:test";
|
|
2
|
+
import { parseCommit, groupCommits, filterCommitsForPackage } from "./commits.ts";
|
|
3
|
+
import type { ParsedCommit } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
describe("parseCommit", () => {
|
|
6
|
+
// [type] description
|
|
7
|
+
test("parses [feat] commit", () => {
|
|
8
|
+
const result = parseCommit("abc123", "[feat] Add user auth");
|
|
9
|
+
expect(result.type).toBe("feature");
|
|
10
|
+
expect(result.commitScope).toBeNull();
|
|
11
|
+
expect(result.files).toEqual([]);
|
|
12
|
+
expect(result.description).toBe("Add user auth");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("parses [feature] commit", () => {
|
|
16
|
+
const result = parseCommit("abc123", "[feature] Add login");
|
|
17
|
+
expect(result.type).toBe("feature");
|
|
18
|
+
expect(result.description).toBe("Add login");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("parses [fix] commit", () => {
|
|
22
|
+
const result = parseCommit("def456", "[fix] Resolve crash on startup");
|
|
23
|
+
expect(result.type).toBe("bugfix");
|
|
24
|
+
expect(result.description).toBe("Resolve crash on startup");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
test("parses [bug] commit", () => {
|
|
28
|
+
const result = parseCommit("def456", "[bug] Fix null pointer");
|
|
29
|
+
expect(result.type).toBe("bugfix");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test("parses [bugfix] commit", () => {
|
|
33
|
+
const result = parseCommit("def456", "[bugfix] Handle edge case");
|
|
34
|
+
expect(result.type).toBe("bugfix");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("parses [test] commit", () => {
|
|
38
|
+
const result = parseCommit("ghi789", "[test] Add unit tests for parser");
|
|
39
|
+
expect(result.type).toBe("test");
|
|
40
|
+
expect(result.description).toBe("Add unit tests for parser");
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("parses [refactor] commit", () => {
|
|
44
|
+
const result = parseCommit("a1", "[refactor] Simplify auth module");
|
|
45
|
+
expect(result.type).toBe("refactor");
|
|
46
|
+
expect(result.description).toBe("Simplify auth module");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("parses [perf] commit", () => {
|
|
50
|
+
const result = parseCommit("a2", "[perf] Speed up query");
|
|
51
|
+
expect(result.type).toBe("perf");
|
|
52
|
+
expect(result.description).toBe("Speed up query");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("parses [style] commit", () => {
|
|
56
|
+
const result = parseCommit("a3", "[style] Fix formatting");
|
|
57
|
+
expect(result.type).toBe("style");
|
|
58
|
+
expect(result.description).toBe("Fix formatting");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("parses [docs] commit", () => {
|
|
62
|
+
const result = parseCommit("a4", "[docs] Update readme");
|
|
63
|
+
expect(result.type).toBe("docs");
|
|
64
|
+
expect(result.description).toBe("Update readme");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("parses [build] commit", () => {
|
|
68
|
+
const result = parseCommit("a5", "[build] Update dependencies");
|
|
69
|
+
expect(result.type).toBe("build");
|
|
70
|
+
expect(result.description).toBe("Update dependencies");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("parses [ops] commit", () => {
|
|
74
|
+
const result = parseCommit("a6", "[ops] Update CI pipeline");
|
|
75
|
+
expect(result.type).toBe("ops");
|
|
76
|
+
expect(result.description).toBe("Update CI pipeline");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("parses [chore] commit", () => {
|
|
80
|
+
const result = parseCommit("a7", "[chore] Initial commit");
|
|
81
|
+
expect(result.type).toBe("chore");
|
|
82
|
+
expect(result.description).toBe("Initial commit");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
// [type]: description
|
|
86
|
+
test("parses [feat]: commit", () => {
|
|
87
|
+
const result = parseCommit("abc123", "[feat]: Add user auth");
|
|
88
|
+
expect(result.type).toBe("feature");
|
|
89
|
+
expect(result.description).toBe("Add user auth");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("parses [fix]: commit", () => {
|
|
93
|
+
const result = parseCommit("def456", "[fix]: Resolve crash");
|
|
94
|
+
expect(result.type).toBe("bugfix");
|
|
95
|
+
expect(result.description).toBe("Resolve crash");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// type: description
|
|
99
|
+
test("parses feat: commit", () => {
|
|
100
|
+
const result = parseCommit("abc123", "feat: Add user auth");
|
|
101
|
+
expect(result.type).toBe("feature");
|
|
102
|
+
expect(result.description).toBe("Add user auth");
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("parses fix: commit", () => {
|
|
106
|
+
const result = parseCommit("def456", "fix: Resolve crash");
|
|
107
|
+
expect(result.type).toBe("bugfix");
|
|
108
|
+
expect(result.description).toBe("Resolve crash");
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("parses test: commit", () => {
|
|
112
|
+
const result = parseCommit("ghi789", "test: Add parser tests");
|
|
113
|
+
expect(result.type).toBe("test");
|
|
114
|
+
expect(result.description).toBe("Add parser tests");
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("parses refactor: commit", () => {
|
|
118
|
+
const result = parseCommit("b1", "refactor: Simplify logic");
|
|
119
|
+
expect(result.type).toBe("refactor");
|
|
120
|
+
expect(result.description).toBe("Simplify logic");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("parses perf: commit", () => {
|
|
124
|
+
const result = parseCommit("b2", "perf: Optimize render");
|
|
125
|
+
expect(result.type).toBe("perf");
|
|
126
|
+
expect(result.description).toBe("Optimize render");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("parses chore: commit", () => {
|
|
130
|
+
const result = parseCommit("b3", "chore: Bump version");
|
|
131
|
+
expect(result.type).toBe("chore");
|
|
132
|
+
expect(result.description).toBe("Bump version");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Scoped commits: [type(scope)] description
|
|
136
|
+
test("parses [feat(auth)] commit with scope", () => {
|
|
137
|
+
const result = parseCommit("s1", "[feat(auth)] Add login");
|
|
138
|
+
expect(result.type).toBe("feature");
|
|
139
|
+
expect(result.commitScope).toBe("auth");
|
|
140
|
+
expect(result.description).toBe("Add login");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("parses [fix(ui)] commit with scope", () => {
|
|
144
|
+
const result = parseCommit("s2", "[fix(ui)] Fix button color");
|
|
145
|
+
expect(result.type).toBe("bugfix");
|
|
146
|
+
expect(result.commitScope).toBe("ui");
|
|
147
|
+
expect(result.description).toBe("Fix button color");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Scoped commits: [type(scope)]: description
|
|
151
|
+
test("parses [feat(api)]: commit with scope and colon", () => {
|
|
152
|
+
const result = parseCommit("s3", "[feat(api)]: Add endpoint");
|
|
153
|
+
expect(result.type).toBe("feature");
|
|
154
|
+
expect(result.commitScope).toBe("api");
|
|
155
|
+
expect(result.description).toBe("Add endpoint");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// Scoped commits: type(scope): description
|
|
159
|
+
test("parses feat(auth): commit with scope", () => {
|
|
160
|
+
const result = parseCommit("s4", "feat(auth): Add logout");
|
|
161
|
+
expect(result.type).toBe("feature");
|
|
162
|
+
expect(result.commitScope).toBe("auth");
|
|
163
|
+
expect(result.description).toBe("Add logout");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("parses fix(parser): commit with scope", () => {
|
|
167
|
+
const result = parseCommit("s5", "fix(parser): Handle edge case");
|
|
168
|
+
expect(result.type).toBe("bugfix");
|
|
169
|
+
expect(result.commitScope).toBe("parser");
|
|
170
|
+
expect(result.description).toBe("Handle edge case");
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// Edge cases
|
|
174
|
+
test("returns null type for unrecognized [prefix]", () => {
|
|
175
|
+
const result = parseCommit("jkl012", "[unknown] Something");
|
|
176
|
+
expect(result.type).toBeNull();
|
|
177
|
+
expect(result.description).toBe("Something");
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test("returns null type for unrecognized prefix:", () => {
|
|
181
|
+
const result = parseCommit("jkl012", "unknown: Something");
|
|
182
|
+
expect(result.type).toBeNull();
|
|
183
|
+
expect(result.description).toBe("Something");
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("returns null type for plain commits", () => {
|
|
187
|
+
const result = parseCommit("mno345", "Regular commit message");
|
|
188
|
+
expect(result.type).toBeNull();
|
|
189
|
+
expect(result.commitScope).toBeNull();
|
|
190
|
+
expect(result.description).toBe("Regular commit message");
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("is case-insensitive for type keyword", () => {
|
|
194
|
+
const result = parseCommit("stu901", "[FEAT] Uppercase type");
|
|
195
|
+
expect(result.type).toBe("feature");
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("is case-insensitive for bare prefix", () => {
|
|
199
|
+
const result = parseCommit("stu901", "FEAT: Uppercase type");
|
|
200
|
+
expect(result.type).toBe("feature");
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("handles missing close bracket", () => {
|
|
204
|
+
const result = parseCommit("vwx234", "[feat No closing bracket");
|
|
205
|
+
expect(result.type).toBeNull();
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
describe("groupCommits", () => {
|
|
210
|
+
test("groups commits by type", () => {
|
|
211
|
+
const commits: ParsedCommit[] = [
|
|
212
|
+
{ hash: "a", message: "", type: "feature", commitScope: null, description: "F1", files: [] },
|
|
213
|
+
{ hash: "b", message: "", type: "bugfix", commitScope: null, description: "B1", files: [] },
|
|
214
|
+
{ hash: "c", message: "", type: "test", commitScope: null, description: "T1", files: [] },
|
|
215
|
+
{ hash: "d", message: "", type: "feature", commitScope: "auth", description: "F2", files: [] },
|
|
216
|
+
{ hash: "e", message: "", type: "refactor", commitScope: null, description: "R1", files: [] },
|
|
217
|
+
{ hash: "f", message: "", type: "chore", commitScope: null, description: "C1", files: [] },
|
|
218
|
+
{ hash: "g", message: "", type: null, commitScope: null, description: "Ignored", files: [] },
|
|
219
|
+
];
|
|
220
|
+
const groups = groupCommits(commits);
|
|
221
|
+
expect(groups.feature).toHaveLength(2);
|
|
222
|
+
expect(groups.bugfix).toHaveLength(1);
|
|
223
|
+
expect(groups.test).toHaveLength(1);
|
|
224
|
+
expect(groups.refactor).toHaveLength(1);
|
|
225
|
+
expect(groups.chore).toHaveLength(1);
|
|
226
|
+
expect(groups.perf).toHaveLength(0);
|
|
227
|
+
expect(groups.docs).toHaveLength(0);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("handles empty commit list", () => {
|
|
231
|
+
const groups = groupCommits([]);
|
|
232
|
+
expect(groups.feature).toHaveLength(0);
|
|
233
|
+
expect(groups.bugfix).toHaveLength(0);
|
|
234
|
+
expect(groups.test).toHaveLength(0);
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe("filterCommitsForPackage", () => {
|
|
239
|
+
const commits: ParsedCommit[] = [
|
|
240
|
+
{ hash: "a", message: "", type: "feature", commitScope: null, description: "A feat", files: ["packages/a/src/index.ts"] },
|
|
241
|
+
{ hash: "b", message: "", type: "bugfix", commitScope: null, description: "B fix", files: ["packages/b/src/main.ts"] },
|
|
242
|
+
{ hash: "c", message: "", type: "feature", commitScope: null, description: "Both", files: ["packages/a/lib/x.ts", "packages/b/lib/y.ts"] },
|
|
243
|
+
{ hash: "d", message: "", type: "feature", commitScope: null, description: "No files", files: [] },
|
|
244
|
+
];
|
|
245
|
+
|
|
246
|
+
test("filters commits to only those touching the package", () => {
|
|
247
|
+
const filtered = filterCommitsForPackage(commits, "/root/packages/a", "/root");
|
|
248
|
+
expect(filtered).toHaveLength(3); // a, c (touches a), d (no files = included)
|
|
249
|
+
expect(filtered.map((c) => c.hash)).toEqual(["a", "c", "d"]);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test("filters for a different package", () => {
|
|
253
|
+
const filtered = filterCommitsForPackage(commits, "/root/packages/b", "/root");
|
|
254
|
+
expect(filtered).toHaveLength(3); // b, c (touches b), d (no files = included)
|
|
255
|
+
expect(filtered.map((c) => c.hash)).toEqual(["b", "c", "d"]);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("includes commits with no files (fallback)", () => {
|
|
259
|
+
const noFileCommits: ParsedCommit[] = [
|
|
260
|
+
{ hash: "x", message: "", type: "feature", commitScope: null, description: "X", files: [] },
|
|
261
|
+
];
|
|
262
|
+
const filtered = filterCommitsForPackage(noFileCommits, "/root/packages/a", "/root");
|
|
263
|
+
expect(filtered).toHaveLength(1);
|
|
264
|
+
});
|
|
265
|
+
});
|
package/src/commits.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { CommitType, ParsedCommit, GroupedCommits } from "./types.ts";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_SECTIONS: CommitType[] = ["feature", "bugfix", "perf"];
|
|
4
|
+
|
|
5
|
+
const TYPE_MAP: Record<string, CommitType> = {
|
|
6
|
+
feat: "feature",
|
|
7
|
+
feature: "feature",
|
|
8
|
+
fix: "bugfix",
|
|
9
|
+
bug: "bugfix",
|
|
10
|
+
bugfix: "bugfix",
|
|
11
|
+
refactor: "refactor",
|
|
12
|
+
perf: "perf",
|
|
13
|
+
style: "style",
|
|
14
|
+
test: "test",
|
|
15
|
+
docs: "docs",
|
|
16
|
+
build: "build",
|
|
17
|
+
ops: "ops",
|
|
18
|
+
chore: "chore",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const COMMIT_TYPES: CommitType[] = [
|
|
22
|
+
"feature",
|
|
23
|
+
"bugfix",
|
|
24
|
+
"refactor",
|
|
25
|
+
"perf",
|
|
26
|
+
"style",
|
|
27
|
+
"test",
|
|
28
|
+
"docs",
|
|
29
|
+
"build",
|
|
30
|
+
"ops",
|
|
31
|
+
"chore",
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
export function normalizeType(keyword: string): CommitType | null {
|
|
35
|
+
return TYPE_MAP[keyword.toLowerCase()] ?? null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Matches: [type(scope)] desc, [type]: desc, type(scope): desc, type: desc
|
|
39
|
+
const COMMIT_PATTERN = /^\[([^\]]+)\]:?\s*(.*)$|^(\w+(?:\([^)]*\))?):\s+(.*)$/;
|
|
40
|
+
const SCOPE_PATTERN = /^(\w+)\(([^)]*)\)$/;
|
|
41
|
+
|
|
42
|
+
export function parseCommit(hash: string, message: string): ParsedCommit {
|
|
43
|
+
const trimmed = message.trim();
|
|
44
|
+
const match = COMMIT_PATTERN.exec(trimmed);
|
|
45
|
+
|
|
46
|
+
if (!match) {
|
|
47
|
+
return { hash, message: trimmed, type: null, commitScope: null, description: trimmed, files: [] };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const raw = (match[1] ?? match[3])!.trim();
|
|
51
|
+
const description = (match[2] ?? match[4])!.trim();
|
|
52
|
+
|
|
53
|
+
const scopeMatch = SCOPE_PATTERN.exec(raw);
|
|
54
|
+
const keyword = scopeMatch ? scopeMatch[1]! : raw;
|
|
55
|
+
const commitScope = scopeMatch ? scopeMatch[2]!.trim() : null;
|
|
56
|
+
|
|
57
|
+
const type = TYPE_MAP[keyword.toLowerCase()] ?? null;
|
|
58
|
+
|
|
59
|
+
return { hash, message: trimmed, type, commitScope, description, files: [] };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function filterCommitsForPackage(
|
|
63
|
+
commits: ParsedCommit[],
|
|
64
|
+
packagePath: string,
|
|
65
|
+
rootDir: string,
|
|
66
|
+
): ParsedCommit[] {
|
|
67
|
+
const relPkgPath = packagePath.startsWith(rootDir)
|
|
68
|
+
? packagePath.slice(rootDir.length + 1)
|
|
69
|
+
: packagePath;
|
|
70
|
+
return commits.filter(
|
|
71
|
+
(c) => c.files.length === 0 || c.files.some((f) => f.startsWith(relPkgPath + "/")),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function groupCommits(commits: ParsedCommit[]): GroupedCommits {
|
|
76
|
+
const groups = Object.fromEntries(
|
|
77
|
+
COMMIT_TYPES.map((t) => [t, [] as ParsedCommit[]]),
|
|
78
|
+
) as GroupedCommits;
|
|
79
|
+
|
|
80
|
+
for (const commit of commits) {
|
|
81
|
+
if (commit.type) {
|
|
82
|
+
groups[commit.type].push(commit);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return groups;
|
|
87
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import type { CliOptions, CommitType } from "./types.ts";
|
|
3
|
+
import { normalizeType } from "./commits.ts";
|
|
4
|
+
|
|
5
|
+
const CONFIG_FILE = ".bunset.toml";
|
|
6
|
+
|
|
7
|
+
const VALID_BUMPS = new Set(["patch", "minor", "major"]);
|
|
8
|
+
const VALID_SCOPES = new Set(["all", "changed"]);
|
|
9
|
+
|
|
10
|
+
export async function loadConfig(
|
|
11
|
+
cwd: string,
|
|
12
|
+
): Promise<Partial<CliOptions>> {
|
|
13
|
+
const file = Bun.file(path.join(cwd, CONFIG_FILE));
|
|
14
|
+
|
|
15
|
+
if (!(await file.exists())) return {};
|
|
16
|
+
|
|
17
|
+
const raw = Bun.TOML.parse(await file.text()) as Record<string, unknown>;
|
|
18
|
+
const config: Partial<CliOptions> = {};
|
|
19
|
+
|
|
20
|
+
if (typeof raw.bump === "string" && VALID_BUMPS.has(raw.bump)) {
|
|
21
|
+
config.bump = raw.bump as CliOptions["bump"];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (typeof raw.scope === "string" && VALID_SCOPES.has(raw.scope)) {
|
|
25
|
+
config.scope = raw.scope as CliOptions["scope"];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (typeof raw.commit === "boolean") {
|
|
29
|
+
config.commit = raw.commit;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (typeof raw.tag === "boolean") {
|
|
33
|
+
config.tag = raw.tag;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (typeof raw["per-package-tags"] === "boolean") {
|
|
37
|
+
config.perPackageTags = raw["per-package-tags"];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (typeof raw["dry-run"] === "boolean") {
|
|
41
|
+
config.dryRun = raw["dry-run"];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (typeof raw["filter-by-package"] === "boolean") {
|
|
45
|
+
config.filterByPackage = raw["filter-by-package"];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (typeof raw["tag-prefix"] === "string") {
|
|
49
|
+
config.tagPrefix = raw["tag-prefix"];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (Array.isArray(raw.sections)) {
|
|
53
|
+
const sections: CommitType[] = [];
|
|
54
|
+
for (const s of raw.sections) {
|
|
55
|
+
if (typeof s === "string") {
|
|
56
|
+
const type = normalizeType(s);
|
|
57
|
+
if (type) sections.push(type);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (sections.length > 0) config.sections = sections;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return config;
|
|
64
|
+
}
|
package/src/deps.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { $ } from "bun";
|
|
2
|
+
import type { UpdatedDependency } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export async function getUpdatedDependencies(
|
|
5
|
+
cwd: string,
|
|
6
|
+
packageJsonPath: string,
|
|
7
|
+
sinceRef: string | null,
|
|
8
|
+
): Promise<UpdatedDependency[]> {
|
|
9
|
+
if (!sinceRef) return [];
|
|
10
|
+
|
|
11
|
+
let oldPkg: Record<string, unknown>;
|
|
12
|
+
try {
|
|
13
|
+
const relativePath = packageJsonPath.startsWith(cwd)
|
|
14
|
+
? packageJsonPath.slice(cwd.length + 1)
|
|
15
|
+
: packageJsonPath;
|
|
16
|
+
const result =
|
|
17
|
+
await $`git -C ${cwd} show ${sinceRef}:${relativePath}`.quiet();
|
|
18
|
+
oldPkg = JSON.parse(result.text());
|
|
19
|
+
} catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const currentPkg = await Bun.file(packageJsonPath).json();
|
|
24
|
+
const updated: UpdatedDependency[] = [];
|
|
25
|
+
|
|
26
|
+
const oldDeps = (oldPkg.dependencies ?? {}) as Record<string, string>;
|
|
27
|
+
const oldDevDeps = (oldPkg.devDependencies ?? {}) as Record<string, string>;
|
|
28
|
+
const newDeps = (currentPkg.dependencies ?? {}) as Record<string, string>;
|
|
29
|
+
const newDevDeps = (currentPkg.devDependencies ?? {}) as Record<
|
|
30
|
+
string,
|
|
31
|
+
string
|
|
32
|
+
>;
|
|
33
|
+
|
|
34
|
+
for (const [name, version] of Object.entries(newDeps)) {
|
|
35
|
+
if (oldDeps[name] && oldDeps[name] !== version) {
|
|
36
|
+
updated.push({ name, newVersion: version });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const [name, version] of Object.entries(newDevDeps)) {
|
|
41
|
+
if (oldDevDeps[name] && oldDevDeps[name] !== version) {
|
|
42
|
+
updated.push({ name, newVersion: version });
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return updated;
|
|
47
|
+
}
|
package/src/git.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { $ } from "bun";
|
|
2
|
+
|
|
3
|
+
export async function getLastTag(cwd: string): Promise<string | null> {
|
|
4
|
+
try {
|
|
5
|
+
const result = await $`git -C ${cwd} describe --tags --abbrev=0`.quiet();
|
|
6
|
+
return result.text().trim() || null;
|
|
7
|
+
} catch {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function getCommitsSince(
|
|
13
|
+
cwd: string,
|
|
14
|
+
sinceRef: string | null,
|
|
15
|
+
): Promise<{ hash: string; message: string }[]> {
|
|
16
|
+
let result;
|
|
17
|
+
if (sinceRef) {
|
|
18
|
+
result =
|
|
19
|
+
await $`git -C ${cwd} log ${sinceRef}..HEAD --pretty=format:%H%n%s --no-merges`.quiet();
|
|
20
|
+
} else {
|
|
21
|
+
result =
|
|
22
|
+
await $`git -C ${cwd} log --pretty=format:%H%n%s --no-merges`.quiet();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const text = result.text().trim();
|
|
26
|
+
if (!text) return [];
|
|
27
|
+
|
|
28
|
+
const lines = text.split("\n");
|
|
29
|
+
const commits: { hash: string; message: string }[] = [];
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < lines.length - 1; i += 2) {
|
|
32
|
+
commits.push({ hash: lines[i]!, message: lines[i + 1]! });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return commits;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function getCommitFiles(
|
|
39
|
+
cwd: string,
|
|
40
|
+
hash: string,
|
|
41
|
+
): Promise<string[]> {
|
|
42
|
+
try {
|
|
43
|
+
const result =
|
|
44
|
+
await $`git -C ${cwd} diff-tree --no-commit-id --name-only -r ${hash}`.quiet();
|
|
45
|
+
return result
|
|
46
|
+
.text()
|
|
47
|
+
.trim()
|
|
48
|
+
.split("\n")
|
|
49
|
+
.filter(Boolean);
|
|
50
|
+
} catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export async function commitAndTag(
|
|
56
|
+
cwd: string,
|
|
57
|
+
message: string,
|
|
58
|
+
tags: string[] = [],
|
|
59
|
+
): Promise<void> {
|
|
60
|
+
await $`git -C ${cwd} add -A`.quiet();
|
|
61
|
+
await $`git -C ${cwd} commit -m ${message}`.quiet();
|
|
62
|
+
for (const tag of tags) {
|
|
63
|
+
await $`git -C ${cwd} tag ${tag}`.quiet();
|
|
64
|
+
}
|
|
65
|
+
}
|