@cerbur/clutch-dsh-worktree 0.1.10 → 0.1.13
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 +278 -498
- package/README.zh.md +231 -432
- package/assets/screenshots/screenshots-dashboard.png +0 -0
- package/assets/screenshots/screenshots-en.png +0 -0
- package/assets/screenshots/screenshots-git-reviewer.png +0 -0
- package/assets/screenshots/screenshots-import.png +0 -0
- package/assets/screenshots/screenshots-zh.png +0 -0
- package/lib/client/WorktreeSurface.d.ts +1 -1
- package/lib/client/WorktreeSurface.d.ts.map +1 -1
- package/lib/client/WorktreeSurface.js +268 -10
- package/lib/client/WorktreeSurface.js.map +1 -1
- package/lib/client/context/WorktreeHeroContext.d.ts +4 -3
- package/lib/client/context/WorktreeHeroContext.d.ts.map +1 -1
- package/lib/client/context/WorktreeHeroContext.js +29 -11
- package/lib/client/context/WorktreeHeroContext.js.map +1 -1
- package/lib/client/dashboard/DashboardHeaderAction.d.ts +20 -0
- package/lib/client/dashboard/DashboardHeaderAction.d.ts.map +1 -0
- package/lib/client/dashboard/DashboardHeaderAction.js +19 -0
- package/lib/client/dashboard/DashboardHeaderAction.js.map +1 -0
- package/lib/client/dashboard/OpenInAppButton.d.ts +9 -0
- package/lib/client/dashboard/OpenInAppButton.d.ts.map +1 -0
- package/lib/client/dashboard/OpenInAppButton.js +123 -0
- package/lib/client/dashboard/OpenInAppButton.js.map +1 -0
- package/lib/client/dashboard/WorktreeDashboard.d.ts +28 -0
- package/lib/client/dashboard/WorktreeDashboard.d.ts.map +1 -0
- package/lib/client/dashboard/WorktreeDashboard.js +362 -0
- package/lib/client/dashboard/WorktreeDashboard.js.map +1 -0
- package/lib/client/dashboard/WorktreeInstructions.d.ts +8 -0
- package/lib/client/dashboard/WorktreeInstructions.d.ts.map +1 -0
- package/lib/client/dashboard/WorktreeInstructions.js +53 -0
- package/lib/client/dashboard/WorktreeInstructions.js.map +1 -0
- package/lib/client/dashboard/dashboard-icon.d.ts +7 -0
- package/lib/client/dashboard/dashboard-icon.d.ts.map +1 -0
- package/lib/client/dashboard/dashboard-icon.js +6 -0
- package/lib/client/dashboard/dashboard-icon.js.map +1 -0
- package/lib/client/dashboard/dashboard-navigation.d.ts +35 -0
- package/lib/client/dashboard/dashboard-navigation.d.ts.map +1 -0
- package/lib/client/dashboard/dashboard-navigation.js +48 -0
- package/lib/client/dashboard/dashboard-navigation.js.map +1 -0
- package/lib/client/dashboard/dashboard-overlay.d.ts +16 -0
- package/lib/client/dashboard/dashboard-overlay.d.ts.map +1 -0
- package/lib/client/dashboard/dashboard-overlay.js +185 -0
- package/lib/client/dashboard/dashboard-overlay.js.map +1 -0
- package/lib/client/dashboard/dashboard-selection.d.ts +33 -0
- package/lib/client/dashboard/dashboard-selection.d.ts.map +1 -0
- package/lib/client/dashboard/dashboard-selection.js +31 -0
- package/lib/client/dashboard/dashboard-selection.js.map +1 -0
- package/lib/client/dashboard/dashboard-sessions.d.ts +6 -0
- package/lib/client/dashboard/dashboard-sessions.d.ts.map +1 -0
- package/lib/client/dashboard/dashboard-sessions.js +21 -0
- package/lib/client/dashboard/dashboard-sessions.js.map +1 -0
- package/lib/client/dashboard/git/GitChangedFiles.d.ts +11 -0
- package/lib/client/dashboard/git/GitChangedFiles.d.ts.map +1 -0
- package/lib/client/dashboard/git/GitChangedFiles.js +101 -0
- package/lib/client/dashboard/git/GitChangedFiles.js.map +1 -0
- package/lib/client/dashboard/git/GitCommitList.d.ts +15 -0
- package/lib/client/dashboard/git/GitCommitList.d.ts.map +1 -0
- package/lib/client/dashboard/git/GitCommitList.js +55 -0
- package/lib/client/dashboard/git/GitCommitList.js.map +1 -0
- package/lib/client/dashboard/git/GitDiffView.d.ts +10 -0
- package/lib/client/dashboard/git/GitDiffView.d.ts.map +1 -0
- package/lib/client/dashboard/git/GitDiffView.js +65 -0
- package/lib/client/dashboard/git/GitDiffView.js.map +1 -0
- package/lib/client/dashboard/git/GitFileTypeIcon.d.ts +8 -0
- package/lib/client/dashboard/git/GitFileTypeIcon.d.ts.map +1 -0
- package/lib/client/dashboard/git/GitFileTypeIcon.js +24 -0
- package/lib/client/dashboard/git/GitFileTypeIcon.js.map +1 -0
- package/lib/client/dashboard/git/GitLineStats.d.ts +10 -0
- package/lib/client/dashboard/git/GitLineStats.d.ts.map +1 -0
- package/lib/client/dashboard/git/GitLineStats.js +9 -0
- package/lib/client/dashboard/git/GitLineStats.js.map +1 -0
- package/lib/client/dashboard/git/WorktreeGitOverview.d.ts +31 -0
- package/lib/client/dashboard/git/WorktreeGitOverview.d.ts.map +1 -0
- package/lib/client/dashboard/git/WorktreeGitOverview.js +89 -0
- package/lib/client/dashboard/git/WorktreeGitOverview.js.map +1 -0
- package/lib/client/dashboard/git/WorktreeGitPanel.d.ts +16 -0
- package/lib/client/dashboard/git/WorktreeGitPanel.d.ts.map +1 -0
- package/lib/client/dashboard/git/WorktreeGitPanel.js +126 -0
- package/lib/client/dashboard/git/WorktreeGitPanel.js.map +1 -0
- package/lib/client/dashboard/git/file-address.d.ts +2 -0
- package/lib/client/dashboard/git/file-address.d.ts.map +1 -0
- package/lib/client/dashboard/git/file-address.js +16 -0
- package/lib/client/dashboard/git/file-address.js.map +1 -0
- package/lib/client/dashboard/git/git-column-split.d.ts +34 -0
- package/lib/client/dashboard/git/git-column-split.d.ts.map +1 -0
- package/lib/client/dashboard/git/git-column-split.js +41 -0
- package/lib/client/dashboard/git/git-column-split.js.map +1 -0
- package/lib/client/dashboard/git/git-diff-parser.d.ts +24 -0
- package/lib/client/dashboard/git/git-diff-parser.d.ts.map +1 -0
- package/lib/client/dashboard/git/git-diff-parser.js +56 -0
- package/lib/client/dashboard/git/git-diff-parser.js.map +1 -0
- package/lib/client/dashboard/git/git-facts.d.ts +18 -0
- package/lib/client/dashboard/git/git-facts.d.ts.map +1 -0
- package/lib/client/dashboard/git/git-facts.js +31 -0
- package/lib/client/dashboard/git/git-facts.js.map +1 -0
- package/lib/client/dashboard/git/git-file-tree.d.ts +16 -0
- package/lib/client/dashboard/git/git-file-tree.d.ts.map +1 -0
- package/lib/client/dashboard/git/git-file-tree.js +52 -0
- package/lib/client/dashboard/git/git-file-tree.js.map +1 -0
- package/lib/client/dashboard/git/git-state-cache.d.ts +49 -0
- package/lib/client/dashboard/git/git-state-cache.d.ts.map +1 -0
- package/lib/client/dashboard/git/git-state-cache.js +80 -0
- package/lib/client/dashboard/git/git-state-cache.js.map +1 -0
- package/lib/client/dashboard/git/useGitPaneSplit.d.ts +33 -0
- package/lib/client/dashboard/git/useGitPaneSplit.d.ts.map +1 -0
- package/lib/client/dashboard/git/useGitPaneSplit.js +119 -0
- package/lib/client/dashboard/git/useGitPaneSplit.js.map +1 -0
- package/lib/client/dashboard/git/useWorktreeGitState.d.ts +60 -0
- package/lib/client/dashboard/git/useWorktreeGitState.d.ts.map +1 -0
- package/lib/client/dashboard/git/useWorktreeGitState.js +654 -0
- package/lib/client/dashboard/git/useWorktreeGitState.js.map +1 -0
- package/lib/client/dashboard/open-in-app-controller.d.ts +30 -0
- package/lib/client/dashboard/open-in-app-controller.d.ts.map +1 -0
- package/lib/client/dashboard/open-in-app-controller.js +94 -0
- package/lib/client/dashboard/open-in-app-controller.js.map +1 -0
- package/lib/client/dashboard/vscode-url.d.ts +3 -0
- package/lib/client/dashboard/vscode-url.d.ts.map +1 -0
- package/lib/client/dashboard/vscode-url.js +9 -0
- package/lib/client/dashboard/vscode-url.js.map +1 -0
- package/lib/client/dashboard/worktree-acquisition-facts.d.ts +16 -0
- package/lib/client/dashboard/worktree-acquisition-facts.d.ts.map +1 -0
- package/lib/client/dashboard/worktree-acquisition-facts.js +14 -0
- package/lib/client/dashboard/worktree-acquisition-facts.js.map +1 -0
- package/lib/client/dsh-slot-contract.d.ts +5 -0
- package/lib/client/dsh-slot-contract.d.ts.map +1 -1
- package/lib/client/entry.d.ts.map +1 -1
- package/lib/client/entry.js +93 -0
- package/lib/client/entry.js.map +1 -1
- package/lib/client/locales.d.ts +296 -0
- package/lib/client/locales.d.ts.map +1 -1
- package/lib/client/locales.js +296 -0
- package/lib/client/locales.js.map +1 -1
- package/lib/client/session/session-labels.d.ts +9 -0
- package/lib/client/session/session-labels.d.ts.map +1 -0
- package/lib/client/session/session-labels.js +44 -0
- package/lib/client/session/session-labels.js.map +1 -0
- package/lib/client/session/session-view.d.ts +10 -0
- package/lib/client/session/session-view.d.ts.map +1 -1
- package/lib/client/session/session-view.js +57 -0
- package/lib/client/session/session-view.js.map +1 -1
- package/lib/client/session/worktree-session-order.d.ts +2 -5
- package/lib/client/session/worktree-session-order.d.ts.map +1 -1
- package/lib/client/session/worktree-session-order.js +35 -10
- package/lib/client/session/worktree-session-order.js.map +1 -1
- package/lib/client/session/worktree-session-position.d.ts +2 -1
- package/lib/client/session/worktree-session-position.d.ts.map +1 -1
- package/lib/client/session/worktree-session-position.js +10 -3
- package/lib/client/session/worktree-session-position.js.map +1 -1
- package/lib/client/surface/actions/useDragActions.d.ts +33 -2
- package/lib/client/surface/actions/useDragActions.d.ts.map +1 -1
- package/lib/client/surface/actions/useDragActions.js +66 -18
- package/lib/client/surface/actions/useDragActions.js.map +1 -1
- package/lib/client/surface/components/ActiveWorktree.d.ts +1 -1
- package/lib/client/surface/components/ActiveWorktree.d.ts.map +1 -1
- package/lib/client/surface/components/ActiveWorktree.js +4 -3
- package/lib/client/surface/components/ActiveWorktree.js.map +1 -1
- package/lib/client/surface/components/ArchivedWorktree.d.ts +1 -1
- package/lib/client/surface/components/ArchivedWorktree.d.ts.map +1 -1
- package/lib/client/surface/components/ArchivedWorktree.js +4 -3
- package/lib/client/surface/components/ArchivedWorktree.js.map +1 -1
- package/lib/client/surface/components/ArchivedWorktrees.d.ts +1 -1
- package/lib/client/surface/components/ArchivedWorktrees.d.ts.map +1 -1
- package/lib/client/surface/components/ArchivedWorktrees.js +2 -2
- package/lib/client/surface/components/ArchivedWorktrees.js.map +1 -1
- package/lib/client/surface/components/SurfaceContent.d.ts +2 -2
- package/lib/client/surface/components/SurfaceContent.d.ts.map +1 -1
- package/lib/client/surface/components/SurfaceContent.js +1 -1
- package/lib/client/surface/components/SurfaceContent.js.map +1 -1
- package/lib/client/surface/components/SurfaceHeader.d.ts +1 -1
- package/lib/client/surface/components/SurfaceHeader.d.ts.map +1 -1
- package/lib/client/surface/components/SurfaceHeader.js +12 -3
- package/lib/client/surface/components/SurfaceHeader.js.map +1 -1
- package/lib/client/surface/components/WorkspaceTree.d.ts +1 -1
- package/lib/client/surface/components/WorkspaceTree.d.ts.map +1 -1
- package/lib/client/surface/components/WorkspaceTree.js +9 -5
- package/lib/client/surface/components/WorkspaceTree.js.map +1 -1
- package/lib/client/surface/components/rows.d.ts +2 -2
- package/lib/client/surface/components/rows.d.ts.map +1 -1
- package/lib/client/surface/components/rows.js +111 -8
- package/lib/client/surface/components/rows.js.map +1 -1
- package/lib/client/surface/selectors.d.ts +10 -0
- package/lib/client/surface/selectors.d.ts.map +1 -1
- package/lib/client/surface/selectors.js +14 -0
- package/lib/client/surface/selectors.js.map +1 -1
- package/lib/client/surface/state/useSessionExpansion.d.ts +2 -1
- package/lib/client/surface/state/useSessionExpansion.d.ts.map +1 -1
- package/lib/client/surface/state/useSessionExpansion.js +26 -13
- package/lib/client/surface/state/useSessionExpansion.js.map +1 -1
- package/lib/client/surface/state/useSessionOrdering.d.ts +1 -1
- package/lib/client/surface/state/useSessionOrdering.d.ts.map +1 -1
- package/lib/client/surface/state/useSessionOrdering.js +23 -8
- package/lib/client/surface/state/useSessionOrdering.js.map +1 -1
- package/lib/client/surface/types.d.ts +23 -3
- package/lib/client/surface/types.d.ts.map +1 -1
- package/lib/client/view/WorktreeModeAction.d.ts.map +1 -1
- package/lib/client/view/WorktreeModeAction.js +8 -0
- package/lib/client/view/WorktreeModeAction.js.map +1 -1
- package/lib/client/view/view-mode-dispatch.d.ts +7 -0
- package/lib/client/view/view-mode-dispatch.d.ts.map +1 -0
- package/lib/client/view/view-mode-dispatch.js +11 -0
- package/lib/client/view/view-mode-dispatch.js.map +1 -0
- package/lib/client/view/worktree-expand-state.d.ts +1 -1
- package/lib/client/view/worktree-expand-state.d.ts.map +1 -1
- package/lib/client/view/worktree-expand-state.js +8 -1
- package/lib/client/view/worktree-expand-state.js.map +1 -1
- package/lib/client/worktree-connection.d.ts +5 -0
- package/lib/client/worktree-connection.d.ts.map +1 -1
- package/lib/client/worktree-connection.js +14 -0
- package/lib/client/worktree-connection.js.map +1 -1
- package/lib/client.js +4865 -158
- package/lib/client.js.map +1 -1
- package/lib/contract/index.contract.js +10 -0
- package/lib/contract/index.contract.js.map +1 -1
- package/lib/contract/index.d.ts +163 -1
- package/lib/contract/index.d.ts.map +1 -1
- package/lib/contract/index.js +19 -0
- package/lib/contract/index.js.map +1 -1
- package/lib/host/dsh-read-adapter.d.ts +3 -1
- package/lib/host/dsh-read-adapter.d.ts.map +1 -1
- package/lib/host/dsh-read-adapter.js +5 -2
- package/lib/host/dsh-read-adapter.js.map +1 -1
- package/lib/host/remote.d.ts.map +1 -1
- package/lib/host/remote.js +5 -0
- package/lib/host/remote.js.map +1 -1
- package/lib/host/service.d.ts +20 -1
- package/lib/host/service.d.ts.map +1 -1
- package/lib/host/service.js +32 -0
- package/lib/host/service.js.map +1 -1
- package/lib/host/worktree-instructions.d.ts +41 -0
- package/lib/host/worktree-instructions.d.ts.map +1 -0
- package/lib/host/worktree-instructions.js +65 -0
- package/lib/host/worktree-instructions.js.map +1 -0
- package/lib/index.d.ts +4 -4
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +3 -3
- package/lib/index.js.map +1 -1
- package/lib/manage/manager-git-history.d.ts +17 -0
- package/lib/manage/manager-git-history.d.ts.map +1 -0
- package/lib/manage/manager-git-history.js +717 -0
- package/lib/manage/manager-git-history.js.map +1 -0
- package/lib/manage/manager-worktrees.d.ts.map +1 -1
- package/lib/manage/manager-worktrees.js +27 -0
- package/lib/manage/manager-worktrees.js.map +1 -1
- package/lib/manage/manager.d.ts +21 -1
- package/lib/manage/manager.d.ts.map +1 -1
- package/lib/manage/manager.js +60 -0
- package/lib/manage/manager.js.map +1 -1
- package/lib/manage/types.d.ts +1 -0
- package/lib/manage/types.d.ts.map +1 -1
- package/lib/provider/git/adapter.d.ts +54 -1
- package/lib/provider/git/adapter.d.ts.map +1 -1
- package/lib/provider/git/adapter.js +820 -1
- package/lib/provider/git/adapter.js.map +1 -1
- package/lib/provider/index.d.ts +2 -2
- package/lib/provider/index.d.ts.map +1 -1
- package/lib/provider/index.js +1 -1
- package/lib/provider/index.js.map +1 -1
- package/lib/provider/mutation-token.d.ts +1 -1
- package/lib/provider/mutation-token.d.ts.map +1 -1
- package/lib/provider/mutation-token.js +1 -0
- package/lib/provider/mutation-token.js.map +1 -1
- package/lib/provider/sidecar/repository.d.ts +1 -1
- package/lib/provider/sidecar/repository.d.ts.map +1 -1
- package/lib/provider/sidecar/repository.js +7 -2
- package/lib/provider/sidecar/repository.js.map +1 -1
- package/lib/provider/sidecar/sidecar-schema.d.ts +16 -3
- package/lib/provider/sidecar/sidecar-schema.d.ts.map +1 -1
- package/lib/provider/sidecar/sidecar-schema.js +117 -28
- package/lib/provider/sidecar/sidecar-schema.js.map +1 -1
- package/lib/provider/transaction/operations/create.d.ts.map +1 -1
- package/lib/provider/transaction/operations/create.js +21 -6
- package/lib/provider/transaction/operations/create.js.map +1 -1
- package/lib/provider/transaction/operations/import.d.ts.map +1 -1
- package/lib/provider/transaction/operations/import.js +1 -0
- package/lib/provider/transaction/operations/import.js.map +1 -1
- package/lib/provider/transaction/recovery/failure-recovery.d.ts.map +1 -1
- package/lib/provider/transaction/recovery/failure-recovery.js +11 -4
- package/lib/provider/transaction/recovery/failure-recovery.js.map +1 -1
- package/lib/provider/transaction/recovery/recover.js +11 -11
- package/lib/provider/transaction/recovery/recover.js.map +1 -1
- package/lib/provider/transaction/support/inspection.d.ts +0 -2
- package/lib/provider/transaction/support/inspection.d.ts.map +1 -1
- package/lib/provider/transaction/support/inspection.js +0 -4
- package/lib/provider/transaction/support/inspection.js.map +1 -1
- package/lib/provider/transaction/support/journal.d.ts.map +1 -1
- package/lib/provider/transaction/support/journal.js +4 -0
- package/lib/provider/transaction/support/journal.js.map +1 -1
- package/lib/provider/transaction/types.d.ts +1 -0
- package/lib/provider/transaction/types.d.ts.map +1 -1
- package/lib/provider/types.d.ts +53 -5
- package/lib/provider/types.d.ts.map +1 -1
- package/lib/provider/types.js +1 -1
- package/lib/provider/types.js.map +1 -1
- package/lib/typert.host.js +349 -33
- package/lib/typert.remote-client.d.ts +11 -1
- package/lib/typert.remote-client.d.ts.map +1 -1
- package/lib/typert.remote-client.js +349 -33
- package/package.json +23 -20
|
@@ -0,0 +1,717 @@
|
|
|
1
|
+
import { WORKTREE_GIT_COMMIT_PATTERN, WORKTREE_GIT_SUMMARY, WORKTREE_GIT_WORKING_TREE, } from '../contract/index.js';
|
|
2
|
+
import { providerError } from '../provider/types.js';
|
|
3
|
+
import { isDirectory, requireWorkspace, samePhysicalPath } from './manager-support.js';
|
|
4
|
+
/** Shared with Contract/Provider/Client so commit-SHA validation cannot drift. */
|
|
5
|
+
const COMMIT_PATTERN = WORKTREE_GIT_COMMIT_PATTERN;
|
|
6
|
+
/**
|
|
7
|
+
* Resolve an optional adapter read once, with its adapter receiver preserved.
|
|
8
|
+
* Centralizing the capability check keeps the Manage layer free of repeated
|
|
9
|
+
* `=== undefined` guards and reports one stable provider error.
|
|
10
|
+
*/
|
|
11
|
+
function gitRead(context, name, worktreeId) {
|
|
12
|
+
const method = gitReadOptional(context, name);
|
|
13
|
+
if (method === undefined) {
|
|
14
|
+
throw providerError('GIT_OPERATION_FAILED', `Git ${name} is unavailable`, { worktreeId });
|
|
15
|
+
}
|
|
16
|
+
return method;
|
|
17
|
+
}
|
|
18
|
+
/** Resolve an optional adapter read, or undefined when the adapter omits it. */
|
|
19
|
+
function gitReadOptional(context, name) {
|
|
20
|
+
const method = context.git[name];
|
|
21
|
+
if (method === undefined)
|
|
22
|
+
return undefined;
|
|
23
|
+
return method.bind(context.git);
|
|
24
|
+
}
|
|
25
|
+
/** Find one changed file by its new or previous path. */
|
|
26
|
+
function findChangedFile(files, requestedPath) {
|
|
27
|
+
return files.find((file) => file.path === requestedPath || file.oldPath === requestedPath);
|
|
28
|
+
}
|
|
29
|
+
function isMainWorktreeId(worktreeId) {
|
|
30
|
+
return worktreeId === 'main' || worktreeId.startsWith('main:');
|
|
31
|
+
}
|
|
32
|
+
function unavailableHistory(reason, divergence) {
|
|
33
|
+
return {
|
|
34
|
+
commits: [],
|
|
35
|
+
truncated: false,
|
|
36
|
+
...(divergence ?? {}),
|
|
37
|
+
unavailableReason: reason,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function workingTreeCommit(headCommit) {
|
|
41
|
+
return {
|
|
42
|
+
sha: WORKTREE_GIT_WORKING_TREE,
|
|
43
|
+
kind: 'working-tree',
|
|
44
|
+
parents: [headCommit],
|
|
45
|
+
subject: '',
|
|
46
|
+
authorName: '',
|
|
47
|
+
authoredAt: '',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async function findLiveWorktree(worktrees, absolutePath) {
|
|
51
|
+
for (const worktree of worktrees) {
|
|
52
|
+
if (await samePhysicalPath(worktree.absolutePath, absolutePath))
|
|
53
|
+
return worktree;
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
async function resolveWorktree(context, input) {
|
|
58
|
+
const workspace = await requireWorkspace(context, input.workspaceId);
|
|
59
|
+
if (isMainWorktreeId(input.worktreeId)) {
|
|
60
|
+
return { main: true, workspaceRoot: workspace.rootPath };
|
|
61
|
+
}
|
|
62
|
+
const snapshot = await context.sidecar.read(input.workspaceId);
|
|
63
|
+
if (snapshot.pendingOperation !== undefined || (snapshot.recoveryIssues?.length ?? 0) > 0) {
|
|
64
|
+
throw providerError('WORKTREE_RECOVERY_REQUIRED', `Workspace Worktree state needs recovery: ${input.workspaceId}`, {
|
|
65
|
+
workspaceId: input.workspaceId,
|
|
66
|
+
...(snapshot.pendingOperation ? { operationId: snapshot.pendingOperation.id } : {}),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const record = snapshot.worktrees.find((candidate) => candidate.worktreeId === input.worktreeId);
|
|
70
|
+
if (!record) {
|
|
71
|
+
throw providerError('WORKTREE_NOT_FOUND', `Worktree not found: ${input.worktreeId}`, {
|
|
72
|
+
workspaceId: input.workspaceId,
|
|
73
|
+
worktreeId: input.worktreeId,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (record.diskCleanup === 'completed' || record.status === 'removed') {
|
|
77
|
+
throw providerError('WORKTREE_REMOVED', `Worktree is no longer active: ${input.worktreeId}`, {
|
|
78
|
+
workspaceId: input.workspaceId,
|
|
79
|
+
worktreeId: input.worktreeId,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (!(await isDirectory(record.absolutePath))) {
|
|
83
|
+
throw providerError('WORKTREE_NOT_FOUND', `Worktree directory is unavailable: ${record.absolutePath}`, {
|
|
84
|
+
workspaceId: input.workspaceId,
|
|
85
|
+
worktreeId: input.worktreeId,
|
|
86
|
+
absolutePath: record.absolutePath,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const repositoryRoot = context.git.resolveRepositoryRoot
|
|
90
|
+
? await context.git.resolveRepositoryRoot(workspace.rootPath, { signal: context.signal })
|
|
91
|
+
: workspace.rootPath;
|
|
92
|
+
const live = await findLiveWorktree(await context.git.listWorktrees(repositoryRoot, { signal: context.signal }), record.absolutePath);
|
|
93
|
+
if (!live) {
|
|
94
|
+
throw providerError('WORKTREE_NOT_FOUND', `Git Worktree registration is unavailable: ${record.absolutePath}`, {
|
|
95
|
+
workspaceId: input.workspaceId,
|
|
96
|
+
worktreeId: input.worktreeId,
|
|
97
|
+
absolutePath: record.absolutePath,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
main: false,
|
|
102
|
+
value: {
|
|
103
|
+
workspaceRoot: workspace.rootPath,
|
|
104
|
+
repositoryRoot,
|
|
105
|
+
record,
|
|
106
|
+
live,
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/** True when a branch name can be safely handed to Git as a ref argument. */
|
|
111
|
+
function isSafeBranchName(branch) {
|
|
112
|
+
if (branch.length === 0 || branch.startsWith('-'))
|
|
113
|
+
return false;
|
|
114
|
+
// Only a local branch short name may be selected. A full ref path is an
|
|
115
|
+
// arbitrary-ref attempt and is rejected instead of degrading to unavailable.
|
|
116
|
+
if (branch.startsWith('refs/'))
|
|
117
|
+
return false;
|
|
118
|
+
for (const character of branch) {
|
|
119
|
+
const code = character.codePointAt(0) ?? 0;
|
|
120
|
+
if (code < 0x20 || code === 0x7f)
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Resolve one local branch tip for the Dashboard baseline. The branch must exist
|
|
127
|
+
* under `refs/heads/`, so tags, remote-tracking branches, and arbitrary refs are
|
|
128
|
+
* rejected even though the browser may send any string.
|
|
129
|
+
*/
|
|
130
|
+
async function resolveLocalBranchCommit(context, resolved, baseBranch, options = {}) {
|
|
131
|
+
const unavailable = () => {
|
|
132
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'Selected baseline branch is unavailable: ' + baseBranch, {
|
|
133
|
+
worktreeId: resolved.record.worktreeId,
|
|
134
|
+
baseBranch,
|
|
135
|
+
});
|
|
136
|
+
};
|
|
137
|
+
// An unsafe ref is always a hard failure: arbitrary refs must never resolve,
|
|
138
|
+
// even for a read that is allowed to degrade to an unavailable projection.
|
|
139
|
+
if (!isSafeBranchName(baseBranch))
|
|
140
|
+
unavailable();
|
|
141
|
+
const listBranches = gitRead(context, 'listBranches', resolved.record.worktreeId);
|
|
142
|
+
const resolveCommit = gitRead(context, 'resolveCommit', resolved.record.worktreeId);
|
|
143
|
+
const branches = await listBranches(resolved.repositoryRoot, { signal: context.signal });
|
|
144
|
+
// A baseline branch that no longer exists is an expected unavailable state for
|
|
145
|
+
// reads, so the Dashboard can show an honest empty projection instead of a
|
|
146
|
+
// generic Git failure. Writes keep the strict behaviour.
|
|
147
|
+
if (!branches.includes(baseBranch)) {
|
|
148
|
+
if (options.allowUnavailable === true)
|
|
149
|
+
return undefined;
|
|
150
|
+
unavailable();
|
|
151
|
+
}
|
|
152
|
+
const commit = await resolveCommit(resolved.record.absolutePath, baseBranch, {
|
|
153
|
+
signal: context.signal,
|
|
154
|
+
});
|
|
155
|
+
if (!COMMIT_PATTERN.test(commit)) {
|
|
156
|
+
throw providerError('GIT_OPERATION_FAILED', 'Git returned an invalid selected baseline commit', {
|
|
157
|
+
worktreeId: resolved.record.worktreeId,
|
|
158
|
+
baseBranch,
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return commit;
|
|
162
|
+
}
|
|
163
|
+
async function resolveSelectedBaseline(context, resolved, rawBranch) {
|
|
164
|
+
const baseBranch = rawBranch.trim();
|
|
165
|
+
if (baseBranch.length === 0)
|
|
166
|
+
return undefined;
|
|
167
|
+
// A vanished baseline branch degrades to an unavailable projection for reads.
|
|
168
|
+
const commit = await resolveLocalBranchCommit(context, resolved, baseBranch, {
|
|
169
|
+
allowUnavailable: true,
|
|
170
|
+
});
|
|
171
|
+
if (commit === undefined)
|
|
172
|
+
return undefined;
|
|
173
|
+
// A selected branch can move after the Worktree was created. Match GitHub's
|
|
174
|
+
// pull-request range by diffing from the two heads' merge base to Worktree
|
|
175
|
+
// HEAD, while retaining the selected branch tip for ahead/behind counts.
|
|
176
|
+
const headCommit = await baselineHeadCommit(context, resolved);
|
|
177
|
+
const findMergeBase = gitReadOptional(context, 'findMergeBase');
|
|
178
|
+
const mergeBase = headCommit !== undefined && findMergeBase !== undefined
|
|
179
|
+
? await findMergeBase(resolved.record.absolutePath, commit, headCommit, { signal: context.signal })
|
|
180
|
+
: undefined;
|
|
181
|
+
if (mergeBase !== undefined && !COMMIT_PATTERN.test(mergeBase)) {
|
|
182
|
+
throw providerError('GIT_OPERATION_FAILED', 'Git returned an invalid selected branch merge base', {
|
|
183
|
+
worktreeId: resolved.record.worktreeId,
|
|
184
|
+
baseBranch,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
return {
|
|
188
|
+
baseline: {
|
|
189
|
+
commit,
|
|
190
|
+
ref: baseBranch,
|
|
191
|
+
source: 'branch',
|
|
192
|
+
},
|
|
193
|
+
baseHeadCommit: commit,
|
|
194
|
+
treeBaseCommit: mergeBase ?? commit,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
/** Persist a user-selected local branch as the Worktree's Dashboard baseline. */
|
|
198
|
+
export async function updateWorktreeBaseBranch(context, input) {
|
|
199
|
+
if (typeof input.baseBranch !== 'string') {
|
|
200
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'The baseline branch must be a string', {
|
|
201
|
+
worktreeId: input.worktreeId,
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
const baseBranch = input.baseBranch.trim();
|
|
205
|
+
if (baseBranch.length === 0) {
|
|
206
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'The baseline branch cannot be empty', {
|
|
207
|
+
worktreeId: input.worktreeId,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
if (input.expectedBaseBranch !== undefined && typeof input.expectedBaseBranch !== 'string') {
|
|
211
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'The expected baseline branch must be a string', {
|
|
212
|
+
worktreeId: input.worktreeId,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
const expectedBaseBranch = input.expectedBaseBranch?.trim() || undefined;
|
|
216
|
+
const resolved = await resolveWorktree(context, input);
|
|
217
|
+
if (resolved.main) {
|
|
218
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'The local Workspace has no editable baseline', {
|
|
219
|
+
workspaceId: input.workspaceId,
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
const currentBranch = resolved.value.live.detached === true ? undefined : resolved.value.live.branch;
|
|
223
|
+
if (currentBranch === baseBranch) {
|
|
224
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'The baseline branch cannot be the current Worktree branch', {
|
|
225
|
+
worktreeId: input.worktreeId,
|
|
226
|
+
baseBranch,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
// Resolve the replacement branch against the pinned repository root before the
|
|
230
|
+
// sidecar mutation, so an unavailable branch fails closed without a write.
|
|
231
|
+
await resolveLocalBranchCommit(context, resolved.value, baseBranch);
|
|
232
|
+
return context.sidecar.mutate(input.workspaceId, (snapshot) => {
|
|
233
|
+
if (snapshot.pendingOperation !== undefined || (snapshot.recoveryIssues?.length ?? 0) > 0) {
|
|
234
|
+
throw providerError('WORKTREE_RECOVERY_REQUIRED', 'Workspace Worktree state needs recovery: ' + input.workspaceId, { workspaceId: input.workspaceId });
|
|
235
|
+
}
|
|
236
|
+
const record = snapshot.worktrees.find((candidate) => candidate.worktreeId === input.worktreeId);
|
|
237
|
+
if (!record) {
|
|
238
|
+
throw providerError('WORKTREE_NOT_FOUND', 'Worktree not found: ' + input.worktreeId, {
|
|
239
|
+
workspaceId: input.workspaceId,
|
|
240
|
+
worktreeId: input.worktreeId,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
if (record.status === 'removed' || record.diskCleanup === 'completed') {
|
|
244
|
+
throw providerError('WORKTREE_REMOVED', 'Worktree is no longer active: ' + input.worktreeId, {
|
|
245
|
+
workspaceId: input.workspaceId,
|
|
246
|
+
worktreeId: input.worktreeId,
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
if (input.expectedBaseBranch !== undefined && (record.baseBranch ?? undefined) !== expectedBaseBranch) {
|
|
250
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'Baseline branch changed; reopen the editor before saving', { workspaceId: input.workspaceId, worktreeId: input.worktreeId });
|
|
251
|
+
}
|
|
252
|
+
if (record.baseBranch === baseBranch) {
|
|
253
|
+
return { result: baseBranch, snapshot, changed: false };
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
result: baseBranch,
|
|
257
|
+
snapshot: {
|
|
258
|
+
...snapshot,
|
|
259
|
+
worktrees: snapshot.worktrees.map((item) => item === record ? { ...item, baseBranch } : item),
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
async function resolveBaseline(context, resolved, requestedBaseBranch) {
|
|
265
|
+
const { record } = resolved;
|
|
266
|
+
if (requestedBaseBranch !== undefined) {
|
|
267
|
+
if (typeof requestedBaseBranch !== 'string') {
|
|
268
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'The selected baseline branch must be a string', {
|
|
269
|
+
worktreeId: record.worktreeId,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
return resolveSelectedBaseline(context, resolved, requestedBaseBranch);
|
|
273
|
+
}
|
|
274
|
+
if (record.baseCommit !== undefined) {
|
|
275
|
+
if (!COMMIT_PATTERN.test(record.baseCommit)) {
|
|
276
|
+
throw providerError('SIDECAR_CORRUPT', 'Worktree has an invalid acquisition baseline', {
|
|
277
|
+
worktreeId: record.worktreeId,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
const commit = context.git.resolveCommit
|
|
281
|
+
? await context.git.resolveCommit(record.absolutePath, record.baseCommit, { signal: context.signal })
|
|
282
|
+
: record.baseCommit;
|
|
283
|
+
return {
|
|
284
|
+
baseline: {
|
|
285
|
+
commit,
|
|
286
|
+
...(record.baseBranch !== undefined ? { ref: record.baseBranch } : {}),
|
|
287
|
+
source: 'captured',
|
|
288
|
+
},
|
|
289
|
+
baseHeadCommit: commit,
|
|
290
|
+
treeBaseCommit: commit,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
// External and ambiguous legacy records have no provable acquisition point.
|
|
294
|
+
// Never persist a runtime-derived merge base back into the sidecar.
|
|
295
|
+
if (record.source === 'external' || record.baseBranch === undefined || resolved.live.detached === true) {
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
const currentBranch = resolved.live.branch;
|
|
299
|
+
if (currentBranch === undefined || currentBranch === record.baseBranch || context.git.findMergeBase === undefined) {
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
const commit = await context.git.findMergeBase(record.absolutePath, record.baseBranch, 'HEAD', { signal: context.signal });
|
|
303
|
+
if (commit === undefined)
|
|
304
|
+
return undefined;
|
|
305
|
+
if (!COMMIT_PATTERN.test(commit)) {
|
|
306
|
+
throw providerError('GIT_OPERATION_FAILED', 'Git returned an invalid derived baseline', {
|
|
307
|
+
worktreeId: record.worktreeId,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
return {
|
|
311
|
+
baseline: {
|
|
312
|
+
commit,
|
|
313
|
+
ref: record.baseBranch,
|
|
314
|
+
source: 'derived',
|
|
315
|
+
},
|
|
316
|
+
baseHeadCommit: commit,
|
|
317
|
+
treeBaseCommit: commit,
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
async function requireBaseline(context, resolved, requestedBaseBranch) {
|
|
321
|
+
const baseline = await resolveBaseline(context, resolved, requestedBaseBranch);
|
|
322
|
+
if (baseline === undefined) {
|
|
323
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'Select a local baseline branch before reading Git changes', { worktreeId: resolved.record.worktreeId });
|
|
324
|
+
}
|
|
325
|
+
return baseline;
|
|
326
|
+
}
|
|
327
|
+
async function baselineHeadCommit(context, resolved) {
|
|
328
|
+
if (resolved.live.headCommit !== undefined)
|
|
329
|
+
return resolved.live.headCommit;
|
|
330
|
+
const resolveCommit = gitReadOptional(context, 'resolveCommit');
|
|
331
|
+
return resolveCommit === undefined
|
|
332
|
+
? undefined
|
|
333
|
+
: await resolveCommit(resolved.record.absolutePath, 'HEAD', { signal: context.signal });
|
|
334
|
+
}
|
|
335
|
+
/** Count commits unique to each head for one resolved comparison boundary. */
|
|
336
|
+
async function readCommitDivergence(context, resolved, comparison, head) {
|
|
337
|
+
const getCommitDivergence = gitReadOptional(context, 'getCommitDivergence');
|
|
338
|
+
if (getCommitDivergence === undefined || head === undefined)
|
|
339
|
+
return undefined;
|
|
340
|
+
// Count each branch from its current tip. The tree boundary may be the
|
|
341
|
+
// merge base, but ahead/behind must include commits unique to both heads.
|
|
342
|
+
return getCommitDivergence(resolved.record.absolutePath, comparison.baseHeadCommit, head, { signal: context.signal });
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Authorize commit SHAs against one pinned comparison projection.
|
|
346
|
+
*
|
|
347
|
+
* One bounded `listCommits` read yields exactly the authorized set: commits
|
|
348
|
+
* reachable from the Worktree HEAD and not reachable from the resolved baseline
|
|
349
|
+
* head. That is the same membership the previous per-SHA ancestry walks proved,
|
|
350
|
+
* but a wide multi-commit selection now costs one projection read instead of
|
|
351
|
+
* several Git processes per candidate SHA. The visible projection is therefore
|
|
352
|
+
* the authorization boundary, matching the truncated 200-commit history the
|
|
353
|
+
* browser can actually select from.
|
|
354
|
+
*/
|
|
355
|
+
async function authorizeCommits(context, resolved, comparison, candidates) {
|
|
356
|
+
const worktreeId = resolved.record.worktreeId;
|
|
357
|
+
const requested = candidates.map((candidate) => {
|
|
358
|
+
if (typeof candidate !== 'string' || !COMMIT_PATTERN.test(candidate)) {
|
|
359
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'The requested commit is not a valid commit SHA', {
|
|
360
|
+
worktreeId,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
return candidate;
|
|
364
|
+
});
|
|
365
|
+
const listCommits = gitRead(context, 'listCommits', worktreeId);
|
|
366
|
+
const history = await listCommits(resolved.record.absolutePath, comparison.baseHeadCommit, {
|
|
367
|
+
signal: context.signal,
|
|
368
|
+
});
|
|
369
|
+
const members = new Set(history.commits.map((commit) => commit.sha));
|
|
370
|
+
for (const commit of requested) {
|
|
371
|
+
if (!members.has(commit)) {
|
|
372
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'The requested commit is outside this Worktree history', {
|
|
373
|
+
worktreeId,
|
|
374
|
+
commit,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return requested;
|
|
379
|
+
}
|
|
380
|
+
async function authorizeCommit(context, resolved, commitInput, requestedBaseBranch) {
|
|
381
|
+
const comparison = await requireBaseline(context, resolved, requestedBaseBranch);
|
|
382
|
+
const [commit] = await authorizeCommits(context, resolved, comparison, [commitInput]);
|
|
383
|
+
return { commit: commit, baseline: comparison.baseline };
|
|
384
|
+
}
|
|
385
|
+
async function authorizeWorkingTree(context, resolved, requestedBaseBranch) {
|
|
386
|
+
// The selected branch may be unrelated to the Worktree history. The Git
|
|
387
|
+
// provider can still compare two arbitrary committed trees, so only require
|
|
388
|
+
// a valid resolved baseline here.
|
|
389
|
+
await requireBaseline(context, resolved, requestedBaseBranch);
|
|
390
|
+
}
|
|
391
|
+
export async function listWorktreeCommits(context, input) {
|
|
392
|
+
const resolved = await resolveWorktree(context, input);
|
|
393
|
+
if (resolved.main)
|
|
394
|
+
return unavailableHistory('main');
|
|
395
|
+
const baseline = await resolveBaseline(context, resolved.value, input.baseBranch);
|
|
396
|
+
if (baseline === undefined) {
|
|
397
|
+
const selectedBranch = input.baseBranch ?? resolved.value.record.baseBranch;
|
|
398
|
+
return unavailableHistory(selectedBranch === undefined || selectedBranch.trim().length === 0
|
|
399
|
+
? 'baseline-unselected'
|
|
400
|
+
: 'baseline-unknown');
|
|
401
|
+
}
|
|
402
|
+
const listCommits = gitRead(context, 'listCommits', input.worktreeId);
|
|
403
|
+
// The working-tree entry only needs the changed-path projection here; its file
|
|
404
|
+
// list and line counts are read on demand when the entry is selected.
|
|
405
|
+
const readWorkingTreePaths = gitReadOptional(context, 'listWorkingTreeChangedPaths')
|
|
406
|
+
?? gitReadOptional(context, 'listWorkingTreeFiles');
|
|
407
|
+
const [history, workingTreeFiles] = await Promise.all([
|
|
408
|
+
listCommits(resolved.value.record.absolutePath, baseline.baseHeadCommit, { signal: context.signal }),
|
|
409
|
+
readWorkingTreePaths === undefined
|
|
410
|
+
? Promise.resolve({ files: [], truncated: false })
|
|
411
|
+
: readWorkingTreePaths(resolved.value.record.absolutePath, { signal: context.signal }),
|
|
412
|
+
]);
|
|
413
|
+
const divergence = await readCommitDivergence(context, resolved.value, baseline, history.headCommit);
|
|
414
|
+
return {
|
|
415
|
+
...history,
|
|
416
|
+
...(divergence ?? {}),
|
|
417
|
+
commits: workingTreeFiles.files.length > 0
|
|
418
|
+
? [workingTreeCommit(history.headCommit), ...history.commits]
|
|
419
|
+
: history.commits,
|
|
420
|
+
baseline: baseline.baseline,
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
const MAX_AGGREGATE_COMMITS = 200;
|
|
424
|
+
function requestedDiff(input, worktreeId) {
|
|
425
|
+
const hasCommit = input.commit !== undefined;
|
|
426
|
+
const hasSelection = input.selection !== undefined;
|
|
427
|
+
if (hasCommit === hasSelection) {
|
|
428
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'Provide exactly one Git diff selection', {
|
|
429
|
+
worktreeId,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
if (hasCommit)
|
|
433
|
+
return { kind: 'commit', commit: input.commit };
|
|
434
|
+
const selection = input.selection;
|
|
435
|
+
if (typeof selection !== 'object' || selection === null ||
|
|
436
|
+
(selection.kind !== 'summary' && selection.kind !== 'commits')) {
|
|
437
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'The Git diff selection is invalid', { worktreeId });
|
|
438
|
+
}
|
|
439
|
+
if (selection.kind === 'summary' && selection.includeWorkingTree !== undefined &&
|
|
440
|
+
typeof selection.includeWorkingTree !== 'boolean') {
|
|
441
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'The summary working-tree option is invalid', { worktreeId });
|
|
442
|
+
}
|
|
443
|
+
return { kind: 'aggregate', selection };
|
|
444
|
+
}
|
|
445
|
+
async function authorizeAggregate(context, resolved, selection, requestedBaseBranch) {
|
|
446
|
+
const comparison = await requireBaseline(context, resolved, requestedBaseBranch);
|
|
447
|
+
const baseline = comparison.baseline;
|
|
448
|
+
const normalizedSelection = selection.kind === 'summary'
|
|
449
|
+
? selection.includeWorkingTree === undefined
|
|
450
|
+
? { kind: 'summary' }
|
|
451
|
+
: { kind: 'summary', includeWorkingTree: selection.includeWorkingTree }
|
|
452
|
+
: selection;
|
|
453
|
+
const headCommit = await baselineHeadCommit(context, resolved);
|
|
454
|
+
if (headCommit === undefined || !COMMIT_PATTERN.test(headCommit)) {
|
|
455
|
+
throw providerError('GIT_OPERATION_FAILED', 'Git HEAD resolution is unavailable', {
|
|
456
|
+
worktreeId: resolved.record.worktreeId,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
if (normalizedSelection.kind === 'summary') {
|
|
460
|
+
return { selection: normalizedSelection, baseline, treeBaseCommit: comparison.treeBaseCommit, headCommit };
|
|
461
|
+
}
|
|
462
|
+
if (!Array.isArray(normalizedSelection.commits) || normalizedSelection.commits.length === 0 || normalizedSelection.commits.length > MAX_AGGREGATE_COMMITS) {
|
|
463
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'Select between 1 and ' + MAX_AGGREGATE_COMMITS + ' commits', {
|
|
464
|
+
worktreeId: resolved.record.worktreeId,
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
const commits = await authorizeCommits(context, resolved, comparison, normalizedSelection.commits);
|
|
468
|
+
if (new Set(commits).size !== commits.length) {
|
|
469
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'A Git diff selection cannot contain duplicate commits', {
|
|
470
|
+
worktreeId: resolved.record.worktreeId,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
selection: { kind: 'commits', commits },
|
|
475
|
+
baseline,
|
|
476
|
+
treeBaseCommit: comparison.treeBaseCommit,
|
|
477
|
+
headCommit,
|
|
478
|
+
commits,
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
async function selectedCommitFiles(context, resolved, commits) {
|
|
482
|
+
const listCommitFiles = gitRead(context, 'listCommitFiles', resolved.record.worktreeId);
|
|
483
|
+
return Promise.all(commits.map(async (commit) => {
|
|
484
|
+
const read = await listCommitFiles(resolved.record.absolutePath, commit, { signal: context.signal });
|
|
485
|
+
return { commit, files: read.files, truncated: read.truncated };
|
|
486
|
+
}));
|
|
487
|
+
}
|
|
488
|
+
function mergeLineCount(left, right) {
|
|
489
|
+
if (left === undefined || right === undefined)
|
|
490
|
+
return undefined;
|
|
491
|
+
return left + right;
|
|
492
|
+
}
|
|
493
|
+
function mergeSelectedFiles(groups) {
|
|
494
|
+
const files = new Map();
|
|
495
|
+
for (const group of groups) {
|
|
496
|
+
for (const file of group.files) {
|
|
497
|
+
const previous = files.get(file.path);
|
|
498
|
+
if (previous === undefined) {
|
|
499
|
+
files.set(file.path, { ...file, commits: [group.commit] });
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
const countsUnknown = previous.additions === undefined ||
|
|
503
|
+
file.additions === undefined ||
|
|
504
|
+
previous.deletions === undefined ||
|
|
505
|
+
file.deletions === undefined;
|
|
506
|
+
const additions = countsUnknown ? undefined : mergeLineCount(previous.additions, file.additions);
|
|
507
|
+
const deletions = countsUnknown ? undefined : mergeLineCount(previous.deletions, file.deletions);
|
|
508
|
+
const { additions: _previousAdditions, deletions: _previousDeletions, ...metadata } = previous;
|
|
509
|
+
void _previousAdditions;
|
|
510
|
+
void _previousDeletions;
|
|
511
|
+
files.set(file.path, {
|
|
512
|
+
...metadata,
|
|
513
|
+
...(additions === undefined ? {} : { additions }),
|
|
514
|
+
...(deletions === undefined ? {} : { deletions }),
|
|
515
|
+
commits: [...new Set([...(previous.commits ?? []), group.commit])],
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return [...files.values()];
|
|
520
|
+
}
|
|
521
|
+
async function authorizeLiveSummaryHead(context, resolved) {
|
|
522
|
+
const resolveCommit = gitReadOptional(context, 'resolveCommit');
|
|
523
|
+
if (resolveCommit === undefined)
|
|
524
|
+
return;
|
|
525
|
+
const currentHead = await resolveCommit(resolved.record.absolutePath, 'HEAD', { signal: context.signal });
|
|
526
|
+
if (!COMMIT_PATTERN.test(currentHead)) {
|
|
527
|
+
throw providerError('GIT_OPERATION_FAILED', 'Git returned an invalid live Worktree HEAD', {
|
|
528
|
+
worktreeId: resolved.record.worktreeId,
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
async function listSummaryFiles(context, resolved, selection, baseCommit, headCommit) {
|
|
533
|
+
if (selection.includeWorkingTree === true) {
|
|
534
|
+
const listWorkingTreeDiffFiles = gitRead(context, 'listWorkingTreeDiffFiles', resolved.record.worktreeId);
|
|
535
|
+
await authorizeLiveSummaryHead(context, resolved);
|
|
536
|
+
return listWorkingTreeDiffFiles(resolved.record.absolutePath, baseCommit, { signal: context.signal });
|
|
537
|
+
}
|
|
538
|
+
const listDiffFiles = gitRead(context, 'listDiffFiles', resolved.record.worktreeId);
|
|
539
|
+
return listDiffFiles(resolved.record.absolutePath, baseCommit, headCommit, { signal: context.signal });
|
|
540
|
+
}
|
|
541
|
+
async function readSummaryFileDiff(context, resolved, selection, baseCommit, headCommit, filePath) {
|
|
542
|
+
if (selection.includeWorkingTree === true) {
|
|
543
|
+
const readWorkingTreeDiffFileDiff = gitRead(context, 'readWorkingTreeDiffFileDiff', resolved.record.worktreeId);
|
|
544
|
+
await authorizeLiveSummaryHead(context, resolved);
|
|
545
|
+
return readWorkingTreeDiffFileDiff(resolved.record.absolutePath, baseCommit, filePath, { signal: context.signal });
|
|
546
|
+
}
|
|
547
|
+
const readDiffFileDiff = gitRead(context, 'readDiffFileDiff', resolved.record.worktreeId);
|
|
548
|
+
return readDiffFileDiff(resolved.record.absolutePath, baseCommit, headCommit, filePath, { signal: context.signal });
|
|
549
|
+
}
|
|
550
|
+
async function listAggregateFiles(context, resolved, selection, requestedBaseBranch) {
|
|
551
|
+
const authorized = await authorizeAggregate(context, resolved, selection, requestedBaseBranch);
|
|
552
|
+
if (authorized.selection.kind === 'summary') {
|
|
553
|
+
const read = await listSummaryFiles(context, resolved, authorized.selection, authorized.treeBaseCommit, authorized.headCommit);
|
|
554
|
+
return {
|
|
555
|
+
commit: WORKTREE_GIT_SUMMARY,
|
|
556
|
+
selection: authorized.selection,
|
|
557
|
+
files: read.files,
|
|
558
|
+
...(read.truncated ? { truncated: true } : {}),
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
const groups = await selectedCommitFiles(context, resolved, authorized.commits);
|
|
562
|
+
return {
|
|
563
|
+
commit: authorized.commits[0],
|
|
564
|
+
selection: authorized.selection,
|
|
565
|
+
files: mergeSelectedFiles(groups),
|
|
566
|
+
...(groups.some((group) => group.truncated) ? { truncated: true } : {}),
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
async function aggregateFileDiff(context, resolved, selection, requestedPath, requestedBaseBranch) {
|
|
570
|
+
if (requestedPath.length === 0) {
|
|
571
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'A changed file path is required', {
|
|
572
|
+
worktreeId: resolved.record.worktreeId,
|
|
573
|
+
});
|
|
574
|
+
}
|
|
575
|
+
const authorized = await authorizeAggregate(context, resolved, selection, requestedBaseBranch);
|
|
576
|
+
if (authorized.selection.kind === 'summary') {
|
|
577
|
+
const summaryRead = await listSummaryFiles(context, resolved, authorized.selection, authorized.treeBaseCommit, authorized.headCommit);
|
|
578
|
+
const changedFile = findChangedFile(summaryRead.files, requestedPath);
|
|
579
|
+
if (changedFile === undefined) {
|
|
580
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'The requested path is not changed in the summary', {
|
|
581
|
+
worktreeId: resolved.record.worktreeId,
|
|
582
|
+
path: requestedPath,
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
const diff = await readSummaryFileDiff(context, resolved, authorized.selection, authorized.treeBaseCommit, authorized.headCommit, changedFile.path);
|
|
586
|
+
return {
|
|
587
|
+
...diff,
|
|
588
|
+
commit: WORKTREE_GIT_SUMMARY,
|
|
589
|
+
path: requestedPath,
|
|
590
|
+
selection: authorized.selection,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
const groups = await selectedCommitFiles(context, resolved, authorized.commits);
|
|
594
|
+
const readCommitFileDiff = gitRead(context, 'readCommitFileDiff', resolved.record.worktreeId);
|
|
595
|
+
const segments = [];
|
|
596
|
+
for (const group of groups) {
|
|
597
|
+
const changedFile = findChangedFile(group.files, requestedPath);
|
|
598
|
+
if (changedFile === undefined)
|
|
599
|
+
continue;
|
|
600
|
+
const diff = await readCommitFileDiff(resolved.record.absolutePath, group.commit, changedFile.path, { signal: context.signal });
|
|
601
|
+
segments.push({
|
|
602
|
+
commit: group.commit,
|
|
603
|
+
patch: diff.patch,
|
|
604
|
+
binary: diff.binary,
|
|
605
|
+
...(diff.truncated === true ? { truncated: true } : {}),
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
if (segments.length === 0) {
|
|
609
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'The requested path is not changed by the selected commits', {
|
|
610
|
+
worktreeId: resolved.record.worktreeId,
|
|
611
|
+
path: requestedPath,
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
return {
|
|
615
|
+
commit: authorized.commits[0],
|
|
616
|
+
path: requestedPath,
|
|
617
|
+
patch: '',
|
|
618
|
+
binary: segments.every((segment) => segment.binary),
|
|
619
|
+
...(segments.some((segment) => segment.truncated === true) ? { truncated: true } : {}),
|
|
620
|
+
selection: authorized.selection,
|
|
621
|
+
segments,
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
export async function listWorktreeCommitFiles(context, input) {
|
|
625
|
+
const resolved = await resolveWorktree(context, input);
|
|
626
|
+
if (resolved.main) {
|
|
627
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'Git history is unavailable for the Main projection', {
|
|
628
|
+
workspaceId: input.workspaceId,
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
const requested = requestedDiff(input, input.worktreeId);
|
|
632
|
+
if (requested.kind === 'aggregate') {
|
|
633
|
+
return listAggregateFiles(context, resolved.value, requested.selection, input.baseBranch);
|
|
634
|
+
}
|
|
635
|
+
const commit = requested.commit;
|
|
636
|
+
if (commit === WORKTREE_GIT_WORKING_TREE) {
|
|
637
|
+
await authorizeWorkingTree(context, resolved.value, input.baseBranch);
|
|
638
|
+
const listWorkingTreeFiles = gitRead(context, 'listWorkingTreeFiles', input.worktreeId);
|
|
639
|
+
const read = await listWorkingTreeFiles(resolved.value.record.absolutePath, { signal: context.signal });
|
|
640
|
+
return {
|
|
641
|
+
commit: WORKTREE_GIT_WORKING_TREE,
|
|
642
|
+
files: read.files,
|
|
643
|
+
...(read.truncated ? { truncated: true } : {}),
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
const authorized = await authorizeCommit(context, resolved.value, commit, input.baseBranch);
|
|
647
|
+
const listCommitFiles = gitRead(context, 'listCommitFiles', input.worktreeId);
|
|
648
|
+
const read = await listCommitFiles(resolved.value.record.absolutePath, authorized.commit, { signal: context.signal });
|
|
649
|
+
return {
|
|
650
|
+
commit: authorized.commit,
|
|
651
|
+
files: read.files,
|
|
652
|
+
...(read.truncated ? { truncated: true } : {}),
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
export async function getWorktreeCommitFileDiff(context, input) {
|
|
656
|
+
const resolved = await resolveWorktree(context, input);
|
|
657
|
+
if (resolved.main) {
|
|
658
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'Git history is unavailable for the Main projection', {
|
|
659
|
+
workspaceId: input.workspaceId,
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
const requested = requestedDiff(input, input.worktreeId);
|
|
663
|
+
if (requested.kind === 'aggregate') {
|
|
664
|
+
return aggregateFileDiff(context, resolved.value, requested.selection, input.path, input.baseBranch);
|
|
665
|
+
}
|
|
666
|
+
const commit = requested.commit;
|
|
667
|
+
if (typeof input.path !== 'string' || input.path.length === 0) {
|
|
668
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'A changed file path is required', {
|
|
669
|
+
worktreeId: input.worktreeId,
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
if (commit === WORKTREE_GIT_WORKING_TREE) {
|
|
673
|
+
await authorizeWorkingTree(context, resolved.value, input.baseBranch);
|
|
674
|
+
const readWorkingTreeFileDiff = gitRead(context, 'readWorkingTreeFileDiff', input.worktreeId);
|
|
675
|
+
// Path authorization only needs the changed-path projection: reading line
|
|
676
|
+
// statistics for every untracked file here would re-run one Git process per
|
|
677
|
+
// untracked file on each file click.
|
|
678
|
+
const readChangedPaths = gitReadOptional(context, 'listWorkingTreeChangedPaths')
|
|
679
|
+
?? gitRead(context, 'listWorkingTreeFiles', input.worktreeId);
|
|
680
|
+
const readPaths = await readChangedPaths(resolved.value.record.absolutePath, { signal: context.signal });
|
|
681
|
+
const changedFile = findChangedFile(readPaths.files, input.path);
|
|
682
|
+
if (changedFile === undefined) {
|
|
683
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'The requested path is not changed in the working tree', {
|
|
684
|
+
worktreeId: input.worktreeId,
|
|
685
|
+
commit: WORKTREE_GIT_WORKING_TREE,
|
|
686
|
+
path: input.path,
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
const diff = await readWorkingTreeFileDiff(resolved.value.record.absolutePath, changedFile.path, { signal: context.signal });
|
|
690
|
+
return {
|
|
691
|
+
...diff,
|
|
692
|
+
commit: WORKTREE_GIT_WORKING_TREE,
|
|
693
|
+
path: input.path,
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
const authorized = await authorizeCommit(context, resolved.value, commit, input.baseBranch);
|
|
697
|
+
const readCommitFileDiff = gitRead(context, 'readCommitFileDiff', input.worktreeId);
|
|
698
|
+
const listCommitFiles = gitRead(context, 'listCommitFiles', input.worktreeId);
|
|
699
|
+
// The changed-file projection is the authorization boundary for the path, so
|
|
700
|
+
// the diff is only read for an exact member of that projection.
|
|
701
|
+
const filesRead = await listCommitFiles(resolved.value.record.absolutePath, authorized.commit, { signal: context.signal });
|
|
702
|
+
const changedFile = findChangedFile(filesRead.files, input.path);
|
|
703
|
+
if (changedFile === undefined) {
|
|
704
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'The requested path is not changed by this commit', {
|
|
705
|
+
worktreeId: input.worktreeId,
|
|
706
|
+
commit: authorized.commit,
|
|
707
|
+
path: input.path,
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
const diff = await readCommitFileDiff(resolved.value.record.absolutePath, authorized.commit, changedFile.path, { signal: context.signal });
|
|
711
|
+
return {
|
|
712
|
+
...diff,
|
|
713
|
+
commit: authorized.commit,
|
|
714
|
+
path: input.path,
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
//# sourceMappingURL=manager-git-history.js.map
|