clawt 2.5.1 → 2.7.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/.claude/agent-memory/docs-sync-updater/MEMORY.md +18 -21
- package/.claude/agents/docs-sync-updater.md +5 -7
- package/README.md +19 -4
- package/dist/index.js +153 -77
- package/docs/spec.md +64 -13
- package/package.json +1 -1
- package/src/commands/resume.ts +13 -93
- package/src/commands/validate.ts +68 -26
- package/src/constants/messages.ts +9 -0
- package/src/types/command.ts +2 -2
- package/src/utils/git.ts +40 -0
- package/src/utils/index.ts +6 -1
- package/src/utils/validate-snapshot.ts +40 -5
- package/src/utils/worktree-matcher.ts +111 -0
- package/CLAUDE.md +0 -105
package/docs/spec.md
CHANGED
|
@@ -143,6 +143,7 @@ git show-ref --verify refs/heads/<branchName> 2>/dev/null
|
|
|
143
143
|
├── validate-snapshots/ # validate 快照目录
|
|
144
144
|
│ └── <project-name>/ # 以项目名分组
|
|
145
145
|
│ ├── <branchName>.tree # 每个分支一个 tree hash 快照文件(存储 git tree 对象的 hash)
|
|
146
|
+
│ ├── <branchName>.head # 每个分支一个 HEAD commit hash 快照文件(存储快照时主 worktree 的 HEAD commit hash)
|
|
146
147
|
│ └── ...
|
|
147
148
|
└── worktrees/ # 所有 worktree 的统一存放目录
|
|
148
149
|
└── <project-name>/ # 以项目名分组
|
|
@@ -345,14 +346,18 @@ Claude Code CLI 以 `--output-format json` 运行时,退出后会在 stdout
|
|
|
345
346
|
**命令:**
|
|
346
347
|
|
|
347
348
|
```bash
|
|
349
|
+
# 指定分支名(支持模糊匹配)
|
|
348
350
|
clawt validate -b <branchName> [--clean]
|
|
351
|
+
|
|
352
|
+
# 不指定分支名(列出所有分支供选择)
|
|
353
|
+
clawt validate [--clean]
|
|
349
354
|
```
|
|
350
355
|
|
|
351
356
|
**参数:**
|
|
352
357
|
|
|
353
358
|
| 参数 | 必填 | 说明 |
|
|
354
359
|
| --------- | ---- | ------------------------------------------------------------------------ |
|
|
355
|
-
| `-b` |
|
|
360
|
+
| `-b` | 否 | 要验证的 worktree 分支名(支持模糊匹配,不传则列出所有分支供选择) |
|
|
356
361
|
| `--clean` | 否 | 清理 validate 状态(重置主 worktree 并删除快照) |
|
|
357
362
|
|
|
358
363
|
> **限制:** 单次只能验证一个分支,不支持批量验证。
|
|
@@ -363,7 +368,7 @@ Git worktree 不会包含 `node_modules`、`.venv` 等依赖文件,每次安
|
|
|
363
368
|
|
|
364
369
|
**快照机制:**
|
|
365
370
|
|
|
366
|
-
validate 命令引入了**快照(snapshot)机制**来支持增量对比。每次 validate 执行成功后,会将当前全量变更通过 `git write-tree` 保存为 git tree 对象,并将 tree hash 记录到文件(`~/.clawt/validate-snapshots/<project>/<branchName>.tree
|
|
371
|
+
validate 命令引入了**快照(snapshot)机制**来支持增量对比。每次 validate 执行成功后,会将当前全量变更通过 `git write-tree` 保存为 git tree 对象,并将 tree hash 记录到文件(`~/.clawt/validate-snapshots/<project>/<branchName>.tree`),同时将主 worktree 的 HEAD commit hash 记录到文件(`~/.clawt/validate-snapshots/<project>/<branchName>.head`),用于增量 validate 时对齐基准。当再次执行 validate 时,如果主分支 HEAD 未变化,通过 `git read-tree` 将上次快照的 tree 对象载入暂存区;如果主分支 HEAD 已变化(如合并了其他分支),则将旧变更 patch(旧 tree 相对于旧 HEAD 的差异)重放到当前 HEAD 暂存区上,避免新旧 tree 基准不同导致 diff 混入 HEAD 变化的内容。最终用户可通过 `git diff` 查看两次 validate 之间的增量差异。
|
|
367
372
|
|
|
368
373
|
**运行流程:**
|
|
369
374
|
|
|
@@ -372,13 +377,30 @@ validate 命令引入了**快照(snapshot)机制**来支持增量对比。
|
|
|
372
377
|
当指定 `--clean` 选项时,执行清理逻辑后直接返回,不进入常规 validate 流程:
|
|
373
378
|
|
|
374
379
|
1. **主 worktree 校验** (2.1)
|
|
375
|
-
2.
|
|
376
|
-
3.
|
|
377
|
-
4.
|
|
378
|
-
5.
|
|
380
|
+
2. **解析目标 worktree**:通过模糊匹配解析目标分支(匹配策略同下文常规 validate 流程中的描述)
|
|
381
|
+
3. 如果配置项 `confirmDestructiveOps` 为 `true`,提示确认(显示即将执行的危险指令和操作后果),用户取消则退出
|
|
382
|
+
4. 如果主 worktree 有未提交更改,执行 `git reset --hard` + `git clean -fd` 清空
|
|
383
|
+
5. 删除对应分支的快照文件
|
|
384
|
+
6. 输出清理成功提示
|
|
379
385
|
|
|
380
386
|
#### 首次 validate(无历史快照)
|
|
381
387
|
|
|
388
|
+
##### 步骤 0:解析目标 worktree
|
|
389
|
+
|
|
390
|
+
根据 `-b` 参数解析目标 worktree,匹配策略如下:
|
|
391
|
+
|
|
392
|
+
- **未传 `-b` 参数**:
|
|
393
|
+
- 获取当前项目所有 worktree
|
|
394
|
+
- 无可用 worktree → 报错退出
|
|
395
|
+
- 仅 1 个 worktree → 直接使用,无需选择
|
|
396
|
+
- 多个 worktree → 通过交互式列表(Enquirer.Select)让用户选择
|
|
397
|
+
- **传了 `-b` 参数**:
|
|
398
|
+
1. **精确匹配优先**:在 worktree 列表中查找分支名完全相同的 worktree,找到则直接使用
|
|
399
|
+
2. **模糊匹配**(子串匹配,大小写不敏感):
|
|
400
|
+
- 唯一匹配 → 直接使用
|
|
401
|
+
- 多个匹配 → 通过交互式列表让用户从匹配结果中选择
|
|
402
|
+
3. **无匹配** → 报错退出,并列出所有可用分支名
|
|
403
|
+
|
|
382
404
|
##### 步骤 1:检测主 worktree 工作区状态
|
|
383
405
|
|
|
384
406
|
执行 `git status --porcelain`,判断主 worktree 是否有未提交的更改。
|
|
@@ -444,11 +466,12 @@ git restore --staged .
|
|
|
444
466
|
|
|
445
467
|
##### 步骤 4:保存快照为 git tree 对象
|
|
446
468
|
|
|
447
|
-
将主 worktree 工作目录的全量变更保存为 git tree
|
|
469
|
+
将主 worktree 工作目录的全量变更保存为 git tree 对象,同时记录当前 HEAD commit hash:
|
|
448
470
|
|
|
449
471
|
```bash
|
|
450
472
|
git add .
|
|
451
473
|
git write-tree # → 返回 tree hash,写入 ~/.clawt/validate-snapshots/<project>/<branchName>.tree
|
|
474
|
+
git rev-parse HEAD # → 返回 HEAD commit hash,写入 ~/.clawt/validate-snapshots/<project>/<branchName>.head
|
|
452
475
|
git restore --staged .
|
|
453
476
|
```
|
|
454
477
|
|
|
@@ -465,9 +488,9 @@ git restore --staged .
|
|
|
465
488
|
|
|
466
489
|
当 `~/.clawt/validate-snapshots/<project>/<branchName>.tree` 存在时,自动进入增量模式:
|
|
467
490
|
|
|
468
|
-
##### 步骤 1
|
|
491
|
+
##### 步骤 1:读取旧快照
|
|
469
492
|
|
|
470
|
-
在清空主 worktree 之前,读取上次保存的快照 tree hash。
|
|
493
|
+
在清空主 worktree 之前,读取上次保存的快照 tree hash 及当时的 HEAD commit hash。
|
|
471
494
|
|
|
472
495
|
##### 步骤 2:确保主 worktree 干净
|
|
473
496
|
|
|
@@ -479,9 +502,15 @@ git restore --staged .
|
|
|
479
502
|
|
|
480
503
|
##### 步骤 4:保存最新快照为 git tree 对象
|
|
481
504
|
|
|
482
|
-
将最新全量变更保存为新的 tree
|
|
505
|
+
将最新全量变更保存为新的 tree 对象(覆盖旧快照),同时记录当前 HEAD commit hash(流程同首次 validate 的步骤 4)。
|
|
506
|
+
|
|
507
|
+
##### 步骤 5:将旧变更状态载入暂存区
|
|
508
|
+
|
|
509
|
+
根据主分支 HEAD 是否发生变化,选择不同的策略将旧变更载入暂存区:
|
|
510
|
+
|
|
511
|
+
**情况 A:HEAD 未变化(或旧版快照无 HEAD 信息)**
|
|
483
512
|
|
|
484
|
-
|
|
513
|
+
直接通过 `git read-tree` 将旧 tree 对象载入暂存区:
|
|
485
514
|
|
|
486
515
|
```bash
|
|
487
516
|
git read-tree <旧 tree hash>
|
|
@@ -490,6 +519,28 @@ git read-tree <旧 tree hash>
|
|
|
490
519
|
- **读取成功** → 结果:暂存区=上次快照,工作目录=最新全量变更(用户可通过 `git diff` 查看增量差异)
|
|
491
520
|
- **读取失败**(tree 对象可能被 git gc 回收)→ 降级为全量模式,暂存区保持为空,等同于首次 validate 的结果
|
|
492
521
|
|
|
522
|
+
**情况 B:HEAD 发生了变化(如主分支合并了其他分支)**
|
|
523
|
+
|
|
524
|
+
此时旧 tree 对象基于旧 HEAD,直接 read-tree 会导致 diff 混入 HEAD 变化的内容。需要将旧变更 patch(旧 tree 相对于旧 HEAD 的差异)重放到当前 HEAD 暂存区上:
|
|
525
|
+
|
|
526
|
+
```bash
|
|
527
|
+
# 获取旧 HEAD 对应的 tree hash
|
|
528
|
+
git rev-parse <旧 HEAD commit hash>^{tree} # → 旧 HEAD tree hash
|
|
529
|
+
|
|
530
|
+
# 提取旧变更 patch(旧 HEAD tree → 旧快照 tree 的差异)
|
|
531
|
+
git diff-tree -p --binary <旧 HEAD tree hash> <旧快照 tree hash>
|
|
532
|
+
|
|
533
|
+
# 检测 patch 能否无冲突地应用到暂存区
|
|
534
|
+
git apply --cached --check < patch
|
|
535
|
+
|
|
536
|
+
# 无冲突:apply --cached 到当前 HEAD 暂存区
|
|
537
|
+
git apply --cached < patch
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
- **patch 为空**(旧变更为空)→ 暂存区保持干净
|
|
541
|
+
- **无冲突** → apply --cached 到当前 HEAD 暂存区,结果与情况 A 一致
|
|
542
|
+
- **有冲突** → 降级为全量模式(暂存区保持为空),等同于首次 validate 的结果
|
|
543
|
+
|
|
493
544
|
##### 步骤 6:输出成功提示
|
|
494
545
|
|
|
495
546
|
```
|
|
@@ -670,7 +721,7 @@ clawt merge -b <branchName> [-m <commitMessage>]
|
|
|
670
721
|
- 输出清理成功提示:`✓ 已清理 worktree 和分支: <branchName>`
|
|
671
722
|
|
|
672
723
|
10. **清理 validate 快照**
|
|
673
|
-
- merge 成功后,如果存在该分支的 validate 快照(`~/.clawt/validate-snapshots/<project>/<branchName>.tree
|
|
724
|
+
- merge 成功后,如果存在该分支的 validate 快照(`~/.clawt/validate-snapshots/<project>/<branchName>.tree` 和 `<branchName>.head`),自动删除这些快照文件(merge 成功后快照已无意义)
|
|
674
725
|
|
|
675
726
|
> **注意:** 清理确认和清理操作均在 merge 成功后执行。只有 merge 成功才会询问用户是否清理 worktree 和分支,避免 merge 冲突时用户被提前询问造成困惑。
|
|
676
727
|
|
|
@@ -936,7 +987,7 @@ clawt sync -b <branchName>
|
|
|
936
987
|
clawt validate -b <branch> 验证变更
|
|
937
988
|
```
|
|
938
989
|
- **无冲突** → 继续
|
|
939
|
-
7. **清除 validate 快照**:合并成功后,如果该分支存在 validate 快照(`.tree` 文件),自动删除(代码基础已变化,旧快照无效)
|
|
990
|
+
7. **清除 validate 快照**:合并成功后,如果该分支存在 validate 快照(`.tree` 和 `.head` 文件),自动删除(代码基础已变化,旧快照无效)
|
|
940
991
|
8. **输出成功提示**:
|
|
941
992
|
```
|
|
942
993
|
✓ 已将 <mainBranch> 的最新代码同步到 <branchName>
|
package/package.json
CHANGED
package/src/commands/resume.ts
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
-
import Enquirer from 'enquirer';
|
|
3
2
|
import { logger } from '../logger/index.js';
|
|
4
|
-
import { ClawtError } from '../errors/index.js';
|
|
5
3
|
import { MESSAGES } from '../constants/index.js';
|
|
6
|
-
import type { ResumeOptions
|
|
4
|
+
import type { ResumeOptions } from '../types/index.js';
|
|
7
5
|
import {
|
|
8
6
|
validateMainWorktree,
|
|
9
7
|
validateClaudeCodeInstalled,
|
|
10
8
|
getProjectWorktrees,
|
|
11
9
|
launchInteractiveClaude,
|
|
10
|
+
resolveTargetWorktree,
|
|
12
11
|
} from '../utils/index.js';
|
|
12
|
+
import type { WorktreeResolveMessages } from '../utils/index.js';
|
|
13
|
+
|
|
14
|
+
/** resume 命令的分支解析消息配置 */
|
|
15
|
+
const RESUME_RESOLVE_MESSAGES: WorktreeResolveMessages = {
|
|
16
|
+
noWorktrees: MESSAGES.RESUME_NO_WORKTREES,
|
|
17
|
+
selectBranch: MESSAGES.RESUME_SELECT_BRANCH,
|
|
18
|
+
multipleMatches: MESSAGES.RESUME_MULTIPLE_MATCHES,
|
|
19
|
+
noMatch: MESSAGES.RESUME_NO_MATCH,
|
|
20
|
+
};
|
|
13
21
|
|
|
14
22
|
/**
|
|
15
23
|
* 注册 resume 命令:在已有 worktree 中恢复 Claude Code 会话
|
|
@@ -25,95 +33,6 @@ export function registerResumeCommand(program: Command): void {
|
|
|
25
33
|
});
|
|
26
34
|
}
|
|
27
35
|
|
|
28
|
-
/**
|
|
29
|
-
* 在 worktree 列表中精确匹配分支名
|
|
30
|
-
* @param {WorktreeInfo[]} worktrees - worktree 列表
|
|
31
|
-
* @param {string} branchName - 目标分支名
|
|
32
|
-
* @returns {WorktreeInfo | undefined} 匹配的 worktree,未找到返回 undefined
|
|
33
|
-
*/
|
|
34
|
-
function findExactMatch(worktrees: WorktreeInfo[], branchName: string): WorktreeInfo | undefined {
|
|
35
|
-
return worktrees.find((wt) => wt.branch === branchName);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* 在 worktree 列表中进行模糊匹配(子串匹配,大小写不敏感)
|
|
40
|
-
* @param {WorktreeInfo[]} worktrees - worktree 列表
|
|
41
|
-
* @param {string} keyword - 匹配关键词
|
|
42
|
-
* @returns {WorktreeInfo[]} 匹配到的 worktree 列表
|
|
43
|
-
*/
|
|
44
|
-
function findFuzzyMatches(worktrees: WorktreeInfo[], keyword: string): WorktreeInfo[] {
|
|
45
|
-
const lowerKeyword = keyword.toLowerCase();
|
|
46
|
-
return worktrees.filter((wt) => wt.branch.toLowerCase().includes(lowerKeyword));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
/**
|
|
50
|
-
* 通过交互式列表让用户从 worktree 列表中选择一个分支
|
|
51
|
-
* @param {WorktreeInfo[]} worktrees - 可供选择的 worktree 列表
|
|
52
|
-
* @param {string} message - 选择提示信息
|
|
53
|
-
* @returns {Promise<WorktreeInfo>} 用户选择的 worktree
|
|
54
|
-
*/
|
|
55
|
-
async function promptSelectBranch(worktrees: WorktreeInfo[], message: string): Promise<WorktreeInfo> {
|
|
56
|
-
// @ts-expect-error enquirer 类型声明未导出 Select 类,但运行时存在
|
|
57
|
-
const selectedBranch: string = await new Enquirer.Select({
|
|
58
|
-
message,
|
|
59
|
-
choices: worktrees.map((wt) => ({
|
|
60
|
-
name: wt.branch,
|
|
61
|
-
message: wt.branch,
|
|
62
|
-
})),
|
|
63
|
-
}).run();
|
|
64
|
-
|
|
65
|
-
return worktrees.find((wt) => wt.branch === selectedBranch)!;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* 根据用户输入解析目标 worktree
|
|
70
|
-
* 匹配策略:精确匹配 → 模糊匹配(唯一直接使用,多个交互选择) → 无匹配报错
|
|
71
|
-
* 不传分支名时列出所有可用分支供选择
|
|
72
|
-
* @param {string} [branchName] - 用户输入的分支名(可选)
|
|
73
|
-
* @returns {Promise<WorktreeInfo>} 解析后的目标 worktree
|
|
74
|
-
* @throws {ClawtError} 无可用 worktree 或无匹配结果时抛出
|
|
75
|
-
*/
|
|
76
|
-
async function resolveTargetWorktree(branchName?: string): Promise<WorktreeInfo> {
|
|
77
|
-
const worktrees = getProjectWorktrees();
|
|
78
|
-
|
|
79
|
-
// 无可用 worktree,直接报错
|
|
80
|
-
if (worktrees.length === 0) {
|
|
81
|
-
throw new ClawtError(MESSAGES.RESUME_NO_WORKTREES);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// 未传 -b 参数:列出所有分支供选择
|
|
85
|
-
if (!branchName) {
|
|
86
|
-
// 只有一个 worktree 时直接使用,无需选择
|
|
87
|
-
if (worktrees.length === 1) {
|
|
88
|
-
return worktrees[0];
|
|
89
|
-
}
|
|
90
|
-
return promptSelectBranch(worktrees, MESSAGES.RESUME_SELECT_BRANCH);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// 1. 精确匹配优先
|
|
94
|
-
const exactMatch = findExactMatch(worktrees, branchName);
|
|
95
|
-
if (exactMatch) {
|
|
96
|
-
return exactMatch;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
// 2. 模糊匹配
|
|
100
|
-
const fuzzyMatches = findFuzzyMatches(worktrees, branchName);
|
|
101
|
-
|
|
102
|
-
// 2a. 唯一匹配,直接使用
|
|
103
|
-
if (fuzzyMatches.length === 1) {
|
|
104
|
-
return fuzzyMatches[0];
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// 2b. 多个匹配,交互选择
|
|
108
|
-
if (fuzzyMatches.length > 1) {
|
|
109
|
-
return promptSelectBranch(fuzzyMatches, MESSAGES.RESUME_MULTIPLE_MATCHES(branchName));
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// 3. 无匹配,抛出错误并列出所有可用分支
|
|
113
|
-
const allBranches = worktrees.map((wt) => wt.branch);
|
|
114
|
-
throw new ClawtError(MESSAGES.RESUME_NO_MATCH(branchName, allBranches));
|
|
115
|
-
}
|
|
116
|
-
|
|
117
36
|
/**
|
|
118
37
|
* 执行 resume 命令的核心逻辑
|
|
119
38
|
* 解析目标 worktree 并恢复 Claude Code 会话
|
|
@@ -126,7 +45,8 @@ async function handleResume(options: ResumeOptions): Promise<void> {
|
|
|
126
45
|
logger.info(`resume 命令执行,分支: ${options.branch ?? '(未指定)'}`);
|
|
127
46
|
|
|
128
47
|
// 解析目标 worktree(精确匹配 / 模糊匹配 / 交互选择)
|
|
129
|
-
const
|
|
48
|
+
const worktrees = getProjectWorktrees();
|
|
49
|
+
const worktree = await resolveTargetWorktree(worktrees, RESUME_RESOLVE_MESSAGES, options.branch);
|
|
130
50
|
|
|
131
51
|
// 启动 Claude Code 交互式界面
|
|
132
52
|
launchInteractiveClaude(worktree);
|
package/src/commands/validate.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { existsSync } from 'node:fs';
|
|
4
2
|
import Enquirer from 'enquirer';
|
|
5
3
|
import { logger } from '../logger/index.js';
|
|
6
4
|
import { ClawtError } from '../errors/index.js';
|
|
@@ -10,7 +8,7 @@ import {
|
|
|
10
8
|
validateMainWorktree,
|
|
11
9
|
getProjectName,
|
|
12
10
|
getGitTopLevel,
|
|
13
|
-
|
|
11
|
+
getProjectWorktrees,
|
|
14
12
|
getConfigValue,
|
|
15
13
|
isWorkingDirClean,
|
|
16
14
|
gitAddAll,
|
|
@@ -21,19 +19,34 @@ import {
|
|
|
21
19
|
gitCleanForce,
|
|
22
20
|
gitDiffBinaryAgainstBranch,
|
|
23
21
|
gitApplyFromStdin,
|
|
22
|
+
gitApplyCachedFromStdin,
|
|
24
23
|
gitResetSoft,
|
|
25
24
|
gitWriteTree,
|
|
26
25
|
gitReadTree,
|
|
26
|
+
getHeadCommitHash,
|
|
27
|
+
getCommitTreeHash,
|
|
28
|
+
gitDiffTree,
|
|
29
|
+
gitApplyCachedCheck,
|
|
27
30
|
hasLocalCommits,
|
|
28
31
|
hasSnapshot,
|
|
29
|
-
|
|
32
|
+
readSnapshot,
|
|
30
33
|
writeSnapshot,
|
|
31
34
|
removeSnapshot,
|
|
32
35
|
confirmDestructiveAction,
|
|
33
36
|
printSuccess,
|
|
34
37
|
printWarning,
|
|
35
38
|
printInfo,
|
|
39
|
+
resolveTargetWorktree,
|
|
36
40
|
} from '../utils/index.js';
|
|
41
|
+
import type { WorktreeResolveMessages } from '../utils/index.js';
|
|
42
|
+
|
|
43
|
+
/** validate 命令的分支解析消息配置 */
|
|
44
|
+
const VALIDATE_RESOLVE_MESSAGES: WorktreeResolveMessages = {
|
|
45
|
+
noWorktrees: MESSAGES.VALIDATE_NO_WORKTREES,
|
|
46
|
+
selectBranch: MESSAGES.VALIDATE_SELECT_BRANCH,
|
|
47
|
+
multipleMatches: MESSAGES.VALIDATE_MULTIPLE_MATCHES,
|
|
48
|
+
noMatch: MESSAGES.VALIDATE_NO_MATCH,
|
|
49
|
+
};
|
|
37
50
|
|
|
38
51
|
/**
|
|
39
52
|
* 注册 validate 命令:在主 worktree 验证其他分支的变更
|
|
@@ -43,7 +56,7 @@ export function registerValidateCommand(program: Command): void {
|
|
|
43
56
|
program
|
|
44
57
|
.command('validate')
|
|
45
58
|
.description('在主 worktree 验证某个 worktree 分支的变更')
|
|
46
|
-
.
|
|
59
|
+
.option('-b, --branch <branchName>', '要验证的分支名(支持模糊匹配,不传则列出所有分支)')
|
|
47
60
|
.option('--clean', '清理 validate 状态(重置主 worktree 并删除快照)')
|
|
48
61
|
.action(async (options: ValidateOptions) => {
|
|
49
62
|
await handleValidate(options);
|
|
@@ -148,6 +161,7 @@ function migrateChangesViaPatch(targetWorktreePath: string, mainWorktreePath: st
|
|
|
148
161
|
/**
|
|
149
162
|
* 保存当前主 worktree 工作目录变更为 git tree 对象快照
|
|
150
163
|
* 操作序列:git add . → git write-tree → git restore --staged .
|
|
164
|
+
* 同时保存当前 HEAD commit hash,用于增量 validate 时对齐基准
|
|
151
165
|
* @param {string} mainWorktreePath - 主 worktree 路径
|
|
152
166
|
* @param {string} projectName - 项目名
|
|
153
167
|
* @param {string} branchName - 分支名
|
|
@@ -157,7 +171,8 @@ function saveCurrentSnapshotTree(mainWorktreePath: string, projectName: string,
|
|
|
157
171
|
gitAddAll(mainWorktreePath);
|
|
158
172
|
const treeHash = gitWriteTree(mainWorktreePath);
|
|
159
173
|
gitRestoreStaged(mainWorktreePath);
|
|
160
|
-
|
|
174
|
+
const headCommitHash = getHeadCommitHash(mainWorktreePath);
|
|
175
|
+
writeSnapshot(projectName, branchName, treeHash, headCommitHash);
|
|
161
176
|
return treeHash;
|
|
162
177
|
}
|
|
163
178
|
|
|
@@ -171,13 +186,18 @@ async function handleValidateClean(options: ValidateOptions): Promise<void> {
|
|
|
171
186
|
const projectName = getProjectName();
|
|
172
187
|
const mainWorktreePath = getGitTopLevel();
|
|
173
188
|
|
|
174
|
-
|
|
189
|
+
// 通过模糊匹配解析目标 worktree
|
|
190
|
+
const worktrees = getProjectWorktrees();
|
|
191
|
+
const worktree = await resolveTargetWorktree(worktrees, VALIDATE_RESOLVE_MESSAGES, options.branch);
|
|
192
|
+
const branchName = worktree.branch;
|
|
193
|
+
|
|
194
|
+
logger.info(`validate --clean 执行,分支: ${branchName}`);
|
|
175
195
|
|
|
176
196
|
// 根据配置决定是否需要确认
|
|
177
197
|
if (getConfigValue('confirmDestructiveOps')) {
|
|
178
198
|
const confirmed = await confirmDestructiveAction(
|
|
179
199
|
'git reset --hard + git clean -fd',
|
|
180
|
-
`重置主 worktree 并删除分支 ${
|
|
200
|
+
`重置主 worktree 并删除分支 ${branchName} 的 validate 快照`,
|
|
181
201
|
);
|
|
182
202
|
if (!confirmed) {
|
|
183
203
|
printInfo(MESSAGES.DESTRUCTIVE_OP_CANCELLED);
|
|
@@ -192,9 +212,9 @@ async function handleValidateClean(options: ValidateOptions): Promise<void> {
|
|
|
192
212
|
}
|
|
193
213
|
|
|
194
214
|
// 删除对应的快照文件
|
|
195
|
-
removeSnapshot(projectName,
|
|
215
|
+
removeSnapshot(projectName, branchName);
|
|
196
216
|
|
|
197
|
-
printSuccess(MESSAGES.VALIDATE_CLEANED(
|
|
217
|
+
printSuccess(MESSAGES.VALIDATE_CLEANED(branchName));
|
|
198
218
|
}
|
|
199
219
|
|
|
200
220
|
/**
|
|
@@ -225,8 +245,8 @@ function handleFirstValidate(targetWorktreePath: string, mainWorktreePath: strin
|
|
|
225
245
|
* @param {boolean} hasUncommitted - 目标 worktree 是否有未提交修改
|
|
226
246
|
*/
|
|
227
247
|
function handleIncrementalValidate(targetWorktreePath: string, mainWorktreePath: string, projectName: string, branchName: string, hasUncommitted: boolean): void {
|
|
228
|
-
// 步骤 1
|
|
229
|
-
const oldTreeHash =
|
|
248
|
+
// 步骤 1:读取旧快照(tree hash + 当时的 HEAD commit hash)
|
|
249
|
+
const { treeHash: oldTreeHash, headCommitHash: oldHeadCommitHash } = readSnapshot(projectName, branchName);
|
|
230
250
|
|
|
231
251
|
// 步骤 2:确保主 worktree 干净(调用方已通过 handleDirtyMainWorktree 处理)
|
|
232
252
|
// 这里做兜底清理,防止 handleDirtyMainWorktree 之后仍有残留
|
|
@@ -238,12 +258,35 @@ function handleIncrementalValidate(targetWorktreePath: string, mainWorktreePath:
|
|
|
238
258
|
// 步骤 3:通过 patch 从目标分支获取最新全量变更
|
|
239
259
|
migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
|
|
240
260
|
|
|
241
|
-
// 步骤 4:保存最新快照为 git tree
|
|
261
|
+
// 步骤 4:保存最新快照为 git tree 对象(同时记录当前 HEAD)
|
|
242
262
|
saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
|
|
243
263
|
|
|
244
|
-
// 步骤 5
|
|
264
|
+
// 步骤 5:将旧变更状态载入暂存区
|
|
245
265
|
try {
|
|
246
|
-
|
|
266
|
+
const currentHeadCommitHash = getHeadCommitHash(mainWorktreePath);
|
|
267
|
+
|
|
268
|
+
if (oldHeadCommitHash && oldHeadCommitHash !== currentHeadCommitHash) {
|
|
269
|
+
// HEAD 发生了变化(如主分支合并了其他分支):
|
|
270
|
+
// 将旧变更 patch(旧 tree 相对于旧 HEAD 的差异)重放到当前 HEAD 暂存区上,
|
|
271
|
+
// 避免新旧 tree 基准不同导致 diff 混入 HEAD 变化的内容
|
|
272
|
+
const oldHeadTreeHash = getCommitTreeHash(oldHeadCommitHash, mainWorktreePath);
|
|
273
|
+
const oldChangePatch = gitDiffTree(oldHeadTreeHash, oldTreeHash, mainWorktreePath);
|
|
274
|
+
|
|
275
|
+
if (oldChangePatch.length > 0 && gitApplyCachedCheck(oldChangePatch, mainWorktreePath)) {
|
|
276
|
+
// 无冲突:apply --cached 到当前 HEAD 暂存区
|
|
277
|
+
gitApplyCachedFromStdin(oldChangePatch, mainWorktreePath);
|
|
278
|
+
} else if (oldChangePatch.length > 0) {
|
|
279
|
+
// 有冲突:降级为全量模式(暂存区保持为空)
|
|
280
|
+
logger.warn('旧变更 patch 与当前 HEAD 冲突,降级为全量模式');
|
|
281
|
+
printWarning(MESSAGES.INCREMENTAL_VALIDATE_FALLBACK);
|
|
282
|
+
printSuccess(MESSAGES.VALIDATE_SUCCESS(branchName));
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
// oldChangePatch 为空表示旧变更为空,暂存区保持干净即可
|
|
286
|
+
} else {
|
|
287
|
+
// HEAD 未变化(或旧版快照无 HEAD 信息):直接 read-tree 旧快照
|
|
288
|
+
gitReadTree(oldTreeHash, mainWorktreePath);
|
|
289
|
+
}
|
|
247
290
|
} catch (error) {
|
|
248
291
|
// 旧 tree 对象无法读取(可能被 git gc 回收),降级为全量模式
|
|
249
292
|
logger.warn(`增量 read-tree 失败: ${error}`);
|
|
@@ -272,19 +315,18 @@ async function handleValidate(options: ValidateOptions): Promise<void> {
|
|
|
272
315
|
|
|
273
316
|
const projectName = getProjectName();
|
|
274
317
|
const mainWorktreePath = getGitTopLevel();
|
|
275
|
-
const projectDir = getProjectWorktreeDir();
|
|
276
|
-
const targetWorktreePath = join(projectDir, options.branch);
|
|
277
318
|
|
|
278
|
-
|
|
319
|
+
// 通过模糊匹配解析目标 worktree
|
|
320
|
+
const worktrees = getProjectWorktrees();
|
|
321
|
+
const worktree = await resolveTargetWorktree(worktrees, VALIDATE_RESOLVE_MESSAGES, options.branch);
|
|
322
|
+
const branchName = worktree.branch;
|
|
323
|
+
const targetWorktreePath = worktree.path;
|
|
279
324
|
|
|
280
|
-
|
|
281
|
-
if (!existsSync(targetWorktreePath)) {
|
|
282
|
-
throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(options.branch));
|
|
283
|
-
}
|
|
325
|
+
logger.info(`validate 命令执行,分支: ${branchName}`);
|
|
284
326
|
|
|
285
327
|
// 统一检测未提交修改 + 已提交 commit
|
|
286
328
|
const hasUncommitted = !isWorkingDirClean(targetWorktreePath);
|
|
287
|
-
const hasCommitted = hasLocalCommits(
|
|
329
|
+
const hasCommitted = hasLocalCommits(branchName, mainWorktreePath);
|
|
288
330
|
|
|
289
331
|
if (!hasUncommitted && !hasCommitted) {
|
|
290
332
|
printInfo(MESSAGES.TARGET_WORKTREE_CLEAN);
|
|
@@ -292,20 +334,20 @@ async function handleValidate(options: ValidateOptions): Promise<void> {
|
|
|
292
334
|
}
|
|
293
335
|
|
|
294
336
|
// 判断是否为增量 validate(tree 对象不依赖主分支 HEAD,无需一致性校验)
|
|
295
|
-
const isIncremental = hasSnapshot(projectName,
|
|
337
|
+
const isIncremental = hasSnapshot(projectName, branchName);
|
|
296
338
|
|
|
297
339
|
if (isIncremental) {
|
|
298
340
|
// 增量模式:主 worktree 有残留状态时让用户选择处理方式
|
|
299
341
|
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
300
342
|
await handleDirtyMainWorktree(mainWorktreePath);
|
|
301
343
|
}
|
|
302
|
-
handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName,
|
|
344
|
+
handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
|
|
303
345
|
} else {
|
|
304
346
|
// 首次模式:先确保主 worktree 干净
|
|
305
347
|
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
306
348
|
await handleDirtyMainWorktree(mainWorktreePath);
|
|
307
349
|
}
|
|
308
350
|
|
|
309
|
-
handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName,
|
|
351
|
+
handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
|
|
310
352
|
}
|
|
311
353
|
}
|
|
@@ -119,4 +119,13 @@ export const MESSAGES = {
|
|
|
119
119
|
RESUME_SELECT_BRANCH: '请选择要恢复的分支',
|
|
120
120
|
/** resume 模糊匹配到多个结果提示 */
|
|
121
121
|
RESUME_MULTIPLE_MATCHES: (name: string) => `"${name}" 匹配到多个分支,请选择:`,
|
|
122
|
+
/** validate 无可用 worktree */
|
|
123
|
+
VALIDATE_NO_WORKTREES: '当前项目没有可用的 worktree,请先通过 clawt run 或 clawt create 创建',
|
|
124
|
+
/** validate 模糊匹配无结果,列出可用分支 */
|
|
125
|
+
VALIDATE_NO_MATCH: (name: string, branches: string[]) =>
|
|
126
|
+
`未找到与 "${name}" 匹配的分支\n 可用分支:\n${branches.map((b) => ` - ${b}`).join('\n')}`,
|
|
127
|
+
/** validate 交互选择提示 */
|
|
128
|
+
VALIDATE_SELECT_BRANCH: '请选择要验证的分支',
|
|
129
|
+
/** validate 模糊匹配到多个结果提示 */
|
|
130
|
+
VALIDATE_MULTIPLE_MATCHES: (name: string) => `"${name}" 匹配到多个分支,请选择:`,
|
|
122
131
|
} as const;
|
package/src/types/command.ts
CHANGED
package/src/utils/git.ts
CHANGED
|
@@ -425,3 +425,43 @@ export function gitWriteTree(cwd?: string): string {
|
|
|
425
425
|
export function gitReadTree(treeHash: string, cwd?: string): void {
|
|
426
426
|
execCommand(`git read-tree ${treeHash}`, { cwd });
|
|
427
427
|
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* 获取指定 commit 对应的 tree 对象 hash
|
|
431
|
+
* @param {string} commitHash - commit hash
|
|
432
|
+
* @param {string} [cwd] - 工作目录
|
|
433
|
+
* @returns {string} tree 对象的 hash
|
|
434
|
+
*/
|
|
435
|
+
export function getCommitTreeHash(commitHash: string, cwd?: string): string {
|
|
436
|
+
return execCommand(`git rev-parse ${commitHash}^{tree}`, { cwd });
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* 获取两个 tree 对象之间的 diff(patch 格式,含二进制)
|
|
441
|
+
* @param {string} baseTreeHash - 基准 tree hash
|
|
442
|
+
* @param {string} targetTreeHash - 目标 tree hash
|
|
443
|
+
* @param {string} [cwd] - 工作目录
|
|
444
|
+
* @returns {Buffer} diff patch 内容
|
|
445
|
+
*/
|
|
446
|
+
export function gitDiffTree(baseTreeHash: string, targetTreeHash: string, cwd?: string): Buffer {
|
|
447
|
+
logger.debug(`执行命令: git diff-tree -p --binary ${baseTreeHash} ${targetTreeHash}${cwd ? ` (cwd: ${cwd})` : ''}`);
|
|
448
|
+
return execSync(`git diff-tree -p --binary ${baseTreeHash} ${targetTreeHash}`, {
|
|
449
|
+
cwd,
|
|
450
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* 检测 patch 能否无冲突地应用到暂存区(干运行,不实际修改)
|
|
456
|
+
* @param {Buffer} patchContent - patch 内容(Buffer 格式)
|
|
457
|
+
* @param {string} [cwd] - 工作目录
|
|
458
|
+
* @returns {boolean} patch 能否成功应用
|
|
459
|
+
*/
|
|
460
|
+
export function gitApplyCachedCheck(patchContent: Buffer, cwd?: string): boolean {
|
|
461
|
+
try {
|
|
462
|
+
execCommandWithInput('git', ['apply', '--cached', '--check'], { input: patchContent, cwd });
|
|
463
|
+
return true;
|
|
464
|
+
} catch {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -40,6 +40,9 @@ export {
|
|
|
40
40
|
gitResetSoftTo,
|
|
41
41
|
gitWriteTree,
|
|
42
42
|
gitReadTree,
|
|
43
|
+
getCommitTreeHash,
|
|
44
|
+
gitDiffTree,
|
|
45
|
+
gitApplyCachedCheck,
|
|
43
46
|
} from './git.js';
|
|
44
47
|
export { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } from './branch.js';
|
|
45
48
|
export { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled } from './validation.js';
|
|
@@ -49,4 +52,6 @@ export { printSuccess, printError, printWarning, printInfo, printSeparator, prin
|
|
|
49
52
|
export { ensureDir, removeEmptyDir } from './fs.js';
|
|
50
53
|
export { multilineInput } from './prompt.js';
|
|
51
54
|
export { launchInteractiveClaude } from './claude.js';
|
|
52
|
-
export { getSnapshotPath, hasSnapshot, readSnapshotTreeHash, writeSnapshot, removeSnapshot, removeProjectSnapshots } from './validate-snapshot.js';
|
|
55
|
+
export { getSnapshotPath, hasSnapshot, readSnapshotTreeHash, readSnapshot, writeSnapshot, removeSnapshot, removeProjectSnapshots } from './validate-snapshot.js';
|
|
56
|
+
export { findExactMatch, findFuzzyMatches, promptSelectBranch, resolveTargetWorktree } from './worktree-matcher.js';
|
|
57
|
+
export type { WorktreeResolveMessages } from './worktree-matcher.js';
|