kanna-code 0.24.0 → 0.25.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/README.md +6 -2
- package/dist/client/assets/index-Hy4LEj8j.css +32 -0
- package/dist/client/assets/index-Tg96DNSK.js +2554 -0
- package/dist/client/index.html +2 -2
- package/package.json +1 -1
- package/src/server/diff-store.test.ts +173 -31
- package/src/server/diff-store.ts +545 -82
- package/src/server/generate-commit-message.test.ts +0 -2
- package/src/server/generate-commit-message.ts +9 -4
- package/src/server/keybindings.test.ts +12 -0
- package/src/server/keybindings.ts +3 -0
- package/src/server/read-models.test.ts +1 -2
- package/src/server/read-models.ts +1 -4
- package/src/server/ws-router.test.ts +275 -8
- package/src/server/ws-router.ts +79 -86
- package/src/shared/protocol.ts +37 -1
- package/src/shared/types.ts +49 -45
- package/dist/client/assets/index-0UyEIBeu.js +0 -2532
- package/dist/client/assets/index-CYUEw41P.css +0 -32
package/dist/client/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<link rel="icon" type="image/png" href="/favicon.png" />
|
|
7
7
|
<title>Kanna</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-Tg96DNSK.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Hy4LEj8j.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -31,6 +31,12 @@ async function createRepo() {
|
|
|
31
31
|
return root
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
async function createBareRemote() {
|
|
35
|
+
const root = await mkdtemp(path.join(tmpdir(), "kanna-diff-remote-"))
|
|
36
|
+
await run(["git", "init", "--bare"], root)
|
|
37
|
+
return root
|
|
38
|
+
}
|
|
39
|
+
|
|
34
40
|
const tempDirs: string[] = []
|
|
35
41
|
|
|
36
42
|
describe("DiffStore", () => {
|
|
@@ -48,15 +54,18 @@ describe("DiffStore", () => {
|
|
|
48
54
|
|
|
49
55
|
const store = new DiffStore(repoRoot)
|
|
50
56
|
await store.initialize()
|
|
51
|
-
await store.refreshSnapshot("
|
|
57
|
+
await store.refreshSnapshot("project-1", repoRoot)
|
|
52
58
|
|
|
53
|
-
const snapshot = store.
|
|
59
|
+
const snapshot = store.getProjectSnapshot("project-1")
|
|
54
60
|
expect(snapshot.status).toBe("ready")
|
|
55
61
|
expect(snapshot.files).toHaveLength(1)
|
|
56
62
|
expect(snapshot.files[0]?.path).toBe("app.txt")
|
|
57
63
|
expect(snapshot.files[0]?.isUntracked).toBe(false)
|
|
58
|
-
expect(snapshot.files[0]?.
|
|
59
|
-
expect(snapshot.files[0]?.
|
|
64
|
+
expect(snapshot.files[0]?.additions).toBe(1)
|
|
65
|
+
expect(snapshot.files[0]?.deletions).toBe(1)
|
|
66
|
+
await expect(store.readPatch({ projectPath: repoRoot, path: "app.txt" })).resolves.toMatchObject({
|
|
67
|
+
patch: expect.stringContaining("-base"),
|
|
68
|
+
})
|
|
60
69
|
})
|
|
61
70
|
|
|
62
71
|
test("returns no_repo outside a git repository", async () => {
|
|
@@ -65,9 +74,9 @@ describe("DiffStore", () => {
|
|
|
65
74
|
|
|
66
75
|
const store = new DiffStore(root)
|
|
67
76
|
await store.initialize()
|
|
68
|
-
await store.refreshSnapshot("
|
|
77
|
+
await store.refreshSnapshot("project-1", root)
|
|
69
78
|
|
|
70
|
-
expect(store.
|
|
79
|
+
expect(store.getProjectSnapshot("project-1")).toEqual({
|
|
71
80
|
status: "no_repo",
|
|
72
81
|
branchName: undefined,
|
|
73
82
|
files: [],
|
|
@@ -88,10 +97,10 @@ describe("DiffStore", () => {
|
|
|
88
97
|
|
|
89
98
|
const store = new DiffStore(repoRoot)
|
|
90
99
|
await store.initialize()
|
|
91
|
-
await store.refreshSnapshot("
|
|
100
|
+
await store.refreshSnapshot("project-1", repoRoot)
|
|
92
101
|
|
|
93
102
|
await store.commitFiles({
|
|
94
|
-
|
|
103
|
+
projectId: "project-1",
|
|
95
104
|
projectPath: repoRoot,
|
|
96
105
|
paths: ["app.txt"],
|
|
97
106
|
summary: "Update app",
|
|
@@ -99,7 +108,7 @@ describe("DiffStore", () => {
|
|
|
99
108
|
mode: "commit_only",
|
|
100
109
|
})
|
|
101
110
|
|
|
102
|
-
const snapshot = store.
|
|
111
|
+
const snapshot = store.getProjectSnapshot("project-1")
|
|
103
112
|
expect(snapshot.status).toBe("ready")
|
|
104
113
|
expect(snapshot.files).toHaveLength(1)
|
|
105
114
|
expect(snapshot.files[0]?.path).toBe("notes.txt")
|
|
@@ -108,6 +117,37 @@ describe("DiffStore", () => {
|
|
|
108
117
|
expect(lastMessage).toBe("Update app\n\nOnly app changes")
|
|
109
118
|
})
|
|
110
119
|
|
|
120
|
+
test("commit_and_push publishes an unpublished branch", async () => {
|
|
121
|
+
const repoRoot = await createRepo()
|
|
122
|
+
const remoteRoot = await createBareRemote()
|
|
123
|
+
tempDirs.push(repoRoot, remoteRoot)
|
|
124
|
+
await run(["git", "remote", "add", "origin", remoteRoot], repoRoot)
|
|
125
|
+
await writeFile(path.join(repoRoot, "app.txt"), "base\n", "utf8")
|
|
126
|
+
await run(["git", "add", "."], repoRoot)
|
|
127
|
+
await run(["git", "commit", "-m", "init"], repoRoot)
|
|
128
|
+
await run(["git", "switch", "-c", "feature/publish-me"], repoRoot)
|
|
129
|
+
await writeFile(path.join(repoRoot, "app.txt"), "changed\n", "utf8")
|
|
130
|
+
|
|
131
|
+
const store = new DiffStore(repoRoot)
|
|
132
|
+
await store.initialize()
|
|
133
|
+
await store.refreshSnapshot("project-1", repoRoot)
|
|
134
|
+
|
|
135
|
+
const result = await store.commitFiles({
|
|
136
|
+
projectId: "project-1",
|
|
137
|
+
projectPath: repoRoot,
|
|
138
|
+
paths: ["app.txt"],
|
|
139
|
+
summary: "Publish branch",
|
|
140
|
+
mode: "commit_and_push",
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
expect(result).toMatchObject({
|
|
144
|
+
ok: true,
|
|
145
|
+
mode: "commit_and_push",
|
|
146
|
+
pushed: true,
|
|
147
|
+
})
|
|
148
|
+
expect((await run(["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], repoRoot)).trim()).toBe("origin/feature/publish-me")
|
|
149
|
+
})
|
|
150
|
+
|
|
111
151
|
test("detects renamed files", async () => {
|
|
112
152
|
const repoRoot = await createRepo()
|
|
113
153
|
tempDirs.push(repoRoot)
|
|
@@ -118,9 +158,9 @@ describe("DiffStore", () => {
|
|
|
118
158
|
|
|
119
159
|
const store = new DiffStore(repoRoot)
|
|
120
160
|
await store.initialize()
|
|
121
|
-
await store.refreshSnapshot("
|
|
161
|
+
await store.refreshSnapshot("project-1", repoRoot)
|
|
122
162
|
|
|
123
|
-
const snapshot = store.
|
|
163
|
+
const snapshot = store.getProjectSnapshot("project-1")
|
|
124
164
|
expect(snapshot.status).toBe("ready")
|
|
125
165
|
expect(snapshot.files).toHaveLength(1)
|
|
126
166
|
expect(snapshot.files[0]?.path).toBe("after.txt")
|
|
@@ -138,9 +178,9 @@ describe("DiffStore", () => {
|
|
|
138
178
|
|
|
139
179
|
const store = new DiffStore(repoRoot)
|
|
140
180
|
await store.initialize()
|
|
141
|
-
await store.refreshSnapshot("
|
|
181
|
+
await store.refreshSnapshot("project-1", repoRoot)
|
|
142
182
|
|
|
143
|
-
const snapshot = store.
|
|
183
|
+
const snapshot = store.getProjectSnapshot("project-1")
|
|
144
184
|
expect(snapshot.files).toHaveLength(1)
|
|
145
185
|
expect(snapshot.files[0]).toMatchObject({
|
|
146
186
|
path: "scratch.log",
|
|
@@ -159,15 +199,15 @@ describe("DiffStore", () => {
|
|
|
159
199
|
|
|
160
200
|
const store = new DiffStore(repoRoot)
|
|
161
201
|
await store.initialize()
|
|
162
|
-
await store.refreshSnapshot("
|
|
202
|
+
await store.refreshSnapshot("project-1", repoRoot)
|
|
163
203
|
await store.discardFile({
|
|
164
|
-
|
|
204
|
+
projectId: "project-1",
|
|
165
205
|
projectPath: repoRoot,
|
|
166
206
|
path: "app.txt",
|
|
167
207
|
})
|
|
168
208
|
|
|
169
209
|
expect(await readFile(path.join(repoRoot, "app.txt"), "utf8")).toBe("base\n")
|
|
170
|
-
expect(store.
|
|
210
|
+
expect(store.getProjectSnapshot("project-1").files).toHaveLength(0)
|
|
171
211
|
})
|
|
172
212
|
|
|
173
213
|
test("discardFile deletes an untracked file", async () => {
|
|
@@ -180,15 +220,15 @@ describe("DiffStore", () => {
|
|
|
180
220
|
|
|
181
221
|
const store = new DiffStore(repoRoot)
|
|
182
222
|
await store.initialize()
|
|
183
|
-
await store.refreshSnapshot("
|
|
223
|
+
await store.refreshSnapshot("project-1", repoRoot)
|
|
184
224
|
await store.discardFile({
|
|
185
|
-
|
|
225
|
+
projectId: "project-1",
|
|
186
226
|
projectPath: repoRoot,
|
|
187
227
|
path: "scratch.log",
|
|
188
228
|
})
|
|
189
229
|
|
|
190
230
|
expect(await Bun.file(path.join(repoRoot, "scratch.log")).exists()).toBe(false)
|
|
191
|
-
expect(store.
|
|
231
|
+
expect(store.getProjectSnapshot("project-1").files).toHaveLength(0)
|
|
192
232
|
})
|
|
193
233
|
|
|
194
234
|
test("discardFile reverts a renamed file", async () => {
|
|
@@ -201,16 +241,16 @@ describe("DiffStore", () => {
|
|
|
201
241
|
|
|
202
242
|
const store = new DiffStore(repoRoot)
|
|
203
243
|
await store.initialize()
|
|
204
|
-
await store.refreshSnapshot("
|
|
244
|
+
await store.refreshSnapshot("project-1", repoRoot)
|
|
205
245
|
await store.discardFile({
|
|
206
|
-
|
|
246
|
+
projectId: "project-1",
|
|
207
247
|
projectPath: repoRoot,
|
|
208
248
|
path: "after.txt",
|
|
209
249
|
})
|
|
210
250
|
|
|
211
251
|
expect(await Bun.file(path.join(repoRoot, "before.txt")).exists()).toBe(true)
|
|
212
252
|
expect(await Bun.file(path.join(repoRoot, "after.txt")).exists()).toBe(false)
|
|
213
|
-
expect(store.
|
|
253
|
+
expect(store.getProjectSnapshot("project-1").files).toHaveLength(0)
|
|
214
254
|
})
|
|
215
255
|
|
|
216
256
|
test("ignoreFile appends a .gitignore entry once", async () => {
|
|
@@ -223,9 +263,9 @@ describe("DiffStore", () => {
|
|
|
223
263
|
|
|
224
264
|
const store = new DiffStore(repoRoot)
|
|
225
265
|
await store.initialize()
|
|
226
|
-
await store.refreshSnapshot("
|
|
266
|
+
await store.refreshSnapshot("project-1", repoRoot)
|
|
227
267
|
await store.ignoreFile({
|
|
228
|
-
|
|
268
|
+
projectId: "project-1",
|
|
229
269
|
projectPath: repoRoot,
|
|
230
270
|
path: "scratch.log",
|
|
231
271
|
})
|
|
@@ -256,9 +296,9 @@ describe("DiffStore", () => {
|
|
|
256
296
|
|
|
257
297
|
const store = new DiffStore(repoRoot)
|
|
258
298
|
await store.initialize()
|
|
259
|
-
await store.refreshSnapshot("
|
|
299
|
+
await store.refreshSnapshot("project-1", repoRoot)
|
|
260
300
|
|
|
261
|
-
const snapshot = store.
|
|
301
|
+
const snapshot = store.getProjectSnapshot("project-1")
|
|
262
302
|
expect(snapshot.branchHistory?.entries).toHaveLength(1)
|
|
263
303
|
expect(snapshot.branchHistory?.entries[0]).toMatchObject({
|
|
264
304
|
summary: "Initial commit",
|
|
@@ -278,10 +318,10 @@ describe("DiffStore", () => {
|
|
|
278
318
|
|
|
279
319
|
const store = new DiffStore(repoRoot)
|
|
280
320
|
await store.initialize()
|
|
281
|
-
await store.refreshSnapshot("
|
|
321
|
+
await store.refreshSnapshot("project-1", repoRoot)
|
|
282
322
|
|
|
283
323
|
await expect(store.ignoreFile({
|
|
284
|
-
|
|
324
|
+
projectId: "project-1",
|
|
285
325
|
projectPath: repoRoot,
|
|
286
326
|
path: "app.txt",
|
|
287
327
|
})).rejects.toThrow("Only untracked files can be ignored from the diff viewer")
|
|
@@ -414,7 +454,7 @@ describe("DiffStore", () => {
|
|
|
414
454
|
const store = new DiffStore(repoRoot)
|
|
415
455
|
await store.initialize()
|
|
416
456
|
const result = await store.checkoutBranch({
|
|
417
|
-
|
|
457
|
+
projectId: "project-1",
|
|
418
458
|
projectPath: repoRoot,
|
|
419
459
|
branch: { kind: "remote", name: "feature/remote", remoteRef: "origin/feature/remote" },
|
|
420
460
|
})
|
|
@@ -436,7 +476,7 @@ describe("DiffStore", () => {
|
|
|
436
476
|
const store = new DiffStore(repoRoot)
|
|
437
477
|
await store.initialize()
|
|
438
478
|
const result = await store.checkoutBranch({
|
|
439
|
-
|
|
479
|
+
projectId: "project-1",
|
|
440
480
|
projectPath: repoRoot,
|
|
441
481
|
branch: { kind: "local", name: "feature/other" },
|
|
442
482
|
bringChanges: false,
|
|
@@ -459,7 +499,7 @@ describe("DiffStore", () => {
|
|
|
459
499
|
const store = new DiffStore(repoRoot)
|
|
460
500
|
await store.initialize()
|
|
461
501
|
const result = await store.createBranch({
|
|
462
|
-
|
|
502
|
+
projectId: "project-1",
|
|
463
503
|
projectPath: repoRoot,
|
|
464
504
|
name: "feature/new",
|
|
465
505
|
baseBranchName: "feature/base",
|
|
@@ -468,4 +508,106 @@ describe("DiffStore", () => {
|
|
|
468
508
|
expect(result.ok).toBe(true)
|
|
469
509
|
expect((await run(["git", "branch", "--show-current"], repoRoot)).trim()).toBe("feature/new")
|
|
470
510
|
})
|
|
511
|
+
|
|
512
|
+
test("previewMergeBranch reports up-to-date and mergeable states", async () => {
|
|
513
|
+
const repoRoot = await createRepo()
|
|
514
|
+
tempDirs.push(repoRoot)
|
|
515
|
+
await writeFile(path.join(repoRoot, "app.txt"), "base\n", "utf8")
|
|
516
|
+
await run(["git", "add", "."], repoRoot)
|
|
517
|
+
await run(["git", "commit", "-m", "init"], repoRoot)
|
|
518
|
+
await run(["git", "switch", "-c", "feature/preview"], repoRoot)
|
|
519
|
+
|
|
520
|
+
const store = new DiffStore(repoRoot)
|
|
521
|
+
await store.initialize()
|
|
522
|
+
|
|
523
|
+
const upToDatePreview = await store.previewMergeBranch({
|
|
524
|
+
projectPath: repoRoot,
|
|
525
|
+
branch: { kind: "local", name: "main" },
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
expect(upToDatePreview.status).toBe("up_to_date")
|
|
529
|
+
expect(upToDatePreview.commitCount).toBe(0)
|
|
530
|
+
|
|
531
|
+
await writeFile(path.join(repoRoot, "app.txt"), "feature\n", "utf8")
|
|
532
|
+
await run(["git", "commit", "-am", "feature"], repoRoot)
|
|
533
|
+
await run(["git", "switch", "main"], repoRoot)
|
|
534
|
+
|
|
535
|
+
const mergeablePreview = await store.previewMergeBranch({
|
|
536
|
+
projectPath: repoRoot,
|
|
537
|
+
branch: { kind: "local", name: "feature/preview" },
|
|
538
|
+
})
|
|
539
|
+
|
|
540
|
+
expect(mergeablePreview.status).toBe("mergeable")
|
|
541
|
+
expect(mergeablePreview.commitCount).toBe(1)
|
|
542
|
+
expect(mergeablePreview.hasConflicts).toBe(false)
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
test("previewMergeBranch detects likely conflicts", async () => {
|
|
546
|
+
const repoRoot = await createRepo()
|
|
547
|
+
tempDirs.push(repoRoot)
|
|
548
|
+
await writeFile(path.join(repoRoot, "conflict.txt"), "base\n", "utf8")
|
|
549
|
+
await run(["git", "add", "."], repoRoot)
|
|
550
|
+
await run(["git", "commit", "-m", "init"], repoRoot)
|
|
551
|
+
await run(["git", "switch", "-c", "feature/conflict"], repoRoot)
|
|
552
|
+
await writeFile(path.join(repoRoot, "conflict.txt"), "feature\n", "utf8")
|
|
553
|
+
await run(["git", "commit", "-am", "feature"], repoRoot)
|
|
554
|
+
await run(["git", "switch", "main"], repoRoot)
|
|
555
|
+
await writeFile(path.join(repoRoot, "conflict.txt"), "main\n", "utf8")
|
|
556
|
+
await run(["git", "commit", "-am", "main"], repoRoot)
|
|
557
|
+
|
|
558
|
+
const store = new DiffStore(repoRoot)
|
|
559
|
+
await store.initialize()
|
|
560
|
+
|
|
561
|
+
const preview = await store.previewMergeBranch({
|
|
562
|
+
projectPath: repoRoot,
|
|
563
|
+
branch: { kind: "local", name: "feature/conflict" },
|
|
564
|
+
})
|
|
565
|
+
|
|
566
|
+
expect(preview.status).toBe("conflicts")
|
|
567
|
+
expect(preview.hasConflicts).toBe(true)
|
|
568
|
+
expect(preview.commitCount).toBe(1)
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
test("mergeBranch blocks dirty worktrees and merges clean branches", async () => {
|
|
572
|
+
const repoRoot = await createRepo()
|
|
573
|
+
tempDirs.push(repoRoot)
|
|
574
|
+
await writeFile(path.join(repoRoot, "app.txt"), "base\n", "utf8")
|
|
575
|
+
await run(["git", "add", "."], repoRoot)
|
|
576
|
+
await run(["git", "commit", "-m", "init"], repoRoot)
|
|
577
|
+
await run(["git", "switch", "-c", "feature/merge"], repoRoot)
|
|
578
|
+
await writeFile(path.join(repoRoot, "app.txt"), "feature\n", "utf8")
|
|
579
|
+
await run(["git", "commit", "-am", "feature"], repoRoot)
|
|
580
|
+
await run(["git", "switch", "main"], repoRoot)
|
|
581
|
+
await writeFile(path.join(repoRoot, "scratch.txt"), "dirty\n", "utf8")
|
|
582
|
+
|
|
583
|
+
const store = new DiffStore(repoRoot)
|
|
584
|
+
await store.initialize()
|
|
585
|
+
|
|
586
|
+
const blockedResult = await store.mergeBranch({
|
|
587
|
+
projectId: "project-1",
|
|
588
|
+
projectPath: repoRoot,
|
|
589
|
+
branch: { kind: "local", name: "feature/merge" },
|
|
590
|
+
})
|
|
591
|
+
|
|
592
|
+
expect(blockedResult).toMatchObject({
|
|
593
|
+
ok: false,
|
|
594
|
+
title: "Merge blocked",
|
|
595
|
+
snapshotChanged: false,
|
|
596
|
+
})
|
|
597
|
+
|
|
598
|
+
await rm(path.join(repoRoot, "scratch.txt"))
|
|
599
|
+
|
|
600
|
+
const mergeResult = await store.mergeBranch({
|
|
601
|
+
projectId: "project-1",
|
|
602
|
+
projectPath: repoRoot,
|
|
603
|
+
branch: { kind: "local", name: "feature/merge" },
|
|
604
|
+
})
|
|
605
|
+
|
|
606
|
+
expect(mergeResult).toMatchObject({
|
|
607
|
+
ok: true,
|
|
608
|
+
snapshotChanged: true,
|
|
609
|
+
})
|
|
610
|
+
expect((await run(["git", "branch", "--show-current"], repoRoot)).trim()).toBe("main")
|
|
611
|
+
expect((await run(["git", "log", "--format=%s", "-1"], repoRoot)).trim()).toBe("feature")
|
|
612
|
+
})
|
|
471
613
|
})
|