clawt 3.2.1 → 3.4.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/README.md +16 -0
- package/dist/index.js +131 -14
- package/dist/postinstall.js +31 -1
- package/docs/cover-validate.md +93 -0
- package/docs/home.md +54 -0
- package/docs/spec.md +8 -2
- package/docs/sync.md +15 -1
- package/package.json +1 -1
- package/src/commands/cover-validate.ts +151 -0
- package/src/commands/home.ts +43 -0
- package/src/commands/sync.ts +1 -11
- package/src/constants/messages/cover-validate.ts +21 -0
- package/src/constants/messages/home.ts +7 -0
- package/src/constants/messages/index.ts +4 -0
- package/src/index.ts +4 -0
- package/src/utils/validate-snapshot.ts +15 -11
- package/tests/unit/commands/cover-validate.test.ts +204 -0
- package/tests/unit/commands/sync.test.ts +0 -44
- package/tests/unit/utils/validate-snapshot.test.ts +18 -2
package/README.md
CHANGED
|
@@ -148,6 +148,14 @@ clawt validate -b <branch> -r "pnpm test & pnpm build" # 并行执行多个命
|
|
|
148
148
|
| `-r "npm lint && npm test"` | `&&` 不拆分,同步执行 |
|
|
149
149
|
| `-r "pnpm test & pnpm build"` | 并行执行,等全部完成后汇总结果 |
|
|
150
150
|
|
|
151
|
+
### `clawt cover` — 将验证分支修改覆盖回目标 worktree
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
clawt cover # 在验证分支上执行,自动推导目标分支
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
在 validate 验证过程中,如果在主 worktree(验证分支上)修改了代码,可通过 `cover` 将修改覆盖回目标 worktree。当工作区无修改时会提示确认,避免误操作。
|
|
158
|
+
|
|
151
159
|
### `clawt sync` — 同步主分支代码到目标 worktree
|
|
152
160
|
|
|
153
161
|
```bash
|
|
@@ -203,6 +211,14 @@ clawt status -i # 交互式面板模式(实时刷新,支持键盘导
|
|
|
203
211
|
clawt reset
|
|
204
212
|
```
|
|
205
213
|
|
|
214
|
+
### `clawt home` — 切换回主工作分支
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
clawt home
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
如果当前已在主工作分支上,会提示无需切换。
|
|
221
|
+
|
|
206
222
|
### `clawt projects` — 跨项目 worktree 概览
|
|
207
223
|
|
|
208
224
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -473,6 +473,34 @@ var INIT_MESSAGES = {
|
|
|
473
473
|
INIT_SET_SUCCESS: (key, value) => `\u2713 \u9879\u76EE\u914D\u7F6E ${key} \u5DF2\u8BBE\u7F6E\u4E3A ${value}`
|
|
474
474
|
};
|
|
475
475
|
|
|
476
|
+
// src/constants/messages/cover-validate.ts
|
|
477
|
+
var COVER_VALIDATE_MESSAGES = {
|
|
478
|
+
/** 当前不在验证分支上 */
|
|
479
|
+
COVER_VALIDATE_NOT_ON_VALIDATE_BRANCH: "\u5F53\u524D\u5206\u652F\u4E0D\u662F\u9A8C\u8BC1\u5206\u652F\uFF08\u9700\u4EE5 clawt-validate- \u5F00\u5934\uFF09\n \u8BF7\u5148\u901A\u8FC7 clawt validate \u5207\u6362\u5230\u9A8C\u8BC1\u5206\u652F",
|
|
480
|
+
/** 无增量修改 */
|
|
481
|
+
COVER_VALIDATE_NO_CHANGES: "\u9A8C\u8BC1\u5206\u652F\u4E0A\u6CA1\u6709\u76F8\u5BF9\u4E8E\u5FEB\u7167\u7684\u589E\u91CF\u4FEE\u6539\uFF0C\u65E0\u9700\u8986\u76D6",
|
|
482
|
+
/** 目标 worktree 不存在 */
|
|
483
|
+
COVER_VALIDATE_TARGET_NOT_FOUND: (branch) => `\u672A\u627E\u5230\u5206\u652F ${branch} \u5BF9\u5E94\u7684 worktree\uFF0C\u8BF7\u786E\u8BA4\u8BE5 worktree \u5C1A\u672A\u88AB\u79FB\u9664`,
|
|
484
|
+
/** 无快照,提示先执行 validate */
|
|
485
|
+
COVER_VALIDATE_NO_SNAPSHOT: (branch) => `\u672A\u627E\u5230\u5206\u652F ${branch} \u7684 validate \u5FEB\u7167
|
|
486
|
+
\u8BF7\u5148\u6267\u884C clawt validate -b ${branch} \u521B\u5EFA\u5FEB\u7167`,
|
|
487
|
+
/** patch 应用失败 */
|
|
488
|
+
COVER_VALIDATE_APPLY_FAILED: (branch) => `\u8986\u76D6\u53D8\u66F4\u5230 worktree ${branch} \u5931\u8D25\uFF1Apatch \u5E94\u7528\u51FA\u9519
|
|
489
|
+
\u8BF7\u68C0\u67E5\u76EE\u6807 worktree \u5DE5\u4F5C\u533A\u72B6\u6001\u540E\u91CD\u8BD5`,
|
|
490
|
+
/** 工作区和暂存区无修改,可能为误操作 */
|
|
491
|
+
COVER_VALIDATE_WORKING_DIR_CLEAN: "\u5F53\u524D\u9A8C\u8BC1\u5206\u652F\u7684\u5DE5\u4F5C\u533A\u548C\u6682\u5B58\u533A\u6CA1\u6709\u4EFB\u4F55\u4FEE\u6539\uFF0C\u53EF\u80FD\u4E3A\u8BEF\u64CD\u4F5C",
|
|
492
|
+
/** 覆盖成功 */
|
|
493
|
+
COVER_VALIDATE_SUCCESS: (branch) => `\u2713 \u5DF2\u5C06\u9A8C\u8BC1\u5206\u652F\u4E0A\u7684\u4FEE\u6539\u8986\u76D6\u5230 worktree => ${branch}`
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
// src/constants/messages/home.ts
|
|
497
|
+
var HOME_MESSAGES = {
|
|
498
|
+
/** 已在主工作分支上 */
|
|
499
|
+
HOME_ALREADY_ON_MAIN: (branch) => `\u5DF2\u5728\u4E3B\u5DE5\u4F5C\u5206\u652F ${branch} \u4E0A\uFF0C\u65E0\u9700\u5207\u6362`,
|
|
500
|
+
/** 切换成功 */
|
|
501
|
+
HOME_SWITCH_SUCCESS: (from, to) => `\u2713 \u5DF2\u4ECE ${from} \u5207\u6362\u5230\u4E3B\u5DE5\u4F5C\u5206\u652F ${to}`
|
|
502
|
+
};
|
|
503
|
+
|
|
476
504
|
// src/constants/messages/interactive-panel.ts
|
|
477
505
|
import chalk from "chalk";
|
|
478
506
|
|
|
@@ -545,7 +573,9 @@ var MESSAGES = {
|
|
|
545
573
|
...ALIAS_MESSAGES,
|
|
546
574
|
...PROJECTS_MESSAGES,
|
|
547
575
|
...COMPLETION_MESSAGES,
|
|
548
|
-
...INIT_MESSAGES
|
|
576
|
+
...INIT_MESSAGES,
|
|
577
|
+
...COVER_VALIDATE_MESSAGES,
|
|
578
|
+
...HOME_MESSAGES
|
|
549
579
|
};
|
|
550
580
|
|
|
551
581
|
// src/constants/exitCodes.ts
|
|
@@ -1700,16 +1730,19 @@ function readSnapshot(projectName, branchName) {
|
|
|
1700
1730
|
const stagedTreeHash = existsSync8(stagedPath) ? readFileSync3(stagedPath, "utf-8").trim() : "";
|
|
1701
1731
|
return { treeHash, headCommitHash, stagedTreeHash };
|
|
1702
1732
|
}
|
|
1703
|
-
function writeSnapshot(projectName, branchName, treeHash, headCommitHash, stagedTreeHash
|
|
1704
|
-
const snapshotPath = getSnapshotPath(projectName, branchName);
|
|
1705
|
-
const headPath = getSnapshotHeadPath(projectName, branchName);
|
|
1706
|
-
const stagedPath = getSnapshotStagedPath(projectName, branchName);
|
|
1733
|
+
function writeSnapshot(projectName, branchName, treeHash, headCommitHash, stagedTreeHash) {
|
|
1707
1734
|
const snapshotDir = join6(VALIDATE_SNAPSHOTS_DIR, projectName);
|
|
1708
1735
|
ensureDir(snapshotDir);
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1736
|
+
if (treeHash !== void 0) {
|
|
1737
|
+
writeFileSync3(getSnapshotPath(projectName, branchName), treeHash, "utf-8");
|
|
1738
|
+
}
|
|
1739
|
+
if (headCommitHash !== void 0) {
|
|
1740
|
+
writeFileSync3(getSnapshotHeadPath(projectName, branchName), headCommitHash, "utf-8");
|
|
1741
|
+
}
|
|
1742
|
+
if (stagedTreeHash !== void 0) {
|
|
1743
|
+
writeFileSync3(getSnapshotStagedPath(projectName, branchName), stagedTreeHash, "utf-8");
|
|
1744
|
+
}
|
|
1745
|
+
logger.info(`\u5DF2\u5199\u5165 validate \u5FEB\u7167 (project=${projectName}, branch=${branchName})`);
|
|
1713
1746
|
}
|
|
1714
1747
|
function removeSnapshot(projectName, branchName) {
|
|
1715
1748
|
const snapshotPath = getSnapshotPath(projectName, branchName);
|
|
@@ -4065,11 +4098,6 @@ async function executeSyncForBranch(targetWorktreePath, branch) {
|
|
|
4065
4098
|
printWarning(MESSAGES.SYNC_CONFLICT(targetWorktreePath));
|
|
4066
4099
|
return { success: false, hasConflict: true };
|
|
4067
4100
|
}
|
|
4068
|
-
const projectName = getProjectName();
|
|
4069
|
-
if (hasSnapshot(projectName, branch)) {
|
|
4070
|
-
removeSnapshot(projectName, branch);
|
|
4071
|
-
logger.info(`\u5DF2\u6E05\u9664\u5206\u652F ${branch} \u7684 validate \u5FEB\u7167`);
|
|
4072
|
-
}
|
|
4073
4101
|
printSuccess(MESSAGES.SYNC_SUCCESS(branch, mainBranch));
|
|
4074
4102
|
await rebuildValidateBranch(branch, mainWorktreePath);
|
|
4075
4103
|
const validateBranchName = getValidateBranchName(branch);
|
|
@@ -4230,6 +4258,74 @@ async function handleValidate(options) {
|
|
|
4230
4258
|
}
|
|
4231
4259
|
}
|
|
4232
4260
|
|
|
4261
|
+
// src/commands/cover-validate.ts
|
|
4262
|
+
function registerCoverValidateCommand(program2) {
|
|
4263
|
+
program2.command("cover").description("\u5C06\u9A8C\u8BC1\u5206\u652F\u4E0A\u7684\u4FEE\u6539\u8986\u76D6\u56DE\u76EE\u6807 worktree\uFF08\u81EA\u52A8\u63A8\u5BFC\u76EE\u6807\u5206\u652F\uFF09").action(async () => {
|
|
4264
|
+
await handleCoverValidate();
|
|
4265
|
+
});
|
|
4266
|
+
}
|
|
4267
|
+
function extractTargetBranchName(currentBranch) {
|
|
4268
|
+
return currentBranch.slice(VALIDATE_BRANCH_PREFIX.length);
|
|
4269
|
+
}
|
|
4270
|
+
function findTargetWorktreePath(branchName) {
|
|
4271
|
+
const worktrees = getProjectWorktrees();
|
|
4272
|
+
const match = findExactMatch(worktrees, branchName);
|
|
4273
|
+
if (!match) {
|
|
4274
|
+
throw new ClawtError(MESSAGES.COVER_VALIDATE_TARGET_NOT_FOUND(branchName));
|
|
4275
|
+
}
|
|
4276
|
+
return match.path;
|
|
4277
|
+
}
|
|
4278
|
+
function computeIncrementalPatch(snapshotTreeHash, mainWorktreePath) {
|
|
4279
|
+
const savedIndexTreeHash = gitWriteTree(mainWorktreePath);
|
|
4280
|
+
let currentTreeHash;
|
|
4281
|
+
try {
|
|
4282
|
+
gitAddAll(mainWorktreePath);
|
|
4283
|
+
currentTreeHash = gitWriteTree(mainWorktreePath);
|
|
4284
|
+
} finally {
|
|
4285
|
+
gitReadTree(savedIndexTreeHash, mainWorktreePath);
|
|
4286
|
+
}
|
|
4287
|
+
if (snapshotTreeHash === currentTreeHash) {
|
|
4288
|
+
return null;
|
|
4289
|
+
}
|
|
4290
|
+
const patch = gitDiffTree(snapshotTreeHash, currentTreeHash, mainWorktreePath);
|
|
4291
|
+
return { patch, currentTreeHash };
|
|
4292
|
+
}
|
|
4293
|
+
async function handleCoverValidate() {
|
|
4294
|
+
validateMainWorktree();
|
|
4295
|
+
requireProjectConfig();
|
|
4296
|
+
const projectName = getProjectName();
|
|
4297
|
+
const mainWorktreePath = getGitTopLevel();
|
|
4298
|
+
const currentBranch = getCurrentBranch(mainWorktreePath);
|
|
4299
|
+
if (!currentBranch.startsWith(VALIDATE_BRANCH_PREFIX)) {
|
|
4300
|
+
throw new ClawtError(MESSAGES.COVER_VALIDATE_NOT_ON_VALIDATE_BRANCH);
|
|
4301
|
+
}
|
|
4302
|
+
const targetBranchName = extractTargetBranchName(currentBranch);
|
|
4303
|
+
logger.info(`cover-validate \u547D\u4EE4\u6267\u884C\uFF0C\u76EE\u6807\u5206\u652F: ${targetBranchName}`);
|
|
4304
|
+
const targetWorktreePath = findTargetWorktreePath(targetBranchName);
|
|
4305
|
+
if (!hasSnapshot(projectName, targetBranchName)) {
|
|
4306
|
+
throw new ClawtError(MESSAGES.COVER_VALIDATE_NO_SNAPSHOT(targetBranchName));
|
|
4307
|
+
}
|
|
4308
|
+
const { treeHash: snapshotTreeHash } = readSnapshot(projectName, targetBranchName);
|
|
4309
|
+
if (isWorkingDirClean(mainWorktreePath)) {
|
|
4310
|
+
printInfo(MESSAGES.COVER_VALIDATE_WORKING_DIR_CLEAN);
|
|
4311
|
+
const confirmed = await confirmAction("\u662F\u5426\u7EE7\u7EED\u6267\u884C\u8986\u76D6\uFF1F");
|
|
4312
|
+
if (!confirmed) return;
|
|
4313
|
+
}
|
|
4314
|
+
const result = computeIncrementalPatch(snapshotTreeHash, mainWorktreePath);
|
|
4315
|
+
if (!result) {
|
|
4316
|
+
printInfo(MESSAGES.COVER_VALIDATE_NO_CHANGES);
|
|
4317
|
+
return;
|
|
4318
|
+
}
|
|
4319
|
+
try {
|
|
4320
|
+
gitApplyFromStdin(result.patch, targetWorktreePath);
|
|
4321
|
+
} catch (error) {
|
|
4322
|
+
logger.error(`cover-validate patch apply \u5931\u8D25: ${error}`);
|
|
4323
|
+
throw new ClawtError(MESSAGES.COVER_VALIDATE_APPLY_FAILED(targetBranchName));
|
|
4324
|
+
}
|
|
4325
|
+
writeSnapshot(projectName, targetBranchName, result.currentTreeHash);
|
|
4326
|
+
printSuccess(MESSAGES.COVER_VALIDATE_SUCCESS(targetBranchName));
|
|
4327
|
+
}
|
|
4328
|
+
|
|
4233
4329
|
// src/commands/merge.ts
|
|
4234
4330
|
function registerMergeCommand(program2) {
|
|
4235
4331
|
program2.command("merge").description("\u5408\u5E76\u67D0\u4E2A\u5DF2\u9A8C\u8BC1\u7684 worktree \u5206\u652F\u5230\u4E3B worktree").option("-b, --branch <branchName>", "\u8981\u5408\u5E76\u7684\u5206\u652F\u540D\uFF08\u652F\u6301\u6A21\u7CCA\u5339\u914D\uFF0C\u4E0D\u4F20\u5219\u5217\u51FA\u6240\u6709\u5206\u652F\u4F9B\u9009\u62E9\uFF09").option("-m, --message <commitMessage>", "\u63D0\u4EA4\u4FE1\u606F\uFF08\u76EE\u6807 worktree \u5DE5\u4F5C\u533A\u6709\u4FEE\u6539\u65F6\u5FC5\u586B\uFF09").action(async (options) => {
|
|
@@ -5158,6 +5254,25 @@ async function handleInit(options) {
|
|
|
5158
5254
|
}
|
|
5159
5255
|
}
|
|
5160
5256
|
|
|
5257
|
+
// src/commands/home.ts
|
|
5258
|
+
function registerHomeCommand(program2) {
|
|
5259
|
+
program2.command("home").description("\u5207\u6362\u56DE\u4E3B\u5DE5\u4F5C\u5206\u652F").action(async () => {
|
|
5260
|
+
await handleHome();
|
|
5261
|
+
});
|
|
5262
|
+
}
|
|
5263
|
+
async function handleHome() {
|
|
5264
|
+
validateMainWorktree();
|
|
5265
|
+
requireProjectConfig();
|
|
5266
|
+
const mainBranch = getMainWorkBranch();
|
|
5267
|
+
const currentBranch = getCurrentBranch();
|
|
5268
|
+
if (currentBranch === mainBranch) {
|
|
5269
|
+
printInfo(MESSAGES.HOME_ALREADY_ON_MAIN(mainBranch));
|
|
5270
|
+
return;
|
|
5271
|
+
}
|
|
5272
|
+
await ensureOnMainWorkBranch();
|
|
5273
|
+
printSuccess(MESSAGES.HOME_SWITCH_SUCCESS(currentBranch, mainBranch));
|
|
5274
|
+
}
|
|
5275
|
+
|
|
5161
5276
|
// src/index.ts
|
|
5162
5277
|
var require2 = createRequire(import.meta.url);
|
|
5163
5278
|
var { version } = require2("../package.json");
|
|
@@ -5175,6 +5290,7 @@ registerRemoveCommand(program);
|
|
|
5175
5290
|
registerRunCommand(program);
|
|
5176
5291
|
registerResumeCommand(program);
|
|
5177
5292
|
registerValidateCommand(program);
|
|
5293
|
+
registerCoverValidateCommand(program);
|
|
5178
5294
|
registerMergeCommand(program);
|
|
5179
5295
|
registerConfigCommand(program);
|
|
5180
5296
|
registerSyncCommand(program);
|
|
@@ -5184,6 +5300,7 @@ registerAliasCommand(program);
|
|
|
5184
5300
|
registerProjectsCommand(program);
|
|
5185
5301
|
registerCompletionCommand(program);
|
|
5186
5302
|
registerInitCommand(program);
|
|
5303
|
+
registerHomeCommand(program);
|
|
5187
5304
|
var config = loadConfig();
|
|
5188
5305
|
applyAliases(program, config.aliases);
|
|
5189
5306
|
process.on("uncaughtException", (error) => {
|
package/dist/postinstall.js
CHANGED
|
@@ -450,6 +450,34 @@ var INIT_MESSAGES = {
|
|
|
450
450
|
INIT_SET_SUCCESS: (key, value) => `\u2713 \u9879\u76EE\u914D\u7F6E ${key} \u5DF2\u8BBE\u7F6E\u4E3A ${value}`
|
|
451
451
|
};
|
|
452
452
|
|
|
453
|
+
// src/constants/messages/cover-validate.ts
|
|
454
|
+
var COVER_VALIDATE_MESSAGES = {
|
|
455
|
+
/** 当前不在验证分支上 */
|
|
456
|
+
COVER_VALIDATE_NOT_ON_VALIDATE_BRANCH: "\u5F53\u524D\u5206\u652F\u4E0D\u662F\u9A8C\u8BC1\u5206\u652F\uFF08\u9700\u4EE5 clawt-validate- \u5F00\u5934\uFF09\n \u8BF7\u5148\u901A\u8FC7 clawt validate \u5207\u6362\u5230\u9A8C\u8BC1\u5206\u652F",
|
|
457
|
+
/** 无增量修改 */
|
|
458
|
+
COVER_VALIDATE_NO_CHANGES: "\u9A8C\u8BC1\u5206\u652F\u4E0A\u6CA1\u6709\u76F8\u5BF9\u4E8E\u5FEB\u7167\u7684\u589E\u91CF\u4FEE\u6539\uFF0C\u65E0\u9700\u8986\u76D6",
|
|
459
|
+
/** 目标 worktree 不存在 */
|
|
460
|
+
COVER_VALIDATE_TARGET_NOT_FOUND: (branch) => `\u672A\u627E\u5230\u5206\u652F ${branch} \u5BF9\u5E94\u7684 worktree\uFF0C\u8BF7\u786E\u8BA4\u8BE5 worktree \u5C1A\u672A\u88AB\u79FB\u9664`,
|
|
461
|
+
/** 无快照,提示先执行 validate */
|
|
462
|
+
COVER_VALIDATE_NO_SNAPSHOT: (branch) => `\u672A\u627E\u5230\u5206\u652F ${branch} \u7684 validate \u5FEB\u7167
|
|
463
|
+
\u8BF7\u5148\u6267\u884C clawt validate -b ${branch} \u521B\u5EFA\u5FEB\u7167`,
|
|
464
|
+
/** patch 应用失败 */
|
|
465
|
+
COVER_VALIDATE_APPLY_FAILED: (branch) => `\u8986\u76D6\u53D8\u66F4\u5230 worktree ${branch} \u5931\u8D25\uFF1Apatch \u5E94\u7528\u51FA\u9519
|
|
466
|
+
\u8BF7\u68C0\u67E5\u76EE\u6807 worktree \u5DE5\u4F5C\u533A\u72B6\u6001\u540E\u91CD\u8BD5`,
|
|
467
|
+
/** 工作区和暂存区无修改,可能为误操作 */
|
|
468
|
+
COVER_VALIDATE_WORKING_DIR_CLEAN: "\u5F53\u524D\u9A8C\u8BC1\u5206\u652F\u7684\u5DE5\u4F5C\u533A\u548C\u6682\u5B58\u533A\u6CA1\u6709\u4EFB\u4F55\u4FEE\u6539\uFF0C\u53EF\u80FD\u4E3A\u8BEF\u64CD\u4F5C",
|
|
469
|
+
/** 覆盖成功 */
|
|
470
|
+
COVER_VALIDATE_SUCCESS: (branch) => `\u2713 \u5DF2\u5C06\u9A8C\u8BC1\u5206\u652F\u4E0A\u7684\u4FEE\u6539\u8986\u76D6\u5230 worktree => ${branch}`
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
// src/constants/messages/home.ts
|
|
474
|
+
var HOME_MESSAGES = {
|
|
475
|
+
/** 已在主工作分支上 */
|
|
476
|
+
HOME_ALREADY_ON_MAIN: (branch) => `\u5DF2\u5728\u4E3B\u5DE5\u4F5C\u5206\u652F ${branch} \u4E0A\uFF0C\u65E0\u9700\u5207\u6362`,
|
|
477
|
+
/** 切换成功 */
|
|
478
|
+
HOME_SWITCH_SUCCESS: (from, to) => `\u2713 \u5DF2\u4ECE ${from} \u5207\u6362\u5230\u4E3B\u5DE5\u4F5C\u5206\u652F ${to}`
|
|
479
|
+
};
|
|
480
|
+
|
|
453
481
|
// src/constants/messages/interactive-panel.ts
|
|
454
482
|
import chalk from "chalk";
|
|
455
483
|
|
|
@@ -502,7 +530,9 @@ var MESSAGES = {
|
|
|
502
530
|
...ALIAS_MESSAGES,
|
|
503
531
|
...PROJECTS_MESSAGES,
|
|
504
532
|
...COMPLETION_MESSAGES,
|
|
505
|
-
...INIT_MESSAGES
|
|
533
|
+
...INIT_MESSAGES,
|
|
534
|
+
...COVER_VALIDATE_MESSAGES,
|
|
535
|
+
...HOME_MESSAGES
|
|
506
536
|
};
|
|
507
537
|
|
|
508
538
|
// src/constants/terminal.ts
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
### 5.21 将验证分支修改覆盖回目标 Worktree
|
|
2
|
+
|
|
3
|
+
**命令:**
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
clawt cover
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
> 无需指定分支名,自动从当前验证分支名(`clawt-validate-<branchName>`)中推导目标分支。
|
|
10
|
+
|
|
11
|
+
**参数:**
|
|
12
|
+
|
|
13
|
+
无额外参数。必须在验证分支上执行。
|
|
14
|
+
|
|
15
|
+
**使用场景:**
|
|
16
|
+
|
|
17
|
+
在 `validate` 验证过程中,用户可能会在主 worktree(验证分支上)对代码进行修改(如修复测试失败、调整逻辑等)。`cover` 命令用于将这些修改覆盖回目标 worktree,使目标 worktree 的代码与验证分支上的最新状态同步。
|
|
18
|
+
|
|
19
|
+
**运行流程:**
|
|
20
|
+
|
|
21
|
+
##### 步骤 1:前置校验
|
|
22
|
+
|
|
23
|
+
1. **主 worktree 校验**(2.1)
|
|
24
|
+
2. **项目级配置校验**(`requireProjectConfig`)
|
|
25
|
+
3. **验证分支校验**:当前分支必须以 `clawt-validate-` 开头,否则报错退出
|
|
26
|
+
|
|
27
|
+
##### 步骤 2:查找目标 worktree
|
|
28
|
+
|
|
29
|
+
从验证分支名中提取目标分支名(去掉 `clawt-validate-` 前缀),然后在项目的 worktree 列表中精确匹配目标分支对应的 worktree。如果目标 worktree 不存在(可能已被移除),报错退出。
|
|
30
|
+
|
|
31
|
+
##### 步骤 3:校验快照存在并读取
|
|
32
|
+
|
|
33
|
+
校验目标分支的 validate 快照是否存在。如果快照不存在,提示用户先执行 `clawt validate -b <branch>` 创建快照。读取快照中的 tree hash(`snapshotTreeHash`),作为增量计算的基准。
|
|
34
|
+
|
|
35
|
+
##### 步骤 3.5:工作区干净检查
|
|
36
|
+
|
|
37
|
+
检测主 worktree(验证分支上)的工作区和暂存区是否干净(`isWorkingDirClean`):
|
|
38
|
+
|
|
39
|
+
- **不干净**(有修改)→ 正常继续,这是 cover 的典型使用场景
|
|
40
|
+
- **干净**(无修改)→ 输出提示信息 `当前验证分支的工作区和暂存区没有任何修改,可能为误操作`,并通过 `confirmAction` 询问用户 `是否继续执行覆盖?`:
|
|
41
|
+
- 用户确认 → 继续执行
|
|
42
|
+
- 用户取消 → 直接返回,不执行后续步骤
|
|
43
|
+
|
|
44
|
+
> 工作区干净时通常意味着用户没有在验证分支上做任何修改就执行了 cover,这大概率是误操作。增加确认提示可以避免不必要的覆盖操作。
|
|
45
|
+
|
|
46
|
+
##### 步骤 4:计算增量 patch
|
|
47
|
+
|
|
48
|
+
通过 `computeIncrementalPatch()` 计算验证分支上相对于快照的增量变更:
|
|
49
|
+
|
|
50
|
+
1. 保存当前暂存区的 tree hash(`savedIndexTreeHash`),用于后续恢复
|
|
51
|
+
2. `git add .` + `git write-tree` 获取当前工作区的完整 tree hash(`currentTreeHash`)
|
|
52
|
+
3. 通过 `git read-tree` 恢复原始暂存区状态(无论成功失败都执行,在 `finally` 块中)
|
|
53
|
+
4. 比较 `snapshotTreeHash` 与 `currentTreeHash`:
|
|
54
|
+
- **相同** → 无增量变更,输出提示后返回
|
|
55
|
+
- **不同** → 通过 `git diff-tree` 生成 patch
|
|
56
|
+
|
|
57
|
+
##### 步骤 5:应用 patch 到目标 worktree
|
|
58
|
+
|
|
59
|
+
将增量 patch 通过 `git apply --binary` 应用到目标 worktree 的工作区。如果 patch apply 失败,报错退出并提示用户检查目标 worktree 工作区状态。
|
|
60
|
+
|
|
61
|
+
##### 步骤 6:更新快照
|
|
62
|
+
|
|
63
|
+
将 `currentTreeHash` 写入快照的 `.tree` 文件,使后续再次 cover 时的基准正确。**只更新 `treeHash`,不更新 `headCommitHash` 和 `stagedTreeHash`**(保留 validate 时写入的原值)。
|
|
64
|
+
|
|
65
|
+
##### 步骤 7:输出成功提示
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
✓ 已将验证分支上的修改覆盖到 worktree => <branchName>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
**错误消息:**
|
|
72
|
+
|
|
73
|
+
| 消息常量 | 触发条件 | 提示内容 |
|
|
74
|
+
| -------- | -------- | -------- |
|
|
75
|
+
| `COVER_VALIDATE_NOT_ON_VALIDATE_BRANCH` | 当前分支不是验证分支 | 提示先通过 `clawt validate` 切换到验证分支 |
|
|
76
|
+
| `COVER_VALIDATE_TARGET_NOT_FOUND` | 目标 worktree 不存在 | 提示确认该 worktree 尚未被移除 |
|
|
77
|
+
| `COVER_VALIDATE_NO_SNAPSHOT` | 无快照 | 提示先执行 `clawt validate -b <branch>` 创建快照 |
|
|
78
|
+
| `COVER_VALIDATE_NO_CHANGES` | 无增量变更 | 提示无需覆盖 |
|
|
79
|
+
| `COVER_VALIDATE_WORKING_DIR_CLEAN` | 工作区干净 | 提示可能为误操作,需确认是否继续 |
|
|
80
|
+
| `COVER_VALIDATE_APPLY_FAILED` | patch 应用失败 | 提示检查目标 worktree 工作区状态后重试 |
|
|
81
|
+
|
|
82
|
+
**实现要点:**
|
|
83
|
+
|
|
84
|
+
- 命令注册名为 `cover`(非 `cover-validate`),用户通过 `clawt cover` 调用
|
|
85
|
+
- 核心函数:`handleCoverValidate()`(`src/commands/cover-validate.ts`)
|
|
86
|
+
- 辅助函数:
|
|
87
|
+
- `extractTargetBranchName()`:从验证分支名提取目标分支名
|
|
88
|
+
- `findTargetWorktreePath()`:查找目标 worktree 路径
|
|
89
|
+
- `computeIncrementalPatch()`:计算增量 patch
|
|
90
|
+
- 消息常量:`COVER_VALIDATE_MESSAGES`(`src/constants/messages/cover-validate.ts`)
|
|
91
|
+
- `writeSnapshot` 调用时只传 `treeHash`,利用其可选参数特性保留磁盘上的 `headCommitHash` 和 `stagedTreeHash` 原值
|
|
92
|
+
|
|
93
|
+
---
|
package/docs/home.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
### 5.20 切换回主工作分支
|
|
2
|
+
|
|
3
|
+
**命令:**
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
clawt home
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
**参数:**
|
|
10
|
+
|
|
11
|
+
无参数。
|
|
12
|
+
|
|
13
|
+
**功能说明:**
|
|
14
|
+
|
|
15
|
+
快速切换回项目的主工作分支。当用户在主 worktree 中处于验证分支或其他分支时,可通过 `clawt home` 一键切换回 `clawtMainWorkBranch` 所记录的主工作分支。
|
|
16
|
+
|
|
17
|
+
**运行流程:**
|
|
18
|
+
|
|
19
|
+
1. **主 worktree 校验** (2.1)
|
|
20
|
+
2. **项目级配置校验**:调用 `requireProjectConfig()` 检查项目是否已初始化
|
|
21
|
+
3. **获取分支信息**:
|
|
22
|
+
- 通过 `getMainWorkBranch()` 获取主工作分支名
|
|
23
|
+
- 通过 `getCurrentBranch()` 获取当前所在分支名
|
|
24
|
+
4. **判断是否需要切换**:
|
|
25
|
+
- 当前分支 === 主工作分支 → 输出提示信息,无需切换
|
|
26
|
+
- 当前分支 !== 主工作分支 → 调用 `ensureOnMainWorkBranch()` 执行切换
|
|
27
|
+
5. **输出结果**
|
|
28
|
+
|
|
29
|
+
**输出格式:**
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
# 已在主工作分支上
|
|
33
|
+
ℹ 已在主工作分支 main 上,无需切换
|
|
34
|
+
|
|
35
|
+
# 切换成功
|
|
36
|
+
✓ 已从 clawt-validate-feat-login 切换到主工作分支 main
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
**消息常量:**
|
|
40
|
+
|
|
41
|
+
定义在 `src/constants/messages/home.ts`:
|
|
42
|
+
|
|
43
|
+
| 常量 | 说明 |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| `HOME_ALREADY_ON_MAIN(branch)` | 已在主工作分支上,无需切换 |
|
|
46
|
+
| `HOME_SWITCH_SUCCESS(from, to)` | 切换成功提示,显示原分支和目标分支 |
|
|
47
|
+
|
|
48
|
+
**实现要点:**
|
|
49
|
+
|
|
50
|
+
- 命令注册函数 `registerHomeCommand` 位于 `src/commands/home.ts`
|
|
51
|
+
- 依赖 `validateMainWorktree`、`requireProjectConfig`、`ensureOnMainWorkBranch`、`getCurrentBranch`、`getMainWorkBranch` 等工具函数
|
|
52
|
+
- `requireProjectConfig()` 为显式调用,因为 home 命令不经过 `ensureOnMainWorkBranch()` 的隐式校验路径(仅在需要切换时才调用 `ensureOnMainWorkBranch()`)
|
|
53
|
+
|
|
54
|
+
---
|
package/docs/spec.md
CHANGED
|
@@ -34,6 +34,8 @@
|
|
|
34
34
|
- [5.17 自动更新检查](./update-check.md)
|
|
35
35
|
- [5.18 跨项目 Worktree 概览](./projects.md)
|
|
36
36
|
- [5.19 初始化项目级配置](./init.md)
|
|
37
|
+
- [5.20 切换回主工作分支](./home.md)
|
|
38
|
+
- [5.21 将验证分支修改覆盖回目标 Worktree](./cover-validate.md)
|
|
37
39
|
- [6. 验证架构规则](#6-验证架构规则)
|
|
38
40
|
- [7. 错误处理规范](#7-错误处理规范)
|
|
39
41
|
- [8. 非功能性需求](#8-非功能性需求)
|
|
@@ -292,6 +294,8 @@ async function interactiveConfigEditor<T extends object>(
|
|
|
292
294
|
| `clawt alias` | 管理命令别名(列出 / 设置 / 移除) | 5.15 |
|
|
293
295
|
| `clawt completion` | 为终端提供 shell 自动补全功能(bash/zsh) | 5.16 |
|
|
294
296
|
| `clawt projects` | 展示所有项目的 worktree 概览,或查看指定项目的 worktree 详情 | 5.17 |
|
|
297
|
+
| `clawt home` | 快速切换回主工作分支 | 5.20 |
|
|
298
|
+
| `clawt cover` | 将验证分支上的修改覆盖回目标 worktree(自动推导目标分支) | 5.21 |
|
|
295
299
|
|
|
296
300
|
**全局选项:**
|
|
297
301
|
|
|
@@ -325,6 +329,8 @@ async function interactiveConfigEditor<T extends object>(
|
|
|
325
329
|
- [5.17 自动更新检查](./update-check.md)
|
|
326
330
|
- [5.18 跨项目 Worktree 概览](./projects.md)
|
|
327
331
|
- [5.19 初始化项目级配置](./init.md)
|
|
332
|
+
- [5.20 切换回主工作分支](./home.md)
|
|
333
|
+
- [5.21 将验证分支修改覆盖回目标 Worktree](./cover-validate.md)
|
|
328
334
|
|
|
329
335
|
---
|
|
330
336
|
|
|
@@ -333,12 +339,12 @@ async function interactiveConfigEditor<T extends object>(
|
|
|
333
339
|
以下规则适用于验证分支架构的所有实现工作:
|
|
334
340
|
|
|
335
341
|
1. **不兼容旧版本**:本次重构不考虑旧版本数据、旧版本创建的 worktree 或旧版本配置的兼容性。所有命令均假定验证分支和项目级配置已按新架构存在。用户需删除旧 worktree 后重新创建。
|
|
336
|
-
2. **项目级配置前置校验**:仅对 create、run、validate、sync、remove、merge、reset 这
|
|
342
|
+
2. **项目级配置前置校验**:仅对 create、run、validate、cover、sync、remove、merge、reset、home 这 9 个核心命令添加检测,执行时必须先检查项目级配置(`~/.clawt/projects/<projectName>/config.json`)是否存在且包含 `clawtMainWorkBranch`。如果不存在,直接报错退出并提示用户先执行 `clawt init`:
|
|
337
343
|
```
|
|
338
344
|
✗ 该项目尚未初始化,请先执行 clawt init -b<branchName>设置主工作分支
|
|
339
345
|
```
|
|
340
346
|
其他命令(list、resume、config、status、alias、projects、completion)不受影响,无需添加该校验。
|
|
341
|
-
> **实现细节**:`ensureOnMainWorkBranch()` 内部已通过 `getMainWorkBranch()` → `requireProjectConfig()` 完成了项目配置校验,因此调用了 `ensureOnMainWorkBranch` 的命令(create、run、validate、merge)**无需再显式调用 `requireProjectConfig()`**,避免重复校验。sync 和
|
|
347
|
+
> **实现细节**:`ensureOnMainWorkBranch()` 内部已通过 `getMainWorkBranch()` → `requireProjectConfig()` 完成了项目配置校验,因此调用了 `ensureOnMainWorkBranch` 的命令(create、run、validate、merge)**无需再显式调用 `requireProjectConfig()`**,避免重复校验。sync、remove 和 cover 命令因不依赖主 worktree 的分支状态而不调用 `ensureOnMainWorkBranch`,需自行显式调用 `requireProjectConfig()`。reset 和 home 命令同理,也需自行调用 `requireProjectConfig()`。
|
|
342
348
|
3. **主分支名统一从项目级配置获取**:所有需要获取主分支名的场景(sync 中合并主分支、merge 中计算 merge-base、切回主分支等),统一使用项目级配置中的 `clawtMainWorkBranch`,不再通过 `getCurrentBranch(mainWorktreePath)` 动态获取。因为在新架构下,主 worktree 可能处于验证分支上,`getCurrentBranch` 会返回验证分支名而非真正的主工作分支名。
|
|
343
349
|
4. **测试文件全量更新**:本次重构涉及的所有命令(init、create、run、validate、sync、remove、merge、reset),其对应的测试文件必须同步更新,确保覆盖新增的验证分支逻辑、项目级配置逻辑和变更后的流程。
|
|
344
350
|
|
package/docs/sync.md
CHANGED
|
@@ -76,7 +76,21 @@ export interface SyncResult {
|
|
|
76
76
|
```
|
|
77
77
|
- 返回 `{ success: false, hasConflict: true }`
|
|
78
78
|
- **无冲突** → 继续
|
|
79
|
-
5.
|
|
79
|
+
5. **保留 validate 快照**:sync 合并成功后,不清除该分支的 validate 快照。因为 validate 使用三点 diff(`main...feature`),sync 后 merge-base 更新为合并提交,三点 diff 仍然只包含 feature 分支自身的修改,旧快照依然有效。增量 validate 时若检测到 HEAD 变化,会自动通过 diff-tree + apply 路径正确恢复暂存区状态。
|
|
80
|
+
示意图:
|
|
81
|
+
场景:将 HEAD(master) 合并到 branchName
|
|
82
|
+
|
|
83
|
+
执行 git checkout branchName && git merge master 后:
|
|
84
|
+
|
|
85
|
+
A -- B -- C (HEAD/master)
|
|
86
|
+
/ \
|
|
87
|
+
* M (branchName, merge commit)
|
|
88
|
+
\ /
|
|
89
|
+
D -- E ----
|
|
90
|
+
|
|
91
|
+
此时执行 git diff HEAD...branchName:
|
|
92
|
+
|
|
93
|
+
- merge-base 变成了 C(因为合并后,HEAD 和 branchName 的最近共同祖先就是 C)
|
|
80
94
|
6. **重建验证分支**(`rebuildValidateBranch`,async 函数):sync 将主分支合并到目标 worktree 后,目标分支的代码基点发生变化。为保持验证分支与目标分支基点一致,需要重建验证分支。
|
|
81
95
|
- 确保在主工作分支上创建验证分支,处理三种情况:
|
|
82
96
|
- **已在主工作分支上** → 直接重建
|
package/package.json
CHANGED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { logger } from '../logger/index.js';
|
|
3
|
+
import { ClawtError } from '../errors/index.js';
|
|
4
|
+
import { MESSAGES, VALIDATE_BRANCH_PREFIX } from '../constants/index.js';
|
|
5
|
+
import {
|
|
6
|
+
validateMainWorktree,
|
|
7
|
+
requireProjectConfig,
|
|
8
|
+
getProjectName,
|
|
9
|
+
getGitTopLevel,
|
|
10
|
+
getCurrentBranch,
|
|
11
|
+
getProjectWorktrees,
|
|
12
|
+
findExactMatch,
|
|
13
|
+
hasSnapshot,
|
|
14
|
+
readSnapshot,
|
|
15
|
+
writeSnapshot,
|
|
16
|
+
gitAddAll,
|
|
17
|
+
gitWriteTree,
|
|
18
|
+
gitReadTree,
|
|
19
|
+
gitDiffTree,
|
|
20
|
+
gitApplyFromStdin,
|
|
21
|
+
printSuccess,
|
|
22
|
+
printInfo,
|
|
23
|
+
isWorkingDirClean,
|
|
24
|
+
confirmAction,
|
|
25
|
+
} from '../utils/index.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 注册 cover-validate 命令:将验证分支上的修改覆盖回目标 worktree
|
|
29
|
+
* @param {Command} program - Commander 实例
|
|
30
|
+
*/
|
|
31
|
+
export function registerCoverValidateCommand(program: Command): void {
|
|
32
|
+
program
|
|
33
|
+
.command('cover')
|
|
34
|
+
.description('将验证分支上的修改覆盖回目标 worktree(自动推导目标分支)')
|
|
35
|
+
.action(async () => {
|
|
36
|
+
await handleCoverValidate();
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 从验证分支名中提取目标分支名
|
|
42
|
+
* 验证分支名格式为 clawt-validate-<targetBranch>,去掉前缀即为目标分支名
|
|
43
|
+
* @param {string} currentBranch - 当前分支名
|
|
44
|
+
* @returns {string} 目标分支名
|
|
45
|
+
*/
|
|
46
|
+
export function extractTargetBranchName(currentBranch: string): string {
|
|
47
|
+
return currentBranch.slice(VALIDATE_BRANCH_PREFIX.length);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 在项目 worktree 列表中查找目标分支对应的 worktree 路径
|
|
52
|
+
* @param {string} branchName - 目标分支名
|
|
53
|
+
* @returns {string} 目标 worktree 的绝对路径
|
|
54
|
+
* @throws {ClawtError} 目标 worktree 不存在时抛出
|
|
55
|
+
*/
|
|
56
|
+
export function findTargetWorktreePath(branchName: string): string {
|
|
57
|
+
const worktrees = getProjectWorktrees();
|
|
58
|
+
const match = findExactMatch(worktrees, branchName);
|
|
59
|
+
if (!match) {
|
|
60
|
+
throw new ClawtError(MESSAGES.COVER_VALIDATE_TARGET_NOT_FOUND(branchName));
|
|
61
|
+
}
|
|
62
|
+
return match.path;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* 计算验证分支上相对于快照的增量 patch
|
|
67
|
+
* 操作序列:git write-tree(保存暂存区)→ git add . → git write-tree(获取工作区 tree)→ git read-tree(恢复暂存区)
|
|
68
|
+
* 当 snapshotTreeHash 与当前 tree hash 相同时返回 null 表示无变更
|
|
69
|
+
* @param {string} snapshotTreeHash - 快照中记录的 tree hash
|
|
70
|
+
* @param {string} mainWorktreePath - 主 worktree 路径
|
|
71
|
+
* @returns {{ patch: Buffer; currentTreeHash: string } | null} 增量 patch 和当前 tree hash,无变更时返回 null
|
|
72
|
+
*/
|
|
73
|
+
export function computeIncrementalPatch(snapshotTreeHash: string, mainWorktreePath: string): { patch: Buffer; currentTreeHash: string } | null {
|
|
74
|
+
// 先保存当前暂存区的 tree hash,用于后续恢复
|
|
75
|
+
const savedIndexTreeHash = gitWriteTree(mainWorktreePath);
|
|
76
|
+
let currentTreeHash: string;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
gitAddAll(mainWorktreePath);
|
|
80
|
+
currentTreeHash = gitWriteTree(mainWorktreePath);
|
|
81
|
+
} finally {
|
|
82
|
+
// 无论成功或失败,都通过 git read-tree 恢复原始暂存区状态
|
|
83
|
+
gitReadTree(savedIndexTreeHash, mainWorktreePath);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 快照 tree 与当前 tree 相同,无增量变更
|
|
87
|
+
if (snapshotTreeHash === currentTreeHash) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const patch = gitDiffTree(snapshotTreeHash, currentTreeHash, mainWorktreePath);
|
|
92
|
+
return { patch, currentTreeHash };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 执行 cover-validate 命令的核心逻辑
|
|
97
|
+
* 将验证分支上的增量修改(相对于 validate 快照)覆盖到目标 worktree 工作区
|
|
98
|
+
*/
|
|
99
|
+
async function handleCoverValidate(): Promise<void> {
|
|
100
|
+
// 步骤 1:前置校验
|
|
101
|
+
validateMainWorktree();
|
|
102
|
+
requireProjectConfig();
|
|
103
|
+
const projectName = getProjectName();
|
|
104
|
+
const mainWorktreePath = getGitTopLevel();
|
|
105
|
+
const currentBranch = getCurrentBranch(mainWorktreePath);
|
|
106
|
+
|
|
107
|
+
// 校验当前分支是验证分支
|
|
108
|
+
if (!currentBranch.startsWith(VALIDATE_BRANCH_PREFIX)) {
|
|
109
|
+
throw new ClawtError(MESSAGES.COVER_VALIDATE_NOT_ON_VALIDATE_BRANCH);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 从验证分支名推导目标分支名
|
|
113
|
+
const targetBranchName = extractTargetBranchName(currentBranch);
|
|
114
|
+
logger.info(`cover-validate 命令执行,目标分支: ${targetBranchName}`);
|
|
115
|
+
|
|
116
|
+
// 步骤 2:查找目标 worktree
|
|
117
|
+
const targetWorktreePath = findTargetWorktreePath(targetBranchName);
|
|
118
|
+
|
|
119
|
+
// 步骤 3:校验快照存在并读取
|
|
120
|
+
if (!hasSnapshot(projectName, targetBranchName)) {
|
|
121
|
+
throw new ClawtError(MESSAGES.COVER_VALIDATE_NO_SNAPSHOT(targetBranchName));
|
|
122
|
+
}
|
|
123
|
+
const { treeHash: snapshotTreeHash } = readSnapshot(projectName, targetBranchName);
|
|
124
|
+
|
|
125
|
+
// 步骤 3.5:工作区干净时提示确认,避免误操作
|
|
126
|
+
if (isWorkingDirClean(mainWorktreePath)) {
|
|
127
|
+
printInfo(MESSAGES.COVER_VALIDATE_WORKING_DIR_CLEAN);
|
|
128
|
+
const confirmed = await confirmAction('是否继续执行覆盖?');
|
|
129
|
+
if (!confirmed) return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 步骤 4:计算增量 patch
|
|
133
|
+
const result = computeIncrementalPatch(snapshotTreeHash, mainWorktreePath);
|
|
134
|
+
if (!result) {
|
|
135
|
+
printInfo(MESSAGES.COVER_VALIDATE_NO_CHANGES);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 步骤 5:应用 patch 到目标 worktree
|
|
140
|
+
try {
|
|
141
|
+
gitApplyFromStdin(result.patch, targetWorktreePath);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
logger.error(`cover-validate patch apply 失败: ${error}`);
|
|
144
|
+
throw new ClawtError(MESSAGES.COVER_VALIDATE_APPLY_FAILED(targetBranchName));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 步骤 6:更新快照 treeHash(使后续再次 cover 的基准正确),HEAD 和 stagedTreeHash 不变
|
|
148
|
+
writeSnapshot(projectName, targetBranchName, result.currentTreeHash);
|
|
149
|
+
|
|
150
|
+
printSuccess(MESSAGES.COVER_VALIDATE_SUCCESS(targetBranchName));
|
|
151
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import { MESSAGES } from '../constants/index.js';
|
|
3
|
+
import {
|
|
4
|
+
validateMainWorktree,
|
|
5
|
+
requireProjectConfig,
|
|
6
|
+
ensureOnMainWorkBranch,
|
|
7
|
+
getCurrentBranch,
|
|
8
|
+
getMainWorkBranch,
|
|
9
|
+
printSuccess,
|
|
10
|
+
printInfo,
|
|
11
|
+
} from '../utils/index.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 注册 home 命令:快速切换回主工作分支
|
|
15
|
+
* @param {Command} program - Commander 实例
|
|
16
|
+
*/
|
|
17
|
+
export function registerHomeCommand(program: Command): void {
|
|
18
|
+
program
|
|
19
|
+
.command('home')
|
|
20
|
+
.description('切换回主工作分支')
|
|
21
|
+
.action(async () => {
|
|
22
|
+
await handleHome();
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 执行 home 命令:切换回主工作分支
|
|
28
|
+
*/
|
|
29
|
+
async function handleHome(): Promise<void> {
|
|
30
|
+
validateMainWorktree();
|
|
31
|
+
requireProjectConfig();
|
|
32
|
+
|
|
33
|
+
const mainBranch = getMainWorkBranch();
|
|
34
|
+
const currentBranch = getCurrentBranch();
|
|
35
|
+
|
|
36
|
+
if (currentBranch === mainBranch) {
|
|
37
|
+
printInfo(MESSAGES.HOME_ALREADY_ON_MAIN(mainBranch));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
await ensureOnMainWorkBranch();
|
|
42
|
+
printSuccess(MESSAGES.HOME_SWITCH_SUCCESS(currentBranch, mainBranch));
|
|
43
|
+
}
|
package/src/commands/sync.ts
CHANGED
|
@@ -6,15 +6,12 @@ import type { SyncOptions } from '../types/index.js';
|
|
|
6
6
|
import {
|
|
7
7
|
validateMainWorktree,
|
|
8
8
|
getGitTopLevel,
|
|
9
|
-
getProjectName,
|
|
10
9
|
getProjectWorktrees,
|
|
11
10
|
isWorkingDirClean,
|
|
12
11
|
gitAddAll,
|
|
13
12
|
gitCommit,
|
|
14
13
|
gitMerge,
|
|
15
14
|
hasMergeConflict,
|
|
16
|
-
hasSnapshot,
|
|
17
|
-
removeSnapshot,
|
|
18
15
|
printSuccess,
|
|
19
16
|
printInfo,
|
|
20
17
|
printWarning,
|
|
@@ -89,7 +86,7 @@ export interface SyncResult {
|
|
|
89
86
|
}
|
|
90
87
|
|
|
91
88
|
/**
|
|
92
|
-
* 执行 sync 核心操作(检查未提交→自动保存→merge
|
|
89
|
+
* 执行 sync 核心操作(检查未提交→自动保存→merge 主分支)
|
|
93
90
|
* 不包含 worktree 解析交互,供 validate 等命令复用
|
|
94
91
|
* @param {string} targetWorktreePath - 目标 worktree 路径
|
|
95
92
|
* @param {string} branch - 分支名
|
|
@@ -114,13 +111,6 @@ export async function executeSyncForBranch(targetWorktreePath: string, branch: s
|
|
|
114
111
|
return { success: false, hasConflict: true };
|
|
115
112
|
}
|
|
116
113
|
|
|
117
|
-
// 合并成功后清除该分支的 validate 快照(代码基础已变化,旧快照无效)
|
|
118
|
-
const projectName = getProjectName();
|
|
119
|
-
if (hasSnapshot(projectName, branch)) {
|
|
120
|
-
removeSnapshot(projectName, branch);
|
|
121
|
-
logger.info(`已清除分支 ${branch} 的 validate 快照`);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
114
|
printSuccess(MESSAGES.SYNC_SUCCESS(branch, mainBranch));
|
|
125
115
|
|
|
126
116
|
// 合并成功后重建验证分支(基于最新的主分支 HEAD)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** cover-validate 命令专属提示消息 */
|
|
2
|
+
export const COVER_VALIDATE_MESSAGES = {
|
|
3
|
+
/** 当前不在验证分支上 */
|
|
4
|
+
COVER_VALIDATE_NOT_ON_VALIDATE_BRANCH: '当前分支不是验证分支(需以 clawt-validate- 开头)\n 请先通过 clawt validate 切换到验证分支',
|
|
5
|
+
/** 无增量修改 */
|
|
6
|
+
COVER_VALIDATE_NO_CHANGES: '验证分支上没有相对于快照的增量修改,无需覆盖',
|
|
7
|
+
/** 目标 worktree 不存在 */
|
|
8
|
+
COVER_VALIDATE_TARGET_NOT_FOUND: (branch: string) =>
|
|
9
|
+
`未找到分支 ${branch} 对应的 worktree,请确认该 worktree 尚未被移除`,
|
|
10
|
+
/** 无快照,提示先执行 validate */
|
|
11
|
+
COVER_VALIDATE_NO_SNAPSHOT: (branch: string) =>
|
|
12
|
+
`未找到分支 ${branch} 的 validate 快照\n 请先执行 clawt validate -b ${branch} 创建快照`,
|
|
13
|
+
/** patch 应用失败 */
|
|
14
|
+
COVER_VALIDATE_APPLY_FAILED: (branch: string) =>
|
|
15
|
+
`覆盖变更到 worktree ${branch} 失败:patch 应用出错\n 请检查目标 worktree 工作区状态后重试`,
|
|
16
|
+
/** 工作区和暂存区无修改,可能为误操作 */
|
|
17
|
+
COVER_VALIDATE_WORKING_DIR_CLEAN: '当前验证分支的工作区和暂存区没有任何修改,可能为误操作',
|
|
18
|
+
/** 覆盖成功 */
|
|
19
|
+
COVER_VALIDATE_SUCCESS: (branch: string) =>
|
|
20
|
+
`✓ 已将验证分支上的修改覆盖到 worktree => ${branch}`,
|
|
21
|
+
} as const;
|
|
@@ -14,6 +14,8 @@ import { PROJECTS_MESSAGES } from './projects.js';
|
|
|
14
14
|
import { COMPLETION_MESSAGES } from './completion.js';
|
|
15
15
|
import { UPDATE_MESSAGES, UPDATE_COMMANDS } from './update.js';
|
|
16
16
|
import { INIT_MESSAGES } from './init.js';
|
|
17
|
+
import { COVER_VALIDATE_MESSAGES } from './cover-validate.js';
|
|
18
|
+
import { HOME_MESSAGES } from './home.js';
|
|
17
19
|
import { PANEL_FOOTER_SHORTCUTS, PANEL_FOOTER_COUNTDOWN, PANEL_OVERFLOW_DOWN_HINT, PANEL_OVERFLOW_UP_HINT, PANEL_SNAPSHOT_SUMMARY, PANEL_NO_WORKTREES as PANEL_NO_WORKTREES_MSG, PANEL_PRESS_ENTER_TO_RETURN, PANEL_NOT_TTY, PANEL_TITLE } from './interactive-panel.js';
|
|
18
20
|
|
|
19
21
|
export { CONFIG_ALIAS_DISABLED_HINT };
|
|
@@ -40,4 +42,6 @@ export const MESSAGES = {
|
|
|
40
42
|
...PROJECTS_MESSAGES,
|
|
41
43
|
...COMPLETION_MESSAGES,
|
|
42
44
|
...INIT_MESSAGES,
|
|
45
|
+
...COVER_VALIDATE_MESSAGES,
|
|
46
|
+
...HOME_MESSAGES,
|
|
43
47
|
} as const;
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { registerRemoveCommand } from './commands/remove.js';
|
|
|
10
10
|
import { registerRunCommand } from './commands/run.js';
|
|
11
11
|
import { registerResumeCommand } from './commands/resume.js';
|
|
12
12
|
import { registerValidateCommand } from './commands/validate.js';
|
|
13
|
+
import { registerCoverValidateCommand } from './commands/cover-validate.js';
|
|
13
14
|
import { registerMergeCommand } from './commands/merge.js';
|
|
14
15
|
import { registerConfigCommand } from './commands/config.js';
|
|
15
16
|
import { registerSyncCommand } from './commands/sync.js';
|
|
@@ -19,6 +20,7 @@ import { registerAliasCommand } from './commands/alias.js';
|
|
|
19
20
|
import { registerProjectsCommand } from './commands/projects.js';
|
|
20
21
|
import { registerCompletionCommand } from './commands/completion.js';
|
|
21
22
|
import { registerInitCommand } from './commands/init.js';
|
|
23
|
+
import { registerHomeCommand } from './commands/home.js';
|
|
22
24
|
|
|
23
25
|
// 从 package.json 读取版本号,避免硬编码
|
|
24
26
|
const require = createRequire(import.meta.url);
|
|
@@ -49,6 +51,7 @@ registerRemoveCommand(program);
|
|
|
49
51
|
registerRunCommand(program);
|
|
50
52
|
registerResumeCommand(program);
|
|
51
53
|
registerValidateCommand(program);
|
|
54
|
+
registerCoverValidateCommand(program);
|
|
52
55
|
registerMergeCommand(program);
|
|
53
56
|
registerConfigCommand(program);
|
|
54
57
|
registerSyncCommand(program);
|
|
@@ -58,6 +61,7 @@ registerAliasCommand(program);
|
|
|
58
61
|
registerProjectsCommand(program);
|
|
59
62
|
registerCompletionCommand(program);
|
|
60
63
|
registerInitCommand(program);
|
|
64
|
+
registerHomeCommand(program);
|
|
61
65
|
|
|
62
66
|
// 加载配置并应用命令别名
|
|
63
67
|
const config = loadConfig();
|
|
@@ -90,22 +90,26 @@ export function readSnapshot(projectName: string, branchName: string): { treeHas
|
|
|
90
90
|
/**
|
|
91
91
|
* 写入 validate 快照内容(自动创建目录)
|
|
92
92
|
* tree hash 写入 .tree 文件,HEAD commit hash 写入 .head 文件,staged tree hash 写入 .staged 文件
|
|
93
|
+
* 未提供的可选字段(值为 undefined)不会覆盖,保留磁盘上的原值
|
|
93
94
|
* @param {string} projectName - 项目名
|
|
94
95
|
* @param {string} branchName - 分支名
|
|
95
|
-
* @param {string} treeHash - git tree 对象的 hash
|
|
96
|
-
* @param {string} headCommitHash - 快照时主 worktree 的 HEAD commit hash
|
|
97
|
-
* @param {string} [stagedTreeHash
|
|
96
|
+
* @param {string} [treeHash] - git tree 对象的 hash,不传则保留原值
|
|
97
|
+
* @param {string} [headCommitHash] - 快照时主 worktree 的 HEAD commit hash,不传则保留原值
|
|
98
|
+
* @param {string} [stagedTreeHash] - validate 结束时暂存区对应的 tree hash,不传则保留原值
|
|
98
99
|
*/
|
|
99
|
-
export function writeSnapshot(projectName: string, branchName: string, treeHash
|
|
100
|
-
const snapshotPath = getSnapshotPath(projectName, branchName);
|
|
101
|
-
const headPath = getSnapshotHeadPath(projectName, branchName);
|
|
102
|
-
const stagedPath = getSnapshotStagedPath(projectName, branchName);
|
|
100
|
+
export function writeSnapshot(projectName: string, branchName: string, treeHash?: string, headCommitHash?: string, stagedTreeHash?: string): void {
|
|
103
101
|
const snapshotDir = join(VALIDATE_SNAPSHOTS_DIR, projectName);
|
|
104
102
|
ensureDir(snapshotDir);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
103
|
+
if (treeHash !== undefined) {
|
|
104
|
+
writeFileSync(getSnapshotPath(projectName, branchName), treeHash, 'utf-8');
|
|
105
|
+
}
|
|
106
|
+
if (headCommitHash !== undefined) {
|
|
107
|
+
writeFileSync(getSnapshotHeadPath(projectName, branchName), headCommitHash, 'utf-8');
|
|
108
|
+
}
|
|
109
|
+
if (stagedTreeHash !== undefined) {
|
|
110
|
+
writeFileSync(getSnapshotStagedPath(projectName, branchName), stagedTreeHash, 'utf-8');
|
|
111
|
+
}
|
|
112
|
+
logger.info(`已写入 validate 快照 (project=${projectName}, branch=${branchName})`);
|
|
109
113
|
}
|
|
110
114
|
|
|
111
115
|
/**
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
|
|
4
|
+
vi.mock('../../../src/logger/index.js', () => ({
|
|
5
|
+
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock('../../../src/errors/index.js', () => ({
|
|
9
|
+
ClawtError: class ClawtError extends Error {
|
|
10
|
+
exitCode: number;
|
|
11
|
+
constructor(message: string, exitCode = 1) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.exitCode = exitCode;
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
vi.mock('../../../src/constants/index.js', () => ({
|
|
19
|
+
MESSAGES: {
|
|
20
|
+
COVER_VALIDATE_NOT_ON_VALIDATE_BRANCH: '当前分支不是验证分支',
|
|
21
|
+
COVER_VALIDATE_NO_CHANGES: '验证分支上没有相对于快照的增量修改,无需覆盖',
|
|
22
|
+
COVER_VALIDATE_TARGET_NOT_FOUND: (branch: string) => `未找到分支 ${branch} 对应的 worktree`,
|
|
23
|
+
COVER_VALIDATE_NO_SNAPSHOT: (branch: string) => `未找到分支 ${branch} 的 validate 快照`,
|
|
24
|
+
COVER_VALIDATE_APPLY_FAILED: (branch: string) => `覆盖变更到 worktree ${branch} 失败`,
|
|
25
|
+
COVER_VALIDATE_SUCCESS: (branch: string) => `✓ 已将验证分支上的修改覆盖到 worktree => ${branch}`,
|
|
26
|
+
COVER_VALIDATE_WORKING_DIR_CLEAN: '当前验证分支的工作区和暂存区没有任何修改,可能为误操作',
|
|
27
|
+
},
|
|
28
|
+
VALIDATE_BRANCH_PREFIX: 'clawt-validate-',
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
vi.mock('../../../src/utils/index.js', () => ({
|
|
32
|
+
validateMainWorktree: vi.fn(),
|
|
33
|
+
requireProjectConfig: vi.fn(),
|
|
34
|
+
getProjectName: vi.fn().mockReturnValue('test-project'),
|
|
35
|
+
getGitTopLevel: vi.fn().mockReturnValue('/repo'),
|
|
36
|
+
getCurrentBranch: vi.fn().mockReturnValue('clawt-validate-feature'),
|
|
37
|
+
getProjectWorktrees: vi.fn().mockReturnValue([{ path: '/path/feature', branch: 'feature' }]),
|
|
38
|
+
findExactMatch: vi.fn().mockReturnValue({ path: '/path/feature', branch: 'feature' }),
|
|
39
|
+
hasSnapshot: vi.fn().mockReturnValue(true),
|
|
40
|
+
readSnapshot: vi.fn().mockReturnValue({ treeHash: 'snapshot-tree-hash' }),
|
|
41
|
+
writeSnapshot: vi.fn(),
|
|
42
|
+
gitAddAll: vi.fn(),
|
|
43
|
+
gitWriteTree: vi.fn().mockReturnValue('current-tree-hash'),
|
|
44
|
+
gitReadTree: vi.fn(),
|
|
45
|
+
gitDiffTree: vi.fn().mockReturnValue(Buffer.from('fake-patch')),
|
|
46
|
+
gitApplyFromStdin: vi.fn(),
|
|
47
|
+
printSuccess: vi.fn(),
|
|
48
|
+
printInfo: vi.fn(),
|
|
49
|
+
isWorkingDirClean: vi.fn().mockReturnValue(false),
|
|
50
|
+
confirmAction: vi.fn().mockResolvedValue(true),
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
import { registerCoverValidateCommand, extractTargetBranchName, findTargetWorktreePath, computeIncrementalPatch } from '../../../src/commands/cover-validate.js';
|
|
54
|
+
import {
|
|
55
|
+
getCurrentBranch,
|
|
56
|
+
getProjectWorktrees,
|
|
57
|
+
findExactMatch,
|
|
58
|
+
hasSnapshot,
|
|
59
|
+
readSnapshot,
|
|
60
|
+
writeSnapshot,
|
|
61
|
+
gitAddAll,
|
|
62
|
+
gitWriteTree,
|
|
63
|
+
gitReadTree,
|
|
64
|
+
gitDiffTree,
|
|
65
|
+
gitApplyFromStdin,
|
|
66
|
+
printSuccess,
|
|
67
|
+
printInfo,
|
|
68
|
+
isWorkingDirClean,
|
|
69
|
+
confirmAction,
|
|
70
|
+
} from '../../../src/utils/index.js';
|
|
71
|
+
|
|
72
|
+
const mockedGetCurrentBranch = vi.mocked(getCurrentBranch);
|
|
73
|
+
const mockedGetProjectWorktrees = vi.mocked(getProjectWorktrees);
|
|
74
|
+
const mockedFindExactMatch = vi.mocked(findExactMatch);
|
|
75
|
+
const mockedHasSnapshot = vi.mocked(hasSnapshot);
|
|
76
|
+
const mockedReadSnapshot = vi.mocked(readSnapshot);
|
|
77
|
+
const mockedWriteSnapshot = vi.mocked(writeSnapshot);
|
|
78
|
+
const mockedGitAddAll = vi.mocked(gitAddAll);
|
|
79
|
+
const mockedGitWriteTree = vi.mocked(gitWriteTree);
|
|
80
|
+
const mockedGitReadTree = vi.mocked(gitReadTree);
|
|
81
|
+
const mockedGitDiffTree = vi.mocked(gitDiffTree);
|
|
82
|
+
const mockedGitApplyFromStdin = vi.mocked(gitApplyFromStdin);
|
|
83
|
+
const mockedPrintSuccess = vi.mocked(printSuccess);
|
|
84
|
+
const mockedPrintInfo = vi.mocked(printInfo);
|
|
85
|
+
const mockedIsWorkingDirClean = vi.mocked(isWorkingDirClean);
|
|
86
|
+
const mockedConfirmAction = vi.mocked(confirmAction);
|
|
87
|
+
|
|
88
|
+
beforeEach(() => {
|
|
89
|
+
vi.clearAllMocks();
|
|
90
|
+
// 恢复默认 mock 值
|
|
91
|
+
mockedGetCurrentBranch.mockReturnValue('clawt-validate-feature');
|
|
92
|
+
mockedGetProjectWorktrees.mockReturnValue([{ path: '/path/feature', branch: 'feature' }]);
|
|
93
|
+
mockedFindExactMatch.mockReturnValue({ path: '/path/feature', branch: 'feature' });
|
|
94
|
+
mockedHasSnapshot.mockReturnValue(true);
|
|
95
|
+
mockedReadSnapshot.mockReturnValue({ treeHash: 'snapshot-tree-hash' });
|
|
96
|
+
mockedIsWorkingDirClean.mockReturnValue(false);
|
|
97
|
+
mockedConfirmAction.mockResolvedValue(true);
|
|
98
|
+
mockedGitWriteTree.mockReturnValue('current-tree-hash');
|
|
99
|
+
mockedGitDiffTree.mockReturnValue(Buffer.from('fake-patch'));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe('registerCoverValidateCommand', () => {
|
|
103
|
+
it('注册 cover 命令', () => {
|
|
104
|
+
const program = new Command();
|
|
105
|
+
registerCoverValidateCommand(program);
|
|
106
|
+
const cmd = program.commands.find((c) => c.name() === 'cover');
|
|
107
|
+
expect(cmd).toBeDefined();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('extractTargetBranchName', () => {
|
|
112
|
+
it('从验证分支名中提取目标分支名', () => {
|
|
113
|
+
expect(extractTargetBranchName('clawt-validate-feature')).toBe('feature');
|
|
114
|
+
expect(extractTargetBranchName('clawt-validate-fix/bug-123')).toBe('fix/bug-123');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('findTargetWorktreePath', () => {
|
|
119
|
+
it('找到目标 worktree 时返回路径', () => {
|
|
120
|
+
const path = findTargetWorktreePath('feature');
|
|
121
|
+
expect(path).toBe('/path/feature');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('未找到目标 worktree 时抛出错误', () => {
|
|
125
|
+
mockedFindExactMatch.mockReturnValue(undefined);
|
|
126
|
+
expect(() => findTargetWorktreePath('nonexistent')).toThrow();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('computeIncrementalPatch', () => {
|
|
131
|
+
it('有增量变更时返回 patch 和 currentTreeHash', () => {
|
|
132
|
+
mockedGitWriteTree
|
|
133
|
+
.mockReturnValueOnce('saved-index-tree') // 保存暂存区
|
|
134
|
+
.mockReturnValueOnce('new-tree-hash'); // git add . 后的 tree
|
|
135
|
+
|
|
136
|
+
const result = computeIncrementalPatch('snapshot-tree-hash', '/repo');
|
|
137
|
+
expect(result).not.toBeNull();
|
|
138
|
+
expect(result!.currentTreeHash).toBe('new-tree-hash');
|
|
139
|
+
expect(mockedGitAddAll).toHaveBeenCalledWith('/repo');
|
|
140
|
+
expect(mockedGitReadTree).toHaveBeenCalledWith('saved-index-tree', '/repo');
|
|
141
|
+
expect(mockedGitDiffTree).toHaveBeenCalledWith('snapshot-tree-hash', 'new-tree-hash', '/repo');
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('无增量变更时返回 null', () => {
|
|
145
|
+
mockedGitWriteTree
|
|
146
|
+
.mockReturnValueOnce('saved-index-tree')
|
|
147
|
+
.mockReturnValueOnce('snapshot-tree-hash'); // 与快照相同
|
|
148
|
+
|
|
149
|
+
const result = computeIncrementalPatch('snapshot-tree-hash', '/repo');
|
|
150
|
+
expect(result).toBeNull();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('handleCoverValidate - 工作区干净检查', () => {
|
|
155
|
+
/** 辅助函数:执行 cover 命令 */
|
|
156
|
+
async function runCover(): Promise<void> {
|
|
157
|
+
const program = new Command();
|
|
158
|
+
program.exitOverride();
|
|
159
|
+
registerCoverValidateCommand(program);
|
|
160
|
+
await program.parseAsync(['cover'], { from: 'user' });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
it('工作区干净且用户取消时不执行 patch', async () => {
|
|
164
|
+
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
165
|
+
mockedConfirmAction.mockResolvedValue(false);
|
|
166
|
+
|
|
167
|
+
await runCover();
|
|
168
|
+
|
|
169
|
+
expect(mockedPrintInfo).toHaveBeenCalled();
|
|
170
|
+
expect(mockedConfirmAction).toHaveBeenCalledWith('是否继续执行覆盖?');
|
|
171
|
+
// 用户取消后不应继续执行后续逻辑
|
|
172
|
+
expect(mockedGitAddAll).not.toHaveBeenCalled();
|
|
173
|
+
expect(mockedGitApplyFromStdin).not.toHaveBeenCalled();
|
|
174
|
+
expect(mockedPrintSuccess).not.toHaveBeenCalled();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('工作区干净且用户确认继续时正常执行', async () => {
|
|
178
|
+
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
179
|
+
mockedConfirmAction.mockResolvedValue(true);
|
|
180
|
+
// computeIncrementalPatch 返回有 patch 的结果
|
|
181
|
+
mockedGitWriteTree
|
|
182
|
+
.mockReturnValueOnce('saved-index-tree')
|
|
183
|
+
.mockReturnValueOnce('new-tree-hash');
|
|
184
|
+
|
|
185
|
+
await runCover();
|
|
186
|
+
|
|
187
|
+
expect(mockedConfirmAction).toHaveBeenCalledWith('是否继续执行覆盖?');
|
|
188
|
+
expect(mockedGitApplyFromStdin).toHaveBeenCalled();
|
|
189
|
+
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('工作区不干净时跳过确认直接执行', async () => {
|
|
193
|
+
mockedIsWorkingDirClean.mockReturnValue(false);
|
|
194
|
+
mockedGitWriteTree
|
|
195
|
+
.mockReturnValueOnce('saved-index-tree')
|
|
196
|
+
.mockReturnValueOnce('new-tree-hash');
|
|
197
|
+
|
|
198
|
+
await runCover();
|
|
199
|
+
|
|
200
|
+
expect(mockedConfirmAction).not.toHaveBeenCalled();
|
|
201
|
+
expect(mockedGitApplyFromStdin).toHaveBeenCalled();
|
|
202
|
+
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
203
|
+
});
|
|
204
|
+
});
|
|
@@ -33,7 +33,6 @@ vi.mock('../../../src/constants/index.js', () => ({
|
|
|
33
33
|
vi.mock('../../../src/utils/index.js', () => ({
|
|
34
34
|
validateMainWorktree: vi.fn(),
|
|
35
35
|
getGitTopLevel: vi.fn(),
|
|
36
|
-
getProjectName: vi.fn(),
|
|
37
36
|
getProjectWorktrees: vi.fn(),
|
|
38
37
|
isWorkingDirClean: vi.fn(),
|
|
39
38
|
gitAddAll: vi.fn(),
|
|
@@ -41,8 +40,6 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
41
40
|
gitMerge: vi.fn(),
|
|
42
41
|
hasMergeConflict: vi.fn(),
|
|
43
42
|
getCurrentBranch: vi.fn(),
|
|
44
|
-
hasSnapshot: vi.fn(),
|
|
45
|
-
removeSnapshot: vi.fn(),
|
|
46
43
|
printSuccess: vi.fn(),
|
|
47
44
|
printInfo: vi.fn(),
|
|
48
45
|
printWarning: vi.fn(),
|
|
@@ -56,7 +53,6 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
56
53
|
import { registerSyncCommand } from '../../../src/commands/sync.js';
|
|
57
54
|
import {
|
|
58
55
|
getGitTopLevel,
|
|
59
|
-
getProjectName,
|
|
60
56
|
getProjectWorktrees,
|
|
61
57
|
isWorkingDirClean,
|
|
62
58
|
gitAddAll,
|
|
@@ -64,15 +60,12 @@ import {
|
|
|
64
60
|
gitMerge,
|
|
65
61
|
hasMergeConflict,
|
|
66
62
|
getCurrentBranch,
|
|
67
|
-
hasSnapshot,
|
|
68
|
-
removeSnapshot,
|
|
69
63
|
printSuccess,
|
|
70
64
|
printWarning,
|
|
71
65
|
resolveTargetWorktree,
|
|
72
66
|
} from '../../../src/utils/index.js';
|
|
73
67
|
|
|
74
68
|
const mockedGetGitTopLevel = vi.mocked(getGitTopLevel);
|
|
75
|
-
const mockedGetProjectName = vi.mocked(getProjectName);
|
|
76
69
|
const mockedGetProjectWorktrees = vi.mocked(getProjectWorktrees);
|
|
77
70
|
const mockedIsWorkingDirClean = vi.mocked(isWorkingDirClean);
|
|
78
71
|
const mockedGitAddAll = vi.mocked(gitAddAll);
|
|
@@ -80,15 +73,12 @@ const mockedGitCommit = vi.mocked(gitCommit);
|
|
|
80
73
|
const mockedGitMerge = vi.mocked(gitMerge);
|
|
81
74
|
const mockedHasMergeConflict = vi.mocked(hasMergeConflict);
|
|
82
75
|
const mockedGetCurrentBranch = vi.mocked(getCurrentBranch);
|
|
83
|
-
const mockedHasSnapshot = vi.mocked(hasSnapshot);
|
|
84
|
-
const mockedRemoveSnapshot = vi.mocked(removeSnapshot);
|
|
85
76
|
const mockedPrintSuccess = vi.mocked(printSuccess);
|
|
86
77
|
const mockedPrintWarning = vi.mocked(printWarning);
|
|
87
78
|
const mockedResolveTargetWorktree = vi.mocked(resolveTargetWorktree);
|
|
88
79
|
|
|
89
80
|
beforeEach(() => {
|
|
90
81
|
mockedGetGitTopLevel.mockReturnValue('/repo');
|
|
91
|
-
mockedGetProjectName.mockReturnValue('test-project');
|
|
92
82
|
mockedGetCurrentBranch.mockReturnValue('main');
|
|
93
83
|
mockedGetProjectWorktrees.mockReset();
|
|
94
84
|
mockedIsWorkingDirClean.mockReset();
|
|
@@ -96,8 +86,6 @@ beforeEach(() => {
|
|
|
96
86
|
mockedGitCommit.mockReset();
|
|
97
87
|
mockedGitMerge.mockReset();
|
|
98
88
|
mockedHasMergeConflict.mockReset();
|
|
99
|
-
mockedHasSnapshot.mockReset();
|
|
100
|
-
mockedRemoveSnapshot.mockReset();
|
|
101
89
|
mockedPrintSuccess.mockReset();
|
|
102
90
|
mockedPrintWarning.mockReset();
|
|
103
91
|
mockedResolveTargetWorktree.mockReset();
|
|
@@ -118,7 +106,6 @@ describe('handleSync', () => {
|
|
|
118
106
|
mockedGetProjectWorktrees.mockReturnValue([worktree]);
|
|
119
107
|
mockedResolveTargetWorktree.mockResolvedValue(worktree);
|
|
120
108
|
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
121
|
-
mockedHasSnapshot.mockReturnValue(false);
|
|
122
109
|
|
|
123
110
|
const program = new Command();
|
|
124
111
|
program.exitOverride();
|
|
@@ -135,7 +122,6 @@ describe('handleSync', () => {
|
|
|
135
122
|
mockedGetProjectWorktrees.mockReturnValue([worktree]);
|
|
136
123
|
mockedResolveTargetWorktree.mockResolvedValue(worktree);
|
|
137
124
|
mockedIsWorkingDirClean.mockReturnValue(false);
|
|
138
|
-
mockedHasSnapshot.mockReturnValue(false);
|
|
139
125
|
|
|
140
126
|
const program = new Command();
|
|
141
127
|
program.exitOverride();
|
|
@@ -164,36 +150,6 @@ describe('handleSync', () => {
|
|
|
164
150
|
expect(mockedPrintSuccess).not.toHaveBeenCalled();
|
|
165
151
|
});
|
|
166
152
|
|
|
167
|
-
it('合并成功后清理 validate 快照', async () => {
|
|
168
|
-
const worktree = { path: '/path/feature', branch: 'feature' };
|
|
169
|
-
mockedGetProjectWorktrees.mockReturnValue([worktree]);
|
|
170
|
-
mockedResolveTargetWorktree.mockResolvedValue(worktree);
|
|
171
|
-
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
172
|
-
mockedHasSnapshot.mockReturnValue(true);
|
|
173
|
-
|
|
174
|
-
const program = new Command();
|
|
175
|
-
program.exitOverride();
|
|
176
|
-
registerSyncCommand(program);
|
|
177
|
-
await program.parseAsync(['sync', '-b', 'feature'], { from: 'user' });
|
|
178
|
-
|
|
179
|
-
expect(mockedRemoveSnapshot).toHaveBeenCalledWith('test-project', 'feature');
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it('合并成功且无快照时不调用 removeSnapshot', async () => {
|
|
183
|
-
const worktree = { path: '/path/feature', branch: 'feature' };
|
|
184
|
-
mockedGetProjectWorktrees.mockReturnValue([worktree]);
|
|
185
|
-
mockedResolveTargetWorktree.mockResolvedValue(worktree);
|
|
186
|
-
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
187
|
-
mockedHasSnapshot.mockReturnValue(false);
|
|
188
|
-
|
|
189
|
-
const program = new Command();
|
|
190
|
-
program.exitOverride();
|
|
191
|
-
registerSyncCommand(program);
|
|
192
|
-
await program.parseAsync(['sync', '-b', 'feature'], { from: 'user' });
|
|
193
|
-
|
|
194
|
-
expect(mockedRemoveSnapshot).not.toHaveBeenCalled();
|
|
195
|
-
});
|
|
196
|
-
|
|
197
153
|
it('合并失败(非冲突错误)时向上抛出', async () => {
|
|
198
154
|
const worktree = { path: '/path/feature', branch: 'feature' };
|
|
199
155
|
mockedGetProjectWorktrees.mockReturnValue([worktree]);
|
|
@@ -107,7 +107,7 @@ describe('readSnapshotTreeHash', () => {
|
|
|
107
107
|
|
|
108
108
|
describe('writeSnapshot', () => {
|
|
109
109
|
it('正确写入三个文件', () => {
|
|
110
|
-
writeSnapshot('proj', 'branch', 'tree123', 'head456');
|
|
110
|
+
writeSnapshot('proj', 'branch', 'tree123', 'head456', 'staged789');
|
|
111
111
|
expect(mockedEnsureDir).toHaveBeenCalledWith('/tmp/test-snapshots/proj');
|
|
112
112
|
expect(mockedWriteFileSync).toHaveBeenCalledTimes(3);
|
|
113
113
|
expect(mockedWriteFileSync).toHaveBeenCalledWith(
|
|
@@ -122,10 +122,26 @@ describe('writeSnapshot', () => {
|
|
|
122
122
|
);
|
|
123
123
|
expect(mockedWriteFileSync).toHaveBeenCalledWith(
|
|
124
124
|
'/tmp/test-snapshots/proj/branch.staged',
|
|
125
|
-
'',
|
|
125
|
+
'staged789',
|
|
126
126
|
'utf-8',
|
|
127
127
|
);
|
|
128
128
|
});
|
|
129
|
+
|
|
130
|
+
it('未传 headCommitHash 和 stagedTreeHash 时只写入 treeHash', () => {
|
|
131
|
+
writeSnapshot('proj', 'branch', 'tree123');
|
|
132
|
+
expect(mockedWriteFileSync).toHaveBeenCalledTimes(1);
|
|
133
|
+
expect(mockedWriteFileSync).toHaveBeenCalledWith(
|
|
134
|
+
'/tmp/test-snapshots/proj/branch.tree',
|
|
135
|
+
'tree123',
|
|
136
|
+
'utf-8',
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it('所有字段都未传时不写入任何文件', () => {
|
|
141
|
+
writeSnapshot('proj', 'branch');
|
|
142
|
+
expect(mockedEnsureDir).toHaveBeenCalledWith('/tmp/test-snapshots/proj');
|
|
143
|
+
expect(mockedWriteFileSync).not.toHaveBeenCalled();
|
|
144
|
+
});
|
|
129
145
|
});
|
|
130
146
|
|
|
131
147
|
describe('removeSnapshot', () => {
|