clawt 2.2.0 → 2.3.1
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 +6 -5
- package/CLAUDE.md +5 -4
- package/README.md +1 -1
- package/dist/index.js +33 -69
- package/docs/spec.md +21 -30
- package/package.json +1 -1
- package/src/commands/merge.ts +4 -3
- package/src/commands/validate.ts +30 -47
- package/src/constants/messages.ts +4 -4
- package/src/utils/git.ts +18 -0
- package/src/utils/index.ts +3 -1
- package/src/utils/validate-snapshot.ts +9 -43
|
@@ -72,13 +72,14 @@ Notes:
|
|
|
72
72
|
## validate 快照机制
|
|
73
73
|
|
|
74
74
|
- validate 命令支持首次/增量两种模式,通过 `hasSnapshot()` 判断
|
|
75
|
-
- 快照路径:`~/.clawt/validate-snapshots/<projectName>/<branchName>.
|
|
75
|
+
- 快照路径:`~/.clawt/validate-snapshots/<projectName>/<branchName>.tree`(存储 git tree 对象 hash)
|
|
76
76
|
- 常量 `VALIDATE_SNAPSHOTS_DIR` 定义在 `src/constants/paths.ts`
|
|
77
77
|
- validate 新增 `--clean` 选项(`ValidateOptions.clean?: boolean`)
|
|
78
|
-
-
|
|
79
|
-
-
|
|
80
|
-
-
|
|
81
|
-
-
|
|
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()`(载入暂存区)
|
|
82
83
|
- merge 成功后自动清理对应快照;merge 时主 worktree 脏 + 存在快照会输出警告提示
|
|
83
84
|
- docs/spec.md 中 validate 章节(5.4)按 `--clean 模式`、`首次 validate`、`增量 validate` 三段描述
|
|
84
85
|
- CLAUDE.md 中在 validate + merge 工作流章节用缩进列表描述两种模式
|
package/CLAUDE.md
CHANGED
|
@@ -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 快照
|
|
@@ -85,7 +86,7 @@ run 命令有两种模式:
|
|
|
85
86
|
### 目录层级
|
|
86
87
|
|
|
87
88
|
- `src/commands/` — 各命令的注册与处理逻辑
|
|
88
|
-
- `src/utils/` — 工具函数(git 操作(含三点 diff、分支合并、冲突检测、merge-base 计算、commit message 检测、soft reset 到指定 commit 等)、shell 执行与子进程管理、分支名处理、worktree 管理与批量清理、配置、格式化输出、交互式输入、Claude Code 交互式启动、validate
|
|
89
|
+
- `src/utils/` — 工具函数(git 操作(含三点 diff、分支合并、冲突检测、merge-base 计算、commit message 检测、soft reset 到指定 commit、write-tree/read-tree 等)、shell 执行与子进程管理、分支名处理、worktree 管理与批量清理、配置、格式化输出、交互式输入、Claude Code 交互式启动、validate 快照管理(基于 git tree 对象))
|
|
89
90
|
- `src/constants/` — 常量定义(路径、退出码、消息模板、分支规则、配置默认值、终端控制序列、validate 快照目录、sync 相关消息、git 常量(如 `AUTO_SAVE_COMMIT_MESSAGE`)、squash 相关消息、reset 相关消息)
|
|
90
91
|
- `src/types/` — TypeScript 类型定义
|
|
91
92
|
- `src/errors/` — 自定义 `ClawtError` 错误类(携带退出码)
|
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
|
|
package/dist/index.js
CHANGED
|
@@ -46,11 +46,10 @@ var MESSAGES = {
|
|
|
46
46
|
VALIDATE_SUCCESS: (branch) => `\u2713 \u5DF2\u5C06\u5206\u652F ${branch} \u7684\u53D8\u66F4\u5E94\u7528\u5230\u4E3B worktree
|
|
47
47
|
\u53EF\u4EE5\u5F00\u59CB\u9A8C\u8BC1\u4E86`,
|
|
48
48
|
/** merge 成功 */
|
|
49
|
-
MERGE_SUCCESS: (branch, message) => `\u2713 \u5206\u652F ${branch} \u5DF2\u6210\u529F\u5408\u5E76\u5230\u5F53\u524D\u5206\u652F
|
|
50
|
-
\u63D0\u4EA4\u4FE1\u606F: ${message}
|
|
51
|
-
\u5DF2\u63A8\u9001\u5230\u8FDC\u7A0B\u4ED3\u5E93`,
|
|
49
|
+
MERGE_SUCCESS: (branch, message, pushed) => `\u2713 \u5206\u652F ${branch} \u5DF2\u6210\u529F\u5408\u5E76\u5230\u5F53\u524D\u5206\u652F
|
|
50
|
+
\u63D0\u4EA4\u4FE1\u606F: ${message}${pushed ? "\n \u5DF2\u63A8\u9001\u5230\u8FDC\u7A0B\u4ED3\u5E93" : ""}`,
|
|
52
51
|
/** merge 成功(无提交信息,目标 worktree 已提交过) */
|
|
53
|
-
MERGE_SUCCESS_NO_MESSAGE: (branch) => `\u2713 \u5206\u652F ${branch} \u5DF2\u6210\u529F\u5408\u5E76\u5230\u5F53\u524D\u5206\u652F`,
|
|
52
|
+
MERGE_SUCCESS_NO_MESSAGE: (branch, pushed) => `\u2713 \u5206\u652F ${branch} \u5DF2\u6210\u529F\u5408\u5E76\u5230\u5F53\u524D\u5206\u652F${pushed ? "\n \u5DF2\u63A8\u9001\u5230\u8FDC\u7A0B\u4ED3\u5E93" : ""}`,
|
|
54
53
|
/** merge 冲突 */
|
|
55
54
|
MERGE_CONFLICT: "\u5408\u5E76\u5B58\u5728\u51B2\u7A81\uFF0C\u8BF7\u624B\u52A8\u5904\u7406",
|
|
56
55
|
/** merge 后清理 worktree 和分支成功 */
|
|
@@ -345,22 +344,9 @@ function getDiffStat(worktreePath) {
|
|
|
345
344
|
const output = execCommand("git diff --shortstat HEAD", { cwd: worktreePath });
|
|
346
345
|
return parseShortStat(output);
|
|
347
346
|
}
|
|
348
|
-
function gitDiffCachedBinary(cwd) {
|
|
349
|
-
logger.debug(`\u6267\u884C\u547D\u4EE4: git diff --cached --binary${cwd ? ` (cwd: ${cwd})` : ""}`);
|
|
350
|
-
return execSync2("git diff --cached --binary", {
|
|
351
|
-
cwd,
|
|
352
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
function gitApplyCachedFromStdin(patchContent, cwd) {
|
|
356
|
-
execCommandWithInput("git", ["apply", "--cached"], { input: patchContent, cwd });
|
|
357
|
-
}
|
|
358
347
|
function getCurrentBranch(cwd) {
|
|
359
348
|
return execCommand("git rev-parse --abbrev-ref HEAD", { cwd });
|
|
360
349
|
}
|
|
361
|
-
function getHeadCommitHash(cwd) {
|
|
362
|
-
return execCommand("git rev-parse HEAD", { cwd });
|
|
363
|
-
}
|
|
364
350
|
function gitDiffBinaryAgainstBranch(branchName, cwd) {
|
|
365
351
|
logger.debug(`\u6267\u884C\u547D\u4EE4: git diff HEAD...${branchName} --binary${cwd ? ` (cwd: ${cwd})` : ""}`);
|
|
366
352
|
return execSync2(`git diff HEAD...${branchName} --binary`, {
|
|
@@ -389,6 +375,12 @@ function hasCommitWithMessage(branchName, messagePrefix, cwd) {
|
|
|
389
375
|
function gitResetSoftTo(commitHash, cwd) {
|
|
390
376
|
execCommand(`git reset --soft ${commitHash}`, { cwd });
|
|
391
377
|
}
|
|
378
|
+
function gitWriteTree(cwd) {
|
|
379
|
+
return execCommand("git write-tree", { cwd });
|
|
380
|
+
}
|
|
381
|
+
function gitReadTree(treeHash, cwd) {
|
|
382
|
+
execCommand(`git read-tree ${treeHash}`, { cwd });
|
|
383
|
+
}
|
|
392
384
|
|
|
393
385
|
// src/utils/formatter.ts
|
|
394
386
|
import chalk from "chalk";
|
|
@@ -642,27 +634,21 @@ function launchInteractiveClaude(worktree) {
|
|
|
642
634
|
import { join as join3 } from "path";
|
|
643
635
|
import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync2, unlinkSync, readdirSync as readdirSync3, rmdirSync as rmdirSync2 } from "fs";
|
|
644
636
|
function getSnapshotPath(projectName, branchName) {
|
|
645
|
-
return join3(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.
|
|
646
|
-
}
|
|
647
|
-
function getSnapshotHeadPath(projectName, branchName) {
|
|
648
|
-
return join3(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.head`);
|
|
637
|
+
return join3(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.tree`);
|
|
649
638
|
}
|
|
650
639
|
function hasSnapshot(projectName, branchName) {
|
|
651
640
|
return existsSync5(getSnapshotPath(projectName, branchName));
|
|
652
641
|
}
|
|
653
|
-
function
|
|
642
|
+
function readSnapshotTreeHash(projectName, branchName) {
|
|
654
643
|
const snapshotPath = getSnapshotPath(projectName, branchName);
|
|
655
644
|
logger.debug(`\u8BFB\u53D6 validate \u5FEB\u7167: ${snapshotPath}`);
|
|
656
|
-
return readFileSync2(snapshotPath);
|
|
645
|
+
return readFileSync2(snapshotPath, "utf-8").trim();
|
|
657
646
|
}
|
|
658
|
-
function writeSnapshot(projectName, branchName,
|
|
647
|
+
function writeSnapshot(projectName, branchName, treeHash) {
|
|
659
648
|
const snapshotPath = getSnapshotPath(projectName, branchName);
|
|
660
649
|
const snapshotDir = join3(VALIDATE_SNAPSHOTS_DIR, projectName);
|
|
661
650
|
ensureDir(snapshotDir);
|
|
662
|
-
writeFileSync2(snapshotPath,
|
|
663
|
-
if (headHash) {
|
|
664
|
-
writeFileSync2(getSnapshotHeadPath(projectName, branchName), headHash, "utf-8");
|
|
665
|
-
}
|
|
651
|
+
writeFileSync2(snapshotPath, treeHash, "utf-8");
|
|
666
652
|
logger.info(`\u5DF2\u4FDD\u5B58 validate \u5FEB\u7167: ${snapshotPath}`);
|
|
667
653
|
}
|
|
668
654
|
function removeSnapshot(projectName, branchName) {
|
|
@@ -671,17 +657,6 @@ function removeSnapshot(projectName, branchName) {
|
|
|
671
657
|
unlinkSync(snapshotPath);
|
|
672
658
|
logger.info(`\u5DF2\u5220\u9664 validate \u5FEB\u7167: ${snapshotPath}`);
|
|
673
659
|
}
|
|
674
|
-
const headPath = getSnapshotHeadPath(projectName, branchName);
|
|
675
|
-
if (existsSync5(headPath)) {
|
|
676
|
-
unlinkSync(headPath);
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
function readSnapshotHead(projectName, branchName) {
|
|
680
|
-
const headPath = getSnapshotHeadPath(projectName, branchName);
|
|
681
|
-
if (!existsSync5(headPath)) {
|
|
682
|
-
return null;
|
|
683
|
-
}
|
|
684
|
-
return readFileSync2(headPath, "utf-8").trim();
|
|
685
660
|
}
|
|
686
661
|
|
|
687
662
|
// src/commands/list.ts
|
|
@@ -1056,13 +1031,12 @@ function migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName
|
|
|
1056
1031
|
}
|
|
1057
1032
|
}
|
|
1058
1033
|
}
|
|
1059
|
-
function
|
|
1034
|
+
function saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName) {
|
|
1060
1035
|
gitAddAll(mainWorktreePath);
|
|
1061
|
-
const
|
|
1036
|
+
const treeHash = gitWriteTree(mainWorktreePath);
|
|
1062
1037
|
gitRestoreStaged(mainWorktreePath);
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
return patch;
|
|
1038
|
+
writeSnapshot(projectName, branchName, treeHash);
|
|
1039
|
+
return treeHash;
|
|
1066
1040
|
}
|
|
1067
1041
|
function handleValidateClean(options) {
|
|
1068
1042
|
validateMainWorktree();
|
|
@@ -1078,26 +1052,24 @@ function handleValidateClean(options) {
|
|
|
1078
1052
|
}
|
|
1079
1053
|
function handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted) {
|
|
1080
1054
|
migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
|
|
1081
|
-
|
|
1055
|
+
saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
|
|
1082
1056
|
printSuccess(MESSAGES.VALIDATE_SUCCESS(branchName));
|
|
1083
1057
|
}
|
|
1084
1058
|
function handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted) {
|
|
1085
|
-
const
|
|
1059
|
+
const oldTreeHash = readSnapshotTreeHash(projectName, branchName);
|
|
1086
1060
|
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
1087
1061
|
gitResetHard(mainWorktreePath);
|
|
1088
1062
|
gitCleanForce(mainWorktreePath);
|
|
1089
1063
|
}
|
|
1090
1064
|
migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
return;
|
|
1100
|
-
}
|
|
1065
|
+
saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
|
|
1066
|
+
try {
|
|
1067
|
+
gitReadTree(oldTreeHash, mainWorktreePath);
|
|
1068
|
+
} catch (error) {
|
|
1069
|
+
logger.warn(`\u589E\u91CF read-tree \u5931\u8D25: ${error}`);
|
|
1070
|
+
printWarning(MESSAGES.INCREMENTAL_VALIDATE_FALLBACK);
|
|
1071
|
+
printSuccess(MESSAGES.VALIDATE_SUCCESS(branchName));
|
|
1072
|
+
return;
|
|
1101
1073
|
}
|
|
1102
1074
|
printSuccess(MESSAGES.INCREMENTAL_VALIDATE_SUCCESS(branchName));
|
|
1103
1075
|
}
|
|
@@ -1121,16 +1093,7 @@ async function handleValidate(options) {
|
|
|
1121
1093
|
printInfo(MESSAGES.TARGET_WORKTREE_CLEAN);
|
|
1122
1094
|
return;
|
|
1123
1095
|
}
|
|
1124
|
-
|
|
1125
|
-
if (isIncremental) {
|
|
1126
|
-
const savedHead = readSnapshotHead(projectName, options.branch);
|
|
1127
|
-
const currentHead = getHeadCommitHash(mainWorktreePath);
|
|
1128
|
-
if (!savedHead || savedHead !== currentHead) {
|
|
1129
|
-
logger.info(`\u4E3B\u5206\u652F HEAD \u4E0D\u5339\u914D (${savedHead ?? "null"} \u2192 ${currentHead})\uFF0C\u6E05\u9664\u65E7\u5FEB\u7167`);
|
|
1130
|
-
removeSnapshot(projectName, options.branch);
|
|
1131
|
-
isIncremental = false;
|
|
1132
|
-
}
|
|
1133
|
-
}
|
|
1096
|
+
const isIncremental = hasSnapshot(projectName, options.branch);
|
|
1134
1097
|
if (isIncremental) {
|
|
1135
1098
|
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
1136
1099
|
await handleDirtyMainWorktree(mainWorktreePath);
|
|
@@ -1228,16 +1191,17 @@ async function handleMerge(options) {
|
|
|
1228
1191
|
if (hasMergeConflict(mainWorktreePath)) {
|
|
1229
1192
|
throw new ClawtError(MESSAGES.MERGE_CONFLICT);
|
|
1230
1193
|
}
|
|
1231
|
-
|
|
1194
|
+
const autoPullPush = getConfigValue("autoPullPush");
|
|
1195
|
+
if (autoPullPush) {
|
|
1232
1196
|
gitPull(mainWorktreePath);
|
|
1233
1197
|
gitPush(mainWorktreePath);
|
|
1234
1198
|
} else {
|
|
1235
1199
|
printInfo("\u5DF2\u8DF3\u8FC7\u81EA\u52A8 pull/push\uFF0C\u8BF7\u624B\u52A8\u6267\u884C git pull && git push");
|
|
1236
1200
|
}
|
|
1237
1201
|
if (options.message) {
|
|
1238
|
-
printSuccess(MESSAGES.MERGE_SUCCESS(options.branch, options.message));
|
|
1202
|
+
printSuccess(MESSAGES.MERGE_SUCCESS(options.branch, options.message, autoPullPush));
|
|
1239
1203
|
} else {
|
|
1240
|
-
printSuccess(MESSAGES.MERGE_SUCCESS_NO_MESSAGE(options.branch));
|
|
1204
|
+
printSuccess(MESSAGES.MERGE_SUCCESS_NO_MESSAGE(options.branch, autoPullPush));
|
|
1241
1205
|
}
|
|
1242
1206
|
if (shouldCleanup) {
|
|
1243
1207
|
cleanupWorktreeAndBranch(targetWorktreePath, options.branch);
|
package/docs/spec.md
CHANGED
|
@@ -141,8 +141,7 @@ git show-ref --verify refs/heads/<branchName> 2>/dev/null
|
|
|
141
141
|
│ └── ...
|
|
142
142
|
├── validate-snapshots/ # validate 快照目录
|
|
143
143
|
│ └── <project-name>/ # 以项目名分组
|
|
144
|
-
│ ├── <branchName>.
|
|
145
|
-
│ ├── <branchName>.head # 对应的主分支 HEAD commit hash(用于增量 validate 一致性校验)
|
|
144
|
+
│ ├── <branchName>.tree # 每个分支一个 tree hash 快照文件(存储 git tree 对象的 hash)
|
|
146
145
|
│ └── ...
|
|
147
146
|
└── worktrees/ # 所有 worktree 的统一存放目录
|
|
148
147
|
└── <project-name>/ # 以项目名分组
|
|
@@ -363,7 +362,7 @@ Git worktree 不会包含 `node_modules`、`.venv` 等依赖文件,每次安
|
|
|
363
362
|
|
|
364
363
|
**快照机制:**
|
|
365
364
|
|
|
366
|
-
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,无需一致性校验。
|
|
367
366
|
|
|
368
367
|
**运行流程:**
|
|
369
368
|
|
|
@@ -373,7 +372,7 @@ validate 命令引入了**快照(snapshot)机制**来支持增量对比。
|
|
|
373
372
|
|
|
374
373
|
1. **主 worktree 校验** (2.1)
|
|
375
374
|
2. 如果主 worktree 有未提交更改,执行 `git reset --hard` + `git clean -fd` 清空
|
|
376
|
-
3.
|
|
375
|
+
3. 删除对应分支的快照文件
|
|
377
376
|
4. 输出清理成功提示
|
|
378
377
|
|
|
379
378
|
#### 首次 validate(无历史快照)
|
|
@@ -441,14 +440,13 @@ git restore --staged .
|
|
|
441
440
|
> 此步骤结束后,目标 worktree 的代码保持原样,主 worktree 工作目录包含目标分支的全量变更。
|
|
442
441
|
> 如果 patch apply 失败(目标分支与主分支差异过大),会提示用户先执行 `clawt sync -b <branchName>` 同步主分支后重试。
|
|
443
442
|
|
|
444
|
-
##### 步骤 4
|
|
443
|
+
##### 步骤 4:保存快照为 git tree 对象
|
|
445
444
|
|
|
446
|
-
将主 worktree 工作目录的全量变更保存为
|
|
445
|
+
将主 worktree 工作目录的全量变更保存为 git tree 对象:
|
|
447
446
|
|
|
448
447
|
```bash
|
|
449
448
|
git add .
|
|
450
|
-
git
|
|
451
|
-
git rev-parse HEAD > ~/.clawt/validate-snapshots/<project>/<branchName>.head
|
|
449
|
+
git write-tree # → 返回 tree hash,写入 ~/.clawt/validate-snapshots/<project>/<branchName>.tree
|
|
452
450
|
git restore --staged .
|
|
453
451
|
```
|
|
454
452
|
|
|
@@ -463,41 +461,34 @@ git restore --staged .
|
|
|
463
461
|
|
|
464
462
|
#### 增量 validate(存在历史快照)
|
|
465
463
|
|
|
466
|
-
当 `~/.clawt/validate-snapshots/<project>/<branchName>.
|
|
464
|
+
当 `~/.clawt/validate-snapshots/<project>/<branchName>.tree` 存在时,自动进入增量模式:
|
|
467
465
|
|
|
468
|
-
##### 步骤 1
|
|
466
|
+
##### 步骤 1:读取旧 tree hash
|
|
469
467
|
|
|
470
|
-
|
|
468
|
+
在清空主 worktree 之前,读取上次保存的快照 tree hash。
|
|
471
469
|
|
|
472
|
-
|
|
473
|
-
- **一致** → 继续增量流程
|
|
474
|
-
|
|
475
|
-
##### 步骤 2:读取旧 patch
|
|
476
|
-
|
|
477
|
-
在清空主 worktree 之前,读取上次保存的快照 patch 内容。
|
|
478
|
-
|
|
479
|
-
##### 步骤 3:确保主 worktree 干净
|
|
470
|
+
##### 步骤 2:确保主 worktree 干净
|
|
480
471
|
|
|
481
472
|
如果主 worktree 有残留状态,让用户选择处理方式(同首次 validate 步骤 1 的交互),做兜底清理。
|
|
482
473
|
|
|
483
|
-
##### 步骤
|
|
474
|
+
##### 步骤 3:从目标分支获取最新全量变更
|
|
484
475
|
|
|
485
476
|
通过 patch 方式从目标分支获取最新全量变更(流程同首次 validate 的步骤 3)。
|
|
486
477
|
|
|
487
|
-
##### 步骤
|
|
478
|
+
##### 步骤 4:保存最新快照为 git tree 对象
|
|
488
479
|
|
|
489
|
-
将最新全量变更保存为新的
|
|
480
|
+
将最新全量变更保存为新的 tree 对象(覆盖旧快照,流程同首次 validate 的步骤 4)。
|
|
490
481
|
|
|
491
|
-
##### 步骤
|
|
482
|
+
##### 步骤 5:将旧 tree 对象载入暂存区
|
|
492
483
|
|
|
493
484
|
```bash
|
|
494
|
-
git
|
|
485
|
+
git read-tree <旧 tree hash>
|
|
495
486
|
```
|
|
496
487
|
|
|
497
|
-
-
|
|
498
|
-
-
|
|
488
|
+
- **读取成功** → 结果:暂存区=上次快照,工作目录=最新全量变更(用户可通过 `git diff` 查看增量差异)
|
|
489
|
+
- **读取失败**(tree 对象可能被 git gc 回收)→ 降级为全量模式,暂存区保持为空,等同于首次 validate 的结果
|
|
499
490
|
|
|
500
|
-
##### 步骤
|
|
491
|
+
##### 步骤 6:输出成功提示
|
|
501
492
|
|
|
502
493
|
```
|
|
503
494
|
# 增量模式成功
|
|
@@ -588,7 +579,7 @@ clawt merge -b <branchName> [-m <commitMessage>]
|
|
|
588
579
|
2. **主 worktree 状态检测**
|
|
589
580
|
- 执行 `git status --porcelain`
|
|
590
581
|
- 如果有更改:
|
|
591
|
-
- 如果存在该分支的 validate 快照(`~/.clawt/validate-snapshots/<project>/<branchName>.
|
|
582
|
+
- 如果存在该分支的 validate 快照(`~/.clawt/validate-snapshots/<project>/<branchName>.tree`),额外输出警告提示用户可先执行 `clawt validate -b <branchName> --clean` 清理
|
|
592
583
|
- 提示 `主 worktree 有未提交的更改,请先处理`,退出
|
|
593
584
|
- 无更改 → 继续
|
|
594
585
|
3. **Squash 检测与执行(auto-save 临时提交压缩)**
|
|
@@ -663,7 +654,7 @@ clawt merge -b <branchName> [-m <commitMessage>]
|
|
|
663
654
|
- 输出清理成功提示:`✓ 已清理 worktree 和分支: <branchName>`
|
|
664
655
|
|
|
665
656
|
10. **清理 validate 快照**
|
|
666
|
-
- merge 成功后,如果存在该分支的 validate 快照(`~/.clawt/validate-snapshots/<project>/<branchName>.
|
|
657
|
+
- merge 成功后,如果存在该分支的 validate 快照(`~/.clawt/validate-snapshots/<project>/<branchName>.tree`),自动删除该快照文件(merge 成功后快照已无意义)
|
|
667
658
|
|
|
668
659
|
> **注意:** 清理确认在 merge 操作之前询问(避免 merge 成功后因交互中断而遗留未清理的 worktree),但清理操作在 merge 成功后才执行。
|
|
669
660
|
|
|
@@ -883,7 +874,7 @@ clawt sync -b <branchName>
|
|
|
883
874
|
解决冲突后执行 git add . && git merge --continue
|
|
884
875
|
```
|
|
885
876
|
- **无冲突** → 继续
|
|
886
|
-
7. **清除 validate 快照**:合并成功后,如果该分支存在 validate 快照(`.
|
|
877
|
+
7. **清除 validate 快照**:合并成功后,如果该分支存在 validate 快照(`.tree` 文件),自动删除(代码基础已变化,旧快照无效)
|
|
887
878
|
8. **输出成功提示**:
|
|
888
879
|
```
|
|
889
880
|
✓ 已将 <mainBranch> 的最新代码同步到 <branchName>
|
package/package.json
CHANGED
package/src/commands/merge.ts
CHANGED
|
@@ -191,7 +191,8 @@ async function handleMerge(options: MergeOptions): Promise<void> {
|
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
// 步骤 7:根据配置决定是否自动 pull 和 push
|
|
194
|
-
|
|
194
|
+
const autoPullPush = getConfigValue('autoPullPush');
|
|
195
|
+
if (autoPullPush) {
|
|
195
196
|
gitPull(mainWorktreePath);
|
|
196
197
|
gitPush(mainWorktreePath);
|
|
197
198
|
} else {
|
|
@@ -200,9 +201,9 @@ async function handleMerge(options: MergeOptions): Promise<void> {
|
|
|
200
201
|
|
|
201
202
|
// 步骤 8:输出成功提示(根据是否有 message 选择对应模板)
|
|
202
203
|
if (options.message) {
|
|
203
|
-
printSuccess(MESSAGES.MERGE_SUCCESS(options.branch, options.message));
|
|
204
|
+
printSuccess(MESSAGES.MERGE_SUCCESS(options.branch, options.message, autoPullPush));
|
|
204
205
|
} else {
|
|
205
|
-
printSuccess(MESSAGES.MERGE_SUCCESS_NO_MESSAGE(options.branch));
|
|
206
|
+
printSuccess(MESSAGES.MERGE_SUCCESS_NO_MESSAGE(options.branch, autoPullPush));
|
|
206
207
|
}
|
|
207
208
|
|
|
208
209
|
// 步骤 9:merge 成功后清理 worktree 和分支
|
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 有残留状态时让用户选择处理方式
|
|
@@ -27,11 +27,11 @@ export const MESSAGES = {
|
|
|
27
27
|
VALIDATE_SUCCESS: (branch: string) =>
|
|
28
28
|
`✓ 已将分支 ${branch} 的变更应用到主 worktree\n 可以开始验证了`,
|
|
29
29
|
/** merge 成功 */
|
|
30
|
-
MERGE_SUCCESS: (branch: string, message: string) =>
|
|
31
|
-
`✓ 分支 ${branch} 已成功合并到当前分支\n 提交信息: ${message}\n
|
|
30
|
+
MERGE_SUCCESS: (branch: string, message: string, pushed: boolean) =>
|
|
31
|
+
`✓ 分支 ${branch} 已成功合并到当前分支\n 提交信息: ${message}${pushed ? '\n 已推送到远程仓库' : ''}`,
|
|
32
32
|
/** merge 成功(无提交信息,目标 worktree 已提交过) */
|
|
33
|
-
MERGE_SUCCESS_NO_MESSAGE: (branch: string) =>
|
|
34
|
-
`✓ 分支 ${branch}
|
|
33
|
+
MERGE_SUCCESS_NO_MESSAGE: (branch: string, pushed: boolean) =>
|
|
34
|
+
`✓ 分支 ${branch} 已成功合并到当前分支${pushed ? '\n 已推送到远程仓库' : ''}`,
|
|
35
35
|
/** merge 冲突 */
|
|
36
36
|
MERGE_CONFLICT: '合并存在冲突,请手动处理',
|
|
37
37
|
/** merge 后清理 worktree 和分支成功 */
|
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
|
/**
|