dsh-git-worktree 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +59 -0
- package/LICENSE +21 -21
- package/README.md +115 -37
- package/README.zh.md +115 -37
- package/cordis.patch.yml +7 -7
- package/lib/adapters/registry.js +40 -0
- package/lib/adapters/registry.js.map +1 -1
- package/lib/adapters/safe-file.js +34 -4
- package/lib/adapters/safe-file.js.map +1 -1
- package/lib/client/WorktreeCreateRow.d.ts +7 -0
- package/lib/client/WorktreeCreateRow.js +26 -0
- package/lib/client/WorktreeCreateRow.js.map +1 -0
- package/lib/client/WorktreeReviewRow.d.ts +11 -0
- package/lib/client/WorktreeReviewRow.js +67 -0
- package/lib/client/WorktreeReviewRow.js.map +1 -0
- package/lib/client/actions.d.ts +86 -0
- package/lib/client/actions.js +35 -0
- package/lib/client/actions.js.map +1 -0
- package/lib/client/console-remote/adapter.d.ts +3 -0
- package/lib/client/console-remote/adapter.js +59 -0
- package/lib/client/console-remote/adapter.js.map +1 -0
- package/lib/client/console-remote/index.d.ts +18 -0
- package/lib/client/console-remote/index.js +17 -0
- package/lib/client/console-remote/index.js.map +1 -0
- package/lib/client/index.d.ts +17 -0
- package/lib/client/index.js +44 -0
- package/lib/client/index.js.map +1 -0
- package/lib/client/model.d.ts +68 -0
- package/lib/client/model.js +127 -0
- package/lib/client/model.js.map +1 -0
- package/lib/client/pre-session/PreSessionWorktreeToggle.d.ts +16 -0
- package/lib/client/pre-session/PreSessionWorktreeToggle.js +128 -0
- package/lib/client/pre-session/PreSessionWorktreeToggle.js.map +1 -0
- package/lib/client/pre-session/controller.d.ts +42 -0
- package/lib/client/pre-session/controller.js +149 -0
- package/lib/client/pre-session/controller.js.map +1 -0
- package/lib/client/pre-session/index.d.ts +15 -0
- package/lib/client/pre-session/index.js +16 -0
- package/lib/client/pre-session/index.js.map +1 -0
- package/lib/client/review-console/ReviewActions.d.ts +15 -0
- package/lib/client/review-console/ReviewActions.js +219 -0
- package/lib/client/review-console/ReviewActions.js.map +1 -0
- package/lib/client/review-console/WorktreeReviewPanel.d.ts +20 -0
- package/lib/client/review-console/WorktreeReviewPanel.js +75 -0
- package/lib/client/review-console/WorktreeReviewPanel.js.map +1 -0
- package/lib/client/review-console/index.d.ts +8 -0
- package/lib/client/review-console/index.js +22 -0
- package/lib/client/review-console/index.js.map +1 -0
- package/lib/client/review-console/review-console.styles.d.ts +1 -0
- package/lib/client/review-console/review-console.styles.js +81 -0
- package/lib/client/review-console/review-console.styles.js.map +1 -0
- package/lib/client/review-console/status-events.d.ts +3 -0
- package/lib/client/review-console/status-events.js +6 -0
- package/lib/client/review-console/status-events.js.map +1 -0
- package/lib/client/styles.d.ts +1 -0
- package/lib/client/styles.js +78 -0
- package/lib/client/styles.js.map +1 -0
- package/lib/client/target-console/TargetStatusAction.d.ts +7 -0
- package/lib/client/target-console/TargetStatusAction.js +46 -0
- package/lib/client/target-console/TargetStatusAction.js.map +1 -0
- package/lib/client/target-console/WorktreeConsoleView.d.ts +9 -0
- package/lib/client/target-console/WorktreeConsoleView.js +362 -0
- package/lib/client/target-console/WorktreeConsoleView.js.map +1 -0
- package/lib/client/target-console/WorktreeReviewStatus.d.ts +9 -0
- package/lib/client/target-console/WorktreeReviewStatus.js +91 -0
- package/lib/client/target-console/WorktreeReviewStatus.js.map +1 -0
- package/lib/client/target-console/index.d.ts +14 -0
- package/lib/client/target-console/index.js +23 -0
- package/lib/client/target-console/index.js.map +1 -0
- package/lib/client/target-console/target-console.styles.d.ts +1 -0
- package/lib/client/target-console/target-console.styles.js +130 -0
- package/lib/client/target-console/target-console.styles.js.map +1 -0
- package/lib/client.js +6813 -0
- package/lib/commands.d.ts +2 -4
- package/lib/commands.js +54 -28
- package/lib/commands.js.map +1 -1
- package/lib/console-contract.d.ts +236 -0
- package/lib/console-contract.js +86 -0
- package/lib/console-contract.js.map +1 -0
- package/lib/console-host/control-plane.d.ts +29 -0
- package/lib/console-host/control-plane.js +443 -0
- package/lib/console-host/control-plane.js.map +1 -0
- package/lib/console-host/errors.d.ts +6 -0
- package/lib/console-host/errors.js +31 -0
- package/lib/console-host/errors.js.map +1 -0
- package/lib/console-host/projection.d.ts +12 -0
- package/lib/console-host/projection.js +131 -0
- package/lib/console-host/projection.js.map +1 -0
- package/lib/console-host/review-diff.d.ts +18 -0
- package/lib/console-host/review-diff.js +176 -0
- package/lib/console-host/review-diff.js.map +1 -0
- package/lib/console-host/service.d.ts +24 -0
- package/lib/console-host/service.js +131 -0
- package/lib/console-host/service.js.map +1 -0
- package/lib/console-remote/descriptors.d.ts +3 -0
- package/lib/console-remote/descriptors.js +96 -0
- package/lib/console-remote/descriptors.js.map +1 -0
- package/lib/console-remote/remote.d.ts +27 -0
- package/lib/console-remote/remote.js +4 -0
- package/lib/console-remote/remote.js.map +1 -0
- package/lib/console-remote/schemas.d.ts +29 -0
- package/lib/console-remote/schemas.js +143 -0
- package/lib/console-remote/schemas.js.map +1 -0
- package/lib/console-remote/typert.d.ts +3 -0
- package/lib/console-remote/typert.js +14 -0
- package/lib/console-remote/typert.js.map +1 -0
- package/lib/index.d.ts +22 -6
- package/lib/index.js +28 -10
- package/lib/index.js.map +1 -1
- package/lib/managed-worktree-path.d.ts +14 -13
- package/lib/managed-worktree-path.js +32 -22
- package/lib/managed-worktree-path.js.map +1 -1
- package/lib/ports.d.ts +37 -1
- package/lib/session-checkout-apply.d.ts +54 -1
- package/lib/session-checkout-apply.js +318 -28
- package/lib/session-checkout-apply.js.map +1 -1
- package/lib/session-checkout-module.js +853 -33
- package/lib/session-checkout-module.js.map +1 -1
- package/lib/session-target-context.d.ts +6 -0
- package/lib/session-target-context.js +16 -0
- package/lib/session-target-context.js.map +1 -0
- package/lib/tools.d.ts +5 -6
- package/lib/tools.js +43 -233
- package/lib/tools.js.map +1 -1
- package/lib/types.d.ts +57 -11
- package/lib/types.js +6 -6
- package/lib/types.js.map +1 -1
- package/package.json +116 -67
- package/scripts/check-publish.mjs +193 -0
- package/scripts/clean-lib.mjs +7 -0
- package/scripts/dev-dsh-lib.mjs +453 -0
- package/scripts/dev-dsh.mjs +89 -0
- package/scripts/dsh-source-runner.mjs +14 -0
package/lib/ports.d.ts
CHANGED
|
@@ -87,10 +87,11 @@ export interface ManagedCheckoutCreateJournal extends ManagedCheckoutJournalBase
|
|
|
87
87
|
step: 'creating_worktree';
|
|
88
88
|
}
|
|
89
89
|
export interface ManagedCheckoutMutationJournal extends ManagedCheckoutJournalBase {
|
|
90
|
-
operation: 'apply' | 'finish' | 'cleanup';
|
|
90
|
+
operation: 'apply' | 'preview' | 'rollback_preview' | 'finish' | 'finalize_preview' | 'cleanup';
|
|
91
91
|
step: 'planning' | 'artifacts_retained' | 'writing_local' | 'updating_ref' | 'replacing_index' | 'removing_worktree';
|
|
92
92
|
baseOid?: string;
|
|
93
93
|
planRevision?: string;
|
|
94
|
+
previewId?: string;
|
|
94
95
|
reviewId?: string;
|
|
95
96
|
localFingerprint?: string;
|
|
96
97
|
isolatedFingerprint?: string;
|
|
@@ -100,6 +101,8 @@ export interface ManagedCheckoutMutationJournal extends ManagedCheckoutJournalBa
|
|
|
100
101
|
isolatedHeadOid?: string;
|
|
101
102
|
commitOid?: string;
|
|
102
103
|
retention?: WorktreeRetentionMode;
|
|
104
|
+
/** rollback_preview crash recovery must preserve whether the review returns to working or ready_for_review. */
|
|
105
|
+
resumeRevision?: boolean;
|
|
103
106
|
changedFiles?: string[];
|
|
104
107
|
managedDirectoryIdentity?: DirectoryIdentity;
|
|
105
108
|
cleanupQuarantinePath?: string;
|
|
@@ -119,6 +122,26 @@ export interface ManagedWorktreeReviewRecord {
|
|
|
119
122
|
isolatedFingerprint: string;
|
|
120
123
|
isolatedHeadOid: string;
|
|
121
124
|
}
|
|
125
|
+
export interface ManagedPreviewReceipt {
|
|
126
|
+
previewId: string;
|
|
127
|
+
reviewId: string;
|
|
128
|
+
iteration: number;
|
|
129
|
+
previewedAt: number;
|
|
130
|
+
configuredBaseOid: string;
|
|
131
|
+
effectiveBaseOid: string;
|
|
132
|
+
baseStrategy: ApplyBaseStrategy;
|
|
133
|
+
localHeadOid: string;
|
|
134
|
+
localHeadRef: string | null;
|
|
135
|
+
localFingerprintBefore: string;
|
|
136
|
+
localFingerprintPreview: string;
|
|
137
|
+
localWorkingTreeOid: string;
|
|
138
|
+
localIndexTreeOid: string;
|
|
139
|
+
previewWorkingTreeOid: string;
|
|
140
|
+
isolatedHeadOid: string;
|
|
141
|
+
isolatedFingerprint: string;
|
|
142
|
+
isolatedSnapshotOid: string;
|
|
143
|
+
changedFiles: string[];
|
|
144
|
+
}
|
|
122
145
|
export interface ManagedDeliveryProof {
|
|
123
146
|
localBranch: string | null;
|
|
124
147
|
localHeadBefore: string;
|
|
@@ -131,6 +154,17 @@ export type ManagedCheckoutDelivery = {
|
|
|
131
154
|
} | {
|
|
132
155
|
state: 'ready_for_review';
|
|
133
156
|
review: ManagedWorktreeReviewRecord;
|
|
157
|
+
} | {
|
|
158
|
+
state: 'preview_active';
|
|
159
|
+
review: ManagedWorktreeReviewRecord;
|
|
160
|
+
preview: ManagedPreviewReceipt;
|
|
161
|
+
} | {
|
|
162
|
+
state: 'preview_detached';
|
|
163
|
+
review: ManagedWorktreeReviewRecord;
|
|
164
|
+
preview: ManagedPreviewReceipt;
|
|
165
|
+
detachedAt: number;
|
|
166
|
+
reason: 'stale_local' | 'preview_modified';
|
|
167
|
+
attemptedAction: 'rollback_preview' | 'finalize_preview' | 'discard';
|
|
134
168
|
} | {
|
|
135
169
|
state: 'finalized';
|
|
136
170
|
review: ManagedWorktreeReviewRecord;
|
|
@@ -165,6 +199,8 @@ export interface ManagedCheckoutRecord {
|
|
|
165
199
|
projectId: string;
|
|
166
200
|
projectName: string;
|
|
167
201
|
ownerSessionId: string;
|
|
202
|
+
/** Local session that created this target; historical records may omit it. */
|
|
203
|
+
sourceSessionId?: string;
|
|
168
204
|
/** Canonical root of the project in the user's Local Checkout. */
|
|
169
205
|
localRoot: string;
|
|
170
206
|
/** Canonical root of the project in the managed worktree; also the lease cwd. */
|
|
@@ -64,6 +64,37 @@ export interface FinishSuccessResult {
|
|
|
64
64
|
nextBaseOid: string;
|
|
65
65
|
}
|
|
66
66
|
export type FinishResult = FinishSuccessResult | ApplyErrorResult;
|
|
67
|
+
export interface PreviewReceipt {
|
|
68
|
+
previewId: string;
|
|
69
|
+
reviewId: string;
|
|
70
|
+
iteration: number;
|
|
71
|
+
previewedAt: number;
|
|
72
|
+
configuredBaseOid: string;
|
|
73
|
+
effectiveBaseOid: string;
|
|
74
|
+
baseStrategy: ApplyBaseStrategy;
|
|
75
|
+
localHeadOid: string;
|
|
76
|
+
localHeadRef: string | null;
|
|
77
|
+
localFingerprintBefore: string;
|
|
78
|
+
localFingerprintPreview: string;
|
|
79
|
+
localWorkingTreeOid: string;
|
|
80
|
+
localIndexTreeOid: string;
|
|
81
|
+
previewWorkingTreeOid: string;
|
|
82
|
+
isolatedHeadOid: string;
|
|
83
|
+
isolatedFingerprint: string;
|
|
84
|
+
isolatedSnapshotOid: string;
|
|
85
|
+
changedFiles: string[];
|
|
86
|
+
}
|
|
87
|
+
export interface PreviewSuccessResult {
|
|
88
|
+
status: 'previewed';
|
|
89
|
+
receipt: PreviewReceipt;
|
|
90
|
+
changedFiles: string[];
|
|
91
|
+
}
|
|
92
|
+
export type PreviewResult = PreviewSuccessResult | ApplyErrorResult;
|
|
93
|
+
export interface RollbackSuccessResult {
|
|
94
|
+
status: 'preview_rolled_back';
|
|
95
|
+
changedFiles: string[];
|
|
96
|
+
}
|
|
97
|
+
export type RollbackResult = RollbackSuccessResult | ApplyErrorResult;
|
|
67
98
|
export interface InvalidInputApplyError {
|
|
68
99
|
code: 'invalid_input';
|
|
69
100
|
message: string;
|
|
@@ -92,7 +123,11 @@ export interface OperationNotAllowedApplyError {
|
|
|
92
123
|
code: 'operation_not_allowed';
|
|
93
124
|
message: string;
|
|
94
125
|
}
|
|
95
|
-
export
|
|
126
|
+
export interface PreviewModifiedApplyError {
|
|
127
|
+
code: 'preview_modified';
|
|
128
|
+
message: string;
|
|
129
|
+
}
|
|
130
|
+
export type ApplyError = InvalidInputApplyError | InvalidPlanApplyError | StaleLocalApplyError | StaleIsolatedApplyError | GitApplyError | CommitIsolationApplyError | OperationNotAllowedApplyError | PreviewModifiedApplyError;
|
|
96
131
|
export interface SessionCheckoutApplyEngine {
|
|
97
132
|
inspectReview(input: ApplyPlanInput): Promise<InspectReviewResult>;
|
|
98
133
|
/** 复用真实 Apply merge 计算,但不持久化可执行 plan,也不修改任一 checkout。 */
|
|
@@ -102,6 +137,24 @@ export interface SessionCheckoutApplyEngine {
|
|
|
102
137
|
finish(plan: ApplyPlan, options: {
|
|
103
138
|
commitMessage: string;
|
|
104
139
|
}): Promise<FinishResult>;
|
|
140
|
+
preview(plan: ApplyPlan, options: {
|
|
141
|
+
previewId: string;
|
|
142
|
+
reviewId: string;
|
|
143
|
+
iteration: number;
|
|
144
|
+
/** Local 写入前持久化 receipt artifacts/journal;失败时不得触碰 Local。 */
|
|
145
|
+
beforeWrite?(receipt: PreviewReceipt): Promise<void>;
|
|
146
|
+
}): Promise<PreviewResult>;
|
|
147
|
+
rollback(input: {
|
|
148
|
+
localPath: string;
|
|
149
|
+
receipt: PreviewReceipt;
|
|
150
|
+
}): Promise<RollbackResult>;
|
|
151
|
+
finalize(input: {
|
|
152
|
+
localPath: string;
|
|
153
|
+
receipt: PreviewReceipt;
|
|
154
|
+
commitMessage: string;
|
|
155
|
+
/** Commit object 已创建但 branch ref 尚未更新时持久化恢复信息。 */
|
|
156
|
+
beforeCommit?(commitOid: string): Promise<void>;
|
|
157
|
+
}): Promise<FinishResult>;
|
|
105
158
|
}
|
|
106
159
|
export interface SessionCheckoutApplyEngineOptions {
|
|
107
160
|
/** 测试/宿主 seam:在最后一次 Local fingerprint 校验前执行。 */
|
|
@@ -1,18 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* dsh-git-worktree apply engine, ported from Domi's session-checkout-apply
|
|
3
|
-
* with the Local preview surface removed (preview/rollback/finalize). The
|
|
4
|
-
* remaining surface is the deterministic merge pipeline: plan (conflict
|
|
5
|
-
* detection with ancestry-aware base selection), apply (patch Local with a
|
|
6
|
-
* verified plan), and finish (commit the task delta onto Local HEAD while
|
|
7
|
-
* preserving the user's own staged and working state). All merge computation
|
|
8
|
-
* runs in temporary checkouts over shared object stores; only the final
|
|
9
|
-
* verified patch touches the real Local worktree, and ref/index updates use
|
|
10
|
-
* CAS so a crash leaves a recoverable journal.
|
|
11
|
-
*
|
|
12
|
-
* Ports note: `runGit` still spawns git directly (plugin code is trusted host
|
|
13
|
-
* code, not sandboxed agent code); tests inject a real git in temp repos.
|
|
14
|
-
* @module dsh-git-worktree/session-checkout-apply
|
|
15
|
-
*/
|
|
16
1
|
import { spawn } from 'node:child_process';
|
|
17
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
18
3
|
import { realpathSync } from 'node:fs';
|
|
@@ -196,10 +181,10 @@ async function createSnapshotCommit(checkoutPath, treeOid, parentOid, objectDire
|
|
|
196
181
|
const result = await runGit(checkoutPath, args, {
|
|
197
182
|
env: {
|
|
198
183
|
...(objectDirectory && sourceObjects ? gitObjectEnvironment(objectDirectory, sourceObjects) : {}),
|
|
199
|
-
GIT_AUTHOR_NAME: '
|
|
200
|
-
GIT_AUTHOR_EMAIL: '
|
|
201
|
-
GIT_COMMITTER_NAME: '
|
|
202
|
-
GIT_COMMITTER_EMAIL: '
|
|
184
|
+
GIT_AUTHOR_NAME: 'Domi Apply',
|
|
185
|
+
GIT_AUTHOR_EMAIL: 'domi-apply@localhost',
|
|
186
|
+
GIT_COMMITTER_NAME: 'Domi Apply',
|
|
187
|
+
GIT_COMMITTER_EMAIL: 'domi-apply@localhost',
|
|
203
188
|
},
|
|
204
189
|
input: `${label}\n`,
|
|
205
190
|
});
|
|
@@ -215,8 +200,8 @@ async function computeMerge(tempRoot, sourceObjects, objectDirectory, baseOid, l
|
|
|
215
200
|
await runGit(mergePath, ['init', '--template=']);
|
|
216
201
|
await runGit(mergePath, ['config', 'core.autocrlf', 'false']);
|
|
217
202
|
await runGit(mergePath, ['config', 'core.hooksPath', hooksPath]);
|
|
218
|
-
await runGit(mergePath, ['config', 'user.name', '
|
|
219
|
-
await runGit(mergePath, ['config', 'user.email', '
|
|
203
|
+
await runGit(mergePath, ['config', 'user.name', 'Domi Apply']);
|
|
204
|
+
await runGit(mergePath, ['config', 'user.email', 'domi-apply@localhost']);
|
|
220
205
|
const mergeEnv = {
|
|
221
206
|
GIT_ALTERNATE_OBJECT_DIRECTORIES: [objectDirectory, sourceObjects].join(delimiter),
|
|
222
207
|
};
|
|
@@ -278,8 +263,8 @@ async function computeTreeMerge(tempRoot, sourceObjects, objectDirectory, checko
|
|
|
278
263
|
await runGit(mergePath, ['init', '--template=']);
|
|
279
264
|
await runGit(mergePath, ['config', 'core.autocrlf', 'false']);
|
|
280
265
|
await runGit(mergePath, ['config', 'core.hooksPath', hooksPath]);
|
|
281
|
-
await runGit(mergePath, ['config', 'user.name', '
|
|
282
|
-
await runGit(mergePath, ['config', 'user.email', '
|
|
266
|
+
await runGit(mergePath, ['config', 'user.name', 'Domi Apply']);
|
|
267
|
+
await runGit(mergePath, ['config', 'user.email', 'domi-apply@localhost']);
|
|
283
268
|
const env = {
|
|
284
269
|
GIT_ALTERNATE_OBJECT_DIRECTORIES: [objectDirectory, sourceObjects].join(delimiter),
|
|
285
270
|
};
|
|
@@ -389,7 +374,7 @@ class DefaultSessionCheckoutApplyEngine {
|
|
|
389
374
|
}
|
|
390
375
|
let tempRoot = null;
|
|
391
376
|
try {
|
|
392
|
-
tempRoot = await mkdtemp(join(tmpdir(), '
|
|
377
|
+
tempRoot = await mkdtemp(join(tmpdir(), 'domi-review-snapshot-'));
|
|
393
378
|
const objectDirectory = join(tempRoot, 'objects');
|
|
394
379
|
await mkdir(objectDirectory, { recursive: true });
|
|
395
380
|
const localGitRoot = await resolveGitRoot(input.localPath);
|
|
@@ -437,7 +422,7 @@ class DefaultSessionCheckoutApplyEngine {
|
|
|
437
422
|
}
|
|
438
423
|
let tempRoot = null;
|
|
439
424
|
try {
|
|
440
|
-
tempRoot = await mkdtemp(join(tmpdir(), '
|
|
425
|
+
tempRoot = await mkdtemp(join(tmpdir(), 'domi-apply-plan-'));
|
|
441
426
|
const objectDirectory = join(tempRoot, 'objects');
|
|
442
427
|
await mkdir(objectDirectory, { recursive: true });
|
|
443
428
|
const localGitRoot = await resolveGitRoot(input.localPath);
|
|
@@ -519,7 +504,7 @@ class DefaultSessionCheckoutApplyEngine {
|
|
|
519
504
|
}
|
|
520
505
|
let tempRoot = null;
|
|
521
506
|
try {
|
|
522
|
-
tempRoot = await mkdtemp(join(tmpdir(), '
|
|
507
|
+
tempRoot = await mkdtemp(join(tmpdir(), 'domi-apply-check-'));
|
|
523
508
|
const objectDirectory = join(tempRoot, 'objects');
|
|
524
509
|
await mkdir(objectDirectory, { recursive: true });
|
|
525
510
|
const sourceObjects = await sourceObjectDirectory(stored.scope.localGitRoot);
|
|
@@ -563,6 +548,311 @@ class DefaultSessionCheckoutApplyEngine {
|
|
|
563
548
|
await this.cleanup(tempRoot);
|
|
564
549
|
}
|
|
565
550
|
}
|
|
551
|
+
async preview(plan, options) {
|
|
552
|
+
const stored = this.plans.get(plan.revision);
|
|
553
|
+
if (!stored || !planMatches(stored.plan, plan)) {
|
|
554
|
+
return { status: 'error', error: { code: 'invalid_plan', message: 'Preview plan 不存在、已使用或已被修改' } };
|
|
555
|
+
}
|
|
556
|
+
if (!options.previewId.trim() || !options.reviewId.trim() || !Number.isSafeInteger(options.iteration) || options.iteration < 1) {
|
|
557
|
+
return { status: 'error', error: { code: 'invalid_input', message: 'Preview identity 无效' } };
|
|
558
|
+
}
|
|
559
|
+
let tempRoot = null;
|
|
560
|
+
try {
|
|
561
|
+
tempRoot = await mkdtemp(join(tmpdir(), 'domi-preview-'));
|
|
562
|
+
const objectDirectory = join(tempRoot, 'objects');
|
|
563
|
+
await mkdir(objectDirectory, { recursive: true });
|
|
564
|
+
const sourceObjects = await sourceObjectDirectory(stored.scope.localGitRoot);
|
|
565
|
+
if (!pathsMatch(sourceObjects, stored.scope.sourceObjects)) {
|
|
566
|
+
return { status: 'error', error: { code: 'invalid_plan', message: 'Preview plan 的 Git 仓库身份已变化' } };
|
|
567
|
+
}
|
|
568
|
+
const local = await captureSnapshot(stored.scope.localGitRoot, join(tempRoot, 'local.index'), objectDirectory, sourceObjects);
|
|
569
|
+
if (local.headOid !== stored.plan.localHeadOid
|
|
570
|
+
|| local.headRef !== stored.plan.localHeadRef
|
|
571
|
+
|| local.fingerprint !== stored.plan.localFingerprint) {
|
|
572
|
+
return { status: 'error', error: { code: 'stale_local', message: 'Local 在 plan 后发生变化,请重新计算' } };
|
|
573
|
+
}
|
|
574
|
+
const isolated = await captureSnapshot(stored.scope.isolatedGitRoot, join(tempRoot, 'isolated.index'), objectDirectory, sourceObjects);
|
|
575
|
+
if (isolated.headOid !== stored.plan.isolatedHeadOid || isolated.fingerprint !== stored.plan.isolatedFingerprint) {
|
|
576
|
+
return { status: 'error', error: { code: 'stale_isolated', message: 'Isolated 在 plan 后发生变化,请重新计算' } };
|
|
577
|
+
}
|
|
578
|
+
const persistentLocal = await captureSnapshot(stored.scope.localGitRoot, join(tempRoot, 'persistent-local.index'), null, null);
|
|
579
|
+
const persistentIsolated = await captureSnapshot(stored.scope.isolatedGitRoot, join(tempRoot, 'persistent-isolated.index'), null, null);
|
|
580
|
+
if (persistentLocal.fingerprint !== stored.plan.localFingerprint
|
|
581
|
+
|| persistentIsolated.fingerprint !== stored.plan.isolatedFingerprint) {
|
|
582
|
+
return { status: 'error', error: { code: 'invalid_plan', message: 'Preview 持久快照与审核 plan 不一致' } };
|
|
583
|
+
}
|
|
584
|
+
const previewWorkingTreeOid = await prepareIndexFromPatch(stored.scope.localGitRoot, join(tempRoot, 'preview-working.index'), persistentLocal.treeOid, stored.patch);
|
|
585
|
+
const isolatedSnapshotOid = await createSnapshotCommit(stored.scope.isolatedGitRoot, persistentIsolated.treeOid, stored.plan.effectiveBaseOid, null, null, 'Previewed Isolated snapshot');
|
|
586
|
+
const preparedReceipt = {
|
|
587
|
+
previewId: options.previewId,
|
|
588
|
+
reviewId: options.reviewId,
|
|
589
|
+
iteration: options.iteration,
|
|
590
|
+
previewedAt: Date.now(),
|
|
591
|
+
configuredBaseOid: stored.input.baseOid,
|
|
592
|
+
effectiveBaseOid: stored.plan.effectiveBaseOid,
|
|
593
|
+
baseStrategy: stored.plan.baseStrategy,
|
|
594
|
+
localHeadOid: persistentLocal.headOid,
|
|
595
|
+
localHeadRef: persistentLocal.headRef,
|
|
596
|
+
localFingerprintBefore: persistentLocal.fingerprint,
|
|
597
|
+
localFingerprintPreview: '',
|
|
598
|
+
localWorkingTreeOid: persistentLocal.treeOid,
|
|
599
|
+
localIndexTreeOid: persistentLocal.indexTreeOid,
|
|
600
|
+
previewWorkingTreeOid,
|
|
601
|
+
isolatedHeadOid: persistentIsolated.headOid,
|
|
602
|
+
isolatedFingerprint: persistentIsolated.fingerprint,
|
|
603
|
+
isolatedSnapshotOid,
|
|
604
|
+
changedFiles: [...stored.plan.changedFiles],
|
|
605
|
+
};
|
|
606
|
+
await this.options.beforeFinalLocalValidation?.();
|
|
607
|
+
const finalLocal = await captureSnapshot(stored.scope.localGitRoot, join(tempRoot, 'final-local.index'), objectDirectory, sourceObjects);
|
|
608
|
+
if (finalLocal.headOid !== stored.plan.localHeadOid || finalLocal.fingerprint !== stored.plan.localFingerprint) {
|
|
609
|
+
return { status: 'error', error: { code: 'stale_local', message: 'Local 在 Preview 写入前发生变化,请重新计算' } };
|
|
610
|
+
}
|
|
611
|
+
await options.beforeWrite?.(preparedReceipt);
|
|
612
|
+
if (stored.patch.length > 0) {
|
|
613
|
+
await runGit(stored.scope.localGitRoot, ['apply', '--binary', '--whitespace=nowarn'], { input: stored.patch });
|
|
614
|
+
}
|
|
615
|
+
const previewedLocal = await captureSnapshot(stored.scope.localGitRoot, join(tempRoot, 'previewed-local.index'), null, null);
|
|
616
|
+
if (previewedLocal.treeOid !== previewWorkingTreeOid) {
|
|
617
|
+
return {
|
|
618
|
+
status: 'error',
|
|
619
|
+
error: { code: 'git_error', message: 'Preview 写入后的 Local snapshot 与准备结果不一致,需要恢复确认' },
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
this.plans.delete(plan.revision);
|
|
623
|
+
const receipt = {
|
|
624
|
+
...preparedReceipt,
|
|
625
|
+
localFingerprintPreview: previewedLocal.fingerprint,
|
|
626
|
+
};
|
|
627
|
+
return { status: 'previewed', receipt, changedFiles: [...receipt.changedFiles] };
|
|
628
|
+
}
|
|
629
|
+
catch (error) {
|
|
630
|
+
return { status: 'error', error: { code: 'git_error', message: this.errorMessage(error) } };
|
|
631
|
+
}
|
|
632
|
+
finally {
|
|
633
|
+
if (tempRoot)
|
|
634
|
+
await this.cleanup(tempRoot);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
async rollback(input) {
|
|
638
|
+
let tempRoot = null;
|
|
639
|
+
try {
|
|
640
|
+
tempRoot = await mkdtemp(join(tmpdir(), 'domi-preview-rollback-'));
|
|
641
|
+
const localGitRoot = await resolveGitRoot(input.localPath);
|
|
642
|
+
const objectDirectory = join(tempRoot, 'objects');
|
|
643
|
+
await mkdir(objectDirectory, { recursive: true });
|
|
644
|
+
const sourceObjects = await sourceObjectDirectory(localGitRoot);
|
|
645
|
+
const current = await captureSnapshot(localGitRoot, join(tempRoot, 'current.index'), objectDirectory, sourceObjects);
|
|
646
|
+
if (current.headRef !== input.receipt.localHeadRef) {
|
|
647
|
+
return { status: 'error', error: { code: 'stale_local', message: 'Local branch 已变化,不能自动撤回 Preview' } };
|
|
648
|
+
}
|
|
649
|
+
if (current.headOid !== input.receipt.localHeadOid
|
|
650
|
+
&& !await isAncestor(localGitRoot, input.receipt.localHeadOid, current.headOid)) {
|
|
651
|
+
return { status: 'error', error: { code: 'stale_local', message: 'Local HEAD 不是 Preview 基线的安全快进,不能自动撤回' } };
|
|
652
|
+
}
|
|
653
|
+
let rollbackBaselineTreeOid = input.receipt.localWorkingTreeOid;
|
|
654
|
+
let expectedPreviewTreeOid = input.receipt.previewWorkingTreeOid;
|
|
655
|
+
if (current.headOid !== input.receipt.localHeadOid) {
|
|
656
|
+
const receiptHeadTreeOid = stdoutText(await runGit(localGitRoot, ['rev-parse', `${input.receipt.localHeadOid}^{tree}`]));
|
|
657
|
+
const advancedBaseline = await computeTreeMerge(tempRoot, sourceObjects, objectDirectory, localGitRoot, 'preview-rollback-advanced-baseline', receiptHeadTreeOid, current.headTreeOid, input.receipt.localWorkingTreeOid);
|
|
658
|
+
if (advancedBaseline.status === 'conflict') {
|
|
659
|
+
return {
|
|
660
|
+
status: 'error',
|
|
661
|
+
error: {
|
|
662
|
+
code: 'preview_modified',
|
|
663
|
+
message: `Local 新提交与 Preview 前的本地修改冲突,无法安全撤回:${advancedBaseline.conflictingFiles.join('、')}`,
|
|
664
|
+
},
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
const previewAbsence = await computeTreeMerge(tempRoot, sourceObjects, objectDirectory, localGitRoot, 'preview-rollback-preview-absence', input.receipt.previewWorkingTreeOid, advancedBaseline.treeOid, input.receipt.localWorkingTreeOid);
|
|
668
|
+
if (previewAbsence.status === 'conflict') {
|
|
669
|
+
return {
|
|
670
|
+
status: 'error',
|
|
671
|
+
error: {
|
|
672
|
+
code: 'preview_modified',
|
|
673
|
+
message: `Local 新提交与 Preview 任务增量冲突,无法安全撤回:${previewAbsence.conflictingFiles.join('、')}`,
|
|
674
|
+
},
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
if (previewAbsence.treeOid !== advancedBaseline.treeOid) {
|
|
678
|
+
return {
|
|
679
|
+
status: 'error',
|
|
680
|
+
error: {
|
|
681
|
+
code: 'preview_modified',
|
|
682
|
+
message: 'Local 新提交已经包含部分或全部 Preview 增量,不能通过撤回工作区改动来改写已提交历史',
|
|
683
|
+
},
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
const advancedPreview = await computeTreeMerge(tempRoot, sourceObjects, objectDirectory, localGitRoot, 'preview-rollback-advanced-preview', input.receipt.localWorkingTreeOid, advancedBaseline.treeOid, input.receipt.previewWorkingTreeOid);
|
|
687
|
+
if (advancedPreview.status === 'conflict') {
|
|
688
|
+
return {
|
|
689
|
+
status: 'error',
|
|
690
|
+
error: {
|
|
691
|
+
code: 'preview_modified',
|
|
692
|
+
message: `Local 新提交与 Preview 任务增量冲突,无法安全撤回:${advancedPreview.conflictingFiles.join('、')}`,
|
|
693
|
+
},
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
rollbackBaselineTreeOid = advancedBaseline.treeOid;
|
|
697
|
+
expectedPreviewTreeOid = advancedPreview.treeOid;
|
|
698
|
+
}
|
|
699
|
+
const rollbackTree = await computeTreeMerge(tempRoot, sourceObjects, objectDirectory, localGitRoot, 'preview-rollback', expectedPreviewTreeOid, current.treeOid, rollbackBaselineTreeOid);
|
|
700
|
+
if (rollbackTree.status === 'conflict') {
|
|
701
|
+
return {
|
|
702
|
+
status: 'error',
|
|
703
|
+
error: {
|
|
704
|
+
code: 'preview_modified',
|
|
705
|
+
message: `Local 在 Preview 区域出现额外修改,无法安全撤回:${rollbackTree.conflictingFiles.join('、')}`,
|
|
706
|
+
},
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
const rollbackPatch = await treePatch(localGitRoot, current.treeOid, rollbackTree.treeOid, objectDirectory, sourceObjects);
|
|
710
|
+
await this.options.beforeFinalLocalValidation?.();
|
|
711
|
+
const finalLocal = await captureSnapshot(localGitRoot, join(tempRoot, 'final-local.index'), objectDirectory, sourceObjects);
|
|
712
|
+
if (finalLocal.fingerprint !== current.fingerprint || finalLocal.headOid !== current.headOid) {
|
|
713
|
+
return { status: 'error', error: { code: 'stale_local', message: 'Local 在撤回 Preview 前发生变化,请重试' } };
|
|
714
|
+
}
|
|
715
|
+
if (rollbackPatch.length > 0) {
|
|
716
|
+
await runGit(localGitRoot, ['apply', '--binary', '--whitespace=nowarn'], { input: rollbackPatch });
|
|
717
|
+
}
|
|
718
|
+
const rolledBack = await captureSnapshot(localGitRoot, join(tempRoot, 'rolled-back.index'), null, null);
|
|
719
|
+
if (rolledBack.headOid !== current.headOid
|
|
720
|
+
|| rolledBack.headRef !== current.headRef
|
|
721
|
+
|| rolledBack.indexTreeOid !== current.indexTreeOid
|
|
722
|
+
|| rolledBack.treeOid !== rollbackTree.treeOid) {
|
|
723
|
+
return {
|
|
724
|
+
status: 'error',
|
|
725
|
+
error: { code: 'git_error', message: 'Preview 撤回后的 Local snapshot 与安全恢复结果不一致,需要保留现场确认' },
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
return { status: 'preview_rolled_back', changedFiles: [...input.receipt.changedFiles] };
|
|
729
|
+
}
|
|
730
|
+
catch (error) {
|
|
731
|
+
return { status: 'error', error: { code: 'git_error', message: this.errorMessage(error) } };
|
|
732
|
+
}
|
|
733
|
+
finally {
|
|
734
|
+
if (tempRoot)
|
|
735
|
+
await this.cleanup(tempRoot);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
async finalize(input) {
|
|
739
|
+
const commitMessage = input.commitMessage.trim();
|
|
740
|
+
if (!commitMessage) {
|
|
741
|
+
return { status: 'error', error: { code: 'invalid_input', message: '提交信息不能为空' } };
|
|
742
|
+
}
|
|
743
|
+
if (!input.receipt.localHeadRef?.startsWith('refs/heads/')) {
|
|
744
|
+
return { status: 'error', error: { code: 'operation_not_allowed', message: 'Local 当前不是普通分支,不能自动创建任务提交' } };
|
|
745
|
+
}
|
|
746
|
+
let tempRoot = null;
|
|
747
|
+
let adjacentIndex = null;
|
|
748
|
+
try {
|
|
749
|
+
tempRoot = await mkdtemp(join(tmpdir(), 'domi-preview-finalize-'));
|
|
750
|
+
const localGitRoot = await resolveGitRoot(input.localPath);
|
|
751
|
+
const objectDirectory = join(tempRoot, 'objects');
|
|
752
|
+
await mkdir(objectDirectory, { recursive: true });
|
|
753
|
+
const sourceObjects = await sourceObjectDirectory(localGitRoot);
|
|
754
|
+
const current = await captureSnapshot(localGitRoot, join(tempRoot, 'current.index'), objectDirectory, sourceObjects);
|
|
755
|
+
if (current.headOid !== input.receipt.localHeadOid || current.headRef !== input.receipt.localHeadRef) {
|
|
756
|
+
return { status: 'error', error: { code: 'stale_local', message: 'Local branch/HEAD 已变化,不能完成 Preview 提交' } };
|
|
757
|
+
}
|
|
758
|
+
const previewRemoval = await computeTreeMerge(tempRoot, sourceObjects, objectDirectory, localGitRoot, 'preview-finalize-separation', input.receipt.previewWorkingTreeOid, current.treeOid, input.receipt.localWorkingTreeOid);
|
|
759
|
+
if (previewRemoval.status === 'conflict') {
|
|
760
|
+
return {
|
|
761
|
+
status: 'error',
|
|
762
|
+
error: {
|
|
763
|
+
code: 'preview_modified',
|
|
764
|
+
message: `Local 在 Preview 区域出现额外修改,无法可靠提交:${previewRemoval.conflictingFiles.join('、')}`,
|
|
765
|
+
},
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
if (input.receipt.changedFiles.length === 0) {
|
|
769
|
+
await this.options.beforeFinalLocalValidation?.();
|
|
770
|
+
const finalLocal = await captureSnapshot(localGitRoot, join(tempRoot, 'final-empty.index'), objectDirectory, sourceObjects);
|
|
771
|
+
if (finalLocal.fingerprint !== current.fingerprint) {
|
|
772
|
+
return { status: 'error', error: { code: 'stale_local', message: 'Local 在完成 Preview 前发生变化,请重试' } };
|
|
773
|
+
}
|
|
774
|
+
return {
|
|
775
|
+
status: 'finished',
|
|
776
|
+
changedFiles: [],
|
|
777
|
+
commitOid: null,
|
|
778
|
+
nextBaseOid: input.receipt.isolatedSnapshotOid,
|
|
779
|
+
};
|
|
780
|
+
}
|
|
781
|
+
const taskTree = await computeTreeMerge(tempRoot, sourceObjects, objectDirectory, localGitRoot, 'preview-task-isolation', input.receipt.localWorkingTreeOid, current.headTreeOid, input.receipt.previewWorkingTreeOid);
|
|
782
|
+
if (taskTree.status === 'conflict') {
|
|
783
|
+
return {
|
|
784
|
+
status: 'error',
|
|
785
|
+
error: {
|
|
786
|
+
code: 'commit_isolation_conflict',
|
|
787
|
+
message: `Preview 任务增量无法与 Local HEAD 可靠拆分:${taskTree.conflictingFiles.join('、')}`,
|
|
788
|
+
},
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
const finalIndexTree = await computeTreeMerge(tempRoot, sourceObjects, objectDirectory, localGitRoot, 'preview-index-preservation', current.headTreeOid, taskTree.treeOid, current.indexTreeOid);
|
|
792
|
+
if (finalIndexTree.status === 'conflict') {
|
|
793
|
+
return {
|
|
794
|
+
status: 'error',
|
|
795
|
+
error: {
|
|
796
|
+
code: 'commit_isolation_conflict',
|
|
797
|
+
message: `Preview 提交与 Local staged 修改无法可靠分离:${finalIndexTree.conflictingFiles.join('、')}`,
|
|
798
|
+
},
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
const taskPatch = await treePatch(localGitRoot, current.headTreeOid, taskTree.treeOid, objectDirectory, sourceObjects);
|
|
802
|
+
const finalIndexPatch = await treePatch(localGitRoot, taskTree.treeOid, finalIndexTree.treeOid, objectDirectory, sourceObjects);
|
|
803
|
+
const actualTaskTreeOid = await prepareIndexFromPatch(localGitRoot, join(tempRoot, 'task.index'), current.headOid, taskPatch);
|
|
804
|
+
const commitOid = await createUserCommit(localGitRoot, actualTaskTreeOid, current.headOid, commitMessage);
|
|
805
|
+
const finalIndexPath = join(tempRoot, 'final.index');
|
|
806
|
+
await prepareIndexFromPatch(localGitRoot, finalIndexPath, commitOid, finalIndexPatch);
|
|
807
|
+
await input.beforeCommit?.(commitOid);
|
|
808
|
+
await this.options.beforeFinalLocalValidation?.();
|
|
809
|
+
const finalLocal = await captureSnapshot(localGitRoot, join(tempRoot, 'final-local.index'), objectDirectory, sourceObjects);
|
|
810
|
+
if (finalLocal.fingerprint !== current.fingerprint || finalLocal.headOid !== current.headOid) {
|
|
811
|
+
return { status: 'error', error: { code: 'stale_local', message: 'Local 在 Preview 提交写入前发生变化,请重试' } };
|
|
812
|
+
}
|
|
813
|
+
const realIndexPath = await resolveIndexPath(localGitRoot);
|
|
814
|
+
adjacentIndex = `${realIndexPath}.domi-${randomUUID()}`;
|
|
815
|
+
await copyFile(finalIndexPath, adjacentIndex);
|
|
816
|
+
let refUpdated = false;
|
|
817
|
+
try {
|
|
818
|
+
await runGit(localGitRoot, ['update-ref', input.receipt.localHeadRef, commitOid, current.headOid]);
|
|
819
|
+
refUpdated = true;
|
|
820
|
+
await rename(adjacentIndex, realIndexPath);
|
|
821
|
+
adjacentIndex = null;
|
|
822
|
+
}
|
|
823
|
+
catch (error) {
|
|
824
|
+
if (refUpdated) {
|
|
825
|
+
try {
|
|
826
|
+
await runGit(localGitRoot, ['update-ref', input.receipt.localHeadRef, current.headOid, commitOid]);
|
|
827
|
+
}
|
|
828
|
+
catch (rollbackError) {
|
|
829
|
+
return {
|
|
830
|
+
status: 'error',
|
|
831
|
+
error: {
|
|
832
|
+
code: 'git_error',
|
|
833
|
+
message: `Preview 提交写入失败且 ref 无法回滚:${this.errorMessage(error)};${this.errorMessage(rollbackError)}`,
|
|
834
|
+
},
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return { status: 'error', error: { code: 'git_error', message: `Preview 提交写入失败,已回滚:${this.errorMessage(error)}` } };
|
|
839
|
+
}
|
|
840
|
+
return {
|
|
841
|
+
status: 'finished',
|
|
842
|
+
changedFiles: [...input.receipt.changedFiles],
|
|
843
|
+
commitOid,
|
|
844
|
+
nextBaseOid: input.receipt.isolatedSnapshotOid,
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
catch (error) {
|
|
848
|
+
return { status: 'error', error: { code: 'git_error', message: this.errorMessage(error) } };
|
|
849
|
+
}
|
|
850
|
+
finally {
|
|
851
|
+
await removeBestEffort(adjacentIndex);
|
|
852
|
+
if (tempRoot)
|
|
853
|
+
await this.cleanup(tempRoot);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
566
856
|
async finish(plan, options) {
|
|
567
857
|
const stored = this.plans.get(plan.revision);
|
|
568
858
|
if (!stored || !planMatches(stored.plan, plan)) {
|
|
@@ -581,7 +871,7 @@ class DefaultSessionCheckoutApplyEngine {
|
|
|
581
871
|
let tempRoot = null;
|
|
582
872
|
let adjacentIndex = null;
|
|
583
873
|
try {
|
|
584
|
-
tempRoot = await mkdtemp(join(tmpdir(), '
|
|
874
|
+
tempRoot = await mkdtemp(join(tmpdir(), 'domi-finish-'));
|
|
585
875
|
const objectDirectory = join(tempRoot, 'objects');
|
|
586
876
|
await mkdir(objectDirectory, { recursive: true });
|
|
587
877
|
const sourceObjects = await sourceObjectDirectory(stored.scope.localGitRoot);
|
|
@@ -660,7 +950,7 @@ class DefaultSessionCheckoutApplyEngine {
|
|
|
660
950
|
return { status: 'error', error: { code: 'stale_local', message: 'Local 在 Finish 写入前发生变化,请重新计算' } };
|
|
661
951
|
}
|
|
662
952
|
const realIndexPath = await resolveIndexPath(stored.scope.localGitRoot);
|
|
663
|
-
adjacentIndex = `${realIndexPath}.
|
|
953
|
+
adjacentIndex = `${realIndexPath}.domi-${randomUUID()}`;
|
|
664
954
|
await copyFile(finalIndexPath, adjacentIndex);
|
|
665
955
|
let worktreePatched = false;
|
|
666
956
|
let refUpdated = false;
|