kanna-code 0.24.0 → 0.25.1
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-Cr8ogroL.js +2554 -0
- package/dist/client/assets/index-Hy4LEj8j.css +32 -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/src/server/diff-store.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { createHash } from "node:crypto"
|
|
1
2
|
import { mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises"
|
|
2
3
|
import { tmpdir } from "node:os"
|
|
3
4
|
import path from "node:path"
|
|
4
5
|
import type {
|
|
6
|
+
BranchMetadata,
|
|
5
7
|
ChatBranchHistoryEntry,
|
|
6
8
|
ChatBranchHistorySnapshot,
|
|
7
9
|
ChatBranchListEntry,
|
|
@@ -10,22 +12,18 @@ import type {
|
|
|
10
12
|
ChatCreateBranchResult,
|
|
11
13
|
ChatDiffFile,
|
|
12
14
|
ChatDiffSnapshot,
|
|
15
|
+
ChatMergeBranchResult,
|
|
16
|
+
ChatMergePreviewResult,
|
|
13
17
|
ChatSyncResult,
|
|
14
18
|
DiffCommitMode,
|
|
15
19
|
DiffCommitResult,
|
|
20
|
+
UpstreamStatus,
|
|
16
21
|
} from "../shared/types"
|
|
17
22
|
import { generateCommitMessageDetailed } from "./generate-commit-message"
|
|
18
23
|
import { inferProjectFileContentType } from "./uploads"
|
|
19
24
|
|
|
20
|
-
interface StoredChatDiffState {
|
|
25
|
+
interface StoredChatDiffState extends BranchMetadata, UpstreamStatus {
|
|
21
26
|
status: ChatDiffSnapshot["status"]
|
|
22
|
-
branchName?: string
|
|
23
|
-
defaultBranchName?: string
|
|
24
|
-
originRepoSlug?: string
|
|
25
|
-
hasUpstream?: boolean
|
|
26
|
-
aheadCount?: number
|
|
27
|
-
behindCount?: number
|
|
28
|
-
lastFetchedAt?: string
|
|
29
27
|
files: ChatDiffFile[]
|
|
30
28
|
branchHistory: ChatBranchHistorySnapshot
|
|
31
29
|
}
|
|
@@ -45,22 +43,23 @@ function createEmptyState(): StoredChatDiffState {
|
|
|
45
43
|
}
|
|
46
44
|
}
|
|
47
45
|
|
|
48
|
-
function
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
46
|
+
function branchMetadataEqual(left: BranchMetadata, right: BranchMetadata) {
|
|
47
|
+
return left.branchName === right.branchName
|
|
48
|
+
&& left.defaultBranchName === right.defaultBranchName
|
|
49
|
+
&& left.originRepoSlug === right.originRepoSlug
|
|
50
|
+
&& left.hasUpstream === right.hasUpstream
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function upstreamStatusEqual(left: UpstreamStatus, right: UpstreamStatus) {
|
|
54
|
+
return left.aheadCount === right.aheadCount
|
|
55
|
+
&& left.behindCount === right.behindCount
|
|
56
|
+
&& left.lastFetchedAt === right.lastFetchedAt
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function branchHistoryEqual(left: ChatBranchHistorySnapshot, right: ChatBranchHistorySnapshot) {
|
|
60
|
+
if (left.entries.length !== right.entries.length) return false
|
|
61
|
+
return left.entries.every((entry, index) => {
|
|
62
|
+
const other = right.entries[index]
|
|
64
63
|
return Boolean(other)
|
|
65
64
|
&& entry.sha === other.sha
|
|
66
65
|
&& entry.summary === other.summary
|
|
@@ -71,13 +70,28 @@ function snapshotsEqual(left: StoredChatDiffState | undefined, right: StoredChat
|
|
|
71
70
|
&& entry.tags.length === other.tags.length
|
|
72
71
|
&& entry.tags.every((tag, tagIndex) => tag === other.tags[tagIndex])
|
|
73
72
|
})
|
|
74
|
-
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function snapshotsEqual(left: StoredChatDiffState | undefined, right: StoredChatDiffState) {
|
|
76
|
+
if (!left) {
|
|
77
|
+
return right.status === "unknown" && right.files.length === 0
|
|
78
|
+
}
|
|
79
|
+
if (left.status !== right.status) return false
|
|
80
|
+
if (!branchMetadataEqual(left, right)) return false
|
|
81
|
+
if (!upstreamStatusEqual(left, right)) return false
|
|
82
|
+
if (left.files.length !== right.files.length) return false
|
|
83
|
+
if (!branchHistoryEqual(left.branchHistory, right.branchHistory)) return false
|
|
75
84
|
return left.files.every((file, index) => {
|
|
76
85
|
const other = right.files[index]
|
|
77
86
|
return Boolean(other)
|
|
78
87
|
&& file.path === other.path
|
|
79
88
|
&& file.changeType === other.changeType
|
|
80
|
-
&& file.
|
|
89
|
+
&& file.isUntracked === other.isUntracked
|
|
90
|
+
&& file.additions === other.additions
|
|
91
|
+
&& file.deletions === other.deletions
|
|
92
|
+
&& file.patchDigest === other.patchDigest
|
|
93
|
+
&& file.mimeType === other.mimeType
|
|
94
|
+
&& file.size === other.size
|
|
81
95
|
})
|
|
82
96
|
}
|
|
83
97
|
|
|
@@ -88,6 +102,19 @@ interface DirtyPathEntry {
|
|
|
88
102
|
isUntracked: boolean
|
|
89
103
|
}
|
|
90
104
|
|
|
105
|
+
type SelectedBranch =
|
|
106
|
+
| { kind: "local"; name: string }
|
|
107
|
+
| { kind: "remote"; name: string; remoteRef: string }
|
|
108
|
+
| {
|
|
109
|
+
kind: "pull_request"
|
|
110
|
+
name: string
|
|
111
|
+
prNumber: number
|
|
112
|
+
headRefName: string
|
|
113
|
+
headRepoCloneUrl?: string
|
|
114
|
+
isCrossRepository?: boolean
|
|
115
|
+
remoteRef?: string
|
|
116
|
+
}
|
|
117
|
+
|
|
91
118
|
async function fileExists(filePath: string) {
|
|
92
119
|
try {
|
|
93
120
|
await stat(filePath)
|
|
@@ -188,6 +215,35 @@ function createPushFailure(mode: DiffCommitMode, detail: string, snapshotChanged
|
|
|
188
215
|
}
|
|
189
216
|
}
|
|
190
217
|
|
|
218
|
+
function createSyncPushFailure(detail: string, snapshotChanged: boolean): ChatSyncResult {
|
|
219
|
+
const normalized = detail.toLowerCase()
|
|
220
|
+
let title = "Push failed"
|
|
221
|
+
let message = summarizeGitFailure(detail, "Git could not push this branch.")
|
|
222
|
+
|
|
223
|
+
if (normalized.includes("non-fast-forward") || normalized.includes("fetch first")) {
|
|
224
|
+
title = "Branch is not up to date"
|
|
225
|
+
message = "Your branch is behind its remote. Pull or rebase, then try pushing again."
|
|
226
|
+
} else if (normalized.includes("has no upstream branch") || normalized.includes("set-upstream")) {
|
|
227
|
+
title = "No upstream branch configured"
|
|
228
|
+
message = "This branch does not have an upstream remote branch configured yet."
|
|
229
|
+
} else if (normalized.includes("merge conflict") || normalized.includes("resolve conflicts")) {
|
|
230
|
+
title = "Merge conflicts need resolution"
|
|
231
|
+
message = "Git reported conflicts while preparing the push. Resolve them, then try again."
|
|
232
|
+
} else if (normalized.includes("permission denied") || normalized.includes("authentication failed") || normalized.includes("could not read from remote repository")) {
|
|
233
|
+
title = "Remote authentication failed"
|
|
234
|
+
message = "Git could not authenticate with the remote repository."
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
ok: false,
|
|
239
|
+
action: "push",
|
|
240
|
+
title,
|
|
241
|
+
message,
|
|
242
|
+
detail,
|
|
243
|
+
snapshotChanged,
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
191
247
|
async function resolveRepo(projectPath: string): Promise<{ repoRoot: string; baseCommit: string | null } | null> {
|
|
192
248
|
const topLevel = await runGit(["rev-parse", "--show-toplevel"], projectPath)
|
|
193
249
|
if (topLevel.exitCode !== 0) {
|
|
@@ -366,6 +422,89 @@ async function getRecentBranchNames(repoRoot: string) {
|
|
|
366
422
|
return recent
|
|
367
423
|
}
|
|
368
424
|
|
|
425
|
+
async function resolveSelectedBranchRef(repoRoot: string, branch: SelectedBranch) {
|
|
426
|
+
if (branch.kind === "local") {
|
|
427
|
+
const localBranchNames = await getLocalBranchNames(repoRoot)
|
|
428
|
+
if (!localBranchNames.includes(branch.name)) {
|
|
429
|
+
throw new Error(`Local branch not found: ${branch.name}`)
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
ref: branch.name,
|
|
433
|
+
displayName: branch.name,
|
|
434
|
+
branchName: branch.name,
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (branch.kind === "remote") {
|
|
439
|
+
const remoteRef = branch.remoteRef.trim()
|
|
440
|
+
const remoteBranchNames = await getRemoteBranchNames(repoRoot)
|
|
441
|
+
if (!remoteBranchNames.includes(remoteRef)) {
|
|
442
|
+
throw new Error(`Remote branch not found: ${remoteRef}`)
|
|
443
|
+
}
|
|
444
|
+
return {
|
|
445
|
+
ref: remoteRef,
|
|
446
|
+
displayName: remoteRef,
|
|
447
|
+
branchName: branch.name,
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
const localBranchNames = await getLocalBranchNames(repoRoot)
|
|
452
|
+
if (localBranchNames.includes(branch.name)) {
|
|
453
|
+
return {
|
|
454
|
+
ref: branch.name,
|
|
455
|
+
displayName: `PR #${branch.prNumber}`,
|
|
456
|
+
branchName: branch.name,
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const remoteRef = branch.remoteRef?.trim()
|
|
461
|
+
if (remoteRef) {
|
|
462
|
+
const remoteBranchNames = await getRemoteBranchNames(repoRoot)
|
|
463
|
+
if (remoteBranchNames.includes(remoteRef)) {
|
|
464
|
+
return {
|
|
465
|
+
ref: remoteRef,
|
|
466
|
+
displayName: `PR #${branch.prNumber}`,
|
|
467
|
+
branchName: branch.headRefName || branch.name,
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (branch.isCrossRepository) {
|
|
473
|
+
throw new Error("This pull request branch is not available locally yet. Check it out first before merging.")
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
throw new Error(`Pull request branch not found: ${branch.headRefName || branch.name}`)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
async function getMergeCommitCount(repoRoot: string, sourceRef: string) {
|
|
480
|
+
const result = await runGit(["rev-list", "--count", `HEAD..${sourceRef}`], repoRoot)
|
|
481
|
+
if (result.exitCode !== 0) {
|
|
482
|
+
throw new Error(result.stderr.trim() || "Failed to calculate merge commit count")
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const commitCount = Number.parseInt(result.stdout.trim(), 10)
|
|
486
|
+
return Number.isFinite(commitCount) ? commitCount : 0
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async function predictMergeConflicts(repoRoot: string, sourceRef: string) {
|
|
490
|
+
const result = await runGit(["merge-tree", "--write-tree", "--messages", "HEAD", sourceRef], repoRoot)
|
|
491
|
+
const output = `${result.stdout}\n${result.stderr}`.trim()
|
|
492
|
+
|
|
493
|
+
if (result.exitCode === 0) {
|
|
494
|
+
return { hasConflicts: false }
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const normalizedOutput = output.toLowerCase()
|
|
498
|
+
if (result.exitCode === 1 || normalizedOutput.includes("conflict")) {
|
|
499
|
+
return {
|
|
500
|
+
hasConflicts: true,
|
|
501
|
+
detail: output || "Git reported merge conflicts for this branch pair.",
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
throw new Error(output || "Failed to analyze merge conflicts")
|
|
506
|
+
}
|
|
507
|
+
|
|
369
508
|
export function extractGitHubRepoSlug(remoteUrl: string | null | undefined) {
|
|
370
509
|
if (!remoteUrl) return null
|
|
371
510
|
|
|
@@ -464,16 +603,40 @@ function buildGitHubCommitUrl(remoteUrl: string | null, sha: string) {
|
|
|
464
603
|
return slug ? `https://github.com/${slug}/commit/${sha}` : undefined
|
|
465
604
|
}
|
|
466
605
|
|
|
467
|
-
async function
|
|
468
|
-
const
|
|
469
|
-
if (
|
|
470
|
-
|
|
606
|
+
async function getTagsByCommit(repoRoot: string, shas: string[]): Promise<Map<string, string[]>> {
|
|
607
|
+
const tagMap = new Map<string, string[]>()
|
|
608
|
+
if (shas.length === 0) return tagMap
|
|
609
|
+
|
|
610
|
+
for (const sha of shas) {
|
|
611
|
+
tagMap.set(sha, [])
|
|
471
612
|
}
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
.
|
|
475
|
-
|
|
476
|
-
|
|
613
|
+
|
|
614
|
+
const result = await runGit(
|
|
615
|
+
["log", "--max-count", String(shas.length), "--decorate-refs=refs/tags", "--format=%H %D", shas[0]!],
|
|
616
|
+
repoRoot
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
if (result.exitCode !== 0) return tagMap
|
|
620
|
+
|
|
621
|
+
for (const line of result.stdout.split(/\r?\n/u)) {
|
|
622
|
+
const trimmed = line.trim()
|
|
623
|
+
if (!trimmed) continue
|
|
624
|
+
const spaceIndex = trimmed.indexOf(" ")
|
|
625
|
+
if (spaceIndex < 0) continue
|
|
626
|
+
const sha = trimmed.slice(0, spaceIndex)
|
|
627
|
+
const decorations = trimmed.slice(spaceIndex + 1)
|
|
628
|
+
if (!tagMap.has(sha) || !decorations) continue
|
|
629
|
+
const tags = decorations
|
|
630
|
+
.split(",")
|
|
631
|
+
.map((decoration) => decoration.trim())
|
|
632
|
+
.filter((decoration) => decoration.startsWith("tag: "))
|
|
633
|
+
.map((decoration) => decoration.slice(5))
|
|
634
|
+
.filter(Boolean)
|
|
635
|
+
.sort((left, right) => left.localeCompare(right))
|
|
636
|
+
tagMap.set(sha, tags)
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
return tagMap
|
|
477
640
|
}
|
|
478
641
|
|
|
479
642
|
async function getBranchHistory(args: {
|
|
@@ -497,24 +660,30 @@ async function getBranchHistory(args: {
|
|
|
497
660
|
}
|
|
498
661
|
|
|
499
662
|
const remoteUrl = await getOriginRemoteUrl(args.repoRoot)
|
|
500
|
-
const
|
|
663
|
+
const parsedRecords: Array<{ sha: string; summary: string; description: string; authorName?: string; authoredAt: string }> = []
|
|
501
664
|
|
|
502
665
|
for (const record of logResult.stdout.split("\u001e")) {
|
|
503
666
|
const trimmed = record.trim()
|
|
504
667
|
if (!trimmed) continue
|
|
505
668
|
const [sha, summary, description, authorName, authoredAt] = trimmed.split("\u001f")
|
|
506
669
|
if (!sha || !summary || !authoredAt) continue
|
|
507
|
-
|
|
670
|
+
parsedRecords.push({
|
|
508
671
|
sha,
|
|
509
672
|
summary,
|
|
510
673
|
description: (description ?? "").trim(),
|
|
511
674
|
authorName: authorName?.trim() || undefined,
|
|
512
675
|
authoredAt,
|
|
513
|
-
tags: await listCommitTags(args.repoRoot, sha),
|
|
514
|
-
githubUrl: buildGitHubCommitUrl(remoteUrl, sha),
|
|
515
676
|
})
|
|
516
677
|
}
|
|
517
678
|
|
|
679
|
+
const tagMap = await getTagsByCommit(args.repoRoot, parsedRecords.map((record) => record.sha))
|
|
680
|
+
|
|
681
|
+
const entries: ChatBranchHistoryEntry[] = parsedRecords.map((record) => ({
|
|
682
|
+
...record,
|
|
683
|
+
tags: tagMap.get(record.sha) ?? [],
|
|
684
|
+
githubUrl: buildGitHubCommitUrl(remoteUrl, record.sha),
|
|
685
|
+
}))
|
|
686
|
+
|
|
518
687
|
return { entries }
|
|
519
688
|
}
|
|
520
689
|
|
|
@@ -527,6 +696,21 @@ function createBranchActionFailure(title: string, detail: string, fallback: stri
|
|
|
527
696
|
} as const
|
|
528
697
|
}
|
|
529
698
|
|
|
699
|
+
function createMergeActionFailure(args: {
|
|
700
|
+
title: string
|
|
701
|
+
detail: string
|
|
702
|
+
fallback: string
|
|
703
|
+
snapshotChanged: boolean
|
|
704
|
+
}) {
|
|
705
|
+
return {
|
|
706
|
+
ok: false,
|
|
707
|
+
title: args.title,
|
|
708
|
+
message: summarizeGitFailure(args.detail, args.fallback),
|
|
709
|
+
detail: args.detail,
|
|
710
|
+
snapshotChanged: args.snapshotChanged,
|
|
711
|
+
} as const
|
|
712
|
+
}
|
|
713
|
+
|
|
530
714
|
function parseStatusPaths(output: string): DirtyPathEntry[] {
|
|
531
715
|
const entries: DirtyPathEntry[] = []
|
|
532
716
|
for (const rawLine of output.split(/\r?\n/u)) {
|
|
@@ -637,8 +821,83 @@ async function createPatch(beforePathLabel: string, afterPathLabel: string, befo
|
|
|
637
821
|
}
|
|
638
822
|
}
|
|
639
823
|
|
|
824
|
+
function getContentDigest(args: {
|
|
825
|
+
changeType: ChatDiffFile["changeType"]
|
|
826
|
+
beforePath: string
|
|
827
|
+
afterPath: string
|
|
828
|
+
beforeText: string | null
|
|
829
|
+
afterText: string | null
|
|
830
|
+
}) {
|
|
831
|
+
return createHash("sha1")
|
|
832
|
+
.update(args.changeType)
|
|
833
|
+
.update("\u0000")
|
|
834
|
+
.update(args.beforePath)
|
|
835
|
+
.update("\u0000")
|
|
836
|
+
.update(args.afterPath)
|
|
837
|
+
.update("\u0000")
|
|
838
|
+
.update(args.beforeText ?? "")
|
|
839
|
+
.update("\u0000")
|
|
840
|
+
.update(args.afterText ?? "")
|
|
841
|
+
.digest("hex")
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function parseNumstatValue(value: string) {
|
|
845
|
+
if (value === "-" || value.trim() === "") return 0
|
|
846
|
+
const parsed = Number.parseInt(value, 10)
|
|
847
|
+
return Number.isFinite(parsed) ? parsed : 0
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function countTextLines(text: string | null) {
|
|
851
|
+
if (!text) return 0
|
|
852
|
+
const lines = text.split(/\r?\n/u)
|
|
853
|
+
if (lines.at(-1) === "") {
|
|
854
|
+
lines.pop()
|
|
855
|
+
}
|
|
856
|
+
return lines.length
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
async function getTrackedDiffStats(repoRoot: string, baseCommit: string | null) {
|
|
860
|
+
const statsByPath = new Map<string, { additions: number; deletions: number }>()
|
|
861
|
+
if (!baseCommit) {
|
|
862
|
+
return statsByPath
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
const result = await runGit(["diff", "--numstat", "-z", "-M", baseCommit], repoRoot)
|
|
866
|
+
if (result.exitCode !== 0) {
|
|
867
|
+
throw new Error(result.stderr.trim() || "Failed to read git diff stats")
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
const tokens = result.stdout.split("\u0000")
|
|
871
|
+
for (let index = 0; index < tokens.length;) {
|
|
872
|
+
const header = tokens[index++] ?? ""
|
|
873
|
+
if (!header) continue
|
|
874
|
+
|
|
875
|
+
const [additionsValue, deletionsValue, pathValue = ""] = header.split("\t")
|
|
876
|
+
if (typeof additionsValue !== "string" || typeof deletionsValue !== "string") continue
|
|
877
|
+
|
|
878
|
+
if (pathValue) {
|
|
879
|
+
statsByPath.set(pathValue, {
|
|
880
|
+
additions: parseNumstatValue(additionsValue),
|
|
881
|
+
deletions: parseNumstatValue(deletionsValue),
|
|
882
|
+
})
|
|
883
|
+
continue
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
index += 1
|
|
887
|
+
const nextPath = tokens[index++] ?? ""
|
|
888
|
+
if (!nextPath) continue
|
|
889
|
+
statsByPath.set(nextPath, {
|
|
890
|
+
additions: parseNumstatValue(additionsValue),
|
|
891
|
+
deletions: parseNumstatValue(deletionsValue),
|
|
892
|
+
})
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
return statsByPath
|
|
896
|
+
}
|
|
897
|
+
|
|
640
898
|
async function computeCurrentFiles(repoRoot: string, baseCommit: string | null): Promise<ChatDiffFile[]> {
|
|
641
899
|
const currentDirtyPaths = await listDirtyPaths(repoRoot)
|
|
900
|
+
const trackedStatsByPath = await getTrackedDiffStats(repoRoot, baseCommit)
|
|
642
901
|
const files: ChatDiffFile[] = []
|
|
643
902
|
|
|
644
903
|
for (const entry of currentDirtyPaths) {
|
|
@@ -656,12 +915,22 @@ async function computeCurrentFiles(repoRoot: string, baseCommit: string | null):
|
|
|
656
915
|
continue
|
|
657
916
|
}
|
|
658
917
|
|
|
659
|
-
const
|
|
918
|
+
const trackedStats = trackedStatsByPath.get(relativePath)
|
|
919
|
+
const additions = trackedStats?.additions ?? countTextLines(afterText)
|
|
920
|
+
const deletions = trackedStats?.deletions ?? 0
|
|
660
921
|
files.push({
|
|
661
922
|
path: relativePath,
|
|
662
923
|
changeType: entry.changeType,
|
|
663
924
|
isUntracked: entry.isUntracked,
|
|
664
|
-
|
|
925
|
+
additions,
|
|
926
|
+
deletions,
|
|
927
|
+
patchDigest: getContentDigest({
|
|
928
|
+
changeType: entry.changeType,
|
|
929
|
+
beforePath,
|
|
930
|
+
afterPath: relativePath,
|
|
931
|
+
beforeText,
|
|
932
|
+
afterText,
|
|
933
|
+
}),
|
|
665
934
|
mimeType,
|
|
666
935
|
size,
|
|
667
936
|
})
|
|
@@ -743,8 +1012,31 @@ export class DiffStore {
|
|
|
743
1012
|
|
|
744
1013
|
async initialize() {}
|
|
745
1014
|
|
|
746
|
-
|
|
747
|
-
|
|
1015
|
+
async readPatch(args: {
|
|
1016
|
+
projectPath: string
|
|
1017
|
+
path: string
|
|
1018
|
+
}) {
|
|
1019
|
+
const relativePath = normalizeRepoRelativePath(args.path)
|
|
1020
|
+
const repo = await resolveRepo(args.projectPath)
|
|
1021
|
+
if (!repo) {
|
|
1022
|
+
throw new Error("Project is not in a git repository")
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
const entry = await findDirtyPath(repo.repoRoot, relativePath)
|
|
1026
|
+
if (!entry) {
|
|
1027
|
+
throw new Error(`File is no longer changed: ${relativePath}`)
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
const beforePath = entry.previousPath ?? relativePath
|
|
1031
|
+
const beforeText = await readBaseFile(repo.repoRoot, repo.baseCommit, beforePath)
|
|
1032
|
+
const afterText = await readWorktreeFile(repo.repoRoot, relativePath)
|
|
1033
|
+
const patch = await createPatch(beforePath, relativePath, beforeText, afterText)
|
|
1034
|
+
|
|
1035
|
+
return { patch }
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
getProjectSnapshot(projectId: string): ChatDiffSnapshot {
|
|
1039
|
+
const state = this.states.get(projectId) ?? createEmptyState()
|
|
748
1040
|
return {
|
|
749
1041
|
status: state.status,
|
|
750
1042
|
branchName: state.branchName,
|
|
@@ -764,7 +1056,7 @@ export class DiffStore {
|
|
|
764
1056
|
}
|
|
765
1057
|
}
|
|
766
1058
|
|
|
767
|
-
async refreshSnapshot(
|
|
1059
|
+
async refreshSnapshot(projectId: string, projectPath: string) {
|
|
768
1060
|
const repo = await resolveRepo(projectPath)
|
|
769
1061
|
if (!repo) {
|
|
770
1062
|
const nextState = {
|
|
@@ -779,8 +1071,8 @@ export class DiffStore {
|
|
|
779
1071
|
files: [],
|
|
780
1072
|
branchHistory: { entries: [] },
|
|
781
1073
|
} satisfies StoredChatDiffState
|
|
782
|
-
const changed = !snapshotsEqual(this.states.get(
|
|
783
|
-
this.states.set(
|
|
1074
|
+
const changed = !snapshotsEqual(this.states.get(projectId), nextState)
|
|
1075
|
+
this.states.set(projectId, nextState)
|
|
784
1076
|
return changed
|
|
785
1077
|
}
|
|
786
1078
|
|
|
@@ -813,8 +1105,8 @@ export class DiffStore {
|
|
|
813
1105
|
files,
|
|
814
1106
|
branchHistory,
|
|
815
1107
|
} satisfies StoredChatDiffState
|
|
816
|
-
const changed = !snapshotsEqual(this.states.get(
|
|
817
|
-
this.states.set(
|
|
1108
|
+
const changed = !snapshotsEqual(this.states.get(projectId), nextState)
|
|
1109
|
+
this.states.set(projectId, nextState)
|
|
818
1110
|
return changed
|
|
819
1111
|
}
|
|
820
1112
|
|
|
@@ -962,21 +1254,145 @@ export class DiffStore {
|
|
|
962
1254
|
}
|
|
963
1255
|
}
|
|
964
1256
|
|
|
965
|
-
async
|
|
966
|
-
chatId: string
|
|
1257
|
+
async previewMergeBranch(args: {
|
|
967
1258
|
projectPath: string
|
|
968
|
-
branch:
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1259
|
+
branch: SelectedBranch
|
|
1260
|
+
}): Promise<ChatMergePreviewResult> {
|
|
1261
|
+
const repo = await resolveRepo(args.projectPath)
|
|
1262
|
+
if (!repo) {
|
|
1263
|
+
throw new Error("Project is not in a git repository")
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
const currentBranchName = await getBranchName(repo.repoRoot)
|
|
1267
|
+
const resolvedBranch = await resolveSelectedBranchRef(repo.repoRoot, args.branch)
|
|
1268
|
+
|
|
1269
|
+
if (currentBranchName && resolvedBranch.branchName === currentBranchName) {
|
|
1270
|
+
return {
|
|
1271
|
+
currentBranchName,
|
|
1272
|
+
targetBranchName: resolvedBranch.branchName,
|
|
1273
|
+
targetDisplayName: resolvedBranch.displayName,
|
|
1274
|
+
status: "up_to_date",
|
|
1275
|
+
commitCount: 0,
|
|
1276
|
+
hasConflicts: false,
|
|
1277
|
+
message: `${currentBranchName} is already up to date with ${resolvedBranch.displayName}.`,
|
|
979
1278
|
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
try {
|
|
1282
|
+
const commitCount = await getMergeCommitCount(repo.repoRoot, resolvedBranch.ref)
|
|
1283
|
+
if (commitCount === 0) {
|
|
1284
|
+
return {
|
|
1285
|
+
currentBranchName,
|
|
1286
|
+
targetBranchName: resolvedBranch.branchName,
|
|
1287
|
+
targetDisplayName: resolvedBranch.displayName,
|
|
1288
|
+
status: "up_to_date",
|
|
1289
|
+
commitCount,
|
|
1290
|
+
hasConflicts: false,
|
|
1291
|
+
message: `${currentBranchName ?? "Current branch"} is already up to date with ${resolvedBranch.displayName}.`,
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
const conflictPrediction = await predictMergeConflicts(repo.repoRoot, resolvedBranch.ref)
|
|
1296
|
+
if (conflictPrediction.hasConflicts) {
|
|
1297
|
+
return {
|
|
1298
|
+
currentBranchName,
|
|
1299
|
+
targetBranchName: resolvedBranch.branchName,
|
|
1300
|
+
targetDisplayName: resolvedBranch.displayName,
|
|
1301
|
+
status: "conflicts",
|
|
1302
|
+
commitCount,
|
|
1303
|
+
hasConflicts: true,
|
|
1304
|
+
message: `${commitCount} ${commitCount === 1 ? "commit" : "commits"} from ${resolvedBranch.displayName} would merge into ${currentBranchName ?? "the current branch"}, but conflicts are expected.`,
|
|
1305
|
+
detail: conflictPrediction.detail,
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
return {
|
|
1310
|
+
currentBranchName,
|
|
1311
|
+
targetBranchName: resolvedBranch.branchName,
|
|
1312
|
+
targetDisplayName: resolvedBranch.displayName,
|
|
1313
|
+
status: "mergeable",
|
|
1314
|
+
commitCount,
|
|
1315
|
+
hasConflicts: false,
|
|
1316
|
+
message: `${commitCount} ${commitCount === 1 ? "commit" : "commits"} from ${resolvedBranch.displayName} will merge into ${currentBranchName ?? "the current branch"}.`,
|
|
1317
|
+
}
|
|
1318
|
+
} catch (error) {
|
|
1319
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
1320
|
+
return {
|
|
1321
|
+
currentBranchName,
|
|
1322
|
+
targetBranchName: resolvedBranch.branchName,
|
|
1323
|
+
targetDisplayName: resolvedBranch.displayName,
|
|
1324
|
+
status: "error",
|
|
1325
|
+
commitCount: 0,
|
|
1326
|
+
hasConflicts: false,
|
|
1327
|
+
message: "Could not preview this merge.",
|
|
1328
|
+
detail: message,
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
async mergeBranch(args: {
|
|
1334
|
+
projectId: string
|
|
1335
|
+
projectPath: string
|
|
1336
|
+
branch: SelectedBranch
|
|
1337
|
+
}): Promise<ChatMergeBranchResult> {
|
|
1338
|
+
const repo = await resolveRepo(args.projectPath)
|
|
1339
|
+
if (!repo) {
|
|
1340
|
+
throw new Error("Project is not in a git repository")
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
const currentDirtyPaths = await listDirtyPaths(repo.repoRoot)
|
|
1344
|
+
if (currentDirtyPaths.length > 0) {
|
|
1345
|
+
return {
|
|
1346
|
+
ok: false,
|
|
1347
|
+
title: "Merge blocked",
|
|
1348
|
+
message: "Commit, discard, or stash your local changes before merging.",
|
|
1349
|
+
snapshotChanged: false,
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
const resolvedBranch = await resolveSelectedBranchRef(repo.repoRoot, args.branch)
|
|
1354
|
+
const commitCount = await getMergeCommitCount(repo.repoRoot, resolvedBranch.ref)
|
|
1355
|
+
if (commitCount === 0) {
|
|
1356
|
+
return {
|
|
1357
|
+
ok: false,
|
|
1358
|
+
title: "Already up to date",
|
|
1359
|
+
message: `${resolvedBranch.displayName} is already merged into ${await getBranchName(repo.repoRoot) ?? "the current branch"}.`,
|
|
1360
|
+
snapshotChanged: false,
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
const mergeResult = await runGit(["merge", "--no-edit", resolvedBranch.ref], repo.repoRoot)
|
|
1365
|
+
const detail = formatGitFailure(mergeResult)
|
|
1366
|
+
|
|
1367
|
+
if (mergeResult.exitCode !== 0) {
|
|
1368
|
+
const snapshotChanged = await this.refreshSnapshot(args.projectId, args.projectPath)
|
|
1369
|
+
const normalized = detail.toLowerCase()
|
|
1370
|
+
const title = normalized.includes("conflict")
|
|
1371
|
+
? "Merge conflicts need resolution"
|
|
1372
|
+
: "Merge failed"
|
|
1373
|
+
const fallback = normalized.includes("conflict")
|
|
1374
|
+
? "Git reported merge conflicts while merging this branch."
|
|
1375
|
+
: "Git could not merge this branch."
|
|
1376
|
+
return createMergeActionFailure({
|
|
1377
|
+
title,
|
|
1378
|
+
detail,
|
|
1379
|
+
fallback,
|
|
1380
|
+
snapshotChanged,
|
|
1381
|
+
})
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
const snapshotChanged = await this.refreshSnapshot(args.projectId, args.projectPath)
|
|
1385
|
+
return {
|
|
1386
|
+
ok: true,
|
|
1387
|
+
branchName: await getBranchName(repo.repoRoot),
|
|
1388
|
+
snapshotChanged,
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
async checkoutBranch(args: {
|
|
1393
|
+
projectId: string
|
|
1394
|
+
projectPath: string
|
|
1395
|
+
branch: SelectedBranch
|
|
980
1396
|
bringChanges?: boolean
|
|
981
1397
|
}): Promise<ChatCheckoutBranchResult> {
|
|
982
1398
|
const repo = await resolveRepo(args.projectPath)
|
|
@@ -1049,7 +1465,7 @@ export class DiffStore {
|
|
|
1049
1465
|
return createBranchActionFailure("Checkout failed", formatGitFailure(switchResult), "Git could not switch branches.")
|
|
1050
1466
|
}
|
|
1051
1467
|
|
|
1052
|
-
const snapshotChanged = await this.refreshSnapshot(args.
|
|
1468
|
+
const snapshotChanged = await this.refreshSnapshot(args.projectId, args.projectPath)
|
|
1053
1469
|
return {
|
|
1054
1470
|
ok: true,
|
|
1055
1471
|
branchName: await getBranchName(repo.repoRoot),
|
|
@@ -1058,7 +1474,7 @@ export class DiffStore {
|
|
|
1058
1474
|
}
|
|
1059
1475
|
|
|
1060
1476
|
async createBranch(args: {
|
|
1061
|
-
|
|
1477
|
+
projectId: string
|
|
1062
1478
|
projectPath: string
|
|
1063
1479
|
name: string
|
|
1064
1480
|
baseBranchName?: string
|
|
@@ -1098,7 +1514,7 @@ export class DiffStore {
|
|
|
1098
1514
|
return createBranchActionFailure("Create branch failed", formatGitFailure(switchResult), "Git could not create the branch.")
|
|
1099
1515
|
}
|
|
1100
1516
|
|
|
1101
|
-
const snapshotChanged = await this.refreshSnapshot(args.
|
|
1517
|
+
const snapshotChanged = await this.refreshSnapshot(args.projectId, args.projectPath)
|
|
1102
1518
|
return {
|
|
1103
1519
|
ok: true,
|
|
1104
1520
|
branchName,
|
|
@@ -1107,9 +1523,9 @@ export class DiffStore {
|
|
|
1107
1523
|
}
|
|
1108
1524
|
|
|
1109
1525
|
async syncBranch(args: {
|
|
1110
|
-
|
|
1526
|
+
projectId: string
|
|
1111
1527
|
projectPath: string
|
|
1112
|
-
action: "fetch" | "pull" | "publish"
|
|
1528
|
+
action: "fetch" | "pull" | "push" | "publish"
|
|
1113
1529
|
}): Promise<ChatSyncResult> {
|
|
1114
1530
|
const repo = await resolveRepo(args.projectPath)
|
|
1115
1531
|
if (!repo) {
|
|
@@ -1140,7 +1556,41 @@ export class DiffStore {
|
|
|
1140
1556
|
}
|
|
1141
1557
|
}
|
|
1142
1558
|
|
|
1143
|
-
const snapshotChanged = await this.refreshSnapshot(args.
|
|
1559
|
+
const snapshotChanged = await this.refreshSnapshot(args.projectId, args.projectPath)
|
|
1560
|
+
const branchName = await getBranchName(repo.repoRoot)
|
|
1561
|
+
const nextHasUpstream = await hasUpstreamBranch(repo.repoRoot)
|
|
1562
|
+
const { aheadCount, behindCount } = nextHasUpstream
|
|
1563
|
+
? await getUpstreamStatusCounts(repo.repoRoot)
|
|
1564
|
+
: { aheadCount: undefined, behindCount: undefined }
|
|
1565
|
+
|
|
1566
|
+
return {
|
|
1567
|
+
ok: true,
|
|
1568
|
+
action: args.action,
|
|
1569
|
+
branchName,
|
|
1570
|
+
aheadCount,
|
|
1571
|
+
behindCount,
|
|
1572
|
+
snapshotChanged,
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
if (args.action === "push") {
|
|
1577
|
+
if (!hasUpstream) {
|
|
1578
|
+
return {
|
|
1579
|
+
ok: false,
|
|
1580
|
+
action: args.action,
|
|
1581
|
+
title: "Push failed",
|
|
1582
|
+
message: "This branch does not have an upstream remote branch configured yet.",
|
|
1583
|
+
snapshotChanged: false,
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
const pushResult = await runGit(["push"], repo.repoRoot)
|
|
1588
|
+
if (pushResult.exitCode !== 0) {
|
|
1589
|
+
const detail = formatGitFailure(pushResult)
|
|
1590
|
+
return createSyncPushFailure(detail, false)
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
const snapshotChanged = await this.refreshSnapshot(args.projectId, args.projectPath)
|
|
1144
1594
|
const branchName = await getBranchName(repo.repoRoot)
|
|
1145
1595
|
const nextHasUpstream = await hasUpstreamBranch(repo.repoRoot)
|
|
1146
1596
|
const { aheadCount, behindCount } = nextHasUpstream
|
|
@@ -1195,7 +1645,7 @@ export class DiffStore {
|
|
|
1195
1645
|
}
|
|
1196
1646
|
}
|
|
1197
1647
|
|
|
1198
|
-
const snapshotChanged = await this.refreshSnapshot(args.
|
|
1648
|
+
const snapshotChanged = await this.refreshSnapshot(args.projectId, args.projectPath)
|
|
1199
1649
|
const branchName = await getBranchName(repo.repoRoot)
|
|
1200
1650
|
const nextHasUpstream = await hasUpstreamBranch(repo.repoRoot)
|
|
1201
1651
|
const { aheadCount, behindCount } = nextHasUpstream
|
|
@@ -1226,14 +1676,24 @@ export class DiffStore {
|
|
|
1226
1676
|
throw new Error("Project is not in a git repository")
|
|
1227
1677
|
}
|
|
1228
1678
|
|
|
1229
|
-
const
|
|
1230
|
-
const selectedFiles = normalizedPaths.map((selectedPath) => {
|
|
1231
|
-
const
|
|
1232
|
-
if (!
|
|
1679
|
+
const currentDirtyPaths = await listDirtyPaths(repo.repoRoot)
|
|
1680
|
+
const selectedFiles = await Promise.all(normalizedPaths.map(async (selectedPath) => {
|
|
1681
|
+
const entry = currentDirtyPaths.find((candidate) => candidate.path === selectedPath)
|
|
1682
|
+
if (!entry) {
|
|
1233
1683
|
throw new Error(`File is no longer changed: ${selectedPath}`)
|
|
1234
1684
|
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1685
|
+
|
|
1686
|
+
const beforePath = entry.previousPath ?? selectedPath
|
|
1687
|
+
const beforeText = await readBaseFile(repo.repoRoot, repo.baseCommit, beforePath)
|
|
1688
|
+
const afterText = await readWorktreeFile(repo.repoRoot, selectedPath)
|
|
1689
|
+
const patch = await createPatch(beforePath, selectedPath, beforeText, afterText)
|
|
1690
|
+
|
|
1691
|
+
return {
|
|
1692
|
+
path: selectedPath,
|
|
1693
|
+
changeType: entry.changeType,
|
|
1694
|
+
patch,
|
|
1695
|
+
}
|
|
1696
|
+
}))
|
|
1237
1697
|
|
|
1238
1698
|
const branchName = await getBranchName(repo.repoRoot)
|
|
1239
1699
|
return await generateCommitMessageDetailed({
|
|
@@ -1244,7 +1704,7 @@ export class DiffStore {
|
|
|
1244
1704
|
}
|
|
1245
1705
|
|
|
1246
1706
|
async commitFiles(args: {
|
|
1247
|
-
|
|
1707
|
+
projectId: string
|
|
1248
1708
|
projectPath: string
|
|
1249
1709
|
paths: string[]
|
|
1250
1710
|
summary: string
|
|
@@ -1266,6 +1726,7 @@ export class DiffStore {
|
|
|
1266
1726
|
if (!repo) {
|
|
1267
1727
|
throw new Error("Project is not in a git repository")
|
|
1268
1728
|
}
|
|
1729
|
+
const hasUpstream = await hasUpstreamBranch(repo.repoRoot)
|
|
1269
1730
|
|
|
1270
1731
|
const currentDirtyPaths = new Set((await listDirtyPaths(repo.repoRoot)).map((entry) => entry.path))
|
|
1271
1732
|
const missingPaths = normalizedPaths.filter((relativePath) => !currentDirtyPaths.has(relativePath))
|
|
@@ -1289,7 +1750,7 @@ export class DiffStore {
|
|
|
1289
1750
|
return createCommitFailure(args.mode, formatGitFailure(commitResult))
|
|
1290
1751
|
}
|
|
1291
1752
|
|
|
1292
|
-
const snapshotChanged = await this.refreshSnapshot(args.
|
|
1753
|
+
const snapshotChanged = await this.refreshSnapshot(args.projectId, args.projectPath)
|
|
1293
1754
|
const branchName = await getBranchName(repo.repoRoot)
|
|
1294
1755
|
|
|
1295
1756
|
if (args.mode === "commit_only") {
|
|
@@ -1302,12 +1763,14 @@ export class DiffStore {
|
|
|
1302
1763
|
} satisfies DiffCommitResult
|
|
1303
1764
|
}
|
|
1304
1765
|
|
|
1305
|
-
const pushResult =
|
|
1766
|
+
const pushResult = hasUpstream
|
|
1767
|
+
? await runGit(["push"], repo.repoRoot)
|
|
1768
|
+
: await runGit(["push", "-u", "origin", "HEAD"], repo.repoRoot)
|
|
1306
1769
|
if (pushResult.exitCode !== 0) {
|
|
1307
1770
|
return createPushFailure(args.mode, formatGitFailure(pushResult), snapshotChanged)
|
|
1308
1771
|
}
|
|
1309
1772
|
|
|
1310
|
-
const postPushSnapshotChanged = await this.refreshSnapshot(args.
|
|
1773
|
+
const postPushSnapshotChanged = await this.refreshSnapshot(args.projectId, args.projectPath)
|
|
1311
1774
|
|
|
1312
1775
|
return {
|
|
1313
1776
|
ok: true,
|
|
@@ -1319,7 +1782,7 @@ export class DiffStore {
|
|
|
1319
1782
|
}
|
|
1320
1783
|
|
|
1321
1784
|
async discardFile(args: {
|
|
1322
|
-
|
|
1785
|
+
projectId: string
|
|
1323
1786
|
projectPath: string
|
|
1324
1787
|
path: string
|
|
1325
1788
|
}) {
|
|
@@ -1355,12 +1818,12 @@ export class DiffStore {
|
|
|
1355
1818
|
}
|
|
1356
1819
|
|
|
1357
1820
|
return {
|
|
1358
|
-
snapshotChanged: await this.refreshSnapshot(args.
|
|
1821
|
+
snapshotChanged: await this.refreshSnapshot(args.projectId, args.projectPath),
|
|
1359
1822
|
}
|
|
1360
1823
|
}
|
|
1361
1824
|
|
|
1362
1825
|
async ignoreFile(args: {
|
|
1363
|
-
|
|
1826
|
+
projectId: string
|
|
1364
1827
|
projectPath: string
|
|
1365
1828
|
path: string
|
|
1366
1829
|
}) {
|
|
@@ -1386,7 +1849,7 @@ export class DiffStore {
|
|
|
1386
1849
|
}
|
|
1387
1850
|
|
|
1388
1851
|
return {
|
|
1389
|
-
snapshotChanged: await this.refreshSnapshot(args.
|
|
1852
|
+
snapshotChanged: await this.refreshSnapshot(args.projectId, args.projectPath),
|
|
1390
1853
|
}
|
|
1391
1854
|
}
|
|
1392
1855
|
}
|