clawt 2.16.3 → 2.16.4
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 +4 -0
- package/dist/index.js +98 -62
- package/dist/postinstall.js +10 -1
- package/docs/spec.md +48 -8
- package/package.json +1 -1
- package/src/commands/sync.ts +34 -14
- package/src/commands/validate.ts +50 -8
- package/src/constants/messages/validate.ts +12 -0
package/README.md
CHANGED
|
@@ -128,6 +128,8 @@ clawt validate -b <branch> -r "pnpm test & pnpm build" # 并行执行多个命
|
|
|
128
128
|
|
|
129
129
|
支持增量模式:再次 validate 同一分支时,可通过 `git diff` 查看两次之间的增量差异。
|
|
130
130
|
|
|
131
|
+
当 patch apply 失败(目标分支与主分支差异过大)时,会自动询问是否执行 `sync` 同步主分支到目标 worktree,无需手动操作。
|
|
132
|
+
|
|
131
133
|
`-r, --run` 选项可在 validate 成功后自动在主 worktree 中执行指定命令(如测试、构建等),命令执行失败不影响 validate 结果。支持用 `&` 分隔多个命令并行执行:
|
|
132
134
|
|
|
133
135
|
| 用法 | 行为 |
|
|
@@ -142,6 +144,8 @@ clawt validate -b <branch> -r "pnpm test & pnpm build" # 并行执行多个命
|
|
|
142
144
|
clawt sync -b <branch>
|
|
143
145
|
```
|
|
144
146
|
|
|
147
|
+
当 `validate` 的 patch apply 失败时也会自动提示执行 sync,通常无需手动调用。
|
|
148
|
+
|
|
145
149
|
### `clawt merge` — 合并分支到主 worktree
|
|
146
150
|
|
|
147
151
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -211,7 +211,16 @@ ${branches.map((b) => ` - ${b}`).join("\n")}`,
|
|
|
211
211
|
/** 并行执行中单个命令失败 */
|
|
212
212
|
VALIDATE_PARALLEL_CMD_FAILED: (command, exitCode) => ` \u2717 ${command}\uFF08\u9000\u51FA\u7801: ${exitCode}\uFF09`,
|
|
213
213
|
/** 并行执行中单个命令启动失败 */
|
|
214
|
-
VALIDATE_PARALLEL_CMD_ERROR: (command, errorMessage) => ` \u2717 ${command}\uFF08\u9519\u8BEF: ${errorMessage}\uFF09
|
|
214
|
+
VALIDATE_PARALLEL_CMD_ERROR: (command, errorMessage) => ` \u2717 ${command}\uFF08\u9519\u8BEF: ${errorMessage}\uFF09`,
|
|
215
|
+
/** patch apply 失败后询问用户是否执行 sync */
|
|
216
|
+
VALIDATE_CONFIRM_AUTO_SYNC: (branch) => `\u662F\u5426\u7ACB\u5373\u6267\u884C sync \u540C\u6B65\u4E3B\u5206\u652F\u5230 ${branch}\uFF1F`,
|
|
217
|
+
/** 自动 sync 开始提示 */
|
|
218
|
+
VALIDATE_AUTO_SYNC_START: (branch) => `\u6B63\u5728\u81EA\u52A8\u540C\u6B65\u4E3B\u5206\u652F\u5230 ${branch} ...`,
|
|
219
|
+
/** 自动 sync 存在冲突,无法重试 */
|
|
220
|
+
VALIDATE_AUTO_SYNC_CONFLICT: (worktreePath) => `\u540C\u6B65\u5B58\u5728\u51B2\u7A81\uFF0C\u8BF7\u8FDB\u5165\u76EE\u6807 worktree \u624B\u52A8\u89E3\u51B3\u51B2\u7A81\u540E\u91CD\u8BD5
|
|
221
|
+
cd ${worktreePath}`,
|
|
222
|
+
/** 用户拒绝自动 sync */
|
|
223
|
+
VALIDATE_AUTO_SYNC_DECLINED: (branch) => `\u8BF7\u624B\u52A8\u6267\u884C clawt sync -b ${branch} \u540C\u6B65\u4E3B\u5206\u652F\u540E\u91CD\u8BD5`
|
|
215
224
|
};
|
|
216
225
|
|
|
217
226
|
// src/constants/messages/sync.ts
|
|
@@ -2337,6 +2346,66 @@ async function handleBatchResume(worktrees) {
|
|
|
2337
2346
|
|
|
2338
2347
|
// src/commands/validate.ts
|
|
2339
2348
|
import Enquirer4 from "enquirer";
|
|
2349
|
+
|
|
2350
|
+
// src/commands/sync.ts
|
|
2351
|
+
function registerSyncCommand(program2) {
|
|
2352
|
+
program2.command("sync").description("\u5C06\u4E3B\u5206\u652F\u6700\u65B0\u4EE3\u7801\u540C\u6B65\u5230\u76EE\u6807 worktree").option("-b, --branch <branchName>", "\u8981\u540C\u6B65\u7684\u5206\u652F\u540D\uFF08\u652F\u6301\u6A21\u7CCA\u5339\u914D\uFF0C\u4E0D\u4F20\u5219\u5217\u51FA\u6240\u6709\u5206\u652F\uFF09").action(async (options) => {
|
|
2353
|
+
await handleSync(options);
|
|
2354
|
+
});
|
|
2355
|
+
}
|
|
2356
|
+
var SYNC_RESOLVE_MESSAGES = {
|
|
2357
|
+
noWorktrees: MESSAGES.SYNC_NO_WORKTREES,
|
|
2358
|
+
selectBranch: MESSAGES.SYNC_SELECT_BRANCH,
|
|
2359
|
+
multipleMatches: MESSAGES.SYNC_MULTIPLE_MATCHES,
|
|
2360
|
+
noMatch: MESSAGES.SYNC_NO_MATCH
|
|
2361
|
+
};
|
|
2362
|
+
function autoSaveChanges(worktreePath, branch) {
|
|
2363
|
+
gitAddAll(worktreePath);
|
|
2364
|
+
gitCommit(AUTO_SAVE_COMMIT_MESSAGE, worktreePath);
|
|
2365
|
+
printInfo(MESSAGES.SYNC_AUTO_COMMITTED(branch));
|
|
2366
|
+
logger.info(`\u5DF2\u81EA\u52A8\u4FDD\u5B58 ${branch} \u5206\u652F\u7684\u672A\u63D0\u4EA4\u53D8\u66F4`);
|
|
2367
|
+
}
|
|
2368
|
+
function mergeMainBranch(worktreePath, mainBranch) {
|
|
2369
|
+
try {
|
|
2370
|
+
gitMerge(mainBranch, worktreePath);
|
|
2371
|
+
return false;
|
|
2372
|
+
} catch {
|
|
2373
|
+
if (hasMergeConflict(worktreePath)) {
|
|
2374
|
+
return true;
|
|
2375
|
+
}
|
|
2376
|
+
throw new ClawtError(`\u5408\u5E76 ${mainBranch} \u5931\u8D25`);
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
function executeSyncForBranch(targetWorktreePath, branch) {
|
|
2380
|
+
const mainWorktreePath = getGitTopLevel();
|
|
2381
|
+
const mainBranch = getCurrentBranch(mainWorktreePath);
|
|
2382
|
+
if (!isWorkingDirClean(targetWorktreePath)) {
|
|
2383
|
+
autoSaveChanges(targetWorktreePath, branch);
|
|
2384
|
+
}
|
|
2385
|
+
printInfo(MESSAGES.SYNC_MERGING(branch, mainBranch));
|
|
2386
|
+
const hasConflict = mergeMainBranch(targetWorktreePath, mainBranch);
|
|
2387
|
+
if (hasConflict) {
|
|
2388
|
+
printWarning(MESSAGES.SYNC_CONFLICT(targetWorktreePath));
|
|
2389
|
+
return { success: false, hasConflict: true };
|
|
2390
|
+
}
|
|
2391
|
+
const projectName = getProjectName();
|
|
2392
|
+
if (hasSnapshot(projectName, branch)) {
|
|
2393
|
+
removeSnapshot(projectName, branch);
|
|
2394
|
+
logger.info(`\u5DF2\u6E05\u9664\u5206\u652F ${branch} \u7684 validate \u5FEB\u7167`);
|
|
2395
|
+
}
|
|
2396
|
+
printSuccess(MESSAGES.SYNC_SUCCESS(branch, mainBranch));
|
|
2397
|
+
return { success: true, hasConflict: false };
|
|
2398
|
+
}
|
|
2399
|
+
async function handleSync(options) {
|
|
2400
|
+
validateMainWorktree();
|
|
2401
|
+
logger.info(`sync \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch ?? "(\u672A\u6307\u5B9A)"}`);
|
|
2402
|
+
const worktrees = getProjectWorktrees();
|
|
2403
|
+
const worktree = await resolveTargetWorktree(worktrees, SYNC_RESOLVE_MESSAGES, options.branch);
|
|
2404
|
+
const { path: targetWorktreePath, branch } = worktree;
|
|
2405
|
+
executeSyncForBranch(targetWorktreePath, branch);
|
|
2406
|
+
}
|
|
2407
|
+
|
|
2408
|
+
// src/commands/validate.ts
|
|
2340
2409
|
var VALIDATE_RESOLVE_MESSAGES = {
|
|
2341
2410
|
noWorktrees: MESSAGES.VALIDATE_NO_WORKTREES,
|
|
2342
2411
|
selectBranch: MESSAGES.VALIDATE_SELECT_BRANCH,
|
|
@@ -2397,9 +2466,10 @@ function migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName
|
|
|
2397
2466
|
} catch (error) {
|
|
2398
2467
|
logger.warn(`patch apply \u5931\u8D25: ${error}`);
|
|
2399
2468
|
printWarning(MESSAGES.VALIDATE_PATCH_APPLY_FAILED(branchName));
|
|
2400
|
-
|
|
2469
|
+
return { success: false };
|
|
2401
2470
|
}
|
|
2402
2471
|
}
|
|
2472
|
+
return { success: true };
|
|
2403
2473
|
} finally {
|
|
2404
2474
|
if (didTempCommit) {
|
|
2405
2475
|
try {
|
|
@@ -2415,6 +2485,18 @@ function migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName
|
|
|
2415
2485
|
}
|
|
2416
2486
|
}
|
|
2417
2487
|
}
|
|
2488
|
+
async function handlePatchApplyFailure(targetWorktreePath, branchName) {
|
|
2489
|
+
const confirmed = await confirmAction(MESSAGES.VALIDATE_CONFIRM_AUTO_SYNC(branchName));
|
|
2490
|
+
if (!confirmed) {
|
|
2491
|
+
printWarning(MESSAGES.VALIDATE_AUTO_SYNC_DECLINED(branchName));
|
|
2492
|
+
return;
|
|
2493
|
+
}
|
|
2494
|
+
printInfo(MESSAGES.VALIDATE_AUTO_SYNC_START(branchName));
|
|
2495
|
+
const syncResult = executeSyncForBranch(targetWorktreePath, branchName);
|
|
2496
|
+
if (syncResult.hasConflict) {
|
|
2497
|
+
printWarning(MESSAGES.VALIDATE_AUTO_SYNC_CONFLICT(targetWorktreePath));
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2418
2500
|
function saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName) {
|
|
2419
2501
|
gitAddAll(mainWorktreePath);
|
|
2420
2502
|
const treeHash = gitWriteTree(mainWorktreePath);
|
|
@@ -2448,18 +2530,26 @@ async function handleValidateClean(options) {
|
|
|
2448
2530
|
removeSnapshot(projectName, branchName);
|
|
2449
2531
|
printSuccess(MESSAGES.VALIDATE_CLEANED(branchName));
|
|
2450
2532
|
}
|
|
2451
|
-
function handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted) {
|
|
2452
|
-
migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
|
|
2533
|
+
async function handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted) {
|
|
2534
|
+
const result = migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
|
|
2535
|
+
if (!result.success) {
|
|
2536
|
+
await handlePatchApplyFailure(targetWorktreePath, branchName);
|
|
2537
|
+
return;
|
|
2538
|
+
}
|
|
2453
2539
|
saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
|
|
2454
2540
|
printSuccess(MESSAGES.VALIDATE_SUCCESS(branchName));
|
|
2455
2541
|
}
|
|
2456
|
-
function handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted) {
|
|
2542
|
+
async function handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted) {
|
|
2457
2543
|
const { treeHash: oldTreeHash, headCommitHash: oldHeadCommitHash } = readSnapshot(projectName, branchName);
|
|
2458
2544
|
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
2459
2545
|
gitResetHard(mainWorktreePath);
|
|
2460
2546
|
gitCleanForce(mainWorktreePath);
|
|
2461
2547
|
}
|
|
2462
|
-
migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
|
|
2548
|
+
const result = migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
|
|
2549
|
+
if (!result.success) {
|
|
2550
|
+
await handlePatchApplyFailure(targetWorktreePath, branchName);
|
|
2551
|
+
return;
|
|
2552
|
+
}
|
|
2463
2553
|
saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
|
|
2464
2554
|
try {
|
|
2465
2555
|
const currentHeadCommitHash = getHeadCommitHash(mainWorktreePath);
|
|
@@ -2562,12 +2652,12 @@ async function handleValidate(options) {
|
|
|
2562
2652
|
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
2563
2653
|
await handleDirtyMainWorktree(mainWorktreePath);
|
|
2564
2654
|
}
|
|
2565
|
-
handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
|
|
2655
|
+
await handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
|
|
2566
2656
|
} else {
|
|
2567
2657
|
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
2568
2658
|
await handleDirtyMainWorktree(mainWorktreePath);
|
|
2569
2659
|
}
|
|
2570
|
-
handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
|
|
2660
|
+
await handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
|
|
2571
2661
|
}
|
|
2572
2662
|
if (options.run) {
|
|
2573
2663
|
await executeRunCommand(options.run, mainWorktreePath);
|
|
@@ -2780,60 +2870,6 @@ function handleConfigGet(key) {
|
|
|
2780
2870
|
printInfo(MESSAGES.CONFIG_GET_VALUE(key, String(value)));
|
|
2781
2871
|
}
|
|
2782
2872
|
|
|
2783
|
-
// src/commands/sync.ts
|
|
2784
|
-
function registerSyncCommand(program2) {
|
|
2785
|
-
program2.command("sync").description("\u5C06\u4E3B\u5206\u652F\u6700\u65B0\u4EE3\u7801\u540C\u6B65\u5230\u76EE\u6807 worktree").option("-b, --branch <branchName>", "\u8981\u540C\u6B65\u7684\u5206\u652F\u540D\uFF08\u652F\u6301\u6A21\u7CCA\u5339\u914D\uFF0C\u4E0D\u4F20\u5219\u5217\u51FA\u6240\u6709\u5206\u652F\uFF09").action(async (options) => {
|
|
2786
|
-
await handleSync(options);
|
|
2787
|
-
});
|
|
2788
|
-
}
|
|
2789
|
-
var SYNC_RESOLVE_MESSAGES = {
|
|
2790
|
-
noWorktrees: MESSAGES.SYNC_NO_WORKTREES,
|
|
2791
|
-
selectBranch: MESSAGES.SYNC_SELECT_BRANCH,
|
|
2792
|
-
multipleMatches: MESSAGES.SYNC_MULTIPLE_MATCHES,
|
|
2793
|
-
noMatch: MESSAGES.SYNC_NO_MATCH
|
|
2794
|
-
};
|
|
2795
|
-
function autoSaveChanges(worktreePath, branch) {
|
|
2796
|
-
gitAddAll(worktreePath);
|
|
2797
|
-
gitCommit(AUTO_SAVE_COMMIT_MESSAGE, worktreePath);
|
|
2798
|
-
printInfo(MESSAGES.SYNC_AUTO_COMMITTED(branch));
|
|
2799
|
-
logger.info(`\u5DF2\u81EA\u52A8\u4FDD\u5B58 ${branch} \u5206\u652F\u7684\u672A\u63D0\u4EA4\u53D8\u66F4`);
|
|
2800
|
-
}
|
|
2801
|
-
function mergeMainBranch(worktreePath, mainBranch) {
|
|
2802
|
-
try {
|
|
2803
|
-
gitMerge(mainBranch, worktreePath);
|
|
2804
|
-
return false;
|
|
2805
|
-
} catch {
|
|
2806
|
-
if (hasMergeConflict(worktreePath)) {
|
|
2807
|
-
return true;
|
|
2808
|
-
}
|
|
2809
|
-
throw new ClawtError(`\u5408\u5E76 ${mainBranch} \u5931\u8D25`);
|
|
2810
|
-
}
|
|
2811
|
-
}
|
|
2812
|
-
async function handleSync(options) {
|
|
2813
|
-
validateMainWorktree();
|
|
2814
|
-
logger.info(`sync \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch ?? "(\u672A\u6307\u5B9A)"}`);
|
|
2815
|
-
const worktrees = getProjectWorktrees();
|
|
2816
|
-
const worktree = await resolveTargetWorktree(worktrees, SYNC_RESOLVE_MESSAGES, options.branch);
|
|
2817
|
-
const { path: targetWorktreePath, branch } = worktree;
|
|
2818
|
-
const mainWorktreePath = getGitTopLevel();
|
|
2819
|
-
const mainBranch = getCurrentBranch(mainWorktreePath);
|
|
2820
|
-
if (!isWorkingDirClean(targetWorktreePath)) {
|
|
2821
|
-
autoSaveChanges(targetWorktreePath, branch);
|
|
2822
|
-
}
|
|
2823
|
-
printInfo(MESSAGES.SYNC_MERGING(branch, mainBranch));
|
|
2824
|
-
const hasConflict = mergeMainBranch(targetWorktreePath, mainBranch);
|
|
2825
|
-
if (hasConflict) {
|
|
2826
|
-
printWarning(MESSAGES.SYNC_CONFLICT(targetWorktreePath));
|
|
2827
|
-
return;
|
|
2828
|
-
}
|
|
2829
|
-
const projectName = getProjectName();
|
|
2830
|
-
if (hasSnapshot(projectName, branch)) {
|
|
2831
|
-
removeSnapshot(projectName, branch);
|
|
2832
|
-
logger.info(`\u5DF2\u6E05\u9664\u5206\u652F ${branch} \u7684 validate \u5FEB\u7167`);
|
|
2833
|
-
}
|
|
2834
|
-
printSuccess(MESSAGES.SYNC_SUCCESS(branch, mainBranch));
|
|
2835
|
-
}
|
|
2836
|
-
|
|
2837
2873
|
// src/commands/reset.ts
|
|
2838
2874
|
function registerResetCommand(program2) {
|
|
2839
2875
|
program2.command("reset").description("\u91CD\u7F6E\u4E3B worktree \u5DE5\u4F5C\u533A\u548C\u6682\u5B58\u533A\uFF08\u4FDD\u7559 validate \u5FEB\u7167\uFF09").action(async () => {
|
package/dist/postinstall.js
CHANGED
|
@@ -203,7 +203,16 @@ ${branches.map((b) => ` - ${b}`).join("\n")}`,
|
|
|
203
203
|
/** 并行执行中单个命令失败 */
|
|
204
204
|
VALIDATE_PARALLEL_CMD_FAILED: (command, exitCode) => ` \u2717 ${command}\uFF08\u9000\u51FA\u7801: ${exitCode}\uFF09`,
|
|
205
205
|
/** 并行执行中单个命令启动失败 */
|
|
206
|
-
VALIDATE_PARALLEL_CMD_ERROR: (command, errorMessage) => ` \u2717 ${command}\uFF08\u9519\u8BEF: ${errorMessage}\uFF09
|
|
206
|
+
VALIDATE_PARALLEL_CMD_ERROR: (command, errorMessage) => ` \u2717 ${command}\uFF08\u9519\u8BEF: ${errorMessage}\uFF09`,
|
|
207
|
+
/** patch apply 失败后询问用户是否执行 sync */
|
|
208
|
+
VALIDATE_CONFIRM_AUTO_SYNC: (branch) => `\u662F\u5426\u7ACB\u5373\u6267\u884C sync \u540C\u6B65\u4E3B\u5206\u652F\u5230 ${branch}\uFF1F`,
|
|
209
|
+
/** 自动 sync 开始提示 */
|
|
210
|
+
VALIDATE_AUTO_SYNC_START: (branch) => `\u6B63\u5728\u81EA\u52A8\u540C\u6B65\u4E3B\u5206\u652F\u5230 ${branch} ...`,
|
|
211
|
+
/** 自动 sync 存在冲突,无法重试 */
|
|
212
|
+
VALIDATE_AUTO_SYNC_CONFLICT: (worktreePath) => `\u540C\u6B65\u5B58\u5728\u51B2\u7A81\uFF0C\u8BF7\u8FDB\u5165\u76EE\u6807 worktree \u624B\u52A8\u89E3\u51B3\u51B2\u7A81\u540E\u91CD\u8BD5
|
|
213
|
+
cd ${worktreePath}`,
|
|
214
|
+
/** 用户拒绝自动 sync */
|
|
215
|
+
VALIDATE_AUTO_SYNC_DECLINED: (branch) => `\u8BF7\u624B\u52A8\u6267\u884C clawt sync -b ${branch} \u540C\u6B65\u4E3B\u5206\u652F\u540E\u91CD\u8BD5`
|
|
207
216
|
};
|
|
208
217
|
|
|
209
218
|
// src/constants/messages/sync.ts
|
package/docs/spec.md
CHANGED
|
@@ -576,7 +576,26 @@ git restore --staged .
|
|
|
576
576
|
```
|
|
577
577
|
|
|
578
578
|
> 此步骤结束后,目标 worktree 的代码保持原样,主 worktree 工作目录包含目标分支的全量变更。
|
|
579
|
-
> 如果 patch apply
|
|
579
|
+
> 如果 patch apply 失败(目标分支与主分支差异过大),`migrateChangesViaPatch` 返回 `{ success: false }`,进入自动 sync 交互流程(见下文 [patch apply 失败后的自动 sync 流程](#patch-apply-失败后的自动-sync-流程))。
|
|
580
|
+
|
|
581
|
+
##### patch apply 失败后的自动 sync 流程
|
|
582
|
+
|
|
583
|
+
当 patch apply 失败时,validate 不再直接退出,而是通过 `handlePatchApplyFailure()` 函数进入交互流程:
|
|
584
|
+
|
|
585
|
+
1. **询问用户**:提示 `是否立即执行 sync 同步主分支到 <branchName>?`
|
|
586
|
+
2. **用户拒绝** → 输出提示 `请手动执行 clawt sync -b <branchName> 同步主分支后重试`,退出
|
|
587
|
+
3. **用户确认** → 调用 `executeSyncForBranch(targetWorktreePath, branchName)` 自动执行 sync
|
|
588
|
+
- **sync 成功** → validate 流程结束(用户需重新执行 validate)
|
|
589
|
+
- **sync 存在冲突** → 输出提示 `同步存在冲突,请进入目标 worktree 手动解决冲突后重试`,退出
|
|
590
|
+
|
|
591
|
+
> `executeSyncForBranch` 为 sync 命令抽取的核心操作函数(见 [5.12](#512-将主分支代码同步到目标-worktree)),供 validate 等命令复用。
|
|
592
|
+
|
|
593
|
+
**实现要点:**
|
|
594
|
+
|
|
595
|
+
- `migrateChangesViaPatch()` 返回类型从 `void` 改为 `{ success: boolean }`,patch apply 失败时返回 `{ success: false }` 而非抛出异常
|
|
596
|
+
- `handleFirstValidate()` 和 `handleIncrementalValidate()` 从同步函数改为 `async` 函数,以支持交互式确认
|
|
597
|
+
- `handlePatchApplyFailure()` 为新增的异步函数(`src/commands/validate.ts`),负责 patch 失败后的交互逻辑
|
|
598
|
+
- 消息常量:`MESSAGES.VALIDATE_CONFIRM_AUTO_SYNC`、`MESSAGES.VALIDATE_AUTO_SYNC_START`、`MESSAGES.VALIDATE_AUTO_SYNC_CONFLICT`、`MESSAGES.VALIDATE_AUTO_SYNC_DECLINED`(`src/constants/messages/validate.ts`)
|
|
580
599
|
|
|
581
600
|
##### 步骤 4:保存快照为 git tree 对象
|
|
582
601
|
|
|
@@ -715,7 +734,7 @@ clawt validate -b feature-scheme-1 -r "pnpm test & pnpm build"
|
|
|
715
734
|
|
|
716
735
|
##### 步骤 3:从目标分支获取最新全量变更
|
|
717
736
|
|
|
718
|
-
通过 patch 方式从目标分支获取最新全量变更(流程同首次 validate 的步骤 3
|
|
737
|
+
通过 patch 方式从目标分支获取最新全量变更(流程同首次 validate 的步骤 3)。如果 patch apply 失败,同样进入自动 sync 交互流程(见首次 validate 的 [patch apply 失败后的自动 sync 流程](#patch-apply-失败后的自动-sync-流程)),validate 流程提前结束。
|
|
719
738
|
|
|
720
739
|
##### 步骤 4:保存最新快照为 git tree 对象
|
|
721
740
|
|
|
@@ -1356,16 +1375,36 @@ clawt sync
|
|
|
1356
1375
|
- 唯一匹配 → 直接使用
|
|
1357
1376
|
- 多个匹配 → 通过交互式列表让用户从匹配结果中选择
|
|
1358
1377
|
3. **无匹配** → 报错退出,并列出所有可用分支名
|
|
1359
|
-
3.
|
|
1360
|
-
|
|
1378
|
+
3. 调用 `executeSyncForBranch(targetWorktreePath, branch)` 执行核心同步逻辑
|
|
1379
|
+
|
|
1380
|
+
#### `executeSyncForBranch` — sync 核心操作函数
|
|
1381
|
+
|
|
1382
|
+
`executeSyncForBranch(targetWorktreePath: string, branch: string): SyncResult` 是从 `handleSync` 中抽取的核心同步逻辑,不包含 worktree 解析交互,供 validate 等命令复用。
|
|
1383
|
+
|
|
1384
|
+
**接口定义:**
|
|
1385
|
+
|
|
1386
|
+
```typescript
|
|
1387
|
+
/** sync 核心操作的执行结果 */
|
|
1388
|
+
export interface SyncResult {
|
|
1389
|
+
/** 是否同步成功 */
|
|
1390
|
+
success: boolean;
|
|
1391
|
+
/** 是否存在合并冲突 */
|
|
1392
|
+
hasConflict: boolean;
|
|
1393
|
+
}
|
|
1394
|
+
```
|
|
1395
|
+
|
|
1396
|
+
**执行流程:**
|
|
1397
|
+
|
|
1398
|
+
1. **获取主分支名**:通过 `git rev-parse --abbrev-ref HEAD` 获取主 worktree 当前分支名(不硬编码 main/master)
|
|
1399
|
+
2. **自动保存未提交变更**:检查目标 worktree 是否有未提交修改
|
|
1361
1400
|
- 有修改 → 自动执行 `git add . && git commit -m "<AUTO_SAVE_COMMIT_MESSAGE>"` 保存变更(commit message 由常量 `AUTO_SAVE_COMMIT_MESSAGE` 定义,值为 `chore: auto-save before sync`,同时用于 merge 命令的 squash 检测)
|
|
1362
1401
|
- 无修改 → 跳过
|
|
1363
|
-
|
|
1402
|
+
3. **在目标 worktree 中合并主分支**:
|
|
1364
1403
|
```bash
|
|
1365
1404
|
cd ~/.clawt/worktrees/<project>/<branchName>
|
|
1366
1405
|
git merge <mainBranch>
|
|
1367
1406
|
```
|
|
1368
|
-
|
|
1407
|
+
4. **冲突处理**:
|
|
1369
1408
|
- **有冲突** → 输出警告,提示用户进入目标 worktree 手动解决:
|
|
1370
1409
|
```
|
|
1371
1410
|
合并存在冲突,请进入目标 worktree 手动解决:
|
|
@@ -1373,9 +1412,10 @@ clawt sync
|
|
|
1373
1412
|
解决冲突后执行 git add . && git merge --continue
|
|
1374
1413
|
clawt validate -b <branch> 验证变更
|
|
1375
1414
|
```
|
|
1415
|
+
- 返回 `{ success: false, hasConflict: true }`
|
|
1376
1416
|
- **无冲突** → 继续
|
|
1377
|
-
|
|
1378
|
-
|
|
1417
|
+
5. **清除 validate 快照**:合并成功后,如果该分支存在 validate 快照(`.tree` 和 `.head` 文件),自动删除(代码基础已变化,旧快照无效)
|
|
1418
|
+
6. **输出成功提示**并返回 `{ success: true, hasConflict: false }`:
|
|
1379
1419
|
```
|
|
1380
1420
|
✓ 已将 <mainBranch> 的最新代码同步到 <branchName>
|
|
1381
1421
|
```
|
package/package.json
CHANGED
package/src/commands/sync.ts
CHANGED
|
@@ -77,21 +77,22 @@ function mergeMainBranch(worktreePath: string, mainBranch: string): boolean {
|
|
|
77
77
|
}
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/** sync 核心操作的执行结果 */
|
|
81
|
+
export interface SyncResult {
|
|
82
|
+
/** 是否同步成功 */
|
|
83
|
+
success: boolean;
|
|
84
|
+
/** 是否存在合并冲突 */
|
|
85
|
+
hasConflict: boolean;
|
|
86
|
+
}
|
|
87
|
+
|
|
80
88
|
/**
|
|
81
|
-
* 执行 sync
|
|
82
|
-
*
|
|
83
|
-
* @param {
|
|
89
|
+
* 执行 sync 核心操作(检查未提交→自动保存→merge 主分支→清除快照)
|
|
90
|
+
* 不包含 worktree 解析交互,供 validate 等命令复用
|
|
91
|
+
* @param {string} targetWorktreePath - 目标 worktree 路径
|
|
92
|
+
* @param {string} branch - 分支名
|
|
93
|
+
* @returns {SyncResult} sync 执行结果
|
|
84
94
|
*/
|
|
85
|
-
|
|
86
|
-
validateMainWorktree();
|
|
87
|
-
|
|
88
|
-
logger.info(`sync 命令执行,分支: ${options.branch ?? '(未指定)'}`);
|
|
89
|
-
|
|
90
|
-
// 解析目标 worktree(精确匹配 / 模糊匹配 / 交互选择)
|
|
91
|
-
const worktrees = getProjectWorktrees();
|
|
92
|
-
const worktree = await resolveTargetWorktree(worktrees, SYNC_RESOLVE_MESSAGES, options.branch);
|
|
93
|
-
const { path: targetWorktreePath, branch } = worktree;
|
|
94
|
-
|
|
95
|
+
export function executeSyncForBranch(targetWorktreePath: string, branch: string): SyncResult {
|
|
95
96
|
// 获取主分支名(不硬编码 main/master)
|
|
96
97
|
const mainWorktreePath = getGitTopLevel();
|
|
97
98
|
const mainBranch = getCurrentBranch(mainWorktreePath);
|
|
@@ -107,7 +108,7 @@ async function handleSync(options: SyncOptions): Promise<void> {
|
|
|
107
108
|
|
|
108
109
|
if (hasConflict) {
|
|
109
110
|
printWarning(MESSAGES.SYNC_CONFLICT(targetWorktreePath));
|
|
110
|
-
return;
|
|
111
|
+
return { success: false, hasConflict: true };
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
// 合并成功后清除该分支的 validate 快照(代码基础已变化,旧快照无效)
|
|
@@ -118,4 +119,23 @@ async function handleSync(options: SyncOptions): Promise<void> {
|
|
|
118
119
|
}
|
|
119
120
|
|
|
120
121
|
printSuccess(MESSAGES.SYNC_SUCCESS(branch, mainBranch));
|
|
122
|
+
return { success: true, hasConflict: false };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 执行 sync 命令的核心逻辑
|
|
127
|
+
* 将主分支最新代码同步到目标 worktree
|
|
128
|
+
* @param {SyncOptions} options - 命令选项
|
|
129
|
+
*/
|
|
130
|
+
async function handleSync(options: SyncOptions): Promise<void> {
|
|
131
|
+
validateMainWorktree();
|
|
132
|
+
|
|
133
|
+
logger.info(`sync 命令执行,分支: ${options.branch ?? '(未指定)'}`);
|
|
134
|
+
|
|
135
|
+
// 解析目标 worktree(精确匹配 / 模糊匹配 / 交互选择)
|
|
136
|
+
const worktrees = getProjectWorktrees();
|
|
137
|
+
const worktree = await resolveTargetWorktree(worktrees, SYNC_RESOLVE_MESSAGES, options.branch);
|
|
138
|
+
const { path: targetWorktreePath, branch } = worktree;
|
|
139
|
+
|
|
140
|
+
executeSyncForBranch(targetWorktreePath, branch);
|
|
121
141
|
}
|
package/src/commands/validate.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { logger } from '../logger/index.js';
|
|
|
4
4
|
import { ClawtError } from '../errors/index.js';
|
|
5
5
|
import { MESSAGES } from '../constants/index.js';
|
|
6
6
|
import type { ValidateOptions } from '../types/index.js';
|
|
7
|
+
import { executeSyncForBranch } from './sync.js';
|
|
7
8
|
import {
|
|
8
9
|
validateMainWorktree,
|
|
9
10
|
getProjectName,
|
|
@@ -33,6 +34,7 @@ import {
|
|
|
33
34
|
writeSnapshot,
|
|
34
35
|
removeSnapshot,
|
|
35
36
|
confirmDestructiveAction,
|
|
37
|
+
confirmAction,
|
|
36
38
|
printSuccess,
|
|
37
39
|
printError,
|
|
38
40
|
printWarning,
|
|
@@ -121,8 +123,9 @@ async function handleDirtyMainWorktree(mainWorktreePath: string): Promise<void>
|
|
|
121
123
|
* @param {string} mainWorktreePath - 主 worktree 路径
|
|
122
124
|
* @param {string} branchName - 分支名
|
|
123
125
|
* @param {boolean} hasUncommitted - 目标 worktree 是否有未提交修改
|
|
126
|
+
* @returns {{ success: boolean }} patch 迁移结果
|
|
124
127
|
*/
|
|
125
|
-
function migrateChangesViaPatch(targetWorktreePath: string, mainWorktreePath: string, branchName: string, hasUncommitted: boolean):
|
|
128
|
+
function migrateChangesViaPatch(targetWorktreePath: string, mainWorktreePath: string, branchName: string, hasUncommitted: boolean): { success: boolean } {
|
|
126
129
|
let didTempCommit = false;
|
|
127
130
|
|
|
128
131
|
try {
|
|
@@ -143,9 +146,11 @@ function migrateChangesViaPatch(targetWorktreePath: string, mainWorktreePath: st
|
|
|
143
146
|
} catch (error) {
|
|
144
147
|
logger.warn(`patch apply 失败: ${error}`);
|
|
145
148
|
printWarning(MESSAGES.VALIDATE_PATCH_APPLY_FAILED(branchName));
|
|
146
|
-
|
|
149
|
+
return { success: false };
|
|
147
150
|
}
|
|
148
151
|
}
|
|
152
|
+
|
|
153
|
+
return { success: true };
|
|
149
154
|
} finally {
|
|
150
155
|
// 确保临时 commit 一定会被撤销,恢复目标 worktree 原状
|
|
151
156
|
// 每个操作独立 try-catch,避免前一个失败导致后续操作不执行
|
|
@@ -164,6 +169,31 @@ function migrateChangesViaPatch(targetWorktreePath: string, mainWorktreePath: st
|
|
|
164
169
|
}
|
|
165
170
|
}
|
|
166
171
|
|
|
172
|
+
/**
|
|
173
|
+
* patch apply 失败后的交互处理:询问用户是否自动执行 sync
|
|
174
|
+
* @param {string} targetWorktreePath - 目标 worktree 路径
|
|
175
|
+
* @param {string} branchName - 分支名
|
|
176
|
+
*/
|
|
177
|
+
async function handlePatchApplyFailure(targetWorktreePath: string, branchName: string): Promise<void> {
|
|
178
|
+
// 询问用户是否自动执行 sync
|
|
179
|
+
const confirmed = await confirmAction(MESSAGES.VALIDATE_CONFIRM_AUTO_SYNC(branchName));
|
|
180
|
+
|
|
181
|
+
if (!confirmed) {
|
|
182
|
+
// 用户拒绝自动 sync
|
|
183
|
+
printWarning(MESSAGES.VALIDATE_AUTO_SYNC_DECLINED(branchName));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 用户确认,执行 sync
|
|
188
|
+
printInfo(MESSAGES.VALIDATE_AUTO_SYNC_START(branchName));
|
|
189
|
+
const syncResult = executeSyncForBranch(targetWorktreePath, branchName);
|
|
190
|
+
|
|
191
|
+
if (syncResult.hasConflict) {
|
|
192
|
+
// sync 存在冲突,提示用户手动解决
|
|
193
|
+
printWarning(MESSAGES.VALIDATE_AUTO_SYNC_CONFLICT(targetWorktreePath));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
167
197
|
/**
|
|
168
198
|
* 保存当前主 worktree 工作目录变更为 git tree 对象快照
|
|
169
199
|
* 操作序列:git add . → git write-tree → git restore --staged .
|
|
@@ -231,9 +261,15 @@ async function handleValidateClean(options: ValidateOptions): Promise<void> {
|
|
|
231
261
|
* @param {string} branchName - 分支名
|
|
232
262
|
* @param {boolean} hasUncommitted - 目标 worktree 是否有未提交修改
|
|
233
263
|
*/
|
|
234
|
-
function handleFirstValidate(targetWorktreePath: string, mainWorktreePath: string, projectName: string, branchName: string, hasUncommitted: boolean): void {
|
|
264
|
+
async function handleFirstValidate(targetWorktreePath: string, mainWorktreePath: string, projectName: string, branchName: string, hasUncommitted: boolean): Promise<void> {
|
|
235
265
|
// 通过 patch 迁移目标分支全量变更到主 worktree
|
|
236
|
-
migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
|
|
266
|
+
const result = migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
|
|
267
|
+
|
|
268
|
+
if (!result.success) {
|
|
269
|
+
// patch 失败,询问用户是否自动 sync
|
|
270
|
+
await handlePatchApplyFailure(targetWorktreePath, branchName);
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
237
273
|
|
|
238
274
|
// 保存快照为 git tree 对象
|
|
239
275
|
saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
|
|
@@ -250,7 +286,7 @@ function handleFirstValidate(targetWorktreePath: string, mainWorktreePath: strin
|
|
|
250
286
|
* @param {string} branchName - 分支名
|
|
251
287
|
* @param {boolean} hasUncommitted - 目标 worktree 是否有未提交修改
|
|
252
288
|
*/
|
|
253
|
-
function handleIncrementalValidate(targetWorktreePath: string, mainWorktreePath: string, projectName: string, branchName: string, hasUncommitted: boolean): void {
|
|
289
|
+
async function handleIncrementalValidate(targetWorktreePath: string, mainWorktreePath: string, projectName: string, branchName: string, hasUncommitted: boolean): Promise<void> {
|
|
254
290
|
// 步骤 1:读取旧快照(tree hash + 当时的 HEAD commit hash)
|
|
255
291
|
const { treeHash: oldTreeHash, headCommitHash: oldHeadCommitHash } = readSnapshot(projectName, branchName);
|
|
256
292
|
|
|
@@ -262,7 +298,13 @@ function handleIncrementalValidate(targetWorktreePath: string, mainWorktreePath:
|
|
|
262
298
|
}
|
|
263
299
|
|
|
264
300
|
// 步骤 3:通过 patch 从目标分支获取最新全量变更
|
|
265
|
-
migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
|
|
301
|
+
const result = migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
|
|
302
|
+
|
|
303
|
+
if (!result.success) {
|
|
304
|
+
// patch 失败,询问用户是否自动 sync
|
|
305
|
+
await handlePatchApplyFailure(targetWorktreePath, branchName);
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
266
308
|
|
|
267
309
|
// 步骤 4:保存最新快照为 git tree 对象(同时记录当前 HEAD)
|
|
268
310
|
saveCurrentSnapshotTree(mainWorktreePath, projectName, branchName);
|
|
@@ -441,14 +483,14 @@ async function handleValidate(options: ValidateOptions): Promise<void> {
|
|
|
441
483
|
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
442
484
|
await handleDirtyMainWorktree(mainWorktreePath);
|
|
443
485
|
}
|
|
444
|
-
handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
|
|
486
|
+
await handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
|
|
445
487
|
} else {
|
|
446
488
|
// 首次模式:先确保主 worktree 干净
|
|
447
489
|
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
448
490
|
await handleDirtyMainWorktree(mainWorktreePath);
|
|
449
491
|
}
|
|
450
492
|
|
|
451
|
-
handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
|
|
493
|
+
await handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, branchName, hasUncommitted);
|
|
452
494
|
}
|
|
453
495
|
|
|
454
496
|
// validate 成功后执行用户指定的命令
|
|
@@ -53,4 +53,16 @@ export const VALIDATE_MESSAGES = {
|
|
|
53
53
|
/** 并行执行中单个命令启动失败 */
|
|
54
54
|
VALIDATE_PARALLEL_CMD_ERROR: (command: string, errorMessage: string) =>
|
|
55
55
|
` ✗ ${command}(错误: ${errorMessage})`,
|
|
56
|
+
/** patch apply 失败后询问用户是否执行 sync */
|
|
57
|
+
VALIDATE_CONFIRM_AUTO_SYNC: (branch: string) =>
|
|
58
|
+
`是否立即执行 sync 同步主分支到 ${branch}?`,
|
|
59
|
+
/** 自动 sync 开始提示 */
|
|
60
|
+
VALIDATE_AUTO_SYNC_START: (branch: string) =>
|
|
61
|
+
`正在自动同步主分支到 ${branch} ...`,
|
|
62
|
+
/** 自动 sync 存在冲突,无法重试 */
|
|
63
|
+
VALIDATE_AUTO_SYNC_CONFLICT: (worktreePath: string) =>
|
|
64
|
+
`同步存在冲突,请进入目标 worktree 手动解决冲突后重试\n cd ${worktreePath}`,
|
|
65
|
+
/** 用户拒绝自动 sync */
|
|
66
|
+
VALIDATE_AUTO_SYNC_DECLINED: (branch: string) =>
|
|
67
|
+
`请手动执行 clawt sync -b ${branch} 同步主分支后重试`,
|
|
56
68
|
} as const;
|