clawt 2.1.0 → 2.3.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 +10 -8
- package/CLAUDE.md +14 -6
- package/README.md +14 -1
- package/dist/index.js +51 -63
- package/docs/spec.md +55 -30
- package/package.json +1 -1
- package/src/commands/reset.ts +42 -0
- package/src/commands/validate.ts +30 -47
- package/src/constants/messages.ts +4 -0
- package/src/index.ts +2 -0
- package/src/utils/git.ts +18 -0
- package/src/utils/index.ts +3 -1
- package/src/utils/validate-snapshot.ts +9 -43
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
### docs/spec.md
|
|
6
6
|
- 完整的软件规格说明,包含 7 大章节
|
|
7
|
-
- 命令流程在 `5. 需求场景详细设计` 下,每个命令一个子章节(5.1-5.
|
|
7
|
+
- 命令流程在 `5. 需求场景详细设计` 下,每个命令一个子章节(5.1-5.13)
|
|
8
8
|
- run 命令对应 `5.2 批量创建 Worktree + 执行 Claude Code 任务`,流程按步骤编号描述
|
|
9
9
|
- merge 命令对应 `5.6 合并验证过的分支`,流程按步骤编号描述
|
|
10
10
|
- config 命令对应 `5.10 查看全局配置`,只读展示配置
|
|
@@ -60,24 +60,26 @@ run 命令有两种模式(自 claudeCodeCommand 特性后):
|
|
|
60
60
|
- 传 `--tasks`:并行任务模式(多 worktree + `executeClaudeTask` + spawnProcess)
|
|
61
61
|
- CLAUDE.md 中的核心流程按模式分段描述
|
|
62
62
|
|
|
63
|
-
## 命令清单(
|
|
63
|
+
## 命令清单(10 个)
|
|
64
64
|
|
|
65
|
-
`create`、`run`、`resume`、`list`、`remove`、`validate`、`merge`、`config`
|
|
65
|
+
`create`、`run`、`resume`、`list`、`remove`、`validate`、`merge`、`config`、`sync`、`reset`
|
|
66
66
|
|
|
67
67
|
Notes:
|
|
68
68
|
- resume 和 run(交互式模式)共用 `launchInteractiveClaude()`,该函数从 run.ts 提取到 src/utils/claude.ts
|
|
69
69
|
- `claudeCodeCommand` 配置项同时影响 run 交互式模式和 resume 命令
|
|
70
|
+
- reset 命令与 validate --clean 的区别:reset 不删除快照文件,validate --clean 会删除快照
|
|
70
71
|
|
|
71
72
|
## validate 快照机制
|
|
72
73
|
|
|
73
74
|
- validate 命令支持首次/增量两种模式,通过 `hasSnapshot()` 判断
|
|
74
|
-
- 快照路径:`~/.clawt/validate-snapshots/<projectName>/<branchName>.
|
|
75
|
+
- 快照路径:`~/.clawt/validate-snapshots/<projectName>/<branchName>.tree`(存储 git tree 对象 hash)
|
|
75
76
|
- 常量 `VALIDATE_SNAPSHOTS_DIR` 定义在 `src/constants/paths.ts`
|
|
76
77
|
- validate 新增 `--clean` 选项(`ValidateOptions.clean?: boolean`)
|
|
77
|
-
-
|
|
78
|
-
-
|
|
79
|
-
-
|
|
80
|
-
-
|
|
78
|
+
- 快照保存:`git add . → git write-tree → git restore --staged .`,将 tree hash 写入 `.tree` 文件
|
|
79
|
+
- 增量模式核心:`git read-tree <旧 tree hash>` 将旧快照载入暂存区 + 新全量变更在工作目录 → `git diff` 可查看增量差异
|
|
80
|
+
- tree 对象不依赖主分支 HEAD,无需一致性校验(旧方案需要 `.head` 文件校验 HEAD 一致性)
|
|
81
|
+
- 增量 read-tree 失败时自动降级为全量模式(tree 对象可能被 git gc 回收)
|
|
82
|
+
- git 层有 `gitWriteTree()`(返回 tree hash)和 `gitReadTree()`(载入暂存区)
|
|
81
83
|
- merge 成功后自动清理对应快照;merge 时主 worktree 脏 + 存在快照会输出警告提示
|
|
82
84
|
- docs/spec.md 中 validate 章节(5.4)按 `--clean 模式`、`首次 validate`、`增量 validate` 三段描述
|
|
83
85
|
- CLAUDE.md 中在 validate + merge 工作流章节用缩进列表描述两种模式
|
package/CLAUDE.md
CHANGED
|
@@ -24,7 +24,7 @@ npm i -g . # 本地全局安装进行测试
|
|
|
24
24
|
|
|
25
25
|
每个命令为独立文件 `src/commands/<name>.ts`,导出 `registerXxxCommand(program)` 函数,在 `src/index.ts` 中统一注册到 Commander。命令内部逻辑封装在对应的 `handleXxx` 函数中。
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
十个命令:`create`、`run`、`resume`、`list`、`remove`、`validate`、`merge`、`config`、`sync`、`reset`。
|
|
28
28
|
|
|
29
29
|
### 核心流程(run 命令)
|
|
30
30
|
|
|
@@ -56,10 +56,11 @@ run 命令有两种模式:
|
|
|
56
56
|
### validate + merge 工作流
|
|
57
57
|
|
|
58
58
|
- `validate`:将目标分支的全量变更(已提交 + 未提交)通过 `git diff HEAD...branch --binary` 的 patch 方式迁移到主 worktree,便于在主 worktree 中测试。支持两种模式:
|
|
59
|
-
- **首次 validate**(无历史快照):patch 迁移全量变更 →
|
|
60
|
-
- **增量 validate
|
|
59
|
+
- **首次 validate**(无历史快照):patch 迁移全量变更 → 通过 `git write-tree` 保存快照为 git tree 对象 → 结果:暂存区=空,工作目录=全量变更
|
|
60
|
+
- **增量 validate**(存在历史快照):读取旧 tree hash → 确保主 worktree 干净 → patch 迁移最新变更 → 保存新 tree 对象快照 → `git read-tree` 将旧 tree 载入暂存区 → 结果:暂存区=上次快照,工作目录=最新变更(可通过 `git diff` 查看增量差异)
|
|
61
61
|
- `--clean` 选项:重置主 worktree + 删除对应快照文件
|
|
62
|
-
- 快照存储路径:`~/.clawt/validate-snapshots/<projectName>/<branchName>.
|
|
62
|
+
- 快照存储路径:`~/.clawt/validate-snapshots/<projectName>/<branchName>.tree`(存储 git tree 对象 hash)
|
|
63
|
+
- tree 对象不依赖主分支 HEAD,无需一致性校验
|
|
63
64
|
- 变更检测:同时检测目标 worktree 的未提交修改和已提交 commit,两者均无则提示无需验证
|
|
64
65
|
- 未提交修改处理:有未提交修改时先做临时 commit,diff 完成后通过 `git reset --soft` 撤销恢复原状
|
|
65
66
|
- `merge`:检测目标 worktree 状态(有修改则需 `-m` 提交,已提交则跳过,无变更则报错)→ **squash 检测**(检查目标分支是否存在 `AUTO_SAVE_COMMIT_MESSAGE` 前缀的 auto-save commit,如有则提示用户是否压缩所有提交:用户确认后通过 `gitMergeBase` 计算分叉点、`gitResetSoftTo` 将所有 commit reset 到暂存区;有 `-m` 则直接提交继续流程,无 `-m` 则提示用户自行提交后退出)→ 合并到主 worktree → pull → push → 可选清理 worktree 和分支(受 `autoDeleteBranch` 配置或交互式确认控制)→ 清理对应的 validate 快照
|
|
@@ -75,11 +76,18 @@ run 命令有两种模式:
|
|
|
75
76
|
6. 冲突处理:有冲突时提示用户手动解决,无冲突则输出成功
|
|
76
77
|
7. 合并成功后清除该分支的 validate 快照(代码基础已变化,旧快照无效)
|
|
77
78
|
|
|
79
|
+
### reset 命令流程
|
|
80
|
+
|
|
81
|
+
1. `validateMainWorktree()` 确认在主 worktree 根目录
|
|
82
|
+
2. 检测主 worktree 工作区和暂存区是否干净(`isWorkingDirClean()`)
|
|
83
|
+
3. 不干净 → `gitResetHard()` + `gitCleanForce()` 重置工作区和暂存区(保留 validate 快照)
|
|
84
|
+
4. 已干净 → 提示无需重置
|
|
85
|
+
|
|
78
86
|
### 目录层级
|
|
79
87
|
|
|
80
88
|
- `src/commands/` — 各命令的注册与处理逻辑
|
|
81
|
-
- `src/utils/` — 工具函数(git 操作(含三点 diff、分支合并、冲突检测、merge-base 计算、commit message 检测、soft reset 到指定 commit 等)、shell 执行与子进程管理、分支名处理、worktree 管理与批量清理、配置、格式化输出、交互式输入、Claude Code 交互式启动、validate
|
|
82
|
-
- `src/constants/` — 常量定义(路径、退出码、消息模板、分支规则、配置默认值、终端控制序列、validate 快照目录、sync 相关消息、git 常量(如 `AUTO_SAVE_COMMIT_MESSAGE`)、squash 相关消息)
|
|
89
|
+
- `src/utils/` — 工具函数(git 操作(含三点 diff、分支合并、冲突检测、merge-base 计算、commit message 检测、soft reset 到指定 commit、write-tree/read-tree 等)、shell 执行与子进程管理、分支名处理、worktree 管理与批量清理、配置、格式化输出、交互式输入、Claude Code 交互式启动、validate 快照管理(基于 git tree 对象))
|
|
90
|
+
- `src/constants/` — 常量定义(路径、退出码、消息模板、分支规则、配置默认值、终端控制序列、validate 快照目录、sync 相关消息、git 常量(如 `AUTO_SAVE_COMMIT_MESSAGE`)、squash 相关消息、reset 相关消息)
|
|
83
91
|
- `src/types/` — TypeScript 类型定义
|
|
84
92
|
- `src/errors/` — 自定义 `ClawtError` 错误类(携带退出码)
|
|
85
93
|
- `src/logger/` — winston 日志(按日期滚动,写入 `~/.clawt/logs/`)
|
package/README.md
CHANGED
|
@@ -103,7 +103,7 @@ clawt validate -b <branchName> [--clean]
|
|
|
103
103
|
|
|
104
104
|
将目标 worktree 的变更通过 `git diff`(三点 diff)迁移到主 worktree,方便在主 worktree 中直接测试,无需重新安装依赖。同时检测未提交修改和已提交 commit,确保所有变更都能被捕获。
|
|
105
105
|
|
|
106
|
-
支持增量模式:首次 validate
|
|
106
|
+
支持增量模式:首次 validate 后会自动保存快照(通过 `git write-tree` 将变更存储为 git tree 对象),再次 validate 同一分支时会通过 `git read-tree` 将上次快照载入暂存区、最新变更保留在工作目录,用户可通过 `git diff` 查看两次 validate 之间的增量差异。使用 `--clean` 可清理 validate 状态(重置主 worktree 并删除快照文件)。
|
|
107
107
|
|
|
108
108
|
> **提示:** 如果 validate 时 patch apply 失败(目标分支与主分支差异过大),可先执行 `clawt sync -b <branchName>` 同步主分支后重试。
|
|
109
109
|
|
|
@@ -187,6 +187,19 @@ clawt list
|
|
|
187
187
|
|
|
188
188
|
列出当前项目在 `~/.clawt/worktrees/` 下的所有 worktree 及对应分支。
|
|
189
189
|
|
|
190
|
+
### `clawt reset` — 重置主 worktree 工作区和暂存区
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
clawt reset
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
重置主 worktree 的工作区和暂存区(`git reset --hard` + `git clean -f`),恢复到干净状态。与 `clawt validate --clean` 不同,`reset` 不会删除 validate 快照文件,适用于只想清空变更而保留快照以便后续增量 validate 的场景。如果工作区和暂存区已是干净状态,会提示无需重置。
|
|
197
|
+
|
|
198
|
+
```bash
|
|
199
|
+
# 重置主 worktree 工作区和暂存区
|
|
200
|
+
clawt reset
|
|
201
|
+
```
|
|
202
|
+
|
|
190
203
|
### `clawt config` — 查看全局配置
|
|
191
204
|
|
|
192
205
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -111,7 +111,11 @@ var MESSAGES = {
|
|
|
111
111
|
MERGE_SQUASH_PENDING: (worktreePath, branch) => `\u2713 \u5DF2\u5C06\u6240\u6709\u63D0\u4EA4\u538B\u7F29\u5230\u6682\u5B58\u533A
|
|
112
112
|
\u8BF7\u5728\u76EE\u6807 worktree \u4E2D\u63D0\u4EA4\u540E\u91CD\u65B0\u6267\u884C merge\uFF1A
|
|
113
113
|
cd ${worktreePath}
|
|
114
|
-
\u63D0\u4EA4\u5B8C\u6210\u540E\u6267\u884C\uFF1Aclawt merge -b ${branch}
|
|
114
|
+
\u63D0\u4EA4\u5B8C\u6210\u540E\u6267\u884C\uFF1Aclawt merge -b ${branch}`,
|
|
115
|
+
/** reset 成功 */
|
|
116
|
+
RESET_SUCCESS: "\u2713 \u4E3B worktree \u5DE5\u4F5C\u533A\u548C\u6682\u5B58\u533A\u5DF2\u91CD\u7F6E",
|
|
117
|
+
/** reset 时工作区和暂存区已干净 */
|
|
118
|
+
RESET_ALREADY_CLEAN: "\u4E3B worktree \u5DE5\u4F5C\u533A\u548C\u6682\u5B58\u533A\u5DF2\u662F\u5E72\u51C0\u72B6\u6001\uFF0C\u65E0\u9700\u91CD\u7F6E"
|
|
115
119
|
};
|
|
116
120
|
|
|
117
121
|
// src/constants/exitCodes.ts
|
|
@@ -341,22 +345,9 @@ function getDiffStat(worktreePath) {
|
|
|
341
345
|
const output = execCommand("git diff --shortstat HEAD", { cwd: worktreePath });
|
|
342
346
|
return parseShortStat(output);
|
|
343
347
|
}
|
|
344
|
-
function gitDiffCachedBinary(cwd) {
|
|
345
|
-
logger.debug(`\u6267\u884C\u547D\u4EE4: git diff --cached --binary${cwd ? ` (cwd: ${cwd})` : ""}`);
|
|
346
|
-
return execSync2("git diff --cached --binary", {
|
|
347
|
-
cwd,
|
|
348
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
function gitApplyCachedFromStdin(patchContent, cwd) {
|
|
352
|
-
execCommandWithInput("git", ["apply", "--cached"], { input: patchContent, cwd });
|
|
353
|
-
}
|
|
354
348
|
function getCurrentBranch(cwd) {
|
|
355
349
|
return execCommand("git rev-parse --abbrev-ref HEAD", { cwd });
|
|
356
350
|
}
|
|
357
|
-
function getHeadCommitHash(cwd) {
|
|
358
|
-
return execCommand("git rev-parse HEAD", { cwd });
|
|
359
|
-
}
|
|
360
351
|
function gitDiffBinaryAgainstBranch(branchName, cwd) {
|
|
361
352
|
logger.debug(`\u6267\u884C\u547D\u4EE4: git diff HEAD...${branchName} --binary${cwd ? ` (cwd: ${cwd})` : ""}`);
|
|
362
353
|
return execSync2(`git diff HEAD...${branchName} --binary`, {
|
|
@@ -385,6 +376,12 @@ function hasCommitWithMessage(branchName, messagePrefix, cwd) {
|
|
|
385
376
|
function gitResetSoftTo(commitHash, cwd) {
|
|
386
377
|
execCommand(`git reset --soft ${commitHash}`, { cwd });
|
|
387
378
|
}
|
|
379
|
+
function gitWriteTree(cwd) {
|
|
380
|
+
return execCommand("git write-tree", { cwd });
|
|
381
|
+
}
|
|
382
|
+
function gitReadTree(treeHash, cwd) {
|
|
383
|
+
execCommand(`git read-tree ${treeHash}`, { cwd });
|
|
384
|
+
}
|
|
388
385
|
|
|
389
386
|
// src/utils/formatter.ts
|
|
390
387
|
import chalk from "chalk";
|
|
@@ -638,27 +635,21 @@ function launchInteractiveClaude(worktree) {
|
|
|
638
635
|
import { join as join3 } from "path";
|
|
639
636
|
import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, readdirSync as readdirSync3, rmdirSync as rmdirSync2 } from "fs";
|
|
640
637
|
function getSnapshotPath(projectName, branchName) {
|
|
641
|
-
return join3(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.
|
|
642
|
-
}
|
|
643
|
-
function getSnapshotHeadPath(projectName, branchName) {
|
|
644
|
-
return join3(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.head`);
|
|
638
|
+
return join3(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.tree`);
|
|
645
639
|
}
|
|
646
640
|
function hasSnapshot(projectName, branchName) {
|
|
647
641
|
return existsSync5(getSnapshotPath(projectName, branchName));
|
|
648
642
|
}
|
|
649
|
-
function
|
|
643
|
+
function readSnapshotTreeHash(projectName, branchName) {
|
|
650
644
|
const snapshotPath = getSnapshotPath(projectName, branchName);
|
|
651
645
|
logger.debug(`\u8BFB\u53D6 validate \u5FEB\u7167: ${snapshotPath}`);
|
|
652
|
-
return readFileSync2(snapshotPath);
|
|
646
|
+
return readFileSync2(snapshotPath, "utf-8").trim();
|
|
653
647
|
}
|
|
654
|
-
function writeSnapshot(projectName, branchName,
|
|
648
|
+
function writeSnapshot(projectName, branchName, treeHash) {
|
|
655
649
|
const snapshotPath = getSnapshotPath(projectName, branchName);
|
|
656
650
|
const snapshotDir = join3(VALIDATE_SNAPSHOTS_DIR, projectName);
|
|
657
651
|
ensureDir(snapshotDir);
|
|
658
|
-
writeFileSync2(snapshotPath,
|
|
659
|
-
if (headHash) {
|
|
660
|
-
writeFileSync2(getSnapshotHeadPath(projectName, branchName), headHash, "utf-8");
|
|
661
|
-
}
|
|
652
|
+
writeFileSync2(snapshotPath, treeHash, "utf-8");
|
|
662
653
|
logger.info(`\u5DF2\u4FDD\u5B58 validate \u5FEB\u7167: ${snapshotPath}`);
|
|
663
654
|
}
|
|
664
655
|
function removeSnapshot(projectName, branchName) {
|
|
@@ -667,17 +658,6 @@ function removeSnapshot(projectName, branchName) {
|
|
|
667
658
|
unlinkSync(snapshotPath);
|
|
668
659
|
logger.info(`\u5DF2\u5220\u9664 validate \u5FEB\u7167: ${snapshotPath}`);
|
|
669
660
|
}
|
|
670
|
-
const headPath = getSnapshotHeadPath(projectName, branchName);
|
|
671
|
-
if (existsSync5(headPath)) {
|
|
672
|
-
unlinkSync(headPath);
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
function readSnapshotHead(projectName, branchName) {
|
|
676
|
-
const headPath = getSnapshotHeadPath(projectName, branchName);
|
|
677
|
-
if (!existsSync5(headPath)) {
|
|
678
|
-
return null;
|
|
679
|
-
}
|
|
680
|
-
return readFileSync2(headPath, "utf-8").trim();
|
|
681
661
|
}
|
|
682
662
|
|
|
683
663
|
// src/commands/list.ts
|
|
@@ -1052,13 +1032,12 @@ function migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName
|
|
|
1052
1032
|
}
|
|
1053
1033
|
}
|
|
1054
1034
|
}
|
|
1055
|
-
function
|
|
1035
|
+
function saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName) {
|
|
1056
1036
|
gitAddAll(mainWorktreePath);
|
|
1057
|
-
const
|
|
1037
|
+
const treeHash = gitWriteTree(mainWorktreePath);
|
|
1058
1038
|
gitRestoreStaged(mainWorktreePath);
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
return patch;
|
|
1039
|
+
writeSnapshot(projectName, branchName, treeHash);
|
|
1040
|
+
return treeHash;
|
|
1062
1041
|
}
|
|
1063
1042
|
function handleValidateClean(options) {
|
|
1064
1043
|
validateMainWorktree();
|
|
@@ -1074,26 +1053,24 @@ function handleValidateClean(options) {
|
|
|
1074
1053
|
}
|
|
1075
1054
|
function handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted) {
|
|
1076
1055
|
migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
|
|
1077
|
-
|
|
1056
|
+
saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
|
|
1078
1057
|
printSuccess(MESSAGES.VALIDATE_SUCCESS(branchName));
|
|
1079
1058
|
}
|
|
1080
1059
|
function handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted) {
|
|
1081
|
-
const
|
|
1060
|
+
const oldTreeHash = readSnapshotTreeHash(projectName, branchName);
|
|
1082
1061
|
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
1083
1062
|
gitResetHard(mainWorktreePath);
|
|
1084
1063
|
gitCleanForce(mainWorktreePath);
|
|
1085
1064
|
}
|
|
1086
1065
|
migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
return;
|
|
1096
|
-
}
|
|
1066
|
+
saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
|
|
1067
|
+
try {
|
|
1068
|
+
gitReadTree(oldTreeHash, mainWorktreePath);
|
|
1069
|
+
} catch (error) {
|
|
1070
|
+
logger.warn(`\u589E\u91CF read-tree \u5931\u8D25: ${error}`);
|
|
1071
|
+
printWarning(MESSAGES.INCREMENTAL_VALIDATE_FALLBACK);
|
|
1072
|
+
printSuccess(MESSAGES.VALIDATE_SUCCESS(branchName));
|
|
1073
|
+
return;
|
|
1097
1074
|
}
|
|
1098
1075
|
printSuccess(MESSAGES.INCREMENTAL_VALIDATE_SUCCESS(branchName));
|
|
1099
1076
|
}
|
|
@@ -1117,16 +1094,7 @@ async function handleValidate(options) {
|
|
|
1117
1094
|
printInfo(MESSAGES.TARGET_WORKTREE_CLEAN);
|
|
1118
1095
|
return;
|
|
1119
1096
|
}
|
|
1120
|
-
|
|
1121
|
-
if (isIncremental) {
|
|
1122
|
-
const savedHead = readSnapshotHead(projectName, options.branch);
|
|
1123
|
-
const currentHead = getHeadCommitHash(mainWorktreePath);
|
|
1124
|
-
if (!savedHead || savedHead !== currentHead) {
|
|
1125
|
-
logger.info(`\u4E3B\u5206\u652F HEAD \u4E0D\u5339\u914D (${savedHead ?? "null"} \u2192 ${currentHead})\uFF0C\u6E05\u9664\u65E7\u5FEB\u7167`);
|
|
1126
|
-
removeSnapshot(projectName, options.branch);
|
|
1127
|
-
isIncremental = false;
|
|
1128
|
-
}
|
|
1129
|
-
}
|
|
1097
|
+
const isIncremental = hasSnapshot(projectName, options.branch);
|
|
1130
1098
|
if (isIncremental) {
|
|
1131
1099
|
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
1132
1100
|
await handleDirtyMainWorktree(mainWorktreePath);
|
|
@@ -1329,6 +1297,25 @@ async function handleSync(options) {
|
|
|
1329
1297
|
printSuccess(MESSAGES.SYNC_SUCCESS(branch, mainBranch));
|
|
1330
1298
|
}
|
|
1331
1299
|
|
|
1300
|
+
// src/commands/reset.ts
|
|
1301
|
+
function registerResetCommand(program2) {
|
|
1302
|
+
program2.command("reset").description("\u91CD\u7F6E\u4E3B worktree \u5DE5\u4F5C\u533A\u548C\u6682\u5B58\u533A\uFF08\u4FDD\u7559 validate \u5FEB\u7167\uFF09").action(() => {
|
|
1303
|
+
handleReset();
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
function handleReset() {
|
|
1307
|
+
validateMainWorktree();
|
|
1308
|
+
const mainWorktreePath = getGitTopLevel();
|
|
1309
|
+
logger.info("reset \u547D\u4EE4\u6267\u884C");
|
|
1310
|
+
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
1311
|
+
gitResetHard(mainWorktreePath);
|
|
1312
|
+
gitCleanForce(mainWorktreePath);
|
|
1313
|
+
printSuccess(MESSAGES.RESET_SUCCESS);
|
|
1314
|
+
} else {
|
|
1315
|
+
printInfo(MESSAGES.RESET_ALREADY_CLEAN);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1332
1319
|
// src/index.ts
|
|
1333
1320
|
var require2 = createRequire(import.meta.url);
|
|
1334
1321
|
var { version } = require2("../package.json");
|
|
@@ -1344,6 +1331,7 @@ registerValidateCommand(program);
|
|
|
1344
1331
|
registerMergeCommand(program);
|
|
1345
1332
|
registerConfigCommand(program);
|
|
1346
1333
|
registerSyncCommand(program);
|
|
1334
|
+
registerResetCommand(program);
|
|
1347
1335
|
process.on("uncaughtException", (error) => {
|
|
1348
1336
|
if (error instanceof ClawtError) {
|
|
1349
1337
|
printError(error.message);
|
package/docs/spec.md
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
- [5.10 查看全局配置](#510-查看全局配置)
|
|
25
25
|
- [5.11 在已有 Worktree 中恢复会话](#511-在已有-worktree-中恢复会话)
|
|
26
26
|
- [5.12 将主分支代码同步到目标 Worktree](#512-将主分支代码同步到目标-worktree)
|
|
27
|
+
- [5.13 重置主 Worktree 工作区和暂存区](#513-重置主-worktree-工作区和暂存区)
|
|
27
28
|
- [6. 错误处理规范](#6-错误处理规范)
|
|
28
29
|
- [7. 非功能性需求](#7-非功能性需求)
|
|
29
30
|
|
|
@@ -140,8 +141,7 @@ git show-ref --verify refs/heads/<branchName> 2>/dev/null
|
|
|
140
141
|
│ └── ...
|
|
141
142
|
├── validate-snapshots/ # validate 快照目录
|
|
142
143
|
│ └── <project-name>/ # 以项目名分组
|
|
143
|
-
│ ├── <branchName>.
|
|
144
|
-
│ ├── <branchName>.head # 对应的主分支 HEAD commit hash(用于增量 validate 一致性校验)
|
|
144
|
+
│ ├── <branchName>.tree # 每个分支一个 tree hash 快照文件(存储 git tree 对象的 hash)
|
|
145
145
|
│ └── ...
|
|
146
146
|
└── worktrees/ # 所有 worktree 的统一存放目录
|
|
147
147
|
└── <project-name>/ # 以项目名分组
|
|
@@ -168,6 +168,7 @@ git show-ref --verify refs/heads/<branchName> 2>/dev/null
|
|
|
168
168
|
| `clawt config` | 查看全局配置 | 5.10 |
|
|
169
169
|
| `clawt resume` | 在已有 worktree 中恢复 Claude Code 交互式会话 | 5.11 |
|
|
170
170
|
| `clawt sync` | 将主分支最新代码同步到目标 worktree | 5.12 |
|
|
171
|
+
| `clawt reset` | 重置主 worktree 工作区和暂存区 | 5.13 |
|
|
171
172
|
|
|
172
173
|
所有命令执行前,都必须先执行**主 worktree 校验**(见 [2.1](#21-主-worktree-的定义与定位规则))。
|
|
173
174
|
|
|
@@ -361,7 +362,7 @@ Git worktree 不会包含 `node_modules`、`.venv` 等依赖文件,每次安
|
|
|
361
362
|
|
|
362
363
|
**快照机制:**
|
|
363
364
|
|
|
364
|
-
validate 命令引入了**快照(snapshot)机制**来支持增量对比。每次 validate
|
|
365
|
+
validate 命令引入了**快照(snapshot)机制**来支持增量对比。每次 validate 执行成功后,会将当前全量变更通过 `git write-tree` 保存为 git tree 对象,并将 tree hash 记录到文件(`~/.clawt/validate-snapshots/<project>/<branchName>.tree`)。当再次执行 validate 时,通过 `git read-tree` 将上次快照的 tree 对象载入暂存区、最新变更保留在工作目录,用户可通过 `git diff` 直接查看两次 validate 之间的增量差异。由于 tree 对象存储在 git 对象库中,不依赖主分支 HEAD,无需一致性校验。
|
|
365
366
|
|
|
366
367
|
**运行流程:**
|
|
367
368
|
|
|
@@ -371,7 +372,7 @@ validate 命令引入了**快照(snapshot)机制**来支持增量对比。
|
|
|
371
372
|
|
|
372
373
|
1. **主 worktree 校验** (2.1)
|
|
373
374
|
2. 如果主 worktree 有未提交更改,执行 `git reset --hard` + `git clean -fd` 清空
|
|
374
|
-
3.
|
|
375
|
+
3. 删除对应分支的快照文件
|
|
375
376
|
4. 输出清理成功提示
|
|
376
377
|
|
|
377
378
|
#### 首次 validate(无历史快照)
|
|
@@ -439,14 +440,13 @@ git restore --staged .
|
|
|
439
440
|
> 此步骤结束后,目标 worktree 的代码保持原样,主 worktree 工作目录包含目标分支的全量变更。
|
|
440
441
|
> 如果 patch apply 失败(目标分支与主分支差异过大),会提示用户先执行 `clawt sync -b <branchName>` 同步主分支后重试。
|
|
441
442
|
|
|
442
|
-
##### 步骤 4
|
|
443
|
+
##### 步骤 4:保存快照为 git tree 对象
|
|
443
444
|
|
|
444
|
-
将主 worktree 工作目录的全量变更保存为
|
|
445
|
+
将主 worktree 工作目录的全量变更保存为 git tree 对象:
|
|
445
446
|
|
|
446
447
|
```bash
|
|
447
448
|
git add .
|
|
448
|
-
git
|
|
449
|
-
git rev-parse HEAD > ~/.clawt/validate-snapshots/<project>/<branchName>.head
|
|
449
|
+
git write-tree # → 返回 tree hash,写入 ~/.clawt/validate-snapshots/<project>/<branchName>.tree
|
|
450
450
|
git restore --staged .
|
|
451
451
|
```
|
|
452
452
|
|
|
@@ -461,41 +461,34 @@ git restore --staged .
|
|
|
461
461
|
|
|
462
462
|
#### 增量 validate(存在历史快照)
|
|
463
463
|
|
|
464
|
-
当 `~/.clawt/validate-snapshots/<project>/<branchName>.
|
|
464
|
+
当 `~/.clawt/validate-snapshots/<project>/<branchName>.tree` 存在时,自动进入增量模式:
|
|
465
465
|
|
|
466
|
-
##### 步骤 1
|
|
466
|
+
##### 步骤 1:读取旧 tree hash
|
|
467
467
|
|
|
468
|
-
|
|
468
|
+
在清空主 worktree 之前,读取上次保存的快照 tree hash。
|
|
469
469
|
|
|
470
|
-
|
|
471
|
-
- **一致** → 继续增量流程
|
|
472
|
-
|
|
473
|
-
##### 步骤 2:读取旧 patch
|
|
474
|
-
|
|
475
|
-
在清空主 worktree 之前,读取上次保存的快照 patch 内容。
|
|
476
|
-
|
|
477
|
-
##### 步骤 3:确保主 worktree 干净
|
|
470
|
+
##### 步骤 2:确保主 worktree 干净
|
|
478
471
|
|
|
479
472
|
如果主 worktree 有残留状态,让用户选择处理方式(同首次 validate 步骤 1 的交互),做兜底清理。
|
|
480
473
|
|
|
481
|
-
##### 步骤
|
|
474
|
+
##### 步骤 3:从目标分支获取最新全量变更
|
|
482
475
|
|
|
483
476
|
通过 patch 方式从目标分支获取最新全量变更(流程同首次 validate 的步骤 3)。
|
|
484
477
|
|
|
485
|
-
##### 步骤
|
|
478
|
+
##### 步骤 4:保存最新快照为 git tree 对象
|
|
486
479
|
|
|
487
|
-
将最新全量变更保存为新的
|
|
480
|
+
将最新全量变更保存为新的 tree 对象(覆盖旧快照,流程同首次 validate 的步骤 4)。
|
|
488
481
|
|
|
489
|
-
##### 步骤
|
|
482
|
+
##### 步骤 5:将旧 tree 对象载入暂存区
|
|
490
483
|
|
|
491
484
|
```bash
|
|
492
|
-
git
|
|
485
|
+
git read-tree <旧 tree hash>
|
|
493
486
|
```
|
|
494
487
|
|
|
495
|
-
-
|
|
496
|
-
-
|
|
488
|
+
- **读取成功** → 结果:暂存区=上次快照,工作目录=最新全量变更(用户可通过 `git diff` 查看增量差异)
|
|
489
|
+
- **读取失败**(tree 对象可能被 git gc 回收)→ 降级为全量模式,暂存区保持为空,等同于首次 validate 的结果
|
|
497
490
|
|
|
498
|
-
##### 步骤
|
|
491
|
+
##### 步骤 6:输出成功提示
|
|
499
492
|
|
|
500
493
|
```
|
|
501
494
|
# 增量模式成功
|
|
@@ -586,7 +579,7 @@ clawt merge -b <branchName> [-m <commitMessage>]
|
|
|
586
579
|
2. **主 worktree 状态检测**
|
|
587
580
|
- 执行 `git status --porcelain`
|
|
588
581
|
- 如果有更改:
|
|
589
|
-
- 如果存在该分支的 validate 快照(`~/.clawt/validate-snapshots/<project>/<branchName>.
|
|
582
|
+
- 如果存在该分支的 validate 快照(`~/.clawt/validate-snapshots/<project>/<branchName>.tree`),额外输出警告提示用户可先执行 `clawt validate -b <branchName> --clean` 清理
|
|
590
583
|
- 提示 `主 worktree 有未提交的更改,请先处理`,退出
|
|
591
584
|
- 无更改 → 继续
|
|
592
585
|
3. **Squash 检测与执行(auto-save 临时提交压缩)**
|
|
@@ -661,7 +654,7 @@ clawt merge -b <branchName> [-m <commitMessage>]
|
|
|
661
654
|
- 输出清理成功提示:`✓ 已清理 worktree 和分支: <branchName>`
|
|
662
655
|
|
|
663
656
|
10. **清理 validate 快照**
|
|
664
|
-
- merge 成功后,如果存在该分支的 validate 快照(`~/.clawt/validate-snapshots/<project>/<branchName>.
|
|
657
|
+
- merge 成功后,如果存在该分支的 validate 快照(`~/.clawt/validate-snapshots/<project>/<branchName>.tree`),自动删除该快照文件(merge 成功后快照已无意义)
|
|
665
658
|
|
|
666
659
|
> **注意:** 清理确认在 merge 操作之前询问(避免 merge 成功后因交互中断而遗留未清理的 worktree),但清理操作在 merge 成功后才执行。
|
|
667
660
|
|
|
@@ -881,7 +874,7 @@ clawt sync -b <branchName>
|
|
|
881
874
|
解决冲突后执行 git add . && git merge --continue
|
|
882
875
|
```
|
|
883
876
|
- **无冲突** → 继续
|
|
884
|
-
7. **清除 validate 快照**:合并成功后,如果该分支存在 validate 快照(`.
|
|
877
|
+
7. **清除 validate 快照**:合并成功后,如果该分支存在 validate 快照(`.tree` 文件),自动删除(代码基础已变化,旧快照无效)
|
|
885
878
|
8. **输出成功提示**:
|
|
886
879
|
```
|
|
887
880
|
✓ 已将 <mainBranch> 的最新代码同步到 <branchName>
|
|
@@ -889,6 +882,38 @@ clawt sync -b <branchName>
|
|
|
889
882
|
|
|
890
883
|
---
|
|
891
884
|
|
|
885
|
+
### 5.13 重置主 Worktree 工作区和暂存区
|
|
886
|
+
|
|
887
|
+
**命令:**
|
|
888
|
+
|
|
889
|
+
```bash
|
|
890
|
+
clawt reset
|
|
891
|
+
```
|
|
892
|
+
|
|
893
|
+
**无参数。**
|
|
894
|
+
|
|
895
|
+
**使用场景:**
|
|
896
|
+
|
|
897
|
+
当用户通过 `clawt validate` 将分支变更迁移到主 worktree 后,希望快速清除工作区和暂存区的所有修改,恢复到干净状态。与 `clawt validate --clean` 的区别在于:`reset` 仅重置工作区和暂存区,**不删除** validate 快照文件,适用于只想清空变更而保留快照以便后续增量 validate 的场景。
|
|
898
|
+
|
|
899
|
+
**运行流程:**
|
|
900
|
+
|
|
901
|
+
1. **主 worktree 校验** (2.1)
|
|
902
|
+
2. **检测工作区状态**:通过 `git status --porcelain` 检测主 worktree 是否有未提交的更改
|
|
903
|
+
- **工作区干净** → 输出提示 `主 worktree 工作区和暂存区已是干净状态,无需重置`,退出
|
|
904
|
+
- **工作区不干净** → 继续
|
|
905
|
+
3. **重置工作区和暂存区**:
|
|
906
|
+
```bash
|
|
907
|
+
git reset --hard
|
|
908
|
+
git clean -f
|
|
909
|
+
```
|
|
910
|
+
4. **输出成功提示**:
|
|
911
|
+
```
|
|
912
|
+
✓ 主 worktree 工作区和暂存区已重置
|
|
913
|
+
```
|
|
914
|
+
|
|
915
|
+
---
|
|
916
|
+
|
|
892
917
|
## 6. 错误处理规范
|
|
893
918
|
|
|
894
919
|
### 6.1 通用错误处理
|
package/package.json
CHANGED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { logger } from '../logger/index.js';
|
|
3
|
+
import { MESSAGES } from '../constants/index.js';
|
|
4
|
+
import {
|
|
5
|
+
validateMainWorktree,
|
|
6
|
+
getGitTopLevel,
|
|
7
|
+
isWorkingDirClean,
|
|
8
|
+
gitResetHard,
|
|
9
|
+
gitCleanForce,
|
|
10
|
+
printSuccess,
|
|
11
|
+
printInfo,
|
|
12
|
+
} from '../utils/index.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 注册 reset 命令:重置主 worktree 工作区和暂存区
|
|
16
|
+
* @param {Command} program - Commander 实例
|
|
17
|
+
*/
|
|
18
|
+
export function registerResetCommand(program: Command): void {
|
|
19
|
+
program
|
|
20
|
+
.command('reset')
|
|
21
|
+
.description('重置主 worktree 工作区和暂存区(保留 validate 快照)')
|
|
22
|
+
.action(() => {
|
|
23
|
+
handleReset();
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 执行 reset 命令:重置主 worktree 工作区和暂存区
|
|
29
|
+
*/
|
|
30
|
+
function handleReset(): void {
|
|
31
|
+
validateMainWorktree();
|
|
32
|
+
const mainWorktreePath = getGitTopLevel();
|
|
33
|
+
logger.info('reset 命令执行');
|
|
34
|
+
|
|
35
|
+
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
36
|
+
gitResetHard(mainWorktreePath);
|
|
37
|
+
gitCleanForce(mainWorktreePath);
|
|
38
|
+
printSuccess(MESSAGES.RESET_SUCCESS);
|
|
39
|
+
} else {
|
|
40
|
+
printInfo(MESSAGES.RESET_ALREADY_CLEAN);
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/commands/validate.ts
CHANGED
|
@@ -18,16 +18,14 @@ import {
|
|
|
18
18
|
gitRestoreStaged,
|
|
19
19
|
gitResetHard,
|
|
20
20
|
gitCleanForce,
|
|
21
|
-
gitDiffCachedBinary,
|
|
22
|
-
gitApplyCachedFromStdin,
|
|
23
21
|
gitDiffBinaryAgainstBranch,
|
|
24
22
|
gitApplyFromStdin,
|
|
25
23
|
gitResetSoft,
|
|
26
|
-
|
|
24
|
+
gitWriteTree,
|
|
25
|
+
gitReadTree,
|
|
27
26
|
hasLocalCommits,
|
|
28
27
|
hasSnapshot,
|
|
29
|
-
|
|
30
|
-
readSnapshotHead,
|
|
28
|
+
readSnapshotTreeHash,
|
|
31
29
|
writeSnapshot,
|
|
32
30
|
removeSnapshot,
|
|
33
31
|
printSuccess,
|
|
@@ -137,21 +135,19 @@ function migrateChangesViaPatch(targetWorktreePath: string, mainWorktreePath: st
|
|
|
137
135
|
}
|
|
138
136
|
|
|
139
137
|
/**
|
|
140
|
-
* 保存当前主 worktree
|
|
141
|
-
* 操作序列:git add . → git
|
|
142
|
-
* 同时记录主分支 HEAD hash,用于增量 validate 一致性校验
|
|
138
|
+
* 保存当前主 worktree 工作目录变更为 git tree 对象快照
|
|
139
|
+
* 操作序列:git add . → git write-tree → git restore --staged .
|
|
143
140
|
* @param {string} mainWorktreePath - 主 worktree 路径
|
|
144
141
|
* @param {string} projectName - 项目名
|
|
145
142
|
* @param {string} branchName - 分支名
|
|
146
|
-
* @returns {
|
|
143
|
+
* @returns {string} 生成的 tree hash
|
|
147
144
|
*/
|
|
148
|
-
function
|
|
145
|
+
function saveCurrentSnapshotTree(mainWorktreePath: string, projectName: string, branchName: string): string {
|
|
149
146
|
gitAddAll(mainWorktreePath);
|
|
150
|
-
const
|
|
147
|
+
const treeHash = gitWriteTree(mainWorktreePath);
|
|
151
148
|
gitRestoreStaged(mainWorktreePath);
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
return patch;
|
|
149
|
+
writeSnapshot(projectName, branchName, treeHash);
|
|
150
|
+
return treeHash;
|
|
155
151
|
}
|
|
156
152
|
|
|
157
153
|
/**
|
|
@@ -172,7 +168,7 @@ function handleValidateClean(options: ValidateOptions): void {
|
|
|
172
168
|
gitCleanForce(mainWorktreePath);
|
|
173
169
|
}
|
|
174
170
|
|
|
175
|
-
//
|
|
171
|
+
// 删除对应的快照文件
|
|
176
172
|
removeSnapshot(projectName, options.branch);
|
|
177
173
|
|
|
178
174
|
printSuccess(MESSAGES.VALIDATE_CLEANED(options.branch));
|
|
@@ -190,8 +186,8 @@ function handleFirstValidate(targetWorktreePath: string, mainWorktreePath: strin
|
|
|
190
186
|
// 通过 patch 迁移目标分支全量变更到主 worktree
|
|
191
187
|
migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
|
|
192
188
|
|
|
193
|
-
//
|
|
194
|
-
|
|
189
|
+
// 保存快照为 git tree 对象
|
|
190
|
+
saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
|
|
195
191
|
|
|
196
192
|
// 结果:暂存区=空,工作目录=全量变更
|
|
197
193
|
printSuccess(MESSAGES.VALIDATE_SUCCESS(branchName));
|
|
@@ -206,8 +202,8 @@ function handleFirstValidate(targetWorktreePath: string, mainWorktreePath: strin
|
|
|
206
202
|
* @param {boolean} hasUncommitted - 目标 worktree 是否有未提交修改
|
|
207
203
|
*/
|
|
208
204
|
function handleIncrementalValidate(targetWorktreePath: string, mainWorktreePath: string, projectName: string, branchName: string, hasUncommitted: boolean): void {
|
|
209
|
-
// 步骤 1:读取旧
|
|
210
|
-
const
|
|
205
|
+
// 步骤 1:读取旧 tree hash(在清空前读取)
|
|
206
|
+
const oldTreeHash = readSnapshotTreeHash(projectName, branchName);
|
|
211
207
|
|
|
212
208
|
// 步骤 2:确保主 worktree 干净(调用方已通过 handleDirtyMainWorktree 处理)
|
|
213
209
|
// 这里做兜底清理,防止 handleDirtyMainWorktree 之后仍有残留
|
|
@@ -219,21 +215,19 @@ function handleIncrementalValidate(targetWorktreePath: string, mainWorktreePath:
|
|
|
219
215
|
// 步骤 3:通过 patch 从目标分支获取最新全量变更
|
|
220
216
|
migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
|
|
221
217
|
|
|
222
|
-
// 步骤 4
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
// 步骤 5:将旧
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
return;
|
|
236
|
-
}
|
|
218
|
+
// 步骤 4:保存最新快照为 git tree 对象
|
|
219
|
+
saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
|
|
220
|
+
|
|
221
|
+
// 步骤 5:将旧 tree 对象载入暂存区(恢复上次快照状态)
|
|
222
|
+
try {
|
|
223
|
+
gitReadTree(oldTreeHash, mainWorktreePath);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
// 旧 tree 对象无法读取(可能被 git gc 回收),降级为全量模式
|
|
226
|
+
logger.warn(`增量 read-tree 失败: ${error}`);
|
|
227
|
+
printWarning(MESSAGES.INCREMENTAL_VALIDATE_FALLBACK);
|
|
228
|
+
// 降级后暂存区保持为空,工作目录为最新全量变更,与首次 validate 一致
|
|
229
|
+
printSuccess(MESSAGES.VALIDATE_SUCCESS(branchName));
|
|
230
|
+
return;
|
|
237
231
|
}
|
|
238
232
|
|
|
239
233
|
// 结果:暂存区=上次快照,工作目录=最新全量变更
|
|
@@ -274,19 +268,8 @@ async function handleValidate(options: ValidateOptions): Promise<void> {
|
|
|
274
268
|
return;
|
|
275
269
|
}
|
|
276
270
|
|
|
277
|
-
// 判断是否为增量 validate
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
// 主分支 HEAD 发生变化或旧快照无 .head 记录时,清除后走首次全量模式
|
|
281
|
-
if (isIncremental) {
|
|
282
|
-
const savedHead = readSnapshotHead(projectName, options.branch);
|
|
283
|
-
const currentHead = getHeadCommitHash(mainWorktreePath);
|
|
284
|
-
if (!savedHead || savedHead !== currentHead) {
|
|
285
|
-
logger.info(`主分支 HEAD 不匹配 (${savedHead ?? 'null'} → ${currentHead}),清除旧快照`);
|
|
286
|
-
removeSnapshot(projectName, options.branch);
|
|
287
|
-
isIncremental = false;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
271
|
+
// 判断是否为增量 validate(tree 对象不依赖主分支 HEAD,无需一致性校验)
|
|
272
|
+
const isIncremental = hasSnapshot(projectName, options.branch);
|
|
290
273
|
|
|
291
274
|
if (isIncremental) {
|
|
292
275
|
// 增量模式:主 worktree 有残留状态时让用户选择处理方式
|
|
@@ -95,4 +95,8 @@ export const MESSAGES = {
|
|
|
95
95
|
/** squash 完成但未提供 -m,提示用户自行提交 */
|
|
96
96
|
MERGE_SQUASH_PENDING: (worktreePath: string, branch: string) =>
|
|
97
97
|
`✓ 已将所有提交压缩到暂存区\n 请在目标 worktree 中提交后重新执行 merge:\n cd ${worktreePath}\n 提交完成后执行:clawt merge -b ${branch}`,
|
|
98
|
+
/** reset 成功 */
|
|
99
|
+
RESET_SUCCESS: '✓ 主 worktree 工作区和暂存区已重置',
|
|
100
|
+
/** reset 时工作区和暂存区已干净 */
|
|
101
|
+
RESET_ALREADY_CLEAN: '主 worktree 工作区和暂存区已是干净状态,无需重置',
|
|
98
102
|
} as const;
|
package/src/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { registerValidateCommand } from './commands/validate.js';
|
|
|
13
13
|
import { registerMergeCommand } from './commands/merge.js';
|
|
14
14
|
import { registerConfigCommand } from './commands/config.js';
|
|
15
15
|
import { registerSyncCommand } from './commands/sync.js';
|
|
16
|
+
import { registerResetCommand } from './commands/reset.js';
|
|
16
17
|
|
|
17
18
|
// 从 package.json 读取版本号,避免硬编码
|
|
18
19
|
const require = createRequire(import.meta.url);
|
|
@@ -38,6 +39,7 @@ registerValidateCommand(program);
|
|
|
38
39
|
registerMergeCommand(program);
|
|
39
40
|
registerConfigCommand(program);
|
|
40
41
|
registerSyncCommand(program);
|
|
42
|
+
registerResetCommand(program);
|
|
41
43
|
|
|
42
44
|
// 全局未捕获异常处理
|
|
43
45
|
process.on('uncaughtException', (error) => {
|
package/src/utils/git.ts
CHANGED
|
@@ -407,3 +407,21 @@ export function hasCommitWithMessage(branchName: string, messagePrefix: string,
|
|
|
407
407
|
export function gitResetSoftTo(commitHash: string, cwd?: string): void {
|
|
408
408
|
execCommand(`git reset --soft ${commitHash}`, { cwd });
|
|
409
409
|
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* 将当前暂存区内容写入 git tree 对象并返回其 hash
|
|
413
|
+
* @param {string} [cwd] - 工作目录
|
|
414
|
+
* @returns {string} tree 对象的 hash
|
|
415
|
+
*/
|
|
416
|
+
export function gitWriteTree(cwd?: string): string {
|
|
417
|
+
return execCommand('git write-tree', { cwd });
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* 将指定 tree 对象的内容载入暂存区(不影响工作目录)
|
|
422
|
+
* @param {string} treeHash - tree 对象的 hash
|
|
423
|
+
* @param {string} [cwd] - 工作目录
|
|
424
|
+
*/
|
|
425
|
+
export function gitReadTree(treeHash: string, cwd?: string): void {
|
|
426
|
+
execCommand(`git read-tree ${treeHash}`, { cwd });
|
|
427
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -38,6 +38,8 @@ export {
|
|
|
38
38
|
gitMergeBase,
|
|
39
39
|
hasCommitWithMessage,
|
|
40
40
|
gitResetSoftTo,
|
|
41
|
+
gitWriteTree,
|
|
42
|
+
gitReadTree,
|
|
41
43
|
} from './git.js';
|
|
42
44
|
export { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } from './branch.js';
|
|
43
45
|
export { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled } from './validation.js';
|
|
@@ -47,4 +49,4 @@ export { printSuccess, printError, printWarning, printInfo, printSeparator, prin
|
|
|
47
49
|
export { ensureDir, removeEmptyDir } from './fs.js';
|
|
48
50
|
export { multilineInput } from './prompt.js';
|
|
49
51
|
export { launchInteractiveClaude } from './claude.js';
|
|
50
|
-
export { getSnapshotPath, hasSnapshot,
|
|
52
|
+
export { getSnapshotPath, hasSnapshot, readSnapshotTreeHash, writeSnapshot, removeSnapshot, removeProjectSnapshots } from './validate-snapshot.js';
|
|
@@ -8,20 +8,10 @@ import { logger } from '../logger/index.js';
|
|
|
8
8
|
* 获取指定项目和分支的 validate 快照文件路径
|
|
9
9
|
* @param {string} projectName - 项目名
|
|
10
10
|
* @param {string} branchName - 分支名
|
|
11
|
-
* @returns {string}
|
|
11
|
+
* @returns {string} tree hash 文件的绝对路径
|
|
12
12
|
*/
|
|
13
13
|
export function getSnapshotPath(projectName: string, branchName: string): string {
|
|
14
|
-
return join(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* 获取指定项目和分支的快照 HEAD hash 文件路径
|
|
19
|
-
* @param {string} projectName - 项目名
|
|
20
|
-
* @param {string} branchName - 分支名
|
|
21
|
-
* @returns {string} head 文件的绝对路径
|
|
22
|
-
*/
|
|
23
|
-
function getSnapshotHeadPath(projectName: string, branchName: string): string {
|
|
24
|
-
return join(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.head`);
|
|
14
|
+
return join(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.tree`);
|
|
25
15
|
}
|
|
26
16
|
|
|
27
17
|
/**
|
|
@@ -35,33 +25,28 @@ export function hasSnapshot(projectName: string, branchName: string): boolean {
|
|
|
35
25
|
}
|
|
36
26
|
|
|
37
27
|
/**
|
|
38
|
-
* 读取指定项目和分支的 validate
|
|
28
|
+
* 读取指定项目和分支的 validate 快照中存储的 tree hash
|
|
39
29
|
* @param {string} projectName - 项目名
|
|
40
30
|
* @param {string} branchName - 分支名
|
|
41
|
-
* @returns {
|
|
31
|
+
* @returns {string} tree 对象的 hash
|
|
42
32
|
*/
|
|
43
|
-
export function
|
|
33
|
+
export function readSnapshotTreeHash(projectName: string, branchName: string): string {
|
|
44
34
|
const snapshotPath = getSnapshotPath(projectName, branchName);
|
|
45
35
|
logger.debug(`读取 validate 快照: ${snapshotPath}`);
|
|
46
|
-
return readFileSync(snapshotPath);
|
|
36
|
+
return readFileSync(snapshotPath, 'utf-8').trim();
|
|
47
37
|
}
|
|
48
38
|
|
|
49
39
|
/**
|
|
50
40
|
* 写入 validate 快照内容(自动创建目录)
|
|
51
41
|
* @param {string} projectName - 项目名
|
|
52
42
|
* @param {string} branchName - 分支名
|
|
53
|
-
* @param {
|
|
54
|
-
* @param {string} [headHash] - 主分支 HEAD commit hash(用于增量 validate 一致性校验)
|
|
43
|
+
* @param {string} treeHash - git tree 对象的 hash
|
|
55
44
|
*/
|
|
56
|
-
export function writeSnapshot(projectName: string, branchName: string,
|
|
45
|
+
export function writeSnapshot(projectName: string, branchName: string, treeHash: string): void {
|
|
57
46
|
const snapshotPath = getSnapshotPath(projectName, branchName);
|
|
58
47
|
const snapshotDir = join(VALIDATE_SNAPSHOTS_DIR, projectName);
|
|
59
48
|
ensureDir(snapshotDir);
|
|
60
|
-
writeFileSync(snapshotPath,
|
|
61
|
-
// 保存主分支 HEAD hash,用于下次增量 validate 时校验一致性
|
|
62
|
-
if (headHash) {
|
|
63
|
-
writeFileSync(getSnapshotHeadPath(projectName, branchName), headHash, 'utf-8');
|
|
64
|
-
}
|
|
49
|
+
writeFileSync(snapshotPath, treeHash, 'utf-8');
|
|
65
50
|
logger.info(`已保存 validate 快照: ${snapshotPath}`);
|
|
66
51
|
}
|
|
67
52
|
|
|
@@ -76,25 +61,6 @@ export function removeSnapshot(projectName: string, branchName: string): void {
|
|
|
76
61
|
unlinkSync(snapshotPath);
|
|
77
62
|
logger.info(`已删除 validate 快照: ${snapshotPath}`);
|
|
78
63
|
}
|
|
79
|
-
// 同时删除对应的 .head 文件
|
|
80
|
-
const headPath = getSnapshotHeadPath(projectName, branchName);
|
|
81
|
-
if (existsSync(headPath)) {
|
|
82
|
-
unlinkSync(headPath);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* 读取快照保存时的主分支 HEAD commit hash
|
|
88
|
-
* @param {string} projectName - 项目名
|
|
89
|
-
* @param {string} branchName - 分支名
|
|
90
|
-
* @returns {string | null} HEAD hash,不存在则返回 null
|
|
91
|
-
*/
|
|
92
|
-
export function readSnapshotHead(projectName: string, branchName: string): string | null {
|
|
93
|
-
const headPath = getSnapshotHeadPath(projectName, branchName);
|
|
94
|
-
if (!existsSync(headPath)) {
|
|
95
|
-
return null;
|
|
96
|
-
}
|
|
97
|
-
return readFileSync(headPath, 'utf-8').trim();
|
|
98
64
|
}
|
|
99
65
|
|
|
100
66
|
/**
|