kanna-code 0.23.0 → 0.24.0
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/dist/client/assets/index-0UyEIBeu.js +2532 -0
- package/dist/client/assets/index-CYUEw41P.css +32 -0
- package/dist/client/index.html +2 -2
- package/package.json +1 -1
- package/src/server/diff-store.test.ts +346 -2
- package/src/server/diff-store.ts +938 -3
- package/src/server/generate-commit-message.test.ts +2 -0
- package/src/server/generate-commit-message.ts +2 -2
- package/src/server/ws-router.test.ts +198 -14
- package/src/server/ws-router.ts +125 -2
- package/src/shared/protocol.ts +22 -0
- package/src/shared/types.ts +103 -0
- package/dist/client/assets/index-BGQxheCc.js +0 -2511
- package/dist/client/assets/index-DKD9EePK.css +0 -32
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test"
|
|
2
|
-
import { mkdtemp, rm, writeFile } from "node:fs/promises"
|
|
2
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
|
|
3
3
|
import { tmpdir } from "node:os"
|
|
4
4
|
import path from "node:path"
|
|
5
|
-
import { DiffStore } from "./diff-store"
|
|
5
|
+
import { appendGitIgnoreEntry, DiffStore, extractGitHubRepoSlug, fetchGitHubPullRequests } from "./diff-store"
|
|
6
6
|
|
|
7
7
|
async function run(command: string[], cwd: string) {
|
|
8
8
|
const process = Bun.spawn(command, {
|
|
@@ -54,6 +54,7 @@ describe("DiffStore", () => {
|
|
|
54
54
|
expect(snapshot.status).toBe("ready")
|
|
55
55
|
expect(snapshot.files).toHaveLength(1)
|
|
56
56
|
expect(snapshot.files[0]?.path).toBe("app.txt")
|
|
57
|
+
expect(snapshot.files[0]?.isUntracked).toBe(false)
|
|
57
58
|
expect(snapshot.files[0]?.patch).toContain("-base")
|
|
58
59
|
expect(snapshot.files[0]?.patch).toContain("+changed")
|
|
59
60
|
})
|
|
@@ -70,6 +71,7 @@ describe("DiffStore", () => {
|
|
|
70
71
|
status: "no_repo",
|
|
71
72
|
branchName: undefined,
|
|
72
73
|
files: [],
|
|
74
|
+
branchHistory: { entries: [] },
|
|
73
75
|
})
|
|
74
76
|
})
|
|
75
77
|
|
|
@@ -123,5 +125,347 @@ describe("DiffStore", () => {
|
|
|
123
125
|
expect(snapshot.files).toHaveLength(1)
|
|
124
126
|
expect(snapshot.files[0]?.path).toBe("after.txt")
|
|
125
127
|
expect(snapshot.files[0]?.changeType).toBe("renamed")
|
|
128
|
+
expect(snapshot.files[0]?.isUntracked).toBe(false)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test("marks untracked files so they can be ignored", async () => {
|
|
132
|
+
const repoRoot = await createRepo()
|
|
133
|
+
tempDirs.push(repoRoot)
|
|
134
|
+
await writeFile(path.join(repoRoot, "tracked.txt"), "base\n", "utf8")
|
|
135
|
+
await run(["git", "add", "."], repoRoot)
|
|
136
|
+
await run(["git", "commit", "-m", "init"], repoRoot)
|
|
137
|
+
await writeFile(path.join(repoRoot, "scratch.log"), "tmp\n", "utf8")
|
|
138
|
+
|
|
139
|
+
const store = new DiffStore(repoRoot)
|
|
140
|
+
await store.initialize()
|
|
141
|
+
await store.refreshSnapshot("chat-1", repoRoot)
|
|
142
|
+
|
|
143
|
+
const snapshot = store.getSnapshot("chat-1")
|
|
144
|
+
expect(snapshot.files).toHaveLength(1)
|
|
145
|
+
expect(snapshot.files[0]).toMatchObject({
|
|
146
|
+
path: "scratch.log",
|
|
147
|
+
changeType: "added",
|
|
148
|
+
isUntracked: true,
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
test("discardFile reverts a tracked modified file", async () => {
|
|
153
|
+
const repoRoot = await createRepo()
|
|
154
|
+
tempDirs.push(repoRoot)
|
|
155
|
+
await writeFile(path.join(repoRoot, "app.txt"), "base\n", "utf8")
|
|
156
|
+
await run(["git", "add", "."], repoRoot)
|
|
157
|
+
await run(["git", "commit", "-m", "init"], repoRoot)
|
|
158
|
+
await writeFile(path.join(repoRoot, "app.txt"), "changed\n", "utf8")
|
|
159
|
+
|
|
160
|
+
const store = new DiffStore(repoRoot)
|
|
161
|
+
await store.initialize()
|
|
162
|
+
await store.refreshSnapshot("chat-1", repoRoot)
|
|
163
|
+
await store.discardFile({
|
|
164
|
+
chatId: "chat-1",
|
|
165
|
+
projectPath: repoRoot,
|
|
166
|
+
path: "app.txt",
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
expect(await readFile(path.join(repoRoot, "app.txt"), "utf8")).toBe("base\n")
|
|
170
|
+
expect(store.getSnapshot("chat-1").files).toHaveLength(0)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
test("discardFile deletes an untracked file", async () => {
|
|
174
|
+
const repoRoot = await createRepo()
|
|
175
|
+
tempDirs.push(repoRoot)
|
|
176
|
+
await writeFile(path.join(repoRoot, "tracked.txt"), "base\n", "utf8")
|
|
177
|
+
await run(["git", "add", "."], repoRoot)
|
|
178
|
+
await run(["git", "commit", "-m", "init"], repoRoot)
|
|
179
|
+
await writeFile(path.join(repoRoot, "scratch.log"), "tmp\n", "utf8")
|
|
180
|
+
|
|
181
|
+
const store = new DiffStore(repoRoot)
|
|
182
|
+
await store.initialize()
|
|
183
|
+
await store.refreshSnapshot("chat-1", repoRoot)
|
|
184
|
+
await store.discardFile({
|
|
185
|
+
chatId: "chat-1",
|
|
186
|
+
projectPath: repoRoot,
|
|
187
|
+
path: "scratch.log",
|
|
188
|
+
})
|
|
189
|
+
|
|
190
|
+
expect(await Bun.file(path.join(repoRoot, "scratch.log")).exists()).toBe(false)
|
|
191
|
+
expect(store.getSnapshot("chat-1").files).toHaveLength(0)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
test("discardFile reverts a renamed file", async () => {
|
|
195
|
+
const repoRoot = await createRepo()
|
|
196
|
+
tempDirs.push(repoRoot)
|
|
197
|
+
await writeFile(path.join(repoRoot, "before.txt"), "same\n", "utf8")
|
|
198
|
+
await run(["git", "add", "."], repoRoot)
|
|
199
|
+
await run(["git", "commit", "-m", "init"], repoRoot)
|
|
200
|
+
await run(["git", "mv", "before.txt", "after.txt"], repoRoot)
|
|
201
|
+
|
|
202
|
+
const store = new DiffStore(repoRoot)
|
|
203
|
+
await store.initialize()
|
|
204
|
+
await store.refreshSnapshot("chat-1", repoRoot)
|
|
205
|
+
await store.discardFile({
|
|
206
|
+
chatId: "chat-1",
|
|
207
|
+
projectPath: repoRoot,
|
|
208
|
+
path: "after.txt",
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
expect(await Bun.file(path.join(repoRoot, "before.txt")).exists()).toBe(true)
|
|
212
|
+
expect(await Bun.file(path.join(repoRoot, "after.txt")).exists()).toBe(false)
|
|
213
|
+
expect(store.getSnapshot("chat-1").files).toHaveLength(0)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
test("ignoreFile appends a .gitignore entry once", async () => {
|
|
217
|
+
const repoRoot = await createRepo()
|
|
218
|
+
tempDirs.push(repoRoot)
|
|
219
|
+
await writeFile(path.join(repoRoot, "tracked.txt"), "base\n", "utf8")
|
|
220
|
+
await run(["git", "add", "."], repoRoot)
|
|
221
|
+
await run(["git", "commit", "-m", "init"], repoRoot)
|
|
222
|
+
await writeFile(path.join(repoRoot, "scratch.log"), "tmp\n", "utf8")
|
|
223
|
+
|
|
224
|
+
const store = new DiffStore(repoRoot)
|
|
225
|
+
await store.initialize()
|
|
226
|
+
await store.refreshSnapshot("chat-1", repoRoot)
|
|
227
|
+
await store.ignoreFile({
|
|
228
|
+
chatId: "chat-1",
|
|
229
|
+
projectPath: repoRoot,
|
|
230
|
+
path: "scratch.log",
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
expect(await readFile(path.join(repoRoot, ".gitignore"), "utf8")).toBe("scratch.log\n")
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
test("appendGitIgnoreEntry does not duplicate an existing identical entry", () => {
|
|
237
|
+
expect(appendGitIgnoreEntry("scratch.log\n", "scratch.log")).toBe("scratch.log\n")
|
|
238
|
+
expect(appendGitIgnoreEntry("scratch.log", "scratch.log")).toBe("scratch.log\n")
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
test("extractGitHubRepoSlug supports common remote URL formats", () => {
|
|
242
|
+
expect(extractGitHubRepoSlug("git@github.com:acme/repo.git")).toBe("acme/repo")
|
|
243
|
+
expect(extractGitHubRepoSlug("ssh://git@github.com/acme/repo.git")).toBe("acme/repo")
|
|
244
|
+
expect(extractGitHubRepoSlug("https://github.com/acme/repo.git")).toBe("acme/repo")
|
|
245
|
+
expect(extractGitHubRepoSlug("https://gitlab.com/acme/repo.git")).toBeNull()
|
|
246
|
+
})
|
|
247
|
+
|
|
248
|
+
test("refreshSnapshot includes recent branch history with tags and github URLs", async () => {
|
|
249
|
+
const repoRoot = await createRepo()
|
|
250
|
+
tempDirs.push(repoRoot)
|
|
251
|
+
await writeFile(path.join(repoRoot, "app.txt"), "base\n", "utf8")
|
|
252
|
+
await run(["git", "add", "."], repoRoot)
|
|
253
|
+
await run(["git", "commit", "-m", "Initial commit"], repoRoot)
|
|
254
|
+
await run(["git", "tag", "v1.0.0"], repoRoot)
|
|
255
|
+
await run(["git", "remote", "add", "origin", "git@github.com:acme/repo.git"], repoRoot)
|
|
256
|
+
|
|
257
|
+
const store = new DiffStore(repoRoot)
|
|
258
|
+
await store.initialize()
|
|
259
|
+
await store.refreshSnapshot("chat-1", repoRoot)
|
|
260
|
+
|
|
261
|
+
const snapshot = store.getSnapshot("chat-1")
|
|
262
|
+
expect(snapshot.branchHistory?.entries).toHaveLength(1)
|
|
263
|
+
expect(snapshot.branchHistory?.entries[0]).toMatchObject({
|
|
264
|
+
summary: "Initial commit",
|
|
265
|
+
authorName: "Kanna",
|
|
266
|
+
tags: ["v1.0.0"],
|
|
267
|
+
githubUrl: expect.stringContaining("https://github.com/acme/repo/commit/"),
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
test("ignoreFile rejects tracked files", async () => {
|
|
272
|
+
const repoRoot = await createRepo()
|
|
273
|
+
tempDirs.push(repoRoot)
|
|
274
|
+
await writeFile(path.join(repoRoot, "app.txt"), "base\n", "utf8")
|
|
275
|
+
await run(["git", "add", "."], repoRoot)
|
|
276
|
+
await run(["git", "commit", "-m", "init"], repoRoot)
|
|
277
|
+
await writeFile(path.join(repoRoot, "app.txt"), "changed\n", "utf8")
|
|
278
|
+
|
|
279
|
+
const store = new DiffStore(repoRoot)
|
|
280
|
+
await store.initialize()
|
|
281
|
+
await store.refreshSnapshot("chat-1", repoRoot)
|
|
282
|
+
|
|
283
|
+
await expect(store.ignoreFile({
|
|
284
|
+
chatId: "chat-1",
|
|
285
|
+
projectPath: repoRoot,
|
|
286
|
+
path: "app.txt",
|
|
287
|
+
})).rejects.toThrow("Only untracked files can be ignored from the diff viewer")
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
test("fetchGitHubPullRequests prefers gh api when available", async () => {
|
|
291
|
+
let requestedPath = ""
|
|
292
|
+
|
|
293
|
+
const pulls = await fetchGitHubPullRequests("acme/repo", {
|
|
294
|
+
ghApiImpl: async (path) => {
|
|
295
|
+
requestedPath = path
|
|
296
|
+
return [{ number: 7, title: "Fix bug", head: { ref: "feature/fix" } }]
|
|
297
|
+
},
|
|
298
|
+
fetchImpl: async () => {
|
|
299
|
+
throw new Error("fetch should not be used when gh succeeds")
|
|
300
|
+
},
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
expect(requestedPath).toBe("repos/acme/repo/pulls?state=open&per_page=50")
|
|
304
|
+
expect(pulls).toHaveLength(1)
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
test("fetchGitHubPullRequests falls back to fetch and sends the GitHub accept header", async () => {
|
|
308
|
+
let requestedUrl = ""
|
|
309
|
+
let requestedAcceptHeader = ""
|
|
310
|
+
|
|
311
|
+
const pulls = await fetchGitHubPullRequests("acme/repo", {
|
|
312
|
+
ghApiImpl: async () => null,
|
|
313
|
+
fetchImpl: async (input, init) => {
|
|
314
|
+
requestedUrl = String(input)
|
|
315
|
+
requestedAcceptHeader = String(new Headers(init?.headers).get("Accept"))
|
|
316
|
+
return new Response(JSON.stringify([{ number: 7, title: "Fix bug", head: { ref: "feature/fix" } }]), {
|
|
317
|
+
status: 200,
|
|
318
|
+
headers: { "Content-Type": "application/json" },
|
|
319
|
+
})
|
|
320
|
+
},
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
expect(requestedUrl).toBe("https://api.github.com/repos/acme/repo/pulls?state=open&per_page=50")
|
|
324
|
+
expect(requestedAcceptHeader).toBe("application/vnd.github+json")
|
|
325
|
+
expect(pulls).toHaveLength(1)
|
|
326
|
+
})
|
|
327
|
+
|
|
328
|
+
test("listBranches includes default branch, local and remote branches, and recent branches", async () => {
|
|
329
|
+
const repoRoot = await createRepo()
|
|
330
|
+
tempDirs.push(repoRoot)
|
|
331
|
+
await writeFile(path.join(repoRoot, "app.txt"), "base\n", "utf8")
|
|
332
|
+
await run(["git", "add", "."], repoRoot)
|
|
333
|
+
await run(["git", "commit", "-m", "init"], repoRoot)
|
|
334
|
+
await run(["git", "switch", "-c", "feature/recent"], repoRoot)
|
|
335
|
+
await run(["git", "switch", "-c", "feature/other"], repoRoot)
|
|
336
|
+
await run(["git", "switch", "feature/recent"], repoRoot)
|
|
337
|
+
await run(["git", "switch", "main"], repoRoot).catch(async () => run(["git", "switch", "master"], repoRoot))
|
|
338
|
+
await run(["git", "update-ref", "refs/remotes/origin/main", "HEAD"], repoRoot).catch(() => {})
|
|
339
|
+
await run(["git", "symbolic-ref", "refs/remotes/origin/HEAD", "refs/remotes/origin/main"], repoRoot).catch(() => {})
|
|
340
|
+
await run(["git", "update-ref", "refs/remotes/origin/feature/remote", "HEAD"], repoRoot)
|
|
341
|
+
|
|
342
|
+
const store = new DiffStore(repoRoot)
|
|
343
|
+
await store.initialize()
|
|
344
|
+
|
|
345
|
+
const result = await store.listBranches({ projectPath: repoRoot })
|
|
346
|
+
expect(result.defaultBranchName).toBe("main")
|
|
347
|
+
expect(result.local.some((entry) => entry.name === "feature/recent")).toBe(true)
|
|
348
|
+
expect(result.remote.some((entry) => entry.remoteRef === "origin/feature/remote")).toBe(true)
|
|
349
|
+
expect(result.recent.some((entry) => entry.name === "feature/recent")).toBe(true)
|
|
350
|
+
})
|
|
351
|
+
|
|
352
|
+
test("listBranches hides remote PR head refs from the remote section", async () => {
|
|
353
|
+
const repoRoot = await createRepo()
|
|
354
|
+
tempDirs.push(repoRoot)
|
|
355
|
+
await writeFile(path.join(repoRoot, "app.txt"), "base\n", "utf8")
|
|
356
|
+
await run(["git", "add", "."], repoRoot)
|
|
357
|
+
await run(["git", "commit", "-m", "init"], repoRoot)
|
|
358
|
+
await run(["git", "remote", "add", "origin", "git@github.com:acme/repo.git"], repoRoot)
|
|
359
|
+
await run(["git", "remote", "add", "github-desktop-jane", "git@github.com:jane/repo.git"], repoRoot)
|
|
360
|
+
await run(["git", "update-ref", "refs/remotes/origin/main", "HEAD"], repoRoot).catch(() => {})
|
|
361
|
+
await run(["git", "symbolic-ref", "refs/remotes/origin/HEAD", "refs/remotes/origin/main"], repoRoot).catch(() => {})
|
|
362
|
+
await run(["git", "update-ref", "refs/remotes/github-desktop-jane/feature/pr-branch", "HEAD"], repoRoot)
|
|
363
|
+
await run(["git", "update-ref", "refs/remotes/origin/feature/pr-branch", "HEAD"], repoRoot)
|
|
364
|
+
await run(["git", "update-ref", "refs/remotes/origin/feature/non-pr", "HEAD"], repoRoot)
|
|
365
|
+
|
|
366
|
+
const originalFetch = globalThis.fetch
|
|
367
|
+
globalThis.fetch = Object.assign(
|
|
368
|
+
async () => new Response(JSON.stringify([
|
|
369
|
+
{
|
|
370
|
+
number: 42,
|
|
371
|
+
title: "PR branch",
|
|
372
|
+
head: {
|
|
373
|
+
ref: "feature/pr-branch",
|
|
374
|
+
label: "jane:feature/pr-branch",
|
|
375
|
+
repo: {
|
|
376
|
+
clone_url: "git@github.com:jane/repo.git",
|
|
377
|
+
full_name: "jane/repo",
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
base: {
|
|
381
|
+
ref: "main",
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
]), {
|
|
385
|
+
status: 200,
|
|
386
|
+
headers: { "Content-Type": "application/json" },
|
|
387
|
+
}),
|
|
388
|
+
{ preconnect: originalFetch.preconnect.bind(originalFetch) }
|
|
389
|
+
) as typeof fetch
|
|
390
|
+
|
|
391
|
+
try {
|
|
392
|
+
const store = new DiffStore(repoRoot)
|
|
393
|
+
await store.initialize()
|
|
394
|
+
|
|
395
|
+
const result = await store.listBranches({ projectPath: repoRoot })
|
|
396
|
+
expect(result.pullRequests.some((entry) => entry.prNumber === 42)).toBe(true)
|
|
397
|
+
expect(result.remote.some((entry) => entry.remoteRef === "github-desktop-jane/feature/pr-branch")).toBe(false)
|
|
398
|
+
expect(result.remote.some((entry) => entry.remoteRef === "origin/feature/pr-branch")).toBe(false)
|
|
399
|
+
expect(result.remote.some((entry) => entry.remoteRef === "origin/feature/non-pr")).toBe(true)
|
|
400
|
+
} finally {
|
|
401
|
+
globalThis.fetch = originalFetch
|
|
402
|
+
}
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
test("checkoutBranch creates a local tracking branch from a remote branch", async () => {
|
|
406
|
+
const repoRoot = await createRepo()
|
|
407
|
+
tempDirs.push(repoRoot)
|
|
408
|
+
await writeFile(path.join(repoRoot, "app.txt"), "base\n", "utf8")
|
|
409
|
+
await run(["git", "add", "."], repoRoot)
|
|
410
|
+
await run(["git", "commit", "-m", "init"], repoRoot)
|
|
411
|
+
await run(["git", "remote", "add", "origin", "git@github.com:acme/repo.git"], repoRoot)
|
|
412
|
+
await run(["git", "update-ref", "refs/remotes/origin/feature/remote", "HEAD"], repoRoot)
|
|
413
|
+
|
|
414
|
+
const store = new DiffStore(repoRoot)
|
|
415
|
+
await store.initialize()
|
|
416
|
+
const result = await store.checkoutBranch({
|
|
417
|
+
chatId: "chat-1",
|
|
418
|
+
projectPath: repoRoot,
|
|
419
|
+
branch: { kind: "remote", name: "feature/remote", remoteRef: "origin/feature/remote" },
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
expect(result.ok).toBe(true)
|
|
423
|
+
expect((await run(["git", "branch", "--show-current"], repoRoot)).trim()).toBe("feature/remote")
|
|
424
|
+
})
|
|
425
|
+
|
|
426
|
+
test("checkoutBranch cancels when changes exist and bringChanges is false", async () => {
|
|
427
|
+
const repoRoot = await createRepo()
|
|
428
|
+
tempDirs.push(repoRoot)
|
|
429
|
+
await writeFile(path.join(repoRoot, "app.txt"), "base\n", "utf8")
|
|
430
|
+
await run(["git", "add", "."], repoRoot)
|
|
431
|
+
await run(["git", "commit", "-m", "init"], repoRoot)
|
|
432
|
+
await run(["git", "switch", "-c", "feature/other"], repoRoot)
|
|
433
|
+
await run(["git", "switch", "main"], repoRoot).catch(async () => run(["git", "switch", "master"], repoRoot))
|
|
434
|
+
await writeFile(path.join(repoRoot, "app.txt"), "changed\n", "utf8")
|
|
435
|
+
|
|
436
|
+
const store = new DiffStore(repoRoot)
|
|
437
|
+
await store.initialize()
|
|
438
|
+
const result = await store.checkoutBranch({
|
|
439
|
+
chatId: "chat-1",
|
|
440
|
+
projectPath: repoRoot,
|
|
441
|
+
branch: { kind: "local", name: "feature/other" },
|
|
442
|
+
bringChanges: false,
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
expect(result.ok).toBe(false)
|
|
446
|
+
if (!result.ok) {
|
|
447
|
+
expect(result.cancelled).toBe(true)
|
|
448
|
+
}
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
test("createBranch creates and checks out a branch from a chosen base", async () => {
|
|
452
|
+
const repoRoot = await createRepo()
|
|
453
|
+
tempDirs.push(repoRoot)
|
|
454
|
+
await writeFile(path.join(repoRoot, "app.txt"), "base\n", "utf8")
|
|
455
|
+
await run(["git", "add", "."], repoRoot)
|
|
456
|
+
await run(["git", "commit", "-m", "init"], repoRoot)
|
|
457
|
+
await run(["git", "switch", "-c", "feature/base"], repoRoot)
|
|
458
|
+
|
|
459
|
+
const store = new DiffStore(repoRoot)
|
|
460
|
+
await store.initialize()
|
|
461
|
+
const result = await store.createBranch({
|
|
462
|
+
chatId: "chat-1",
|
|
463
|
+
projectPath: repoRoot,
|
|
464
|
+
name: "feature/new",
|
|
465
|
+
baseBranchName: "feature/base",
|
|
466
|
+
})
|
|
467
|
+
|
|
468
|
+
expect(result.ok).toBe(true)
|
|
469
|
+
expect((await run(["git", "branch", "--show-current"], repoRoot)).trim()).toBe("feature/new")
|
|
126
470
|
})
|
|
127
471
|
})
|