clawt 3.1.3 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +176 -129
- package/dist/postinstall.js +2 -0
- package/docs/spec.md +1 -0
- package/docs/validate.md +51 -10
- package/package.json +1 -1
- package/src/commands/validate.ts +38 -230
- package/src/constants/messages/validate.ts +3 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/validate-core.ts +174 -0
- package/src/utils/validate-runner.ts +105 -0
- package/src/utils/validate-snapshot.ts +29 -9
- package/tests/unit/commands/validate.test.ts +79 -289
- package/tests/unit/utils/validate-snapshot.test.ts +8 -3
package/dist/index.js
CHANGED
|
@@ -180,6 +180,8 @@ var VALIDATE_MESSAGES = {
|
|
|
180
180
|
\u6682\u5B58\u533A = \u4E0A\u6B21\u5FEB\u7167\uFF0C\u5DE5\u4F5C\u76EE\u5F55 = \u6700\u65B0\u53D8\u66F4`,
|
|
181
181
|
/** 增量 validate 降级为全量模式提示 */
|
|
182
182
|
INCREMENTAL_VALIDATE_FALLBACK: "\u589E\u91CF\u5BF9\u6BD4\u5931\u8D25\uFF0C\u5DF2\u964D\u7EA7\u4E3A\u5168\u91CF\u6A21\u5F0F",
|
|
183
|
+
/** 增量 validate 检测到目标 worktree 无新变更 */
|
|
184
|
+
INCREMENTAL_VALIDATE_NO_CHANGES: (branch) => `\u5206\u652F ${branch} \u81EA\u4E0A\u6B21 validate \u4EE5\u6765\u6CA1\u6709\u65B0\u7684\u53D8\u66F4\uFF0C\u5DF2\u6062\u590D\u5230\u4E0A\u6B21\u9A8C\u8BC1\u72B6\u6001`,
|
|
183
185
|
/** validate 状态已清理 */
|
|
184
186
|
VALIDATE_CLEANED: (branch) => `\u2713 \u5206\u652F ${branch} \u7684 validate \u72B6\u6001\u5DF2\u6E05\u7406`,
|
|
185
187
|
/** validate patch apply 失败,提示用户同步主分支 */
|
|
@@ -1672,6 +1674,9 @@ function getSnapshotPath(projectName, branchName) {
|
|
|
1672
1674
|
function getSnapshotHeadPath(projectName, branchName) {
|
|
1673
1675
|
return join6(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.head`);
|
|
1674
1676
|
}
|
|
1677
|
+
function getSnapshotStagedPath(projectName, branchName) {
|
|
1678
|
+
return join6(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.staged`);
|
|
1679
|
+
}
|
|
1675
1680
|
function hasSnapshot(projectName, branchName) {
|
|
1676
1681
|
return existsSync8(getSnapshotPath(projectName, branchName));
|
|
1677
1682
|
}
|
|
@@ -1684,23 +1689,28 @@ function getSnapshotModifiedTime(projectName, branchName) {
|
|
|
1684
1689
|
function readSnapshot(projectName, branchName) {
|
|
1685
1690
|
const snapshotPath = getSnapshotPath(projectName, branchName);
|
|
1686
1691
|
const headPath = getSnapshotHeadPath(projectName, branchName);
|
|
1692
|
+
const stagedPath = getSnapshotStagedPath(projectName, branchName);
|
|
1687
1693
|
logger.debug(`\u8BFB\u53D6 validate \u5FEB\u7167: ${snapshotPath}`);
|
|
1688
1694
|
const treeHash = existsSync8(snapshotPath) ? readFileSync3(snapshotPath, "utf-8").trim() : "";
|
|
1689
1695
|
const headCommitHash = existsSync8(headPath) ? readFileSync3(headPath, "utf-8").trim() : "";
|
|
1690
|
-
|
|
1696
|
+
const stagedTreeHash = existsSync8(stagedPath) ? readFileSync3(stagedPath, "utf-8").trim() : "";
|
|
1697
|
+
return { treeHash, headCommitHash, stagedTreeHash };
|
|
1691
1698
|
}
|
|
1692
|
-
function writeSnapshot(projectName, branchName, treeHash, headCommitHash) {
|
|
1699
|
+
function writeSnapshot(projectName, branchName, treeHash, headCommitHash, stagedTreeHash = "") {
|
|
1693
1700
|
const snapshotPath = getSnapshotPath(projectName, branchName);
|
|
1694
1701
|
const headPath = getSnapshotHeadPath(projectName, branchName);
|
|
1702
|
+
const stagedPath = getSnapshotStagedPath(projectName, branchName);
|
|
1695
1703
|
const snapshotDir = join6(VALIDATE_SNAPSHOTS_DIR, projectName);
|
|
1696
1704
|
ensureDir(snapshotDir);
|
|
1697
1705
|
writeFileSync3(snapshotPath, treeHash, "utf-8");
|
|
1698
1706
|
writeFileSync3(headPath, headCommitHash, "utf-8");
|
|
1699
|
-
|
|
1707
|
+
writeFileSync3(stagedPath, stagedTreeHash, "utf-8");
|
|
1708
|
+
logger.info(`\u5DF2\u4FDD\u5B58 validate \u5FEB\u7167: ${snapshotPath}, ${headPath}, ${stagedPath}`);
|
|
1700
1709
|
}
|
|
1701
1710
|
function removeSnapshot(projectName, branchName) {
|
|
1702
1711
|
const snapshotPath = getSnapshotPath(projectName, branchName);
|
|
1703
1712
|
const headPath = getSnapshotHeadPath(projectName, branchName);
|
|
1713
|
+
const stagedPath = getSnapshotStagedPath(projectName, branchName);
|
|
1704
1714
|
if (existsSync8(snapshotPath)) {
|
|
1705
1715
|
unlinkSync(snapshotPath);
|
|
1706
1716
|
logger.info(`\u5DF2\u5220\u9664 validate \u5FEB\u7167: ${snapshotPath}`);
|
|
@@ -1709,6 +1719,10 @@ function removeSnapshot(projectName, branchName) {
|
|
|
1709
1719
|
unlinkSync(headPath);
|
|
1710
1720
|
logger.info(`\u5DF2\u5220\u9664 validate \u5FEB\u7167: ${headPath}`);
|
|
1711
1721
|
}
|
|
1722
|
+
if (existsSync8(stagedPath)) {
|
|
1723
|
+
unlinkSync(stagedPath);
|
|
1724
|
+
logger.info(`\u5DF2\u5220\u9664 validate \u5FEB\u7167: ${stagedPath}`);
|
|
1725
|
+
}
|
|
1712
1726
|
}
|
|
1713
1727
|
function getProjectSnapshotBranches(projectName) {
|
|
1714
1728
|
const projectDir = join6(VALIDATE_SNAPSHOTS_DIR, projectName);
|
|
@@ -2968,6 +2982,145 @@ async function checkForUpdates(currentVersion) {
|
|
|
2968
2982
|
}
|
|
2969
2983
|
}
|
|
2970
2984
|
|
|
2985
|
+
// src/utils/validate-runner.ts
|
|
2986
|
+
function executeSingleCommand(command, mainWorktreePath) {
|
|
2987
|
+
printInfo(MESSAGES.VALIDATE_RUN_START(command));
|
|
2988
|
+
printSeparator();
|
|
2989
|
+
const result = runCommandInherited(command, { cwd: mainWorktreePath });
|
|
2990
|
+
printSeparator();
|
|
2991
|
+
if (result.error) {
|
|
2992
|
+
printError(MESSAGES.VALIDATE_RUN_ERROR(command, result.error.message));
|
|
2993
|
+
return;
|
|
2994
|
+
}
|
|
2995
|
+
const exitCode = result.status ?? 1;
|
|
2996
|
+
if (exitCode === 0) {
|
|
2997
|
+
printSuccess(MESSAGES.VALIDATE_RUN_SUCCESS(command));
|
|
2998
|
+
} else {
|
|
2999
|
+
printError(MESSAGES.VALIDATE_RUN_FAILED(command, exitCode));
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
function reportParallelResults(results) {
|
|
3003
|
+
printSeparator();
|
|
3004
|
+
const successCount = results.filter((r) => r.exitCode === 0 && !r.error).length;
|
|
3005
|
+
const failedCount = results.length - successCount;
|
|
3006
|
+
for (const result of results) {
|
|
3007
|
+
if (result.error) {
|
|
3008
|
+
printError(MESSAGES.VALIDATE_PARALLEL_CMD_ERROR(result.command, result.error));
|
|
3009
|
+
} else if (result.exitCode === 0) {
|
|
3010
|
+
printSuccess(MESSAGES.VALIDATE_PARALLEL_CMD_SUCCESS(result.command));
|
|
3011
|
+
} else {
|
|
3012
|
+
printError(MESSAGES.VALIDATE_PARALLEL_CMD_FAILED(result.command, result.exitCode));
|
|
3013
|
+
}
|
|
3014
|
+
}
|
|
3015
|
+
if (failedCount === 0) {
|
|
3016
|
+
printSuccess(MESSAGES.VALIDATE_PARALLEL_RUN_ALL_SUCCESS(results.length));
|
|
3017
|
+
} else {
|
|
3018
|
+
printError(MESSAGES.VALIDATE_PARALLEL_RUN_SUMMARY(successCount, failedCount));
|
|
3019
|
+
}
|
|
3020
|
+
}
|
|
3021
|
+
async function executeParallelCommands(commands, mainWorktreePath) {
|
|
3022
|
+
printInfo(MESSAGES.VALIDATE_PARALLEL_RUN_START(commands.length));
|
|
3023
|
+
for (let i = 0; i < commands.length; i++) {
|
|
3024
|
+
printInfo(MESSAGES.VALIDATE_PARALLEL_CMD_START(i + 1, commands.length, commands[i]));
|
|
3025
|
+
}
|
|
3026
|
+
printSeparator();
|
|
3027
|
+
const results = await runParallelCommands(commands, { cwd: mainWorktreePath });
|
|
3028
|
+
reportParallelResults(results);
|
|
3029
|
+
}
|
|
3030
|
+
async function executeRunCommand(command, mainWorktreePath) {
|
|
3031
|
+
printInfo("");
|
|
3032
|
+
const commands = parseParallelCommands(command);
|
|
3033
|
+
if (commands.length <= 1) {
|
|
3034
|
+
executeSingleCommand(commands[0] || command, mainWorktreePath);
|
|
3035
|
+
} else {
|
|
3036
|
+
await executeParallelCommands(commands, mainWorktreePath);
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
// src/utils/validate-core.ts
|
|
3041
|
+
function migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted) {
|
|
3042
|
+
let didTempCommit = false;
|
|
3043
|
+
try {
|
|
3044
|
+
if (hasUncommitted) {
|
|
3045
|
+
gitAddAll(targetWorktreePath);
|
|
3046
|
+
gitCommit("clawt:temp-commit-for-validate", targetWorktreePath);
|
|
3047
|
+
didTempCommit = true;
|
|
3048
|
+
}
|
|
3049
|
+
const patch = gitDiffBinaryAgainstBranch(branchName, mainWorktreePath);
|
|
3050
|
+
if (patch.length > 0) {
|
|
3051
|
+
try {
|
|
3052
|
+
gitApplyFromStdin(patch, mainWorktreePath);
|
|
3053
|
+
} catch (error) {
|
|
3054
|
+
logger.warn(`patch apply \u5931\u8D25: ${error}`);
|
|
3055
|
+
printWarning(MESSAGES.VALIDATE_PATCH_APPLY_FAILED(branchName));
|
|
3056
|
+
return { success: false };
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
return { success: true };
|
|
3060
|
+
} finally {
|
|
3061
|
+
if (didTempCommit) {
|
|
3062
|
+
try {
|
|
3063
|
+
gitResetSoft(1, targetWorktreePath);
|
|
3064
|
+
} catch (error) {
|
|
3065
|
+
logger.error(`\u64A4\u9500\u4E34\u65F6 commit \u5931\u8D25: ${error}`);
|
|
3066
|
+
}
|
|
3067
|
+
try {
|
|
3068
|
+
gitRestoreStaged(targetWorktreePath);
|
|
3069
|
+
} catch (error) {
|
|
3070
|
+
logger.error(`\u6062\u590D\u6682\u5B58\u533A\u5931\u8D25: ${error}`);
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
}
|
|
3074
|
+
}
|
|
3075
|
+
function computeCurrentTreeHash(mainWorktreePath) {
|
|
3076
|
+
gitAddAll(mainWorktreePath);
|
|
3077
|
+
const treeHash = gitWriteTree(mainWorktreePath);
|
|
3078
|
+
gitRestoreStaged(mainWorktreePath);
|
|
3079
|
+
return treeHash;
|
|
3080
|
+
}
|
|
3081
|
+
function saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName, stagedTreeHash = "") {
|
|
3082
|
+
gitAddAll(mainWorktreePath);
|
|
3083
|
+
const treeHash = gitWriteTree(mainWorktreePath);
|
|
3084
|
+
gitRestoreStaged(mainWorktreePath);
|
|
3085
|
+
const headCommitHash = getHeadCommitHash(mainWorktreePath);
|
|
3086
|
+
writeSnapshot(projectName, branchName, treeHash, headCommitHash, stagedTreeHash);
|
|
3087
|
+
return treeHash;
|
|
3088
|
+
}
|
|
3089
|
+
function loadOldSnapshotToStage(oldTreeHash, oldHeadCommitHash, currentHeadCommitHash, mainWorktreePath) {
|
|
3090
|
+
try {
|
|
3091
|
+
if (oldHeadCommitHash && oldHeadCommitHash !== currentHeadCommitHash) {
|
|
3092
|
+
const oldHeadTreeHash = getCommitTreeHash(oldHeadCommitHash, mainWorktreePath);
|
|
3093
|
+
const oldChangePatch = gitDiffTree(oldHeadTreeHash, oldTreeHash, mainWorktreePath);
|
|
3094
|
+
if (oldChangePatch.length > 0 && gitApplyCachedCheck(oldChangePatch, mainWorktreePath)) {
|
|
3095
|
+
gitApplyCachedFromStdin(oldChangePatch, mainWorktreePath);
|
|
3096
|
+
const stagedTreeHash = gitWriteTree(mainWorktreePath);
|
|
3097
|
+
return { success: true, stagedTreeHash };
|
|
3098
|
+
} else if (oldChangePatch.length > 0) {
|
|
3099
|
+
logger.warn("\u65E7\u53D8\u66F4 patch \u4E0E\u5F53\u524D HEAD \u51B2\u7A81\uFF0C\u964D\u7EA7\u4E3A\u5168\u91CF\u6A21\u5F0F");
|
|
3100
|
+
return { success: false, stagedTreeHash: "" };
|
|
3101
|
+
}
|
|
3102
|
+
return { success: true, stagedTreeHash: "" };
|
|
3103
|
+
} else {
|
|
3104
|
+
gitReadTree(oldTreeHash, mainWorktreePath);
|
|
3105
|
+
return { success: true, stagedTreeHash: oldTreeHash };
|
|
3106
|
+
}
|
|
3107
|
+
} catch (error) {
|
|
3108
|
+
logger.warn(`\u589E\u91CF read-tree \u5931\u8D25: ${error}`);
|
|
3109
|
+
return { success: false, stagedTreeHash: "" };
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
function switchToValidateBranch(branchName, mainWorktreePath) {
|
|
3113
|
+
const validateBranchName = getValidateBranchName(branchName);
|
|
3114
|
+
if (!checkBranchExists(validateBranchName)) {
|
|
3115
|
+
throw new ClawtError(MESSAGES.VALIDATE_BRANCH_NOT_FOUND(validateBranchName, branchName));
|
|
3116
|
+
}
|
|
3117
|
+
const currentBranch = getCurrentBranch(mainWorktreePath);
|
|
3118
|
+
if (currentBranch !== validateBranchName) {
|
|
3119
|
+
gitCheckout(validateBranchName, mainWorktreePath);
|
|
3120
|
+
}
|
|
3121
|
+
return validateBranchName;
|
|
3122
|
+
}
|
|
3123
|
+
|
|
2971
3124
|
// src/utils/interactive-panel.ts
|
|
2972
3125
|
import { createInterface as createInterface2 } from "readline";
|
|
2973
3126
|
|
|
@@ -3934,40 +4087,6 @@ function registerValidateCommand(program2) {
|
|
|
3934
4087
|
async function handleDirtyMainWorktree(mainWorktreePath) {
|
|
3935
4088
|
await handleDirtyWorkingDir(mainWorktreePath);
|
|
3936
4089
|
}
|
|
3937
|
-
function migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted) {
|
|
3938
|
-
let didTempCommit = false;
|
|
3939
|
-
try {
|
|
3940
|
-
if (hasUncommitted) {
|
|
3941
|
-
gitAddAll(targetWorktreePath);
|
|
3942
|
-
gitCommit("clawt:temp-commit-for-validate", targetWorktreePath);
|
|
3943
|
-
didTempCommit = true;
|
|
3944
|
-
}
|
|
3945
|
-
const patch = gitDiffBinaryAgainstBranch(branchName, mainWorktreePath);
|
|
3946
|
-
if (patch.length > 0) {
|
|
3947
|
-
try {
|
|
3948
|
-
gitApplyFromStdin(patch, mainWorktreePath);
|
|
3949
|
-
} catch (error) {
|
|
3950
|
-
logger.warn(`patch apply \u5931\u8D25: ${error}`);
|
|
3951
|
-
printWarning(MESSAGES.VALIDATE_PATCH_APPLY_FAILED(branchName));
|
|
3952
|
-
return { success: false };
|
|
3953
|
-
}
|
|
3954
|
-
}
|
|
3955
|
-
return { success: true };
|
|
3956
|
-
} finally {
|
|
3957
|
-
if (didTempCommit) {
|
|
3958
|
-
try {
|
|
3959
|
-
gitResetSoft(1, targetWorktreePath);
|
|
3960
|
-
} catch (error) {
|
|
3961
|
-
logger.error(`\u64A4\u9500\u4E34\u65F6 commit \u5931\u8D25: ${error}`);
|
|
3962
|
-
}
|
|
3963
|
-
try {
|
|
3964
|
-
gitRestoreStaged(targetWorktreePath);
|
|
3965
|
-
} catch (error) {
|
|
3966
|
-
logger.error(`\u6062\u590D\u6682\u5B58\u533A\u5931\u8D25: ${error}`);
|
|
3967
|
-
}
|
|
3968
|
-
}
|
|
3969
|
-
}
|
|
3970
|
-
}
|
|
3971
4090
|
async function handlePatchApplyFailure(targetWorktreePath, branchName) {
|
|
3972
4091
|
const confirmed = await confirmAction(MESSAGES.VALIDATE_CONFIRM_AUTO_SYNC(branchName));
|
|
3973
4092
|
if (!confirmed) {
|
|
@@ -3977,14 +4096,6 @@ async function handlePatchApplyFailure(targetWorktreePath, branchName) {
|
|
|
3977
4096
|
printInfo(MESSAGES.VALIDATE_AUTO_SYNC_START(branchName));
|
|
3978
4097
|
const syncResult = await executeSyncForBranch(targetWorktreePath, branchName);
|
|
3979
4098
|
}
|
|
3980
|
-
function saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName) {
|
|
3981
|
-
gitAddAll(mainWorktreePath);
|
|
3982
|
-
const treeHash = gitWriteTree(mainWorktreePath);
|
|
3983
|
-
gitRestoreStaged(mainWorktreePath);
|
|
3984
|
-
const headCommitHash = getHeadCommitHash(mainWorktreePath);
|
|
3985
|
-
writeSnapshot(projectName, branchName, treeHash, headCommitHash);
|
|
3986
|
-
return treeHash;
|
|
3987
|
-
}
|
|
3988
4099
|
async function handleValidateClean(options) {
|
|
3989
4100
|
validateMainWorktree();
|
|
3990
4101
|
requireProjectConfig();
|
|
@@ -4013,11 +4124,7 @@ async function handleValidateClean(options) {
|
|
|
4013
4124
|
printSuccess(MESSAGES.VALIDATE_CLEANED(branchName));
|
|
4014
4125
|
}
|
|
4015
4126
|
async function handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted) {
|
|
4016
|
-
const validateBranchName =
|
|
4017
|
-
if (!checkBranchExists(validateBranchName)) {
|
|
4018
|
-
throw new ClawtError(MESSAGES.VALIDATE_BRANCH_NOT_FOUND(validateBranchName, branchName));
|
|
4019
|
-
}
|
|
4020
|
-
gitCheckout(validateBranchName, mainWorktreePath);
|
|
4127
|
+
const validateBranchName = switchToValidateBranch(branchName, mainWorktreePath);
|
|
4021
4128
|
const result = migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
|
|
4022
4129
|
if (!result.success) {
|
|
4023
4130
|
await ensureOnMainWorkBranch(mainWorktreePath);
|
|
@@ -4028,102 +4135,42 @@ async function handleFirstValidate(targetWorktreePath, mainWorktreePath, project
|
|
|
4028
4135
|
printSuccess(MESSAGES.VALIDATE_SUCCESS_WITH_BRANCH(branchName, validateBranchName));
|
|
4029
4136
|
}
|
|
4030
4137
|
async function handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted) {
|
|
4031
|
-
const { treeHash: oldTreeHash, headCommitHash: oldHeadCommitHash } = readSnapshot(projectName, branchName);
|
|
4138
|
+
const { treeHash: oldTreeHash, headCommitHash: oldHeadCommitHash, stagedTreeHash: oldStagedTreeHash } = readSnapshot(projectName, branchName);
|
|
4032
4139
|
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
4033
4140
|
gitResetHard(mainWorktreePath);
|
|
4034
4141
|
gitCleanForce(mainWorktreePath);
|
|
4035
4142
|
}
|
|
4036
|
-
const validateBranchName =
|
|
4037
|
-
if (!checkBranchExists(validateBranchName)) {
|
|
4038
|
-
throw new ClawtError(MESSAGES.VALIDATE_BRANCH_NOT_FOUND(validateBranchName, branchName));
|
|
4039
|
-
}
|
|
4040
|
-
const currentBranch = getCurrentBranch(mainWorktreePath);
|
|
4041
|
-
if (currentBranch !== validateBranchName) {
|
|
4042
|
-
gitCheckout(validateBranchName, mainWorktreePath);
|
|
4043
|
-
}
|
|
4143
|
+
const validateBranchName = switchToValidateBranch(branchName, mainWorktreePath);
|
|
4044
4144
|
const result = migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
|
|
4045
4145
|
if (!result.success) {
|
|
4046
4146
|
await ensureOnMainWorkBranch(mainWorktreePath);
|
|
4047
4147
|
await handlePatchApplyFailure(targetWorktreePath, branchName);
|
|
4048
4148
|
return;
|
|
4049
4149
|
}
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
|
|
4054
|
-
|
|
4055
|
-
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
logger.warn("\u65E7\u53D8\u66F4 patch \u4E0E\u5F53\u524D HEAD \u51B2\u7A81\uFF0C\u964D\u7EA7\u4E3A\u5168\u91CF\u6A21\u5F0F");
|
|
4060
|
-
printWarning(MESSAGES.INCREMENTAL_VALIDATE_FALLBACK);
|
|
4061
|
-
printSuccess(MESSAGES.VALIDATE_SUCCESS_WITH_BRANCH(branchName, validateBranchName));
|
|
4062
|
-
return;
|
|
4150
|
+
const newTreeHash = computeCurrentTreeHash(mainWorktreePath);
|
|
4151
|
+
const currentHeadCommitHash = getHeadCommitHash(mainWorktreePath);
|
|
4152
|
+
const hasNewChanges = newTreeHash !== oldTreeHash || oldHeadCommitHash && oldHeadCommitHash !== currentHeadCommitHash;
|
|
4153
|
+
if (!hasNewChanges) {
|
|
4154
|
+
if (oldStagedTreeHash) {
|
|
4155
|
+
try {
|
|
4156
|
+
gitReadTree(oldStagedTreeHash, mainWorktreePath);
|
|
4157
|
+
} catch (error) {
|
|
4158
|
+
logger.warn(`\u6062\u590D\u6682\u5B58\u533A\u5931\u8D25: ${error}`);
|
|
4063
4159
|
}
|
|
4064
|
-
} else {
|
|
4065
|
-
gitReadTree(oldTreeHash, mainWorktreePath);
|
|
4066
4160
|
}
|
|
4067
|
-
|
|
4068
|
-
logger.warn(`\u589E\u91CF read-tree \u5931\u8D25: ${error}`);
|
|
4069
|
-
printWarning(MESSAGES.INCREMENTAL_VALIDATE_FALLBACK);
|
|
4161
|
+
printInfo(MESSAGES.INCREMENTAL_VALIDATE_NO_CHANGES(branchName));
|
|
4070
4162
|
printSuccess(MESSAGES.VALIDATE_SUCCESS_WITH_BRANCH(branchName, validateBranchName));
|
|
4071
4163
|
return;
|
|
4072
4164
|
}
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
4078
|
-
const result = runCommandInherited(command, { cwd: mainWorktreePath });
|
|
4079
|
-
printSeparator();
|
|
4080
|
-
if (result.error) {
|
|
4081
|
-
printError(MESSAGES.VALIDATE_RUN_ERROR(command, result.error.message));
|
|
4165
|
+
const stageResult = loadOldSnapshotToStage(oldTreeHash, oldHeadCommitHash, currentHeadCommitHash, mainWorktreePath);
|
|
4166
|
+
if (!stageResult.success) {
|
|
4167
|
+
printWarning(MESSAGES.INCREMENTAL_VALIDATE_FALLBACK);
|
|
4168
|
+
writeSnapshot(projectName, branchName, newTreeHash, currentHeadCommitHash, "");
|
|
4169
|
+
printSuccess(MESSAGES.VALIDATE_SUCCESS_WITH_BRANCH(branchName, validateBranchName));
|
|
4082
4170
|
return;
|
|
4083
4171
|
}
|
|
4084
|
-
|
|
4085
|
-
|
|
4086
|
-
printSuccess(MESSAGES.VALIDATE_RUN_SUCCESS(command));
|
|
4087
|
-
} else {
|
|
4088
|
-
printError(MESSAGES.VALIDATE_RUN_FAILED(command, exitCode));
|
|
4089
|
-
}
|
|
4090
|
-
}
|
|
4091
|
-
function reportParallelResults(results) {
|
|
4092
|
-
printSeparator();
|
|
4093
|
-
const successCount = results.filter((r) => r.exitCode === 0 && !r.error).length;
|
|
4094
|
-
const failedCount = results.length - successCount;
|
|
4095
|
-
for (const result of results) {
|
|
4096
|
-
if (result.error) {
|
|
4097
|
-
printError(MESSAGES.VALIDATE_PARALLEL_CMD_ERROR(result.command, result.error));
|
|
4098
|
-
} else if (result.exitCode === 0) {
|
|
4099
|
-
printSuccess(MESSAGES.VALIDATE_PARALLEL_CMD_SUCCESS(result.command));
|
|
4100
|
-
} else {
|
|
4101
|
-
printError(MESSAGES.VALIDATE_PARALLEL_CMD_FAILED(result.command, result.exitCode));
|
|
4102
|
-
}
|
|
4103
|
-
}
|
|
4104
|
-
if (failedCount === 0) {
|
|
4105
|
-
printSuccess(MESSAGES.VALIDATE_PARALLEL_RUN_ALL_SUCCESS(results.length));
|
|
4106
|
-
} else {
|
|
4107
|
-
printError(MESSAGES.VALIDATE_PARALLEL_RUN_SUMMARY(successCount, failedCount));
|
|
4108
|
-
}
|
|
4109
|
-
}
|
|
4110
|
-
async function executeParallelCommands(commands, mainWorktreePath) {
|
|
4111
|
-
printInfo(MESSAGES.VALIDATE_PARALLEL_RUN_START(commands.length));
|
|
4112
|
-
for (let i = 0; i < commands.length; i++) {
|
|
4113
|
-
printInfo(MESSAGES.VALIDATE_PARALLEL_CMD_START(i + 1, commands.length, commands[i]));
|
|
4114
|
-
}
|
|
4115
|
-
printSeparator();
|
|
4116
|
-
const results = await runParallelCommands(commands, { cwd: mainWorktreePath });
|
|
4117
|
-
reportParallelResults(results);
|
|
4118
|
-
}
|
|
4119
|
-
async function executeRunCommand(command, mainWorktreePath) {
|
|
4120
|
-
printInfo("");
|
|
4121
|
-
const commands = parseParallelCommands(command);
|
|
4122
|
-
if (commands.length <= 1) {
|
|
4123
|
-
executeSingleCommand(commands[0] || command, mainWorktreePath);
|
|
4124
|
-
} else {
|
|
4125
|
-
await executeParallelCommands(commands, mainWorktreePath);
|
|
4126
|
-
}
|
|
4172
|
+
writeSnapshot(projectName, branchName, newTreeHash, currentHeadCommitHash, stageResult.stagedTreeHash);
|
|
4173
|
+
printSuccess(MESSAGES.INCREMENTAL_VALIDATE_SUCCESS(branchName));
|
|
4127
4174
|
}
|
|
4128
4175
|
function resolveRunCommand(optionRun) {
|
|
4129
4176
|
if (optionRun) {
|
package/dist/postinstall.js
CHANGED
|
@@ -171,6 +171,8 @@ var VALIDATE_MESSAGES = {
|
|
|
171
171
|
\u6682\u5B58\u533A = \u4E0A\u6B21\u5FEB\u7167\uFF0C\u5DE5\u4F5C\u76EE\u5F55 = \u6700\u65B0\u53D8\u66F4`,
|
|
172
172
|
/** 增量 validate 降级为全量模式提示 */
|
|
173
173
|
INCREMENTAL_VALIDATE_FALLBACK: "\u589E\u91CF\u5BF9\u6BD4\u5931\u8D25\uFF0C\u5DF2\u964D\u7EA7\u4E3A\u5168\u91CF\u6A21\u5F0F",
|
|
174
|
+
/** 增量 validate 检测到目标 worktree 无新变更 */
|
|
175
|
+
INCREMENTAL_VALIDATE_NO_CHANGES: (branch) => `\u5206\u652F ${branch} \u81EA\u4E0A\u6B21 validate \u4EE5\u6765\u6CA1\u6709\u65B0\u7684\u53D8\u66F4\uFF0C\u5DF2\u6062\u590D\u5230\u4E0A\u6B21\u9A8C\u8BC1\u72B6\u6001`,
|
|
174
176
|
/** validate 状态已清理 */
|
|
175
177
|
VALIDATE_CLEANED: (branch) => `\u2713 \u5206\u652F ${branch} \u7684 validate \u72B6\u6001\u5DF2\u6E05\u7406`,
|
|
176
178
|
/** validate patch apply 失败,提示用户同步主分支 */
|
package/docs/spec.md
CHANGED
|
@@ -254,6 +254,7 @@ async function interactiveConfigEditor<T extends object>(
|
|
|
254
254
|
│ └── <project-name>/ # 以项目名分组
|
|
255
255
|
│ ├── <branchName>.tree # 每个分支一个 tree hash 快照文件(存储 git tree 对象的 hash)
|
|
256
256
|
│ ├── <branchName>.head # 每个分支一个 HEAD commit hash 快照文件(存储快照时验证分支的 HEAD commit hash)
|
|
257
|
+
│ ├── <branchName>.staged # 每个分支一个 staged tree hash 快照文件(存储 validate 结束时暂存区对应的 tree hash,用于无变更时恢复)
|
|
257
258
|
│ └── ...
|
|
258
259
|
├── projects/<project-name>/ # 项目级配置目录
|
|
259
260
|
│ └── config.json # 项目级配置(含 clawtMainWorkBranch)
|
package/docs/validate.md
CHANGED
|
@@ -30,7 +30,7 @@ validate 不再在主工作分支上直接 apply patch,而是先切换到目
|
|
|
30
30
|
|
|
31
31
|
**快照机制:**
|
|
32
32
|
|
|
33
|
-
validate 命令引入了**快照(snapshot)机制**来支持增量对比。每次 validate 执行成功后,会将当前全量变更通过 `git write-tree` 保存为 git tree 对象,并将 tree hash 记录到文件(`~/.clawt/validate-snapshots/<project>/<branchName>.tree`),同时将验证分支的 HEAD commit hash 记录到文件(`~/.clawt/validate-snapshots/<project>/<branchName>.head`),用于增量 validate
|
|
33
|
+
validate 命令引入了**快照(snapshot)机制**来支持增量对比。每次 validate 执行成功后,会将当前全量变更通过 `git write-tree` 保存为 git tree 对象,并将 tree hash 记录到文件(`~/.clawt/validate-snapshots/<project>/<branchName>.tree`),同时将验证分支的 HEAD commit hash 记录到文件(`~/.clawt/validate-snapshots/<project>/<branchName>.head`),以及 validate 结束时暂存区对应的 tree hash 记录到文件(`~/.clawt/validate-snapshots/<project>/<branchName>.staged`),用于增量 validate 时对齐基准和无变更恢复。当再次执行 validate 时,先计算当前变更的 tree hash 与旧快照对比:如果没有新变更(tree hash 和 HEAD 均未变化),直接通过 `git read-tree` 恢复上次 validate 结束时的暂存区状态,跳过后续步骤;如果有新变更,则继续执行暂存区载入流程——如果验证分支 HEAD 未变化(正常情况),通过 `git read-tree` 将上次快照的 tree 对象载入暂存区;如果验证分支 HEAD 已变化(sync 后重建了验证分支),则将旧变更 patch(旧 tree 相对于旧 HEAD 的差异)重放到当前 HEAD 暂存区上,避免新旧 tree 基准不同导致 diff 混入 HEAD 变化的内容。最终用户可通过 `git diff` 查看两次 validate 之间的增量差异。
|
|
34
34
|
|
|
35
35
|
**运行流程:**
|
|
36
36
|
|
|
@@ -289,7 +289,7 @@ clawt validate -b feature-scheme-1 -r "pnpm test & pnpm build"
|
|
|
289
289
|
|
|
290
290
|
##### 步骤 1:读取旧快照
|
|
291
291
|
|
|
292
|
-
在清空主 worktree 之前,读取上次保存的快照 tree hash
|
|
292
|
+
在清空主 worktree 之前,读取上次保存的快照 tree hash、当时的 HEAD commit hash 和暂存区 tree hash(`stagedTreeHash`)。
|
|
293
293
|
|
|
294
294
|
##### 步骤 2:确保主 worktree 干净
|
|
295
295
|
|
|
@@ -307,9 +307,27 @@ git checkout clawt-validate-<branchName>
|
|
|
307
307
|
|
|
308
308
|
通过 patch 方式从目标分支获取最新全量变更(流程同首次 validate 的步骤 4)。如果 patch apply 失败,同样进入自动 sync 交互流程(见首次 validate 的 [patch apply 失败后的自动 sync 流程](#patch-apply-失败后的自动-sync-流程)),validate 流程提前结束。
|
|
309
309
|
|
|
310
|
-
##### 步骤 5
|
|
310
|
+
##### 步骤 5:检测是否有新变更
|
|
311
311
|
|
|
312
|
-
|
|
312
|
+
计算当前工作目录变更的 tree hash(`git add . → git write-tree → git restore --staged .`),并与旧快照的 tree hash 及 HEAD commit hash 对比:
|
|
313
|
+
|
|
314
|
+
```bash
|
|
315
|
+
# 计算当前变更的 tree hash
|
|
316
|
+
git add .
|
|
317
|
+
git write-tree # → newTreeHash
|
|
318
|
+
git restore --staged .
|
|
319
|
+
|
|
320
|
+
# 获取当前 HEAD
|
|
321
|
+
git rev-parse HEAD # → currentHeadCommitHash
|
|
322
|
+
|
|
323
|
+
# 判断是否有新变更
|
|
324
|
+
hasNewChanges = (newTreeHash !== oldTreeHash) || (oldHeadCommitHash !== currentHeadCommitHash)
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
- **无新变更**(tree hash 和 HEAD 均未变化)→ 不更新快照,直接通过 `git read-tree <oldStagedTreeHash>` 恢复上次 validate 结束时的暂存区状态,输出提示后返回
|
|
328
|
+
- **有新变更** → 继续步骤 6
|
|
329
|
+
|
|
330
|
+
> 无变更检测避免了重复 validate 时不必要的快照更新和暂存区重载操作。恢复上次暂存区状态后,用户看到的 `git diff` 结果与上次 validate 结束时完全一致。
|
|
313
331
|
|
|
314
332
|
##### 步骤 6:将旧变更状态载入暂存区
|
|
315
333
|
|
|
@@ -323,8 +341,8 @@ git checkout clawt-validate-<branchName>
|
|
|
323
341
|
git read-tree <旧 tree hash>
|
|
324
342
|
```
|
|
325
343
|
|
|
326
|
-
- **读取成功** →
|
|
327
|
-
- **读取失败**(tree 对象可能被 git gc 回收)→
|
|
344
|
+
- **读取成功** → 记录 `newStagedTreeHash = oldTreeHash`,结果:暂存区=上次快照,工作目录=最新全量变更(用户可通过 `git diff` 查看增量差异)
|
|
345
|
+
- **读取失败**(tree 对象可能被 git gc 回收)→ 降级为全量模式,写入快照(`stagedTreeHash` 为空)后返回,暂存区保持为空,等同于首次 validate 的结果
|
|
328
346
|
|
|
329
347
|
> 这是最常见的路径。相比重构前,正常情况不再需要处理 HEAD 变化的复杂逻辑,代码路径更简单、更可靠。
|
|
330
348
|
|
|
@@ -344,25 +362,48 @@ git apply --cached --check < patch
|
|
|
344
362
|
|
|
345
363
|
# 无冲突:apply --cached 到当前 HEAD 暂存区
|
|
346
364
|
git apply --cached < patch
|
|
365
|
+
|
|
366
|
+
# 记录暂存区的 tree hash
|
|
367
|
+
git write-tree # → newStagedTreeHash
|
|
347
368
|
```
|
|
348
369
|
|
|
349
370
|
- **patch 为空**(旧变更为空)→ 暂存区保持干净
|
|
350
|
-
- **无冲突** → apply --cached 到当前 HEAD
|
|
351
|
-
- **有冲突** →
|
|
371
|
+
- **无冲突** → apply --cached 到当前 HEAD 暂存区,通过 `git write-tree` 记录 `newStagedTreeHash`,结果与正常情况一致
|
|
372
|
+
- **有冲突** → 降级为全量模式(暂存区保持为空),写入快照(`stagedTreeHash` 为空)后返回
|
|
352
373
|
|
|
353
|
-
##### 步骤 7
|
|
374
|
+
##### 步骤 7:写入新快照
|
|
375
|
+
|
|
376
|
+
将步骤 5 计算的 `newTreeHash`、当前 HEAD commit hash 和步骤 6 记录的 `newStagedTreeHash` 写入快照文件,供下次 validate 使用:
|
|
377
|
+
|
|
378
|
+
```bash
|
|
379
|
+
# 写入 ~/.clawt/validate-snapshots/<project>/<branchName>.tree
|
|
380
|
+
echo <newTreeHash>
|
|
381
|
+
|
|
382
|
+
# 写入 ~/.clawt/validate-snapshots/<project>/<branchName>.head
|
|
383
|
+
echo <currentHeadCommitHash>
|
|
384
|
+
|
|
385
|
+
# 写入 ~/.clawt/validate-snapshots/<project>/<branchName>.staged
|
|
386
|
+
echo <newStagedTreeHash>
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
> `stagedTreeHash` 记录了 validate 结束时暂存区的完整状态。下次 validate 如果检测到无新变更,可直接通过此值恢复暂存区,避免重复执行 read-tree 或 apply --cached 流程。
|
|
390
|
+
|
|
391
|
+
##### 步骤 8:输出成功提示
|
|
354
392
|
|
|
355
393
|
```
|
|
356
394
|
# 增量模式成功
|
|
357
395
|
✓ 已将分支 feature-scheme-1 的最新变更应用到主 worktree(增量模式)
|
|
358
396
|
暂存区 = 上次快照,工作目录 = 最新变更
|
|
359
397
|
|
|
398
|
+
# 增量无变更
|
|
399
|
+
分支 feature-scheme-1 自上次 validate 以来没有新的变更,已恢复到上次验证状态
|
|
400
|
+
|
|
360
401
|
# 增量降级为全量
|
|
361
402
|
✓ 已切换到验证分支 clawt-validate-feature-scheme-1 并应用分支 feature-scheme-1 的变更
|
|
362
403
|
可以开始验证了
|
|
363
404
|
```
|
|
364
405
|
|
|
365
|
-
##### 步骤
|
|
406
|
+
##### 步骤 9:执行 `--run` 命令(可选)
|
|
366
407
|
|
|
367
408
|
与首次 validate 的步骤 7 相同,增量 validate 成功后也会执行 `-r, --run` 指定的命令(或从项目配置 `validateRunCommand` 读取的默认命令)。
|
|
368
409
|
|