clawt 3.1.3 → 3.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.
@@ -1,6 +1,5 @@
1
1
  import type { Command } from 'commander';
2
2
  import { logger } from '../logger/index.js';
3
- import { ClawtError } from '../errors/index.js';
4
3
  import { MESSAGES } from '../constants/index.js';
5
4
  import type { ValidateOptions } from '../types/index.js';
6
5
  import { executeSyncForBranch } from './sync.js';
@@ -11,21 +10,10 @@ import {
11
10
  getProjectWorktrees,
12
11
  getConfigValue,
13
12
  isWorkingDirClean,
14
- gitAddAll,
15
- gitCommit,
16
- gitRestoreStaged,
17
13
  gitResetHard,
18
14
  gitCleanForce,
19
- gitDiffBinaryAgainstBranch,
20
- gitApplyFromStdin,
21
- gitApplyCachedFromStdin,
22
- gitResetSoft,
23
- gitWriteTree,
24
15
  gitReadTree,
25
16
  getHeadCommitHash,
26
- getCommitTreeHash,
27
- gitDiffTree,
28
- gitApplyCachedCheck,
29
17
  hasLocalCommits,
30
18
  hasSnapshot,
31
19
  readSnapshot,
@@ -34,24 +22,21 @@ import {
34
22
  confirmDestructiveAction,
35
23
  confirmAction,
36
24
  printSuccess,
37
- printError,
38
25
  printWarning,
39
26
  printInfo,
40
- printSeparator,
41
27
  resolveTargetWorktree,
42
- runCommandInherited,
43
- parseParallelCommands,
44
- runParallelCommands,
45
28
  requireProjectConfig,
46
- getValidateBranchName,
47
- gitCheckout,
48
29
  ensureOnMainWorkBranch,
49
- checkBranchExists,
50
- getCurrentBranch,
51
30
  handleDirtyWorkingDir,
52
31
  getValidateRunCommand,
32
+ executeRunCommand,
33
+ migrateChangesViaPatch,
34
+ computeCurrentTreeHash,
35
+ saveCurrentSnapshotTree,
36
+ loadOldSnapshotToStage,
37
+ switchToValidateBranch,
53
38
  } from '../utils/index.js';
54
- import type { WorktreeResolveMessages, ParallelCommandResult } from '../utils/index.js';
39
+ import type { WorktreeResolveMessages } from '../utils/index.js';
55
40
 
56
41
  /** validate 命令的分支解析消息配置 */
57
42
  const VALIDATE_RESOLVE_MESSAGES: WorktreeResolveMessages = {
@@ -86,59 +71,6 @@ async function handleDirtyMainWorktree(mainWorktreePath: string): Promise<void>
86
71
  await handleDirtyWorkingDir(mainWorktreePath);
87
72
  }
88
73
 
89
- /**
90
- * 通过 patch 将目标分支的全量变更(已提交 + 未提交)迁移到主 worktree
91
- * 使用 git diff HEAD...branch --binary 获取变更,避免 stash 方式无法检测已提交 commit 的问题
92
- * @param {string} targetWorktreePath - 目标 worktree 路径
93
- * @param {string} mainWorktreePath - 主 worktree 路径
94
- * @param {string} branchName - 分支名
95
- * @param {boolean} hasUncommitted - 目标 worktree 是否有未提交修改
96
- * @returns {{ success: boolean }} patch 迁移结果
97
- */
98
- function migrateChangesViaPatch(targetWorktreePath: string, mainWorktreePath: string, branchName: string, hasUncommitted: boolean): { success: boolean } {
99
- let didTempCommit = false;
100
-
101
- try {
102
- // 如果有未提交修改,先做临时 commit 以便 diff 能捕获全部变更
103
- if (hasUncommitted) {
104
- gitAddAll(targetWorktreePath);
105
- gitCommit('clawt:temp-commit-for-validate', targetWorktreePath);
106
- didTempCommit = true;
107
- }
108
-
109
- // 在主 worktree 执行三点 diff,获取目标分支自分叉点以来的全量变更
110
- const patch = gitDiffBinaryAgainstBranch(branchName, mainWorktreePath);
111
-
112
- // 应用 patch 到主 worktree 工作目录
113
- if (patch.length > 0) {
114
- try {
115
- gitApplyFromStdin(patch, mainWorktreePath);
116
- } catch (error) {
117
- logger.warn(`patch apply 失败: ${error}`);
118
- printWarning(MESSAGES.VALIDATE_PATCH_APPLY_FAILED(branchName));
119
- return { success: false };
120
- }
121
- }
122
-
123
- return { success: true };
124
- } finally {
125
- // 确保临时 commit 一定会被撤销,恢复目标 worktree 原状
126
- // 每个操作独立 try-catch,避免前一个失败导致后续操作不执行
127
- if (didTempCommit) {
128
- try {
129
- gitResetSoft(1, targetWorktreePath);
130
- } catch (error) {
131
- logger.error(`撤销临时 commit 失败: ${error}`);
132
- }
133
- try {
134
- gitRestoreStaged(targetWorktreePath);
135
- } catch (error) {
136
- logger.error(`恢复暂存区失败: ${error}`);
137
- }
138
- }
139
- }
140
- }
141
-
142
74
  /**
143
75
  * patch apply 失败后的交互处理:询问用户是否自动执行 sync
144
76
  * @param {string} targetWorktreePath - 目标 worktree 路径
@@ -161,24 +93,6 @@ async function handlePatchApplyFailure(targetWorktreePath: string, branchName: s
161
93
  // sync 冲突提示已在 executeSyncForBranch 内部输出(SYNC_CONFLICT),此处无需重复提示
162
94
  }
163
95
 
164
- /**
165
- * 保存当前主 worktree 工作目录变更为 git tree 对象快照
166
- * 操作序列:git add . → git write-tree → git restore --staged .
167
- * 同时保存当前 HEAD commit hash,用于增量 validate 时对齐基准
168
- * @param {string} mainWorktreePath - 主 worktree 路径
169
- * @param {string} projectName - 项目名
170
- * @param {string} branchName - 分支名
171
- * @returns {string} 生成的 tree hash
172
- */
173
- function saveCurrentSnapshotTree(mainWorktreePath: string, projectName: string, branchName: string): string {
174
- gitAddAll(mainWorktreePath);
175
- const treeHash = gitWriteTree(mainWorktreePath);
176
- gitRestoreStaged(mainWorktreePath);
177
- const headCommitHash = getHeadCommitHash(mainWorktreePath);
178
- writeSnapshot(projectName, branchName, treeHash, headCommitHash);
179
- return treeHash;
180
- }
181
-
182
96
  /**
183
97
  * 处理 --clean 选项:清理 validate 状态
184
98
  * @param {ValidateOptions} options - 命令选项
@@ -235,11 +149,7 @@ async function handleValidateClean(options: ValidateOptions): Promise<void> {
235
149
  */
236
150
  async function handleFirstValidate(targetWorktreePath: string, mainWorktreePath: string, projectName: string, branchName: string, hasUncommitted: boolean): Promise<void> {
237
151
  // 切换主 worktree 到验证分支
238
- const validateBranchName = getValidateBranchName(branchName);
239
- if (!checkBranchExists(validateBranchName)) {
240
- throw new ClawtError(MESSAGES.VALIDATE_BRANCH_NOT_FOUND(validateBranchName, branchName));
241
- }
242
- gitCheckout(validateBranchName, mainWorktreePath);
152
+ const validateBranchName = switchToValidateBranch(branchName, mainWorktreePath);
243
153
 
244
154
  // 通过 patch 迁移目标分支全量变更到主 worktree
245
155
  const result = migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
@@ -267,8 +177,8 @@ async function handleFirstValidate(targetWorktreePath: string, mainWorktreePath:
267
177
  * @param {boolean} hasUncommitted - 目标 worktree 是否有未提交修改
268
178
  */
269
179
  async function handleIncrementalValidate(targetWorktreePath: string, mainWorktreePath: string, projectName: string, branchName: string, hasUncommitted: boolean): Promise<void> {
270
- // 步骤 1:读取旧快照(tree hash + 当时的 HEAD commit hash)
271
- const { treeHash: oldTreeHash, headCommitHash: oldHeadCommitHash } = readSnapshot(projectName, branchName);
180
+ // 步骤 1:读取旧快照(tree hash + 当时的 HEAD commit hash + 暂存区 tree hash
181
+ const { treeHash: oldTreeHash, headCommitHash: oldHeadCommitHash, stagedTreeHash: oldStagedTreeHash } = readSnapshot(projectName, branchName);
272
182
 
273
183
  // 步骤 2:确保主 worktree 干净(调用方已通过 handleDirtyMainWorktree 处理)
274
184
  // 这里做兜底清理,防止 handleDirtyMainWorktree 之后仍有残留
@@ -278,14 +188,7 @@ async function handleIncrementalValidate(targetWorktreePath: string, mainWorktre
278
188
  }
279
189
 
280
190
  // 步骤 3:切换到验证分支(如果已在该分支上则跳过)
281
- const validateBranchName = getValidateBranchName(branchName);
282
- if (!checkBranchExists(validateBranchName)) {
283
- throw new ClawtError(MESSAGES.VALIDATE_BRANCH_NOT_FOUND(validateBranchName, branchName));
284
- }
285
- const currentBranch = getCurrentBranch(mainWorktreePath);
286
- if (currentBranch !== validateBranchName) {
287
- gitCheckout(validateBranchName, mainWorktreePath);
288
- }
191
+ const validateBranchName = switchToValidateBranch(branchName, mainWorktreePath);
289
192
 
290
193
  // 步骤 4:通过 patch 从目标分支获取最新全量变更
291
194
  const result = migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
@@ -297,140 +200,45 @@ async function handleIncrementalValidate(targetWorktreePath: string, mainWorktre
297
200
  return;
298
201
  }
299
202
 
300
- // 步骤 5:保存最新快照为 git tree 对象(同时记录当前 HEAD)
301
- saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
203
+ // 步骤 5:计算当前变更的 tree hash,检测是否有新变更
204
+ const newTreeHash = computeCurrentTreeHash(mainWorktreePath);
205
+ const currentHeadCommitHash = getHeadCommitHash(mainWorktreePath);
302
206
 
303
- // 步骤 6:将旧变更状态载入暂存区
304
- try {
305
- const currentHeadCommitHash = getHeadCommitHash(mainWorktreePath);
306
-
307
- if (oldHeadCommitHash && oldHeadCommitHash !== currentHeadCommitHash) {
308
- // HEAD 发生了变化:
309
- // 将旧变更 patch(旧 tree 相对于旧 HEAD 的差异)重放到当前 HEAD 暂存区上,
310
- // 避免新旧 tree 基准不同导致 diff 混入 HEAD 变化的内容
311
- const oldHeadTreeHash = getCommitTreeHash(oldHeadCommitHash, mainWorktreePath);
312
- const oldChangePatch = gitDiffTree(oldHeadTreeHash, oldTreeHash, mainWorktreePath);
313
-
314
- if (oldChangePatch.length > 0 && gitApplyCachedCheck(oldChangePatch, mainWorktreePath)) {
315
- // 无冲突:apply --cached 到当前 HEAD 暂存区
316
- gitApplyCachedFromStdin(oldChangePatch, mainWorktreePath);
317
- } else if (oldChangePatch.length > 0) {
318
- // 有冲突:降级为全量模式(暂存区保持为空)
319
- logger.warn('旧变更 patch 与当前 HEAD 冲突,降级为全量模式');
320
- printWarning(MESSAGES.INCREMENTAL_VALIDATE_FALLBACK);
321
- printSuccess(MESSAGES.VALIDATE_SUCCESS_WITH_BRANCH(branchName, validateBranchName));
322
- return;
207
+ // 检测目标 worktree 自上次 validate 以来是否有新变更
208
+ const hasNewChanges = newTreeHash !== oldTreeHash
209
+ || (oldHeadCommitHash && oldHeadCommitHash !== currentHeadCommitHash);
210
+
211
+ if (!hasNewChanges) {
212
+ // 无新变更:不更新快照,恢复到上次 validate 结束时的完整状态
213
+ if (oldStagedTreeHash) {
214
+ try {
215
+ gitReadTree(oldStagedTreeHash, mainWorktreePath);
216
+ } catch (error) {
217
+ logger.warn(`恢复暂存区失败: ${error}`);
323
218
  }
324
- // oldChangePatch 为空表示旧变更为空,暂存区保持干净即可
325
- } else {
326
- // HEAD 未变化(或旧版快照无 HEAD 信息):直接 read-tree 旧快照
327
- gitReadTree(oldTreeHash, mainWorktreePath);
328
219
  }
329
- } catch (error) {
330
- // 旧 tree 对象无法读取(可能被 git gc 回收),降级为全量模式
331
- logger.warn(`增量 read-tree 失败: ${error}`);
332
- printWarning(MESSAGES.INCREMENTAL_VALIDATE_FALLBACK);
333
- // 降级后暂存区保持为空,工作目录为最新全量变更,与首次 validate 一致
220
+ printInfo(MESSAGES.INCREMENTAL_VALIDATE_NO_CHANGES(branchName));
334
221
  printSuccess(MESSAGES.VALIDATE_SUCCESS_WITH_BRANCH(branchName, validateBranchName));
335
222
  return;
336
223
  }
337
224
 
338
- // 结果:暂存区=上次快照,工作目录=最新全量变更
339
- printSuccess(MESSAGES.INCREMENTAL_VALIDATE_SUCCESS(branchName));
340
- }
341
-
342
- /**
343
- * 执行单个命令(同步方式,保持原有行为不变)
344
- * @param {string} command - 要执行的命令字符串
345
- * @param {string} mainWorktreePath - 主 worktree 路径
346
- */
347
- function executeSingleCommand(command: string, mainWorktreePath: string): void {
348
- printInfo(MESSAGES.VALIDATE_RUN_START(command));
349
- printSeparator();
350
-
351
- const result = runCommandInherited(command, { cwd: mainWorktreePath });
225
+ // 有新变更:执行暂存区载入并记录 stagedTreeHash
352
226
 
353
- printSeparator();
354
-
355
- if (result.error) {
356
- // 进程启动失败(如命令不存在)
357
- printError(MESSAGES.VALIDATE_RUN_ERROR(command, result.error.message));
227
+ // 步骤 6:将旧变更状态载入暂存区
228
+ const stageResult = loadOldSnapshotToStage(oldTreeHash, oldHeadCommitHash, currentHeadCommitHash, mainWorktreePath);
229
+ if (!stageResult.success) {
230
+ printWarning(MESSAGES.INCREMENTAL_VALIDATE_FALLBACK);
231
+ // 降级后暂存区保持为空,工作目录为最新全量变更,与首次 validate 一致
232
+ writeSnapshot(projectName, branchName, newTreeHash, currentHeadCommitHash, '');
233
+ printSuccess(MESSAGES.VALIDATE_SUCCESS_WITH_BRANCH(branchName, validateBranchName));
358
234
  return;
359
235
  }
360
236
 
361
- const exitCode = result.status ?? 1;
362
- if (exitCode === 0) {
363
- printSuccess(MESSAGES.VALIDATE_RUN_SUCCESS(command));
364
- } else {
365
- printError(MESSAGES.VALIDATE_RUN_FAILED(command, exitCode));
366
- }
367
- }
368
-
369
- /**
370
- * 汇总输出并行命令的执行结果
371
- * @param {ParallelCommandResult[]} results - 各命令的执行结果数组
372
- */
373
- function reportParallelResults(results: ParallelCommandResult[]): void {
374
- printSeparator();
375
-
376
- const successCount = results.filter((r) => r.exitCode === 0 && !r.error).length;
377
- const failedCount = results.length - successCount;
378
-
379
- for (const result of results) {
380
- if (result.error) {
381
- printError(MESSAGES.VALIDATE_PARALLEL_CMD_ERROR(result.command, result.error));
382
- } else if (result.exitCode === 0) {
383
- printSuccess(MESSAGES.VALIDATE_PARALLEL_CMD_SUCCESS(result.command));
384
- } else {
385
- printError(MESSAGES.VALIDATE_PARALLEL_CMD_FAILED(result.command, result.exitCode));
386
- }
387
- }
388
-
389
- if (failedCount === 0) {
390
- printSuccess(MESSAGES.VALIDATE_PARALLEL_RUN_ALL_SUCCESS(results.length));
391
- } else {
392
- printError(MESSAGES.VALIDATE_PARALLEL_RUN_SUMMARY(successCount, failedCount));
393
- }
394
- }
395
-
396
- /**
397
- * 并行执行多个命令并汇总结果
398
- * @param {string[]} commands - 要并行执行的命令数组
399
- * @param {string} mainWorktreePath - 主 worktree 路径
400
- */
401
- async function executeParallelCommands(commands: string[], mainWorktreePath: string): Promise<void> {
402
- printInfo(MESSAGES.VALIDATE_PARALLEL_RUN_START(commands.length));
237
+ // 步骤 7:写入新快照(包含 stagedTreeHash 供下次无变更时恢复用)
238
+ writeSnapshot(projectName, branchName, newTreeHash, currentHeadCommitHash, stageResult.stagedTreeHash);
403
239
 
404
- for (let i = 0; i < commands.length; i++) {
405
- printInfo(MESSAGES.VALIDATE_PARALLEL_CMD_START(i + 1, commands.length, commands[i]));
406
- }
407
-
408
- printSeparator();
409
-
410
- const results = await runParallelCommands(commands, { cwd: mainWorktreePath });
411
-
412
- reportParallelResults(results);
413
- }
414
-
415
- /**
416
- * 在主 worktree 中执行用户指定的命令
417
- * 根据命令字符串中的 & 分隔符决定是单命令执行还是并行执行
418
- * 命令执行失败不影响 validate 本身的结果,仅输出提示
419
- * @param {string} command - 要执行的命令字符串
420
- * @param {string} mainWorktreePath - 主 worktree 路径
421
- */
422
- async function executeRunCommand(command: string, mainWorktreePath: string): Promise<void> {
423
- printInfo('');
424
-
425
- const commands = parseParallelCommands(command);
426
-
427
- if (commands.length <= 1) {
428
- // 单命令(包括含 && 的串行命令),走原有同步路径
429
- executeSingleCommand(commands[0] || command, mainWorktreePath);
430
- } else {
431
- // 多命令,并行执行
432
- await executeParallelCommands(commands, mainWorktreePath);
433
- }
240
+ // 结果:暂存区=上次快照,工作目录=最新全量变更
241
+ printSuccess(MESSAGES.INCREMENTAL_VALIDATE_SUCCESS(branchName));
434
242
  }
435
243
 
436
244
  /**
@@ -8,6 +8,9 @@ export const VALIDATE_MESSAGES = {
8
8
  `✓ 已将分支 ${branch} 的最新变更应用到主 worktree(增量模式)\n 暂存区 = 上次快照,工作目录 = 最新变更`,
9
9
  /** 增量 validate 降级为全量模式提示 */
10
10
  INCREMENTAL_VALIDATE_FALLBACK: '增量对比失败,已降级为全量模式',
11
+ /** 增量 validate 检测到目标 worktree 无新变更 */
12
+ INCREMENTAL_VALIDATE_NO_CHANGES: (branch: string) =>
13
+ `分支 ${branch} 自上次 validate 以来没有新的变更,已恢复到上次验证状态`,
11
14
  /** validate 状态已清理 */
12
15
  VALIDATE_CLEANED: (branch: string) => `✓ 分支 ${branch} 的 validate 状态已清理`,
13
16
  /** validate patch apply 失败,提示用户同步主分支 */
@@ -73,6 +73,8 @@ export { checkForUpdates } from './update-checker.js';
73
73
  export { getProjectConfigPath, loadProjectConfig, saveProjectConfig, requireProjectConfig, getMainWorkBranch, getValidateRunCommand } from './project-config.js';
74
74
  export { getValidateBranchName, createValidateBranch, deleteValidateBranch, rebuildValidateBranch, ensureOnMainWorkBranch, handleDirtyWorkingDir } from './validate-branch.js';
75
75
  export { safeStringify } from './json.js';
76
+ export { executeRunCommand } from './validate-runner.js';
77
+ export { migrateChangesViaPatch, computeCurrentTreeHash, saveCurrentSnapshotTree, loadOldSnapshotToStage, switchToValidateBranch } from './validate-core.js';
76
78
  export { InteractivePanel } from './interactive-panel.js';
77
79
  export { buildPanelFrame, buildGroupedWorktreeLines, buildDisplayOrder, renderDateSeparator, renderWorktreeBlock, renderSnapshotSummary, renderFooter, calculateVisibleRows } from './interactive-panel-render.js';
78
80
  export type { PanelLine } from './interactive-panel-render.js';
@@ -0,0 +1,174 @@
1
+ import { logger } from '../logger/index.js';
2
+ import { ClawtError } from '../errors/index.js';
3
+ import { MESSAGES } from '../constants/index.js';
4
+ import {
5
+ gitAddAll,
6
+ gitCommit,
7
+ gitRestoreStaged,
8
+ gitDiffBinaryAgainstBranch,
9
+ gitApplyFromStdin,
10
+ gitApplyCachedFromStdin,
11
+ gitResetSoft,
12
+ gitWriteTree,
13
+ gitReadTree,
14
+ getCommitTreeHash,
15
+ gitDiffTree,
16
+ gitApplyCachedCheck,
17
+ getValidateBranchName,
18
+ checkBranchExists,
19
+ gitCheckout,
20
+ getCurrentBranch,
21
+ getHeadCommitHash,
22
+ writeSnapshot,
23
+ printWarning,
24
+ } from './index.js';
25
+
26
+ /**
27
+ * 通过 patch 将目标分支的全量变更(已提交 + 未提交)迁移到主 worktree
28
+ * 使用 git diff HEAD...branch --binary 获取变更,避免 stash 方式无法检测已提交 commit 的问题
29
+ * @param {string} targetWorktreePath - 目标 worktree 路径
30
+ * @param {string} mainWorktreePath - 主 worktree 路径
31
+ * @param {string} branchName - 分支名
32
+ * @param {boolean} hasUncommitted - 目标 worktree 是否有未提交修改
33
+ * @returns {{ success: boolean }} patch 迁移结果
34
+ */
35
+ export function migrateChangesViaPatch(targetWorktreePath: string, mainWorktreePath: string, branchName: string, hasUncommitted: boolean): { success: boolean } {
36
+ let didTempCommit = false;
37
+
38
+ try {
39
+ // 如果有未提交修改,先做临时 commit 以便 diff 能捕获全部变更
40
+ if (hasUncommitted) {
41
+ gitAddAll(targetWorktreePath);
42
+ gitCommit('clawt:temp-commit-for-validate', targetWorktreePath);
43
+ didTempCommit = true;
44
+ }
45
+
46
+ // 在主 worktree 执行三点 diff,获取目标分支自分叉点以来的全量变更
47
+ const patch = gitDiffBinaryAgainstBranch(branchName, mainWorktreePath);
48
+
49
+ // 应用 patch 到主 worktree 工作目录
50
+ if (patch.length > 0) {
51
+ try {
52
+ gitApplyFromStdin(patch, mainWorktreePath);
53
+ } catch (error) {
54
+ logger.warn(`patch apply 失败: ${error}`);
55
+ printWarning(MESSAGES.VALIDATE_PATCH_APPLY_FAILED(branchName));
56
+ return { success: false };
57
+ }
58
+ }
59
+
60
+ return { success: true };
61
+ } finally {
62
+ // 确保临时 commit 一定会被撤销,恢复目标 worktree 原状
63
+ // 每个操作独立 try-catch,避免前一个失败导致后续操作不执行
64
+ if (didTempCommit) {
65
+ try {
66
+ gitResetSoft(1, targetWorktreePath);
67
+ } catch (error) {
68
+ logger.error(`撤销临时 commit 失败: ${error}`);
69
+ }
70
+ try {
71
+ gitRestoreStaged(targetWorktreePath);
72
+ } catch (error) {
73
+ logger.error(`恢复暂存区失败: ${error}`);
74
+ }
75
+ }
76
+ }
77
+ }
78
+
79
+ /**
80
+ * 计算当前主 worktree 工作目录变更的 git tree hash
81
+ * 操作序列:git add . → git write-tree → git restore --staged .
82
+ * 仅计算 tree hash,不写入快照文件
83
+ * @param {string} mainWorktreePath - 主 worktree 路径
84
+ * @returns {string} 当前工作目录变更对应的 tree hash
85
+ */
86
+ export function computeCurrentTreeHash(mainWorktreePath: string): string {
87
+ gitAddAll(mainWorktreePath);
88
+ const treeHash = gitWriteTree(mainWorktreePath);
89
+ gitRestoreStaged(mainWorktreePath);
90
+ return treeHash;
91
+ }
92
+
93
+ /**
94
+ * 保存当前主 worktree 工作目录变更为 git tree 对象快照
95
+ * 操作序列:git add . → git write-tree → git restore --staged .
96
+ * 同时保存当前 HEAD commit hash,用于增量 validate 时对齐基准
97
+ * @param {string} mainWorktreePath - 主 worktree 路径
98
+ * @param {string} projectName - 项目名
99
+ * @param {string} branchName - 分支名
100
+ * @param {string} [stagedTreeHash=''] - validate 结束时暂存区对应的 tree hash
101
+ * @returns {string} 生成的 tree hash
102
+ */
103
+ export function saveCurrentSnapshotTree(mainWorktreePath: string, projectName: string, branchName: string, stagedTreeHash = ''): string {
104
+ gitAddAll(mainWorktreePath);
105
+ const treeHash = gitWriteTree(mainWorktreePath);
106
+ gitRestoreStaged(mainWorktreePath);
107
+ const headCommitHash = getHeadCommitHash(mainWorktreePath);
108
+ writeSnapshot(projectName, branchName, treeHash, headCommitHash, stagedTreeHash);
109
+ return treeHash;
110
+ }
111
+
112
+ /**
113
+ * 将旧快照载入暂存区(从 handleIncrementalValidate 步骤 6 提取)
114
+ * 根据 HEAD 是否变化,选择不同的载入策略:
115
+ * - HEAD 变化:将旧变更 patch 重放到当前 HEAD 暂存区上
116
+ * - HEAD 未变化:直接 read-tree 旧快照
117
+ * @param {string} oldTreeHash - 旧快照的 tree hash
118
+ * @param {string} oldHeadCommitHash - 旧快照时的 HEAD commit hash
119
+ * @param {string} currentHeadCommitHash - 当前的 HEAD commit hash
120
+ * @param {string} mainWorktreePath - 主 worktree 路径
121
+ * @returns {{ success: boolean; stagedTreeHash: string }} 载入结果
122
+ */
123
+ export function loadOldSnapshotToStage(oldTreeHash: string, oldHeadCommitHash: string, currentHeadCommitHash: string, mainWorktreePath: string): { success: boolean; stagedTreeHash: string } {
124
+ try {
125
+ if (oldHeadCommitHash && oldHeadCommitHash !== currentHeadCommitHash) {
126
+ // HEAD 发生了变化:
127
+ // 将旧变更 patch(旧 tree 相对于旧 HEAD 的差异)重放到当前 HEAD 暂存区上,
128
+ // 避免新旧 tree 基准不同导致 diff 混入 HEAD 变化的内容
129
+ const oldHeadTreeHash = getCommitTreeHash(oldHeadCommitHash, mainWorktreePath);
130
+ const oldChangePatch = gitDiffTree(oldHeadTreeHash, oldTreeHash, mainWorktreePath);
131
+
132
+ if (oldChangePatch.length > 0 && gitApplyCachedCheck(oldChangePatch, mainWorktreePath)) {
133
+ // 无冲突:apply --cached 到当前 HEAD 暂存区
134
+ gitApplyCachedFromStdin(oldChangePatch, mainWorktreePath);
135
+ // 记录暂存区的 tree hash(gitWriteTree 不修改暂存区内容,仅生成 tree 对象)
136
+ const stagedTreeHash = gitWriteTree(mainWorktreePath);
137
+ return { success: true, stagedTreeHash };
138
+ } else if (oldChangePatch.length > 0) {
139
+ // 有冲突:降级为全量模式(暂存区保持为空)
140
+ logger.warn('旧变更 patch 与当前 HEAD 冲突,降级为全量模式');
141
+ return { success: false, stagedTreeHash: '' };
142
+ }
143
+ // oldChangePatch 为空表示旧变更为空,暂存区保持干净即可
144
+ return { success: true, stagedTreeHash: '' };
145
+ } else {
146
+ // HEAD 未变化(或旧版快照无 HEAD 信息):直接 read-tree 旧快照
147
+ gitReadTree(oldTreeHash, mainWorktreePath);
148
+ return { success: true, stagedTreeHash: oldTreeHash };
149
+ }
150
+ } catch (error) {
151
+ // 旧 tree 对象无法读取(可能被 git gc 回收),降级为全量模式
152
+ logger.warn(`增量 read-tree 失败: ${error}`);
153
+ return { success: false, stagedTreeHash: '' };
154
+ }
155
+ }
156
+
157
+ /**
158
+ * 切换到验证分支(首次/增量 validate 共享逻辑)
159
+ * 校验验证分支是否存在,若当前不在该分支则执行 checkout
160
+ * @param {string} branchName - 原始分支名
161
+ * @param {string} mainWorktreePath - 主 worktree 路径
162
+ * @returns {string} 验证分支名
163
+ */
164
+ export function switchToValidateBranch(branchName: string, mainWorktreePath: string): string {
165
+ const validateBranchName = getValidateBranchName(branchName);
166
+ if (!checkBranchExists(validateBranchName)) {
167
+ throw new ClawtError(MESSAGES.VALIDATE_BRANCH_NOT_FOUND(validateBranchName, branchName));
168
+ }
169
+ const currentBranch = getCurrentBranch(mainWorktreePath);
170
+ if (currentBranch !== validateBranchName) {
171
+ gitCheckout(validateBranchName, mainWorktreePath);
172
+ }
173
+ return validateBranchName;
174
+ }
@@ -0,0 +1,105 @@
1
+ import { MESSAGES } from '../constants/index.js';
2
+ import {
3
+ printInfo,
4
+ printSuccess,
5
+ printError,
6
+ printSeparator,
7
+ runCommandInherited,
8
+ parseParallelCommands,
9
+ runParallelCommands,
10
+ } from './index.js';
11
+ import type { ParallelCommandResult } from './index.js';
12
+
13
+ /**
14
+ * 执行单个命令(同步方式,保持原有行为不变)
15
+ * @param {string} command - 要执行的命令字符串
16
+ * @param {string} mainWorktreePath - 主 worktree 路径
17
+ */
18
+ function executeSingleCommand(command: string, mainWorktreePath: string): void {
19
+ printInfo(MESSAGES.VALIDATE_RUN_START(command));
20
+ printSeparator();
21
+
22
+ const result = runCommandInherited(command, { cwd: mainWorktreePath });
23
+
24
+ printSeparator();
25
+
26
+ if (result.error) {
27
+ // 进程启动失败(如命令不存在)
28
+ printError(MESSAGES.VALIDATE_RUN_ERROR(command, result.error.message));
29
+ return;
30
+ }
31
+
32
+ const exitCode = result.status ?? 1;
33
+ if (exitCode === 0) {
34
+ printSuccess(MESSAGES.VALIDATE_RUN_SUCCESS(command));
35
+ } else {
36
+ printError(MESSAGES.VALIDATE_RUN_FAILED(command, exitCode));
37
+ }
38
+ }
39
+
40
+ /**
41
+ * 汇总输出并行命令的执行结果
42
+ * @param {ParallelCommandResult[]} results - 各命令的执行结果数组
43
+ */
44
+ function reportParallelResults(results: ParallelCommandResult[]): void {
45
+ printSeparator();
46
+
47
+ const successCount = results.filter((r) => r.exitCode === 0 && !r.error).length;
48
+ const failedCount = results.length - successCount;
49
+
50
+ for (const result of results) {
51
+ if (result.error) {
52
+ printError(MESSAGES.VALIDATE_PARALLEL_CMD_ERROR(result.command, result.error));
53
+ } else if (result.exitCode === 0) {
54
+ printSuccess(MESSAGES.VALIDATE_PARALLEL_CMD_SUCCESS(result.command));
55
+ } else {
56
+ printError(MESSAGES.VALIDATE_PARALLEL_CMD_FAILED(result.command, result.exitCode));
57
+ }
58
+ }
59
+
60
+ if (failedCount === 0) {
61
+ printSuccess(MESSAGES.VALIDATE_PARALLEL_RUN_ALL_SUCCESS(results.length));
62
+ } else {
63
+ printError(MESSAGES.VALIDATE_PARALLEL_RUN_SUMMARY(successCount, failedCount));
64
+ }
65
+ }
66
+
67
+ /**
68
+ * 并行执行多个命令并汇总结果
69
+ * @param {string[]} commands - 要并行执行的命令数组
70
+ * @param {string} mainWorktreePath - 主 worktree 路径
71
+ */
72
+ async function executeParallelCommands(commands: string[], mainWorktreePath: string): Promise<void> {
73
+ printInfo(MESSAGES.VALIDATE_PARALLEL_RUN_START(commands.length));
74
+
75
+ for (let i = 0; i < commands.length; i++) {
76
+ printInfo(MESSAGES.VALIDATE_PARALLEL_CMD_START(i + 1, commands.length, commands[i]));
77
+ }
78
+
79
+ printSeparator();
80
+
81
+ const results = await runParallelCommands(commands, { cwd: mainWorktreePath });
82
+
83
+ reportParallelResults(results);
84
+ }
85
+
86
+ /**
87
+ * 在主 worktree 中执行用户指定的命令
88
+ * 根据命令字符串中的 & 分隔符决定是单命令执行还是并行执行
89
+ * 命令执行失败不影响 validate 本身的结果,仅输出提示
90
+ * @param {string} command - 要执行的命令字符串
91
+ * @param {string} mainWorktreePath - 主 worktree 路径
92
+ */
93
+ export async function executeRunCommand(command: string, mainWorktreePath: string): Promise<void> {
94
+ printInfo('');
95
+
96
+ const commands = parseParallelCommands(command);
97
+
98
+ if (commands.length <= 1) {
99
+ // 单命令(包括含 && 的串行命令),走原有同步路径
100
+ executeSingleCommand(commands[0] || command, mainWorktreePath);
101
+ } else {
102
+ // 多命令,并行执行
103
+ await executeParallelCommands(commands, mainWorktreePath);
104
+ }
105
+ }