@cerbur/clutch-dsh-worktree 0.1.17 → 0.1.18
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 +105 -33
- package/README.zh.md +92 -29
- package/lib/client/WorktreeSurface.d.ts.map +1 -1
- package/lib/client/WorktreeSurface.js +37 -10
- package/lib/client/WorktreeSurface.js.map +1 -1
- package/lib/client/branch/GitBranchSelector.d.ts +23 -0
- package/lib/client/branch/GitBranchSelector.d.ts.map +1 -0
- package/lib/client/branch/GitBranchSelector.js +142 -0
- package/lib/client/branch/GitBranchSelector.js.map +1 -0
- package/lib/client/context/worktree-context-store.d.ts.map +1 -1
- package/lib/client/context/worktree-context-store.js +1 -0
- package/lib/client/context/worktree-context-store.js.map +1 -1
- package/lib/client/context/worktree-context.d.ts +3 -2
- package/lib/client/context/worktree-context.d.ts.map +1 -1
- package/lib/client/context/worktree-context.js +5 -3
- package/lib/client/context/worktree-context.js.map +1 -1
- package/lib/client/dashboard/WorktreeDashboard.d.ts +4 -1
- package/lib/client/dashboard/WorktreeDashboard.d.ts.map +1 -1
- package/lib/client/dashboard/WorktreeDashboard.js +67 -69
- package/lib/client/dashboard/WorktreeDashboard.js.map +1 -1
- package/lib/client/dashboard/dashboard-selection.d.ts +4 -3
- package/lib/client/dashboard/dashboard-selection.d.ts.map +1 -1
- package/lib/client/dashboard/dashboard-selection.js +6 -4
- package/lib/client/dashboard/dashboard-selection.js.map +1 -1
- package/lib/client/dashboard/git/GitChangedFiles.d.ts +2 -1
- package/lib/client/dashboard/git/GitChangedFiles.d.ts.map +1 -1
- package/lib/client/dashboard/git/GitChangedFiles.js +8 -6
- package/lib/client/dashboard/git/GitChangedFiles.js.map +1 -1
- package/lib/client/dashboard/git/GitCommitList.d.ts +6 -1
- package/lib/client/dashboard/git/GitCommitList.d.ts.map +1 -1
- package/lib/client/dashboard/git/GitCommitList.js +34 -29
- package/lib/client/dashboard/git/GitCommitList.js.map +1 -1
- package/lib/client/dashboard/git/GitCommitStats.d.ts +8 -0
- package/lib/client/dashboard/git/GitCommitStats.d.ts.map +1 -0
- package/lib/client/dashboard/git/GitCommitStats.js +7 -0
- package/lib/client/dashboard/git/GitCommitStats.js.map +1 -0
- package/lib/client/dashboard/git/WorktreeGitOverview.d.ts.map +1 -1
- package/lib/client/dashboard/git/WorktreeGitOverview.js +2 -1
- package/lib/client/dashboard/git/WorktreeGitOverview.js.map +1 -1
- package/lib/client/dashboard/git/WorktreeGitPanel.d.ts +1 -1
- package/lib/client/dashboard/git/WorktreeGitPanel.d.ts.map +1 -1
- package/lib/client/dashboard/git/WorktreeGitPanel.js +28 -9
- package/lib/client/dashboard/git/WorktreeGitPanel.js.map +1 -1
- package/lib/client/dashboard/git/file-address.d.ts +5 -2
- package/lib/client/dashboard/git/file-address.d.ts.map +1 -1
- package/lib/client/dashboard/git/file-address.js +50 -7
- package/lib/client/dashboard/git/file-address.js.map +1 -1
- package/lib/client/dashboard/git/git-facts.js +1 -1
- package/lib/client/dashboard/git/git-facts.js.map +1 -1
- package/lib/client/dashboard/git/useWorktreeGitState.d.ts +7 -2
- package/lib/client/dashboard/git/useWorktreeGitState.d.ts.map +1 -1
- package/lib/client/dashboard/git/useWorktreeGitState.js +84 -21
- package/lib/client/dashboard/git/useWorktreeGitState.js.map +1 -1
- package/lib/client/dsh-icons.d.ts +2 -0
- package/lib/client/dsh-icons.d.ts.map +1 -1
- package/lib/client/dsh-icons.js +2 -0
- package/lib/client/dsh-icons.js.map +1 -1
- package/lib/client/locales.d.ts +40 -2
- package/lib/client/locales.d.ts.map +1 -1
- package/lib/client/locales.js +46 -8
- package/lib/client/locales.js.map +1 -1
- package/lib/client/select/SearchableSingleSelect.d.ts +44 -0
- package/lib/client/select/SearchableSingleSelect.d.ts.map +1 -0
- package/lib/client/select/SearchableSingleSelect.js +180 -0
- package/lib/client/select/SearchableSingleSelect.js.map +1 -0
- package/lib/client/select/WorktreeSelector.d.ts +14 -0
- package/lib/client/select/WorktreeSelector.d.ts.map +1 -0
- package/lib/client/select/WorktreeSelector.js +11 -0
- package/lib/client/select/WorktreeSelector.js.map +1 -0
- package/lib/client/surface/actions/useLifecycleActions.js +1 -1
- package/lib/client/surface/actions/useLifecycleActions.js.map +1 -1
- package/lib/client/surface/actions/useWorktreeRegistration.d.ts +1 -1
- package/lib/client/surface/actions/useWorktreeRegistration.d.ts.map +1 -1
- package/lib/client/surface/actions/useWorktreeRegistration.js +32 -11
- package/lib/client/surface/actions/useWorktreeRegistration.js.map +1 -1
- package/lib/client/surface/components/RegistrationDialog.d.ts +3 -3
- package/lib/client/surface/components/RegistrationDialog.d.ts.map +1 -1
- package/lib/client/surface/components/RegistrationDialog.js +70 -5
- package/lib/client/surface/components/RegistrationDialog.js.map +1 -1
- package/lib/client/surface/components/WorkspaceTree.d.ts.map +1 -1
- package/lib/client/surface/components/WorkspaceTree.js +11 -8
- package/lib/client/surface/components/WorkspaceTree.js.map +1 -1
- package/lib/client/surface/components/dialogs.d.ts +1 -1
- package/lib/client/surface/components/dialogs.d.ts.map +1 -1
- package/lib/client/surface/components/dialogs.js +24 -15
- package/lib/client/surface/components/dialogs.js.map +1 -1
- package/lib/client/surface/state/useRegistrationState.d.ts +6 -1
- package/lib/client/surface/state/useRegistrationState.d.ts.map +1 -1
- package/lib/client/surface/state/useRegistrationState.js +7 -0
- package/lib/client/surface/state/useRegistrationState.js.map +1 -1
- package/lib/client/surface/types.d.ts +5 -1
- package/lib/client/surface/types.d.ts.map +1 -1
- package/lib/client/view/worktree-error-copy.d.ts.map +1 -1
- package/lib/client/view/worktree-error-copy.js +4 -0
- package/lib/client/view/worktree-error-copy.js.map +1 -1
- package/lib/client/view/worktree-view-read.d.ts +4 -2
- package/lib/client/view/worktree-view-read.d.ts.map +1 -1
- package/lib/client/view/worktree-view-read.js +34 -17
- package/lib/client/view/worktree-view-read.js.map +1 -1
- package/lib/client/worktree-connection.d.ts +3 -0
- package/lib/client/worktree-connection.d.ts.map +1 -1
- package/lib/client/worktree-connection.js +18 -1
- package/lib/client/worktree-connection.js.map +1 -1
- package/lib/client.js +1386 -434
- package/lib/client.js.map +1 -1
- package/lib/contract/index.contract.js +15 -0
- package/lib/contract/index.contract.js.map +1 -1
- package/lib/contract/index.d.ts +69 -12
- package/lib/contract/index.d.ts.map +1 -1
- package/lib/contract/index.js +3 -0
- package/lib/contract/index.js.map +1 -1
- package/lib/host/remote.d.ts.map +1 -1
- package/lib/host/remote.js +7 -0
- package/lib/host/remote.js.map +1 -1
- package/lib/host/service.d.ts +13 -6
- package/lib/host/service.d.ts.map +1 -1
- package/lib/host/service.js +18 -0
- package/lib/host/service.js.map +1 -1
- package/lib/manage/manager-git-history.d.ts +5 -7
- package/lib/manage/manager-git-history.d.ts.map +1 -1
- package/lib/manage/manager-git-history.js +158 -65
- package/lib/manage/manager-git-history.js.map +1 -1
- package/lib/manage/manager-git-paths.d.ts +7 -0
- package/lib/manage/manager-git-paths.d.ts.map +1 -0
- package/lib/manage/manager-git-paths.js +62 -0
- package/lib/manage/manager-git-paths.js.map +1 -0
- package/lib/manage/manager-worktrees.d.ts +9 -1
- package/lib/manage/manager-worktrees.d.ts.map +1 -1
- package/lib/manage/manager-worktrees.js +69 -14
- package/lib/manage/manager-worktrees.js.map +1 -1
- package/lib/manage/manager.d.ts +12 -5
- package/lib/manage/manager.d.ts.map +1 -1
- package/lib/manage/manager.js +64 -3
- package/lib/manage/manager.js.map +1 -1
- package/lib/provider/git/adapter.d.ts +13 -12
- package/lib/provider/git/adapter.d.ts.map +1 -1
- package/lib/provider/git/adapter.js +152 -29
- package/lib/provider/git/adapter.js.map +1 -1
- package/lib/provider/git/branch-name.d.ts +5 -0
- package/lib/provider/git/branch-name.d.ts.map +1 -0
- package/lib/provider/git/branch-name.js +16 -0
- package/lib/provider/git/branch-name.js.map +1 -0
- package/lib/provider/git/branches.d.ts +11 -0
- package/lib/provider/git/branches.d.ts.map +1 -0
- package/lib/provider/git/branches.js +37 -0
- package/lib/provider/git/branches.js.map +1 -0
- package/lib/provider/sidecar/repository.d.ts.map +1 -1
- package/lib/provider/sidecar/repository.js +2 -1
- package/lib/provider/sidecar/repository.js.map +1 -1
- package/lib/provider/sidecar/sidecar-schema.d.ts.map +1 -1
- package/lib/provider/sidecar/sidecar-schema.js +2 -0
- 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 +18 -6
- package/lib/provider/transaction/operations/create.js.map +1 -1
- package/lib/provider/transaction/recovery/failure-recovery.d.ts.map +1 -1
- package/lib/provider/transaction/recovery/failure-recovery.js +3 -2
- package/lib/provider/transaction/recovery/failure-recovery.js.map +1 -1
- package/lib/provider/transaction/recovery/recover.d.ts.map +1 -1
- package/lib/provider/transaction/recovery/recover.js +8 -5
- package/lib/provider/transaction/recovery/recover.js.map +1 -1
- package/lib/provider/transaction/support/journal.d.ts.map +1 -1
- package/lib/provider/transaction/support/journal.js +1 -0
- package/lib/provider/transaction/support/journal.js.map +1 -1
- package/lib/provider/types.d.ts +24 -4
- package/lib/provider/types.d.ts.map +1 -1
- package/lib/provider/types.js.map +1 -1
- package/lib/tool/list-worktrees.d.ts.map +1 -1
- package/lib/tool/list-worktrees.js +9 -2
- package/lib/tool/list-worktrees.js.map +1 -1
- package/lib/tool/new-worktree.js +2 -2
- package/lib/tool/new-worktree.js.map +1 -1
- package/lib/typert.host.js +216 -46
- package/lib/typert.remote-client.d.ts +11 -5
- package/lib/typert.remote-client.d.ts.map +1 -1
- package/lib/typert.remote-client.js +216 -46
- package/package.json +4 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manager-git-history.d.ts","sourceRoot":"","sources":["../../src/manage/manager-git-history.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"manager-git-history.d.ts","sourceRoot":"","sources":["../../src/manage/manager-git-history.ts"],"names":[],"mappings":"AAGA,OAAO,EAOL,KAAK,sBAAsB,EAC3B,KAAK,6BAA6B,EAElC,KAAK,mBAAmB,EACxB,KAAK,0BAA0B,EAC/B,KAAK,kBAAkB,EACvB,KAAK,yBAAyB,EAE/B,MAAM,sBAAsB,CAAC;AAG9B,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAuSnE,wFAAwF;AACxF,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,sBAAsB,EAC/B,KAAK,EAAE;IACL,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;CACtC,GACA,OAAO,CAAC,MAAM,CAAC,CAkFjB;AA4TD,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,sBAAsB,EAC/B,KAAK,EAAE,yBAAyB,GAC/B,OAAO,CAAC,kBAAkB,CAAC,CAoE7B;AAwnBD,sGAAsG;AACtG,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,sBAAsB,EAC/B,KAAK,EAAE,6BAA6B,GACnC,OAAO,CAAC,sBAAsB,CAAC,CAMjC;AAED,yGAAyG;AACzG,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,sBAAsB,EAC/B,KAAK,EAAE,0BAA0B,GAChC,OAAO,CAAC,mBAAmB,CAAC,CAM9B"}
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
import { isSafeBranchIdentity } from '../provider/git/branch-name.js';
|
|
2
|
+
import { readBranchFacts, findBranchIdentity, canonicalBranchIdentity } from '../provider/git/branches.js';
|
|
1
3
|
import { gitWorktreeRoot } from '../contract/index.js';
|
|
2
4
|
import { WORKTREE_GIT_COMMIT_PATTERN, WORKTREE_GIT_SUMMARY, WORKTREE_GIT_WORKING_TREE, } from '../contract/index.js';
|
|
3
5
|
import { providerError } from '../provider/types.js';
|
|
4
6
|
import { isDirectory, requireWorkspace, samePhysicalPath } from './manager-support.js';
|
|
7
|
+
import { projectWorktreePath } from '../provider/worktree-path.js';
|
|
8
|
+
import { workspaceGitPaths } from './manager-git-paths.js';
|
|
5
9
|
/** Shared with Contract/Provider/Client so commit-SHA validation cannot drift. */
|
|
6
10
|
const COMMIT_PATTERN = WORKTREE_GIT_COMMIT_PATTERN;
|
|
7
11
|
/**
|
|
@@ -57,14 +61,32 @@ async function findLiveWorktree(worktrees, absolutePath) {
|
|
|
57
61
|
}
|
|
58
62
|
return undefined;
|
|
59
63
|
}
|
|
60
|
-
async function resolveWorktree(context, input) {
|
|
64
|
+
async function resolveWorktree(context, input, compareMain = true) {
|
|
61
65
|
const workspace = await requireWorkspace(context, input.workspaceId);
|
|
62
66
|
if (isMainWorktreeId(input.worktreeId, input.workspaceId)) {
|
|
63
67
|
// The imported Workspace can be nested, while committed Git paths are root-relative.
|
|
64
68
|
const repositoryRoot = context.git.resolveRepositoryRoot
|
|
65
69
|
? await context.git.resolveRepositoryRoot(workspace.rootPath, { signal: context.signal })
|
|
66
70
|
: workspace.rootPath;
|
|
67
|
-
|
|
71
|
+
if (compareMain && input.baseBranch !== undefined) {
|
|
72
|
+
// A request-local checkout context reuses comparison and path authorization.
|
|
73
|
+
// It is never read from or written to the sidecar.
|
|
74
|
+
const headCommit = await gitRead(context, 'resolveCommit', input.worktreeId)(repositoryRoot, 'HEAD', { signal: context.signal });
|
|
75
|
+
return { main: false, value: {
|
|
76
|
+
workspaceRoot: workspace.rootPath,
|
|
77
|
+
repositoryRoot,
|
|
78
|
+
record: {
|
|
79
|
+
workspaceId: input.workspaceId, worktreeId: input.worktreeId,
|
|
80
|
+
absolutePath: workspace.rootPath, gitRootPath: repositoryRoot,
|
|
81
|
+
branch: 'HEAD', source: 'external', status: 'active',
|
|
82
|
+
},
|
|
83
|
+
live: { absolutePath: repositoryRoot, headCommit },
|
|
84
|
+
} };
|
|
85
|
+
}
|
|
86
|
+
const headCommit = compareMain && context.git.resolveCommit
|
|
87
|
+
? await context.git.resolveCommit(repositoryRoot, 'HEAD', { signal: context.signal })
|
|
88
|
+
: undefined;
|
|
89
|
+
return { main: true, workspaceRoot: workspace.rootPath, repositoryRoot, headCommit };
|
|
68
90
|
}
|
|
69
91
|
const snapshot = await context.sidecar.read(input.workspaceId);
|
|
70
92
|
if (snapshot.pendingOperation !== undefined || (snapshot.recoveryIssues?.length ?? 0) > 0) {
|
|
@@ -110,31 +132,16 @@ async function resolveWorktree(context, input) {
|
|
|
110
132
|
workspaceRoot: workspace.rootPath,
|
|
111
133
|
repositoryRoot,
|
|
112
134
|
record,
|
|
113
|
-
live
|
|
135
|
+
live: live.headCommit === undefined && context.git.resolveCommit
|
|
136
|
+
? { ...live, headCommit: await context.git.resolveCommit(gitWorktreeRoot(record), 'HEAD', { signal: context.signal }) }
|
|
137
|
+
: live,
|
|
114
138
|
},
|
|
115
139
|
};
|
|
116
140
|
}
|
|
117
|
-
/** True when a branch name can be safely handed to Git as a ref argument. */
|
|
118
|
-
function isSafeBranchName(branch) {
|
|
119
|
-
if (branch.length === 0 || branch.startsWith('-'))
|
|
120
|
-
return false;
|
|
121
|
-
// Only a local branch short name may be selected. A full ref path is an
|
|
122
|
-
// arbitrary-ref attempt and is rejected instead of degrading to unavailable.
|
|
123
|
-
if (branch.startsWith('refs/'))
|
|
124
|
-
return false;
|
|
125
|
-
for (const character of branch) {
|
|
126
|
-
const code = character.codePointAt(0) ?? 0;
|
|
127
|
-
if (code < 0x20 || code === 0x7f)
|
|
128
|
-
return false;
|
|
129
|
-
}
|
|
130
|
-
return true;
|
|
131
|
-
}
|
|
132
141
|
/**
|
|
133
|
-
* Resolve
|
|
134
|
-
* under `refs/heads/`, so tags, remote-tracking branches, and arbitrary refs are
|
|
135
|
-
* rejected even though the browser may send any string.
|
|
142
|
+
* Resolve only enumerated branch identities. Tags and arbitrary revision expressions are rejected.
|
|
136
143
|
*/
|
|
137
|
-
async function
|
|
144
|
+
async function resolveBranchCommit(context, resolved, baseBranch, options = {}) {
|
|
138
145
|
const unavailable = () => {
|
|
139
146
|
throw providerError('WORKTREE_STATE_CONFLICT', 'Selected baseline branch is unavailable: ' + baseBranch, {
|
|
140
147
|
worktreeId: resolved.record.worktreeId,
|
|
@@ -143,20 +150,20 @@ async function resolveLocalBranchCommit(context, resolved, baseBranch, options =
|
|
|
143
150
|
};
|
|
144
151
|
// An unsafe ref is always a hard failure: arbitrary refs must never resolve,
|
|
145
152
|
// even for a read that is allowed to degrade to an unavailable projection.
|
|
146
|
-
if (!
|
|
153
|
+
if (!isSafeBranchIdentity(baseBranch))
|
|
147
154
|
unavailable();
|
|
148
|
-
const listBranches = gitRead(context, 'listBranches', resolved.record.worktreeId);
|
|
149
155
|
const resolveCommit = gitRead(context, 'resolveCommit', resolved.record.worktreeId);
|
|
150
|
-
const branches = await
|
|
156
|
+
const branches = await readBranchFacts(context.git, resolved.repositoryRoot, { signal: context.signal });
|
|
157
|
+
const branch = findBranchIdentity(branches, baseBranch);
|
|
151
158
|
// A baseline branch that no longer exists is an expected unavailable state for
|
|
152
159
|
// reads, so the Dashboard can show an honest empty projection instead of a
|
|
153
160
|
// generic Git failure. Writes keep the strict behaviour.
|
|
154
|
-
if (
|
|
161
|
+
if (branch === undefined) {
|
|
155
162
|
if (options.allowUnavailable === true)
|
|
156
163
|
return undefined;
|
|
157
164
|
unavailable();
|
|
158
165
|
}
|
|
159
|
-
const commit = await resolveCommit(gitWorktreeRoot(resolved.record), baseBranch, {
|
|
166
|
+
const commit = await resolveCommit(gitWorktreeRoot(resolved.record), branch?.ref ?? baseBranch, {
|
|
160
167
|
signal: context.signal,
|
|
161
168
|
});
|
|
162
169
|
if (!COMMIT_PATTERN.test(commit)) {
|
|
@@ -165,18 +172,19 @@ async function resolveLocalBranchCommit(context, resolved, baseBranch, options =
|
|
|
165
172
|
baseBranch,
|
|
166
173
|
});
|
|
167
174
|
}
|
|
168
|
-
return commit;
|
|
175
|
+
return { commit, branch: branch };
|
|
169
176
|
}
|
|
170
177
|
async function resolveSelectedBaseline(context, resolved, rawBranch) {
|
|
171
178
|
const baseBranch = rawBranch.trim();
|
|
172
179
|
if (baseBranch.length === 0)
|
|
173
180
|
return undefined;
|
|
174
181
|
// A vanished baseline branch degrades to an unavailable projection for reads.
|
|
175
|
-
const
|
|
182
|
+
const selected = await resolveBranchCommit(context, resolved, baseBranch, {
|
|
176
183
|
allowUnavailable: true,
|
|
177
184
|
});
|
|
178
|
-
if (
|
|
185
|
+
if (selected === undefined)
|
|
179
186
|
return undefined;
|
|
187
|
+
const { commit } = selected;
|
|
180
188
|
// A selected branch can move after the Worktree was created. Match GitHub's
|
|
181
189
|
// pull-request range by diffing from the two heads' merge base to Worktree
|
|
182
190
|
// HEAD, while retaining the selected branch tip for ahead/behind counts.
|
|
@@ -201,14 +209,14 @@ async function resolveSelectedBaseline(context, resolved, rawBranch) {
|
|
|
201
209
|
treeBaseCommit: mergeBase ?? commit,
|
|
202
210
|
};
|
|
203
211
|
}
|
|
204
|
-
/** Persist a user-selected local branch as the
|
|
212
|
+
/** Persist a user-selected local or fetched remote branch as the Dashboard baseline. */
|
|
205
213
|
export async function updateWorktreeBaseBranch(context, input) {
|
|
206
214
|
if (typeof input.baseBranch !== 'string') {
|
|
207
215
|
throw providerError('WORKTREE_STATE_CONFLICT', 'The baseline branch must be a string', {
|
|
208
216
|
worktreeId: input.worktreeId,
|
|
209
217
|
});
|
|
210
218
|
}
|
|
211
|
-
|
|
219
|
+
let baseBranch = input.baseBranch.trim();
|
|
212
220
|
if (baseBranch.length === 0) {
|
|
213
221
|
throw providerError('WORKTREE_STATE_CONFLICT', 'The baseline branch cannot be empty', {
|
|
214
222
|
worktreeId: input.worktreeId,
|
|
@@ -220,7 +228,7 @@ export async function updateWorktreeBaseBranch(context, input) {
|
|
|
220
228
|
});
|
|
221
229
|
}
|
|
222
230
|
const expectedBaseBranch = input.expectedBaseBranch?.trim() || undefined;
|
|
223
|
-
const resolved = await resolveWorktree(context, input);
|
|
231
|
+
const resolved = await resolveWorktree(context, input, false);
|
|
224
232
|
if (resolved.main) {
|
|
225
233
|
throw providerError('WORKTREE_STATE_CONFLICT', 'The local Workspace has no editable baseline', {
|
|
226
234
|
workspaceId: input.workspaceId,
|
|
@@ -235,7 +243,13 @@ export async function updateWorktreeBaseBranch(context, input) {
|
|
|
235
243
|
}
|
|
236
244
|
// Resolve the replacement branch against the pinned repository root before the
|
|
237
245
|
// sidecar mutation, so an unavailable branch fails closed without a write.
|
|
238
|
-
await
|
|
246
|
+
const selected = await resolveBranchCommit(context, resolved.value, baseBranch);
|
|
247
|
+
if (selected.branch.kind !== 'remote' && selected.branch.name === currentBranch) {
|
|
248
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'The baseline branch cannot be the current Worktree branch', {
|
|
249
|
+
worktreeId: input.worktreeId, baseBranch,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
baseBranch = canonicalBranchIdentity(selected.branch, baseBranch);
|
|
239
253
|
return context.sidecar.mutate(input.workspaceId, (snapshot) => {
|
|
240
254
|
if (snapshot.pendingOperation !== undefined || (snapshot.recoveryIssues?.length ?? 0) > 0) {
|
|
241
255
|
throw providerError('WORKTREE_RECOVERY_REQUIRED', 'Workspace Worktree state needs recovery: ' + input.workspaceId, { workspaceId: input.workspaceId });
|
|
@@ -327,7 +341,7 @@ async function resolveBaseline(context, resolved, requestedBaseBranch) {
|
|
|
327
341
|
async function requireBaseline(context, resolved, requestedBaseBranch) {
|
|
328
342
|
const baseline = await resolveBaseline(context, resolved, requestedBaseBranch);
|
|
329
343
|
if (baseline === undefined) {
|
|
330
|
-
throw providerError('WORKTREE_STATE_CONFLICT', 'Select a
|
|
344
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'Select a baseline branch before reading Git changes', { worktreeId: resolved.record.worktreeId });
|
|
331
345
|
}
|
|
332
346
|
return baseline;
|
|
333
347
|
}
|
|
@@ -351,13 +365,9 @@ async function readCommitDivergence(context, resolved, comparison, head) {
|
|
|
351
365
|
/**
|
|
352
366
|
* Authorize commit SHAs against one pinned comparison projection.
|
|
353
367
|
*
|
|
354
|
-
*
|
|
355
|
-
*
|
|
356
|
-
*
|
|
357
|
-
* but a wide multi-commit selection now costs one projection read instead of
|
|
358
|
-
* several Git processes per candidate SHA. The visible projection is therefore
|
|
359
|
-
* the authorization boundary, matching the truncated 200-commit history the
|
|
360
|
-
* browser can actually select from.
|
|
368
|
+
* Candidates must be reachable from HEAD and excluded from baseline ancestry,
|
|
369
|
+
* independently of which 50-commit page the browser has loaded. Legacy adapters
|
|
370
|
+
* without the membership capability retain bounded projection authorization.
|
|
361
371
|
*/
|
|
362
372
|
async function authorizeCommits(context, resolved, comparison, candidates) {
|
|
363
373
|
const worktreeId = resolved.record.worktreeId;
|
|
@@ -369,6 +379,11 @@ async function authorizeCommits(context, resolved, comparison, candidates) {
|
|
|
369
379
|
}
|
|
370
380
|
return candidate;
|
|
371
381
|
});
|
|
382
|
+
const headCommit = resolved.live.headCommit;
|
|
383
|
+
if (headCommit !== undefined && (context.git.containsCommits || context.git.containsCommit)) {
|
|
384
|
+
await authorizeMembership(context, gitWorktreeRoot(resolved.record), worktreeId, requested, headCommit, comparison.baseHeadCommit);
|
|
385
|
+
return requested;
|
|
386
|
+
}
|
|
372
387
|
const listCommits = gitRead(context, 'listCommits', worktreeId);
|
|
373
388
|
const history = await listCommits(gitWorktreeRoot(resolved.record), comparison.baseHeadCommit, {
|
|
374
389
|
signal: context.signal,
|
|
@@ -384,6 +399,22 @@ async function authorizeCommits(context, resolved, comparison, candidates) {
|
|
|
384
399
|
}
|
|
385
400
|
return requested;
|
|
386
401
|
}
|
|
402
|
+
/** Share a fixed checkout identity across all membership checks in a request. */
|
|
403
|
+
async function authorizeMembership(context, root, worktreeId, commits, headCommit, baseCommit) {
|
|
404
|
+
const options = { signal: context.signal };
|
|
405
|
+
const allowed = context.git.containsCommits
|
|
406
|
+
? await context.git.containsCommits(root, commits, headCommit, baseCommit, options)
|
|
407
|
+
: await (async () => {
|
|
408
|
+
for (const commit of commits) {
|
|
409
|
+
if (!await context.git.containsCommit(root, commit, headCommit, baseCommit, options))
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
return true;
|
|
413
|
+
})();
|
|
414
|
+
if (!allowed) {
|
|
415
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'The requested commits are outside this checkout history', { worktreeId });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
387
418
|
async function authorizeCommit(context, resolved, commitInput, requestedBaseBranch) {
|
|
388
419
|
const comparison = await requireBaseline(context, resolved, requestedBaseBranch);
|
|
389
420
|
const [commit] = await authorizeCommits(context, resolved, comparison, [commitInput]);
|
|
@@ -396,13 +427,13 @@ async function authorizeWorkingTree(context, resolved, requestedBaseBranch) {
|
|
|
396
427
|
await requireBaseline(context, resolved, requestedBaseBranch);
|
|
397
428
|
}
|
|
398
429
|
/** Read Main's bounded HEAD history and expose live Workspace-root changes first. */
|
|
399
|
-
async function listMainCommits(context, workspaceRoot, worktreeId) {
|
|
430
|
+
async function listMainCommits(context, workspaceRoot, worktreeId, offset = 0) {
|
|
400
431
|
const listCommits = gitRead(context, 'listCommits', worktreeId);
|
|
401
432
|
const readWorkingTreePaths = gitReadOptional(context, 'listWorkingTreeChangedPaths')
|
|
402
433
|
?? gitReadOptional(context, 'listWorkingTreeFiles');
|
|
403
434
|
const [history, workingTreeFiles] = await Promise.all([
|
|
404
|
-
listCommits(workspaceRoot, undefined, { signal: context.signal }),
|
|
405
|
-
readWorkingTreePaths === undefined
|
|
435
|
+
listCommits(workspaceRoot, undefined, { signal: context.signal, offset }),
|
|
436
|
+
offset > 0 || readWorkingTreePaths === undefined
|
|
406
437
|
? Promise.resolve({ files: [], truncated: false })
|
|
407
438
|
: readWorkingTreePaths(workspaceRoot, { signal: context.signal }),
|
|
408
439
|
]);
|
|
@@ -414,12 +445,16 @@ async function listMainCommits(context, workspaceRoot, worktreeId) {
|
|
|
414
445
|
: history.commits,
|
|
415
446
|
};
|
|
416
447
|
}
|
|
417
|
-
async function authorizeMainCommit(context, workspaceRoot, worktreeId, commitInput) {
|
|
448
|
+
async function authorizeMainCommit(context, workspaceRoot, worktreeId, commitInput, headCommit) {
|
|
418
449
|
if (typeof commitInput !== 'string' || !COMMIT_PATTERN.test(commitInput)) {
|
|
419
450
|
throw providerError('WORKTREE_STATE_CONFLICT', 'The requested commit is not a valid commit SHA', {
|
|
420
451
|
worktreeId,
|
|
421
452
|
});
|
|
422
453
|
}
|
|
454
|
+
if (headCommit !== undefined && (context.git.containsCommits || context.git.containsCommit)) {
|
|
455
|
+
await authorizeMembership(context, workspaceRoot, worktreeId, [commitInput], headCommit);
|
|
456
|
+
return commitInput;
|
|
457
|
+
}
|
|
423
458
|
const history = await listMainCommits(context, workspaceRoot, worktreeId);
|
|
424
459
|
if (!history.commits.some((commit) => commit.sha === commitInput)) {
|
|
425
460
|
throw providerError('WORKTREE_STATE_CONFLICT', 'The requested commit is outside the visible Main history', {
|
|
@@ -430,7 +465,7 @@ async function authorizeMainCommit(context, workspaceRoot, worktreeId, commitInp
|
|
|
430
465
|
return commitInput;
|
|
431
466
|
}
|
|
432
467
|
const MAX_AGGREGATE_COMMITS = 200;
|
|
433
|
-
async function authorizeMainAggregate(context, workspaceRoot, worktreeId, selection) {
|
|
468
|
+
async function authorizeMainAggregate(context, workspaceRoot, worktreeId, selection, headCommit) {
|
|
434
469
|
if (selection.kind !== 'commits') {
|
|
435
470
|
throw providerError('WORKTREE_STATE_CONFLICT', 'Main Git supports committed selections only', {
|
|
436
471
|
worktreeId,
|
|
@@ -454,6 +489,10 @@ async function authorizeMainAggregate(context, workspaceRoot, worktreeId, select
|
|
|
454
489
|
worktreeId,
|
|
455
490
|
});
|
|
456
491
|
}
|
|
492
|
+
if (headCommit !== undefined && (context.git.containsCommits || context.git.containsCommit)) {
|
|
493
|
+
await authorizeMembership(context, workspaceRoot, worktreeId, requested, headCommit);
|
|
494
|
+
return { selection: { kind: 'commits', commits: requested }, commits: requested };
|
|
495
|
+
}
|
|
457
496
|
const history = await listMainCommits(context, workspaceRoot, worktreeId);
|
|
458
497
|
const members = new Set(history.commits
|
|
459
498
|
.filter((commit) => commit.kind !== 'working-tree')
|
|
@@ -472,10 +511,26 @@ async function authorizeMainAggregate(context, workspaceRoot, worktreeId, select
|
|
|
472
511
|
};
|
|
473
512
|
}
|
|
474
513
|
export async function listWorktreeCommits(context, input) {
|
|
514
|
+
const offset = input.offset ?? 0;
|
|
515
|
+
if (!Number.isSafeInteger(offset) || offset < 0 ||
|
|
516
|
+
(offset > 0 && (input.expectedHeadCommit === undefined ||
|
|
517
|
+
(input.baseBranch !== undefined && input.expectedBaselineCommit === undefined)))) {
|
|
518
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'Invalid history page', { worktreeId: input.worktreeId });
|
|
519
|
+
}
|
|
520
|
+
const checkProjection = (head, baseline) => {
|
|
521
|
+
if ((input.expectedHeadCommit !== undefined && input.expectedHeadCommit !== head) ||
|
|
522
|
+
(input.expectedBaselineCommit !== undefined && input.expectedBaselineCommit !== baseline)) {
|
|
523
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'Git history changed; refresh before loading more', {
|
|
524
|
+
worktreeId: input.worktreeId,
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
};
|
|
475
528
|
const resolved = await resolveWorktree(context, input);
|
|
476
529
|
if (resolved.main) {
|
|
477
|
-
const history = await listMainCommits(context, resolved.
|
|
530
|
+
const history = await listMainCommits(context, resolved.repositoryRoot, input.worktreeId, offset);
|
|
531
|
+
checkProjection(history.headCommit);
|
|
478
532
|
return {
|
|
533
|
+
...(history.nextOffset === undefined ? {} : { nextOffset: history.nextOffset }),
|
|
479
534
|
headCommit: history.headCommit,
|
|
480
535
|
commits: history.commits,
|
|
481
536
|
truncated: history.truncated,
|
|
@@ -488,17 +543,23 @@ export async function listWorktreeCommits(context, input) {
|
|
|
488
543
|
? 'baseline-unselected'
|
|
489
544
|
: 'baseline-unknown');
|
|
490
545
|
}
|
|
546
|
+
if (offset > 0 && input.expectedBaselineCommit === undefined) {
|
|
547
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'A comparison page requires its baseline commit', {
|
|
548
|
+
worktreeId: input.worktreeId,
|
|
549
|
+
});
|
|
550
|
+
}
|
|
491
551
|
const listCommits = gitRead(context, 'listCommits', input.worktreeId);
|
|
492
552
|
// The working-tree entry only needs the changed-path projection here; its file
|
|
493
553
|
// list and line counts are read on demand when the entry is selected.
|
|
494
554
|
const readWorkingTreePaths = gitReadOptional(context, 'listWorkingTreeChangedPaths')
|
|
495
555
|
?? gitReadOptional(context, 'listWorkingTreeFiles');
|
|
496
556
|
const [history, workingTreeFiles] = await Promise.all([
|
|
497
|
-
listCommits(gitWorktreeRoot(resolved.value.record), baseline.baseHeadCommit, { signal: context.signal }),
|
|
498
|
-
readWorkingTreePaths === undefined
|
|
557
|
+
listCommits(gitWorktreeRoot(resolved.value.record), baseline.baseHeadCommit, { signal: context.signal, offset }),
|
|
558
|
+
offset > 0 || readWorkingTreePaths === undefined
|
|
499
559
|
? Promise.resolve({ files: [], truncated: false })
|
|
500
560
|
: readWorkingTreePaths(gitWorktreeRoot(resolved.value.record), { signal: context.signal }),
|
|
501
561
|
]);
|
|
562
|
+
checkProjection(history.headCommit, baseline.baseline.commit);
|
|
502
563
|
const divergence = await readCommitDivergence(context, resolved.value, baseline, history.headCommit);
|
|
503
564
|
return {
|
|
504
565
|
...history,
|
|
@@ -672,17 +733,17 @@ async function selectedAggregateFileDiff(context, absolutePath, worktreeId, sele
|
|
|
672
733
|
segments,
|
|
673
734
|
};
|
|
674
735
|
}
|
|
675
|
-
async function aggregateMainFileDiff(context, workspaceRoot, repositoryRoot, worktreeId, selection, requestedPath) {
|
|
736
|
+
async function aggregateMainFileDiff(context, workspaceRoot, repositoryRoot, worktreeId, selection, requestedPath, headCommit) {
|
|
676
737
|
if (typeof requestedPath !== 'string' || requestedPath.length === 0) {
|
|
677
738
|
throw providerError('WORKTREE_STATE_CONFLICT', 'A changed file path is required', {
|
|
678
739
|
worktreeId,
|
|
679
740
|
});
|
|
680
741
|
}
|
|
681
|
-
const authorized = await authorizeMainAggregate(context, workspaceRoot, worktreeId, selection);
|
|
742
|
+
const authorized = await authorizeMainAggregate(context, workspaceRoot, worktreeId, selection, headCommit);
|
|
682
743
|
return selectedAggregateFileDiff(context, repositoryRoot, worktreeId, authorized.selection, authorized.commits, requestedPath);
|
|
683
744
|
}
|
|
684
|
-
async function listMainAggregateFiles(context, workspaceRoot, repositoryRoot, worktreeId, selection) {
|
|
685
|
-
const authorized = await authorizeMainAggregate(context, workspaceRoot, worktreeId, selection);
|
|
745
|
+
async function listMainAggregateFiles(context, workspaceRoot, repositoryRoot, worktreeId, selection, headCommit) {
|
|
746
|
+
const authorized = await authorizeMainAggregate(context, workspaceRoot, worktreeId, selection, headCommit);
|
|
686
747
|
const groups = await selectedCommitFiles(context, repositoryRoot, worktreeId, authorized.commits);
|
|
687
748
|
return {
|
|
688
749
|
commit: authorized.commits[0],
|
|
@@ -736,23 +797,22 @@ async function aggregateFileDiff(context, resolved, selection, requestedPath, re
|
|
|
736
797
|
}
|
|
737
798
|
return selectedAggregateFileDiff(context, gitWorktreeRoot(resolved.record), resolved.record.worktreeId, authorized.selection, authorized.commits, requestedPath);
|
|
738
799
|
}
|
|
739
|
-
|
|
740
|
-
const resolved = await resolveWorktree(context, input);
|
|
800
|
+
async function listCheckoutCommitFiles(context, input, resolved) {
|
|
741
801
|
const requested = requestedDiff(input, input.worktreeId);
|
|
742
802
|
if (resolved.main) {
|
|
743
803
|
if (requested.kind === 'aggregate') {
|
|
744
|
-
return listMainAggregateFiles(context, resolved.
|
|
804
|
+
return listMainAggregateFiles(context, resolved.repositoryRoot, resolved.repositoryRoot, input.worktreeId, requested.selection, resolved.headCommit);
|
|
745
805
|
}
|
|
746
806
|
if (requested.commit === WORKTREE_GIT_WORKING_TREE) {
|
|
747
807
|
const listWorkingTreeFiles = gitRead(context, 'listWorkingTreeFiles', input.worktreeId);
|
|
748
|
-
const read = await listWorkingTreeFiles(resolved.
|
|
808
|
+
const read = await listWorkingTreeFiles(resolved.repositoryRoot, { signal: context.signal });
|
|
749
809
|
return {
|
|
750
810
|
commit: WORKTREE_GIT_WORKING_TREE,
|
|
751
811
|
files: read.files,
|
|
752
812
|
...(read.truncated ? { truncated: true } : {}),
|
|
753
813
|
};
|
|
754
814
|
}
|
|
755
|
-
const commit = await authorizeMainCommit(context, resolved.
|
|
815
|
+
const commit = await authorizeMainCommit(context, resolved.repositoryRoot, input.worktreeId, requested.commit, resolved.headCommit);
|
|
756
816
|
const listCommitFiles = gitRead(context, 'listCommitFiles', input.worktreeId);
|
|
757
817
|
const read = await listCommitFiles(resolved.repositoryRoot, commit, { signal: context.signal });
|
|
758
818
|
return {
|
|
@@ -784,8 +844,7 @@ export async function listWorktreeCommitFiles(context, input) {
|
|
|
784
844
|
...(read.truncated ? { truncated: true } : {}),
|
|
785
845
|
};
|
|
786
846
|
}
|
|
787
|
-
|
|
788
|
-
const resolved = await resolveWorktree(context, input);
|
|
847
|
+
async function getCheckoutCommitFileDiff(context, input, resolved) {
|
|
789
848
|
const requested = requestedDiff(input, input.worktreeId);
|
|
790
849
|
if (resolved.main) {
|
|
791
850
|
if (requested.kind === 'aggregate') {
|
|
@@ -794,7 +853,7 @@ export async function getWorktreeCommitFileDiff(context, input) {
|
|
|
794
853
|
worktreeId: input.worktreeId,
|
|
795
854
|
});
|
|
796
855
|
}
|
|
797
|
-
return aggregateMainFileDiff(context, resolved.
|
|
856
|
+
return aggregateMainFileDiff(context, resolved.repositoryRoot, resolved.repositoryRoot, input.worktreeId, requested.selection, input.path, resolved.headCommit);
|
|
798
857
|
}
|
|
799
858
|
if (typeof input.path !== 'string' || input.path.length === 0) {
|
|
800
859
|
throw providerError('WORKTREE_STATE_CONFLICT', 'A changed file path is required', {
|
|
@@ -805,7 +864,7 @@ export async function getWorktreeCommitFileDiff(context, input) {
|
|
|
805
864
|
const readWorkingTreeFileDiff = gitRead(context, 'readWorkingTreeFileDiff', input.worktreeId);
|
|
806
865
|
const readChangedPaths = gitReadOptional(context, 'listWorkingTreeChangedPaths')
|
|
807
866
|
?? gitRead(context, 'listWorkingTreeFiles', input.worktreeId);
|
|
808
|
-
const readPaths = await readChangedPaths(resolved.
|
|
867
|
+
const readPaths = await readChangedPaths(resolved.repositoryRoot, { signal: context.signal });
|
|
809
868
|
const changedFile = findChangedFile(readPaths.files, input.path);
|
|
810
869
|
if (changedFile === undefined) {
|
|
811
870
|
throw providerError('WORKTREE_STATE_CONFLICT', 'The requested path is not changed in the Main working tree', {
|
|
@@ -814,10 +873,10 @@ export async function getWorktreeCommitFileDiff(context, input) {
|
|
|
814
873
|
path: input.path,
|
|
815
874
|
});
|
|
816
875
|
}
|
|
817
|
-
const diff = await readWorkingTreeFileDiff(resolved.
|
|
876
|
+
const diff = await readWorkingTreeFileDiff(resolved.repositoryRoot, changedFile.path, { signal: context.signal });
|
|
818
877
|
return { ...diff, commit: WORKTREE_GIT_WORKING_TREE, path: input.path };
|
|
819
878
|
}
|
|
820
|
-
const commit = await authorizeMainCommit(context, resolved.
|
|
879
|
+
const commit = await authorizeMainCommit(context, resolved.repositoryRoot, input.worktreeId, requested.commit, resolved.headCommit);
|
|
821
880
|
const listCommitFiles = gitRead(context, 'listCommitFiles', input.worktreeId);
|
|
822
881
|
const filesRead = await listCommitFiles(resolved.repositoryRoot, commit, { signal: context.signal });
|
|
823
882
|
const changedFile = findChangedFile(filesRead.files, input.path);
|
|
@@ -886,4 +945,38 @@ export async function getWorktreeCommitFileDiff(context, input) {
|
|
|
886
945
|
path: input.path,
|
|
887
946
|
};
|
|
888
947
|
}
|
|
948
|
+
async function pathsFor(resolved) {
|
|
949
|
+
if (resolved.main)
|
|
950
|
+
return workspaceGitPaths(resolved.repositoryRoot, resolved.workspaceRoot);
|
|
951
|
+
const { record, workspaceRoot, repositoryRoot } = resolved.value;
|
|
952
|
+
const projected = await projectWorktreePath(record, workspaceRoot, repositoryRoot);
|
|
953
|
+
return workspaceGitPaths(gitWorktreeRoot(projected), projected.absolutePath);
|
|
954
|
+
}
|
|
955
|
+
/** Reject a projection if checkout HEAD moved during authorization or file reads. */
|
|
956
|
+
async function assertCheckoutHead(context, resolved, worktreeId) {
|
|
957
|
+
const headCommit = resolved.main ? resolved.headCommit : resolved.value.live.headCommit;
|
|
958
|
+
if (headCommit === undefined || context.git.resolveCommit === undefined)
|
|
959
|
+
return;
|
|
960
|
+
const root = resolved.main ? resolved.repositoryRoot : gitWorktreeRoot(resolved.value.record);
|
|
961
|
+
const current = await context.git.resolveCommit(root, 'HEAD', { signal: context.signal });
|
|
962
|
+
if (current !== headCommit) {
|
|
963
|
+
throw providerError('WORKTREE_STATE_CONFLICT', 'Checkout HEAD changed; refresh Git data', { worktreeId });
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
/** Public changed-file identities always start at the Workspace directory, including ../ siblings. */
|
|
967
|
+
export async function listWorktreeCommitFiles(context, input) {
|
|
968
|
+
const resolved = await resolveWorktree(context, input);
|
|
969
|
+
const paths = await pathsFor(resolved);
|
|
970
|
+
const read = await listCheckoutCommitFiles(context, input, resolved);
|
|
971
|
+
await assertCheckoutHead(context, resolved, input.worktreeId);
|
|
972
|
+
return paths.projectFiles(read);
|
|
973
|
+
}
|
|
974
|
+
/** Translate exactly once, then keep the existing changed-file authorization in checkout coordinates. */
|
|
975
|
+
export async function getWorktreeCommitFileDiff(context, input) {
|
|
976
|
+
const resolved = await resolveWorktree(context, input);
|
|
977
|
+
const paths = await pathsFor(resolved);
|
|
978
|
+
const diff = await getCheckoutCommitFileDiff(context, { ...input, path: paths.toCheckout(input.path) }, resolved);
|
|
979
|
+
await assertCheckoutHead(context, resolved, input.worktreeId);
|
|
980
|
+
return { ...diff, path: input.path };
|
|
981
|
+
}
|
|
889
982
|
//# sourceMappingURL=manager-git-history.js.map
|