clawt 3.5.0 → 3.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +158 -9
- package/dist/postinstall.js +25 -1
- package/package.json +1 -1
- package/src/commands/merge.ts +20 -5
- package/src/constants/ai-prompts.ts +14 -0
- package/src/constants/config.ts +9 -0
- package/src/constants/index.ts +1 -0
- package/src/constants/messages/merge.ts +15 -0
- package/src/types/command.ts +2 -0
- package/src/types/config.ts +4 -0
- package/src/utils/conflict-resolver.ts +170 -0
- package/src/utils/git-core.ts +49 -1
- package/src/utils/index.ts +6 -1
- package/tests/unit/commands/merge.test.ts +59 -3
- package/tests/unit/utils/conflict-resolver.test.ts +250 -0
- package/src/constants/messages.ts +0 -179
package/dist/index.js
CHANGED
|
@@ -173,7 +173,22 @@ ${branches.map((b) => ` - ${b}`).join("\n")}`,
|
|
|
173
173
|
/** merge 交互选择提示 */
|
|
174
174
|
MERGE_SELECT_BRANCH: "\u8BF7\u9009\u62E9\u8981\u5408\u5E76\u7684\u5206\u652F",
|
|
175
175
|
/** merge 模糊匹配到多个结果提示 */
|
|
176
|
-
MERGE_MULTIPLE_MATCHES: (name) => `"${name}" \u5339\u914D\u5230\u591A\u4E2A\u5206\u652F\uFF0C\u8BF7\u9009\u62E9\uFF1A
|
|
176
|
+
MERGE_MULTIPLE_MATCHES: (name) => `"${name}" \u5339\u914D\u5230\u591A\u4E2A\u5206\u652F\uFF0C\u8BF7\u9009\u62E9\uFF1A`,
|
|
177
|
+
/** 询问是否使用 AI 辅助解决冲突 */
|
|
178
|
+
MERGE_CONFLICT_ASK_AI: "\u68C0\u6D4B\u5230\u5408\u5E76\u51B2\u7A81\uFF0C\u662F\u5426\u4F7F\u7528 Claude Code \u81EA\u52A8\u89E3\u51B3\uFF1F",
|
|
179
|
+
/** AI 冲突解决开始 */
|
|
180
|
+
MERGE_CONFLICT_AI_START: (fileCount) => `\u6B63\u5728\u4F7F\u7528 Claude Code \u5206\u6790\u5E76\u89E3\u51B3 ${fileCount} \u4E2A\u51B2\u7A81\u6587\u4EF6...`,
|
|
181
|
+
/** AI 冲突解决成功 */
|
|
182
|
+
MERGE_CONFLICT_AI_SUCCESS: "\u2713 Claude Code \u5DF2\u6210\u529F\u89E3\u51B3\u6240\u6709\u51B2\u7A81",
|
|
183
|
+
/** AI 冲突解决后仍有未解决的冲突 */
|
|
184
|
+
MERGE_CONFLICT_AI_PARTIAL: (remaining) => `Claude Code \u5DF2\u5904\u7406\u51B2\u7A81\u6587\u4EF6\uFF0C\u4F46\u4ECD\u6709 ${remaining} \u4E2A\u6587\u4EF6\u5B58\u5728\u51B2\u7A81
|
|
185
|
+
\u8BF7\u624B\u52A8\u5904\u7406\u5269\u4F59\u51B2\u7A81\u540E\u6267\u884C git add . && git merge --continue`,
|
|
186
|
+
/** AI 冲突解决失败 */
|
|
187
|
+
MERGE_CONFLICT_AI_FAILED: (errorMsg) => `Claude Code \u89E3\u51B3\u51B2\u7A81\u5931\u8D25: ${errorMsg}
|
|
188
|
+
\u8BF7\u624B\u52A8\u5904\u7406\uFF1A
|
|
189
|
+
\u89E3\u51B3\u51B2\u7A81\u540E\u6267\u884C git add . && git merge --continue`,
|
|
190
|
+
/** --auto 模式下的冲突手动解决(配置为 manual) */
|
|
191
|
+
MERGE_CONFLICT_MANUAL: "\u5408\u5E76\u5B58\u5728\u51B2\u7A81\uFF0C\u8BF7\u624B\u52A8\u5904\u7406\uFF1A\n \u89E3\u51B3\u51B2\u7A81\u540E\u6267\u884C git add . && git merge --continue"
|
|
177
192
|
};
|
|
178
193
|
|
|
179
194
|
// src/constants/messages/validate.ts
|
|
@@ -667,6 +682,15 @@ var CONFIG_DEFINITIONS = {
|
|
|
667
682
|
autoUpdate: {
|
|
668
683
|
defaultValue: true,
|
|
669
684
|
description: "\u662F\u5426\u542F\u7528\u81EA\u52A8\u66F4\u65B0\u68C0\u67E5\uFF08\u6BCF 24 \u5C0F\u65F6\u68C0\u67E5\u4E00\u6B21 npm registry\uFF09"
|
|
685
|
+
},
|
|
686
|
+
conflictResolveMode: {
|
|
687
|
+
defaultValue: "ask",
|
|
688
|
+
description: "merge \u51B2\u7A81\u65F6\u7684\u89E3\u51B3\u6A21\u5F0F\uFF1Aask\uFF08\u8BE2\u95EE\u662F\u5426\u4F7F\u7528 AI\uFF09\u3001auto\uFF08\u81EA\u52A8 AI \u89E3\u51B3\uFF09\u3001manual\uFF08\u624B\u52A8\u89E3\u51B3\uFF09",
|
|
689
|
+
allowedValues: ["ask", "auto", "manual"]
|
|
690
|
+
},
|
|
691
|
+
conflictResolveTimeoutMs: {
|
|
692
|
+
defaultValue: 3e5,
|
|
693
|
+
description: "Claude Code \u51B2\u7A81\u89E3\u51B3\u8D85\u65F6\u65F6\u95F4\uFF08\u6BEB\u79D2\uFF09\uFF0C\u9ED8\u8BA4 300000\uFF085 \u5206\u949F\uFF09"
|
|
670
694
|
}
|
|
671
695
|
};
|
|
672
696
|
function deriveDefaultConfig(definitions) {
|
|
@@ -768,6 +792,21 @@ var GROUP_SEPARATOR_LABEL = (dateLabel, relativeTime) => `\u2550\u2550\u2550\u25
|
|
|
768
792
|
var UNKNOWN_DATE_GROUP = "\u672A\u77E5\u65E5\u671F";
|
|
769
793
|
var UNKNOWN_DATE_SEPARATOR_LABEL = `\u2550\u2550\u2550\u2550 ${chalk2.bold.hex("#FF8C00")("\u672A\u77E5\u65E5\u671F")} \u2550\u2550\u2550\u2550`;
|
|
770
794
|
|
|
795
|
+
// src/constants/ai-prompts.ts
|
|
796
|
+
var CONFLICT_RESOLVE_PROMPT = `\u4F60\u662F\u4E00\u4E2A Git \u5408\u5E76\u51B2\u7A81\u89E3\u51B3\u4E13\u5BB6\u3002\u5F53\u524D\u4ED3\u5E93\u5904\u4E8E\u5408\u5E76\u51B2\u7A81\u72B6\u6001\u3002
|
|
797
|
+
|
|
798
|
+
## \u4EFB\u52A1
|
|
799
|
+
|
|
800
|
+
1. \u901A\u8FC7 git status \u548C git diff \u7B49\u547D\u4EE4\uFF0C\u81EA\u884C\u67E5\u770B\u5F53\u524D\u4ED3\u5E93\u7684\u51B2\u7A81\u6587\u4EF6\u5217\u8868\u53CA\u51B2\u7A81\u5185\u5BB9
|
|
801
|
+
2. \u901A\u8FC7 git log \u7B49\u547D\u4EE4\uFF0C\u5206\u6790\u4E24\u4E2A\u5206\u652F\u5404\u81EA\u7684\u53D8\u66F4\u610F\u56FE
|
|
802
|
+
3. \u76F4\u63A5\u7F16\u8F91\u6BCF\u4E2A\u51B2\u7A81\u6587\u4EF6\uFF0C\u79FB\u9664\u6240\u6709\u51B2\u7A81\u6807\u8BB0\uFF08<<<<<<<\u3001=======\u3001>>>>>>>\uFF09
|
|
803
|
+
4. \u4FDD\u7559\u53CC\u65B9\u6709\u610F\u4E49\u7684\u53D8\u66F4\uFF0C\u5408\u7406\u5408\u5E76\u4EE3\u7801\u903B\u8F91
|
|
804
|
+
5. \u5982\u679C\u4E24\u4E2A\u5206\u652F\u4FEE\u6539\u4E86\u540C\u4E00\u6BB5\u4EE3\u7801\u4F46\u610F\u56FE\u4E0D\u540C\uFF0C\u4F18\u5148\u4FDD\u8BC1\u4EE3\u7801\u7684\u6B63\u786E\u6027\u548C\u5B8C\u6574\u6027
|
|
805
|
+
6. \u89E3\u51B3\u51B2\u7A81\u540E\uFF0C\u786E\u4FDD\u4EE3\u7801\u8BED\u6CD5\u6B63\u786E\u3001\u903B\u8F91\u5B8C\u6574
|
|
806
|
+
7. \u4E0D\u8981\u6DFB\u52A0\u4EFB\u4F55\u6CE8\u91CA\u8BF4\u660E\u4F60\u505A\u4E86\u4EC0\u4E48\u4FEE\u6539\uFF0C\u53EA\u9700\u8981\u4FEE\u6539\u6587\u4EF6\u5185\u5BB9
|
|
807
|
+
|
|
808
|
+
\u8BF7\u76F4\u63A5\u5F00\u59CB\u3002`;
|
|
809
|
+
|
|
771
810
|
// src/constants/pre-checks.ts
|
|
772
811
|
var PRE_CHECK_CREATE = {
|
|
773
812
|
requireMainWorktree: true,
|
|
@@ -962,7 +1001,7 @@ function runParallelCommands(commands, options) {
|
|
|
962
1001
|
|
|
963
1002
|
// src/utils/git-core.ts
|
|
964
1003
|
import { basename } from "path";
|
|
965
|
-
import { execSync as execSync2 } from "child_process";
|
|
1004
|
+
import { execSync as execSync2, execFileSync as execFileSync2 } from "child_process";
|
|
966
1005
|
function getGitCommonDir(cwd) {
|
|
967
1006
|
return execCommand("git rev-parse --git-common-dir", { cwd });
|
|
968
1007
|
}
|
|
@@ -1085,6 +1124,24 @@ function gitApplyCachedCheck(patchContent, cwd) {
|
|
|
1085
1124
|
return false;
|
|
1086
1125
|
}
|
|
1087
1126
|
}
|
|
1127
|
+
function getConflictFiles(cwd) {
|
|
1128
|
+
const status = getStatusPorcelain(cwd);
|
|
1129
|
+
if (!status) return [];
|
|
1130
|
+
return status.split("\n").filter((line) => /^(UU|AA|DD|DU|UD|AU|UA)/.test(line)).map((line) => line.slice(3));
|
|
1131
|
+
}
|
|
1132
|
+
function gitAddFiles(files, cwd) {
|
|
1133
|
+
if (files.length === 0) return;
|
|
1134
|
+
const args = ["add", "--", ...files];
|
|
1135
|
+
logger.debug(`\u6267\u884C\u547D\u4EE4: git ${args.join(" ")}${cwd ? ` (cwd: ${cwd})` : ""}`);
|
|
1136
|
+
execFileSync2("git", args, {
|
|
1137
|
+
cwd,
|
|
1138
|
+
encoding: "utf-8",
|
|
1139
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
function gitMergeContinue(cwd) {
|
|
1143
|
+
execCommand("GIT_EDITOR=true git merge --continue", { cwd });
|
|
1144
|
+
}
|
|
1088
1145
|
|
|
1089
1146
|
// src/utils/git-branch.ts
|
|
1090
1147
|
function checkBranchExists(branchName, cwd) {
|
|
@@ -1705,7 +1762,7 @@ import { existsSync as existsSync7, readdirSync as readdirSync3 } from "fs";
|
|
|
1705
1762
|
import { join as join5 } from "path";
|
|
1706
1763
|
|
|
1707
1764
|
// src/utils/terminal.ts
|
|
1708
|
-
import { execFileSync as
|
|
1765
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
1709
1766
|
import { existsSync as existsSync6 } from "fs";
|
|
1710
1767
|
function isITerm2Installed() {
|
|
1711
1768
|
return existsSync6(ITERM2_APP_PATH);
|
|
@@ -1764,7 +1821,7 @@ function openCommandInNewTerminalTab(command, tabTitle) {
|
|
|
1764
1821
|
logger.debug(`\u6253\u5F00\u7EC8\u7AEF Tab [${terminalApp}]: ${tabTitle}`);
|
|
1765
1822
|
logger.debug(`\u6267\u884C\u547D\u4EE4: ${command}`);
|
|
1766
1823
|
try {
|
|
1767
|
-
|
|
1824
|
+
execFileSync3("osascript", ["-e", script], {
|
|
1768
1825
|
encoding: "utf-8",
|
|
1769
1826
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1770
1827
|
});
|
|
@@ -3948,6 +4005,89 @@ var InteractivePanel = class {
|
|
|
3948
4005
|
}
|
|
3949
4006
|
};
|
|
3950
4007
|
|
|
4008
|
+
// src/utils/conflict-resolver.ts
|
|
4009
|
+
import { execFileSync as execFileSync4 } from "child_process";
|
|
4010
|
+
var DEFAULT_CONFLICT_RESOLVE_TIMEOUT_MS = 3e5;
|
|
4011
|
+
function buildConflictResolvePrompt() {
|
|
4012
|
+
return CONFLICT_RESOLVE_PROMPT;
|
|
4013
|
+
}
|
|
4014
|
+
function getConflictResolveTimeout() {
|
|
4015
|
+
const configValue = getConfigValue("conflictResolveTimeoutMs");
|
|
4016
|
+
if (typeof configValue === "number" && configValue > 0) {
|
|
4017
|
+
return configValue;
|
|
4018
|
+
}
|
|
4019
|
+
return DEFAULT_CONFLICT_RESOLVE_TIMEOUT_MS;
|
|
4020
|
+
}
|
|
4021
|
+
function invokeClaudeForConflictResolve(prompt, cwd) {
|
|
4022
|
+
const args = ["-p", prompt, "--permission-mode", "bypassPermissions"];
|
|
4023
|
+
logger.info(`\u8C03\u7528 Claude Code \u89E3\u51B3\u51B2\u7A81\uFF0C\u547D\u4EE4: claude -p "..." --permission-mode bypassPermissions`);
|
|
4024
|
+
try {
|
|
4025
|
+
const output = execFileSync4("claude", args, {
|
|
4026
|
+
cwd,
|
|
4027
|
+
encoding: "utf-8",
|
|
4028
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
4029
|
+
timeout: getConflictResolveTimeout()
|
|
4030
|
+
});
|
|
4031
|
+
return output;
|
|
4032
|
+
} catch (error) {
|
|
4033
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
4034
|
+
logger.error(`Claude Code \u51B2\u7A81\u89E3\u51B3\u5931\u8D25: ${errMsg}`);
|
|
4035
|
+
throw new ClawtError(MESSAGES.MERGE_CONFLICT_AI_FAILED(errMsg));
|
|
4036
|
+
}
|
|
4037
|
+
}
|
|
4038
|
+
function resolveConflictsWithAI(currentBranch, incomingBranch, cwd) {
|
|
4039
|
+
const conflictFiles = getConflictFiles(cwd);
|
|
4040
|
+
if (conflictFiles.length === 0) {
|
|
4041
|
+
return true;
|
|
4042
|
+
}
|
|
4043
|
+
printInfo(MESSAGES.MERGE_CONFLICT_AI_START(conflictFiles.length));
|
|
4044
|
+
const prompt = buildConflictResolvePrompt();
|
|
4045
|
+
try {
|
|
4046
|
+
invokeClaudeForConflictResolve(prompt, cwd);
|
|
4047
|
+
} catch (error) {
|
|
4048
|
+
const errMsg = error instanceof ClawtError ? error.message : String(error);
|
|
4049
|
+
printWarning(errMsg);
|
|
4050
|
+
return false;
|
|
4051
|
+
}
|
|
4052
|
+
const remainingConflicts = getConflictFiles(cwd);
|
|
4053
|
+
if (remainingConflicts.length === 0) {
|
|
4054
|
+
gitAddFiles(conflictFiles, cwd);
|
|
4055
|
+
gitMergeContinue(cwd);
|
|
4056
|
+
printSuccess(MESSAGES.MERGE_CONFLICT_AI_SUCCESS);
|
|
4057
|
+
return true;
|
|
4058
|
+
}
|
|
4059
|
+
const resolvedFiles = conflictFiles.filter((f) => !remainingConflicts.includes(f));
|
|
4060
|
+
if (resolvedFiles.length > 0) {
|
|
4061
|
+
gitAddFiles(resolvedFiles, cwd);
|
|
4062
|
+
}
|
|
4063
|
+
printWarning(MESSAGES.MERGE_CONFLICT_AI_PARTIAL(remainingConflicts.length));
|
|
4064
|
+
return false;
|
|
4065
|
+
}
|
|
4066
|
+
function determineConflictResolveMode(autoFlag) {
|
|
4067
|
+
if (autoFlag === true) {
|
|
4068
|
+
return "auto";
|
|
4069
|
+
}
|
|
4070
|
+
const configMode = getConfigValue("conflictResolveMode");
|
|
4071
|
+
if (configMode === "auto" || configMode === "manual") {
|
|
4072
|
+
return configMode;
|
|
4073
|
+
}
|
|
4074
|
+
return "ask";
|
|
4075
|
+
}
|
|
4076
|
+
async function handleMergeConflict(currentBranch, incomingBranch, cwd, autoFlag) {
|
|
4077
|
+
const mode = determineConflictResolveMode(autoFlag);
|
|
4078
|
+
if (mode === "manual") {
|
|
4079
|
+
throw new ClawtError(MESSAGES.MERGE_CONFLICT_MANUAL);
|
|
4080
|
+
}
|
|
4081
|
+
if (mode === "auto") {
|
|
4082
|
+
return resolveConflictsWithAI(currentBranch, incomingBranch, cwd);
|
|
4083
|
+
}
|
|
4084
|
+
const shouldUseAI = await confirmAction(MESSAGES.MERGE_CONFLICT_ASK_AI);
|
|
4085
|
+
if (!shouldUseAI) {
|
|
4086
|
+
throw new ClawtError(MESSAGES.MERGE_CONFLICT_MANUAL);
|
|
4087
|
+
}
|
|
4088
|
+
return resolveConflictsWithAI(currentBranch, incomingBranch, cwd);
|
|
4089
|
+
}
|
|
4090
|
+
|
|
3951
4091
|
// src/commands/list.ts
|
|
3952
4092
|
import chalk10 from "chalk";
|
|
3953
4093
|
function registerListCommand(program2) {
|
|
@@ -4527,7 +4667,7 @@ async function handleCoverValidate() {
|
|
|
4527
4667
|
|
|
4528
4668
|
// src/commands/merge.ts
|
|
4529
4669
|
function registerMergeCommand(program2) {
|
|
4530
|
-
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) => {
|
|
4670
|
+
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").option("--auto", "\u9047\u5230\u51B2\u7A81\u76F4\u63A5\u8C03\u7528 AI \u89E3\u51B3\uFF0C\u4E0D\u518D\u8BE2\u95EE").action(async (options) => {
|
|
4531
4671
|
await handleMerge(options);
|
|
4532
4672
|
});
|
|
4533
4673
|
}
|
|
@@ -4599,16 +4739,25 @@ async function handleMerge(options) {
|
|
|
4599
4739
|
throw new ClawtError(MESSAGES.TARGET_WORKTREE_NO_CHANGES);
|
|
4600
4740
|
}
|
|
4601
4741
|
}
|
|
4742
|
+
let mergeHadConflict = false;
|
|
4602
4743
|
try {
|
|
4603
4744
|
gitMerge(branch, mainWorktreePath);
|
|
4604
4745
|
} catch (error) {
|
|
4605
4746
|
if (hasMergeConflict(mainWorktreePath)) {
|
|
4606
|
-
|
|
4747
|
+
mergeHadConflict = true;
|
|
4748
|
+
} else {
|
|
4749
|
+
throw error;
|
|
4607
4750
|
}
|
|
4608
|
-
throw error;
|
|
4609
4751
|
}
|
|
4610
|
-
if (hasMergeConflict(mainWorktreePath)) {
|
|
4611
|
-
|
|
4752
|
+
if (!mergeHadConflict && hasMergeConflict(mainWorktreePath)) {
|
|
4753
|
+
mergeHadConflict = true;
|
|
4754
|
+
}
|
|
4755
|
+
if (mergeHadConflict) {
|
|
4756
|
+
const currentBranch = getCurrentBranch(mainWorktreePath);
|
|
4757
|
+
const resolved = await handleMergeConflict(currentBranch, branch, mainWorktreePath, options.auto);
|
|
4758
|
+
if (!resolved) {
|
|
4759
|
+
return;
|
|
4760
|
+
}
|
|
4612
4761
|
}
|
|
4613
4762
|
const autoPullPush = getConfigValue("autoPullPush");
|
|
4614
4763
|
if (autoPullPush) {
|
package/dist/postinstall.js
CHANGED
|
@@ -164,7 +164,22 @@ ${branches.map((b) => ` - ${b}`).join("\n")}`,
|
|
|
164
164
|
/** merge 交互选择提示 */
|
|
165
165
|
MERGE_SELECT_BRANCH: "\u8BF7\u9009\u62E9\u8981\u5408\u5E76\u7684\u5206\u652F",
|
|
166
166
|
/** merge 模糊匹配到多个结果提示 */
|
|
167
|
-
MERGE_MULTIPLE_MATCHES: (name) => `"${name}" \u5339\u914D\u5230\u591A\u4E2A\u5206\u652F\uFF0C\u8BF7\u9009\u62E9\uFF1A
|
|
167
|
+
MERGE_MULTIPLE_MATCHES: (name) => `"${name}" \u5339\u914D\u5230\u591A\u4E2A\u5206\u652F\uFF0C\u8BF7\u9009\u62E9\uFF1A`,
|
|
168
|
+
/** 询问是否使用 AI 辅助解决冲突 */
|
|
169
|
+
MERGE_CONFLICT_ASK_AI: "\u68C0\u6D4B\u5230\u5408\u5E76\u51B2\u7A81\uFF0C\u662F\u5426\u4F7F\u7528 Claude Code \u81EA\u52A8\u89E3\u51B3\uFF1F",
|
|
170
|
+
/** AI 冲突解决开始 */
|
|
171
|
+
MERGE_CONFLICT_AI_START: (fileCount) => `\u6B63\u5728\u4F7F\u7528 Claude Code \u5206\u6790\u5E76\u89E3\u51B3 ${fileCount} \u4E2A\u51B2\u7A81\u6587\u4EF6...`,
|
|
172
|
+
/** AI 冲突解决成功 */
|
|
173
|
+
MERGE_CONFLICT_AI_SUCCESS: "\u2713 Claude Code \u5DF2\u6210\u529F\u89E3\u51B3\u6240\u6709\u51B2\u7A81",
|
|
174
|
+
/** AI 冲突解决后仍有未解决的冲突 */
|
|
175
|
+
MERGE_CONFLICT_AI_PARTIAL: (remaining) => `Claude Code \u5DF2\u5904\u7406\u51B2\u7A81\u6587\u4EF6\uFF0C\u4F46\u4ECD\u6709 ${remaining} \u4E2A\u6587\u4EF6\u5B58\u5728\u51B2\u7A81
|
|
176
|
+
\u8BF7\u624B\u52A8\u5904\u7406\u5269\u4F59\u51B2\u7A81\u540E\u6267\u884C git add . && git merge --continue`,
|
|
177
|
+
/** AI 冲突解决失败 */
|
|
178
|
+
MERGE_CONFLICT_AI_FAILED: (errorMsg) => `Claude Code \u89E3\u51B3\u51B2\u7A81\u5931\u8D25: ${errorMsg}
|
|
179
|
+
\u8BF7\u624B\u52A8\u5904\u7406\uFF1A
|
|
180
|
+
\u89E3\u51B3\u51B2\u7A81\u540E\u6267\u884C git add . && git merge --continue`,
|
|
181
|
+
/** --auto 模式下的冲突手动解决(配置为 manual) */
|
|
182
|
+
MERGE_CONFLICT_MANUAL: "\u5408\u5E76\u5B58\u5728\u51B2\u7A81\uFF0C\u8BF7\u624B\u52A8\u5904\u7406\uFF1A\n \u89E3\u51B3\u51B2\u7A81\u540E\u6267\u884C git add . && git merge --continue"
|
|
168
183
|
};
|
|
169
184
|
|
|
170
185
|
// src/constants/messages/validate.ts
|
|
@@ -603,6 +618,15 @@ var CONFIG_DEFINITIONS = {
|
|
|
603
618
|
autoUpdate: {
|
|
604
619
|
defaultValue: true,
|
|
605
620
|
description: "\u662F\u5426\u542F\u7528\u81EA\u52A8\u66F4\u65B0\u68C0\u67E5\uFF08\u6BCF 24 \u5C0F\u65F6\u68C0\u67E5\u4E00\u6B21 npm registry\uFF09"
|
|
621
|
+
},
|
|
622
|
+
conflictResolveMode: {
|
|
623
|
+
defaultValue: "ask",
|
|
624
|
+
description: "merge \u51B2\u7A81\u65F6\u7684\u89E3\u51B3\u6A21\u5F0F\uFF1Aask\uFF08\u8BE2\u95EE\u662F\u5426\u4F7F\u7528 AI\uFF09\u3001auto\uFF08\u81EA\u52A8 AI \u89E3\u51B3\uFF09\u3001manual\uFF08\u624B\u52A8\u89E3\u51B3\uFF09",
|
|
625
|
+
allowedValues: ["ask", "auto", "manual"]
|
|
626
|
+
},
|
|
627
|
+
conflictResolveTimeoutMs: {
|
|
628
|
+
defaultValue: 3e5,
|
|
629
|
+
description: "Claude Code \u51B2\u7A81\u89E3\u51B3\u8D85\u65F6\u65F6\u95F4\uFF08\u6BEB\u79D2\uFF09\uFF0C\u9ED8\u8BA4 300000\uFF085 \u5206\u949F\uFF09"
|
|
606
630
|
}
|
|
607
631
|
};
|
|
608
632
|
function deriveDefaultConfig(definitions) {
|
package/package.json
CHANGED
package/src/commands/merge.ts
CHANGED
|
@@ -33,6 +33,8 @@ import {
|
|
|
33
33
|
gitCheckout,
|
|
34
34
|
resolveTargetWorktree,
|
|
35
35
|
getMainWorkBranch,
|
|
36
|
+
getCurrentBranch,
|
|
37
|
+
handleMergeConflict,
|
|
36
38
|
} from '../utils/index.js';
|
|
37
39
|
import type { WorktreeResolveMessages } from '../utils/index.js';
|
|
38
40
|
|
|
@@ -46,6 +48,7 @@ export function registerMergeCommand(program: Command): void {
|
|
|
46
48
|
.description('合并某个已验证的 worktree 分支到主 worktree')
|
|
47
49
|
.option('-b, --branch <branchName>', '要合并的分支名(支持模糊匹配,不传则列出所有分支供选择)')
|
|
48
50
|
.option('-m, --message <commitMessage>', '提交信息(目标 worktree 工作区有修改时必填)')
|
|
51
|
+
.option('--auto', '遇到冲突直接调用 AI 解决,不再询问')
|
|
49
52
|
.action(async (options: MergeOptions) => {
|
|
50
53
|
await handleMerge(options);
|
|
51
54
|
});
|
|
@@ -182,19 +185,31 @@ async function handleMerge(options: MergeOptions): Promise<void> {
|
|
|
182
185
|
}
|
|
183
186
|
|
|
184
187
|
// 步骤 5:回到主 worktree 进行合并
|
|
188
|
+
let mergeHadConflict = false;
|
|
185
189
|
try {
|
|
186
190
|
gitMerge(branch, mainWorktreePath);
|
|
187
191
|
} catch (error) {
|
|
188
192
|
// 检查是否有冲突
|
|
189
193
|
if (hasMergeConflict(mainWorktreePath)) {
|
|
190
|
-
|
|
194
|
+
mergeHadConflict = true;
|
|
195
|
+
} else {
|
|
196
|
+
throw error;
|
|
191
197
|
}
|
|
192
|
-
throw error;
|
|
193
198
|
}
|
|
194
199
|
|
|
195
|
-
// 步骤
|
|
196
|
-
if (hasMergeConflict(mainWorktreePath)) {
|
|
197
|
-
|
|
200
|
+
// 步骤 5.5:冲突检测(二次确认)
|
|
201
|
+
if (!mergeHadConflict && hasMergeConflict(mainWorktreePath)) {
|
|
202
|
+
mergeHadConflict = true;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 步骤 5.6:如果有冲突,尝试 AI 辅助解决
|
|
206
|
+
if (mergeHadConflict) {
|
|
207
|
+
const currentBranch = getCurrentBranch(mainWorktreePath);
|
|
208
|
+
const resolved = await handleMergeConflict(currentBranch, branch, mainWorktreePath, options.auto);
|
|
209
|
+
if (!resolved) {
|
|
210
|
+
// AI 未能完全解决冲突,流程中止(handleMergeConflict 内部已输出提示)
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
198
213
|
}
|
|
199
214
|
|
|
200
215
|
// 步骤 7:根据配置决定是否自动 pull 和 push
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Claude Code 冲突解决指令性 prompt */
|
|
2
|
+
export const CONFLICT_RESOLVE_PROMPT = `你是一个 Git 合并冲突解决专家。当前仓库处于合并冲突状态。
|
|
3
|
+
|
|
4
|
+
## 任务
|
|
5
|
+
|
|
6
|
+
1. 通过 git status 和 git diff 等命令,自行查看当前仓库的冲突文件列表及冲突内容
|
|
7
|
+
2. 通过 git log 等命令,分析两个分支各自的变更意图
|
|
8
|
+
3. 直接编辑每个冲突文件,移除所有冲突标记(<<<<<<<、=======、>>>>>>>)
|
|
9
|
+
4. 保留双方有意义的变更,合理合并代码逻辑
|
|
10
|
+
5. 如果两个分支修改了同一段代码但意图不同,优先保证代码的正确性和完整性
|
|
11
|
+
6. 解决冲突后,确保代码语法正确、逻辑完整
|
|
12
|
+
7. 不要添加任何注释说明你做了什么修改,只需要修改文件内容
|
|
13
|
+
|
|
14
|
+
请直接开始。`;
|
package/src/constants/config.ts
CHANGED
|
@@ -47,6 +47,15 @@ export const CONFIG_DEFINITIONS: ConfigDefinitions = {
|
|
|
47
47
|
defaultValue: true,
|
|
48
48
|
description: '是否启用自动更新检查(每 24 小时检查一次 npm registry)',
|
|
49
49
|
},
|
|
50
|
+
conflictResolveMode: {
|
|
51
|
+
defaultValue: 'ask',
|
|
52
|
+
description: 'merge 冲突时的解决模式:ask(询问是否使用 AI)、auto(自动 AI 解决)、manual(手动解决)',
|
|
53
|
+
allowedValues: ['ask', 'auto', 'manual'] as const,
|
|
54
|
+
},
|
|
55
|
+
conflictResolveTimeoutMs: {
|
|
56
|
+
defaultValue: 300000,
|
|
57
|
+
description: 'Claude Code 冲突解决超时时间(毫秒),默认 300000(5 分钟)',
|
|
58
|
+
},
|
|
50
59
|
};
|
|
51
60
|
|
|
52
61
|
/**
|
package/src/constants/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ export {
|
|
|
31
31
|
CURSOR_HOME,
|
|
32
32
|
} from './progress.js';
|
|
33
33
|
export { SELECT_ALL_NAME, SELECT_ALL_LABEL, GROUP_SELECT_ALL_PREFIX, GROUP_SELECT_ALL_LABEL, GROUP_SEPARATOR_LABEL, UNKNOWN_DATE_GROUP, UNKNOWN_DATE_SEPARATOR_LABEL } from './prompt.js';
|
|
34
|
+
export { CONFLICT_RESOLVE_PROMPT } from './ai-prompts.js';
|
|
34
35
|
export {
|
|
35
36
|
PANEL_REFRESH_INTERVAL_MS,
|
|
36
37
|
PANEL_COUNTDOWN_INTERVAL_MS,
|
|
@@ -40,4 +40,19 @@ export const MERGE_MESSAGES = {
|
|
|
40
40
|
MERGE_SELECT_BRANCH: '请选择要合并的分支',
|
|
41
41
|
/** merge 模糊匹配到多个结果提示 */
|
|
42
42
|
MERGE_MULTIPLE_MATCHES: (name: string) => `"${name}" 匹配到多个分支,请选择:`,
|
|
43
|
+
/** 询问是否使用 AI 辅助解决冲突 */
|
|
44
|
+
MERGE_CONFLICT_ASK_AI: '检测到合并冲突,是否使用 Claude Code 自动解决?',
|
|
45
|
+
/** AI 冲突解决开始 */
|
|
46
|
+
MERGE_CONFLICT_AI_START: (fileCount: number) =>
|
|
47
|
+
`正在使用 Claude Code 分析并解决 ${fileCount} 个冲突文件...`,
|
|
48
|
+
/** AI 冲突解决成功 */
|
|
49
|
+
MERGE_CONFLICT_AI_SUCCESS: '✓ Claude Code 已成功解决所有冲突',
|
|
50
|
+
/** AI 冲突解决后仍有未解决的冲突 */
|
|
51
|
+
MERGE_CONFLICT_AI_PARTIAL: (remaining: number) =>
|
|
52
|
+
`Claude Code 已处理冲突文件,但仍有 ${remaining} 个文件存在冲突\n 请手动处理剩余冲突后执行 git add . && git merge --continue`,
|
|
53
|
+
/** AI 冲突解决失败 */
|
|
54
|
+
MERGE_CONFLICT_AI_FAILED: (errorMsg: string) =>
|
|
55
|
+
`Claude Code 解决冲突失败: ${errorMsg}\n 请手动处理:\n 解决冲突后执行 git add . && git merge --continue`,
|
|
56
|
+
/** --auto 模式下的冲突手动解决(配置为 manual) */
|
|
57
|
+
MERGE_CONFLICT_MANUAL: '合并存在冲突,请手动处理:\n 解决冲突后执行 git add . && git merge --continue',
|
|
43
58
|
} as const;
|
package/src/types/command.ts
CHANGED
package/src/types/config.ts
CHANGED
|
@@ -18,6 +18,10 @@ export interface ClawtConfig {
|
|
|
18
18
|
aliases: Record<string, string>;
|
|
19
19
|
/** 是否启用自动更新检查 */
|
|
20
20
|
autoUpdate: boolean;
|
|
21
|
+
/** merge 冲突时的解决模式:ask(询问)、auto(自动 AI 解决)、manual(手动解决) */
|
|
22
|
+
conflictResolveMode: 'ask' | 'auto' | 'manual';
|
|
23
|
+
/** Claude Code 冲突解决超时时间(毫秒),默认 300000(5 分钟) */
|
|
24
|
+
conflictResolveTimeoutMs: number;
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
/** 单个配置项的完整定义(默认值 + 描述) */
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { logger } from '../logger/index.js';
|
|
3
|
+
import { ClawtError } from '../errors/index.js';
|
|
4
|
+
import { getConfigValue } from './config.js';
|
|
5
|
+
import { getConflictFiles, gitAddFiles, gitMergeContinue } from './git.js';
|
|
6
|
+
import { printInfo, printSuccess, printWarning } from './formatter.js';
|
|
7
|
+
import { confirmAction } from './formatter.js';
|
|
8
|
+
import { MESSAGES } from '../constants/index.js';
|
|
9
|
+
import { CONFLICT_RESOLVE_PROMPT } from '../constants/ai-prompts.js';
|
|
10
|
+
|
|
11
|
+
/** 默认 Claude Code 冲突解决超时时间(毫秒) */
|
|
12
|
+
const DEFAULT_CONFLICT_RESOLVE_TIMEOUT_MS = 300000;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 构建发送给 Claude Code 的冲突解决 prompt
|
|
16
|
+
* Claude Code 会自动读取当前工作目录下的冲突文件,无需手动传入
|
|
17
|
+
* @returns {string} 纯指令性 AI prompt
|
|
18
|
+
*/
|
|
19
|
+
export function buildConflictResolvePrompt(): string {
|
|
20
|
+
return CONFLICT_RESOLVE_PROMPT;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 获取冲突解决超时时间(毫秒)
|
|
25
|
+
* 优先使用配置项,未配置时使用默认值
|
|
26
|
+
* @returns {number} 超时时间(毫秒)
|
|
27
|
+
*/
|
|
28
|
+
function getConflictResolveTimeout(): number {
|
|
29
|
+
const configValue = getConfigValue('conflictResolveTimeoutMs');
|
|
30
|
+
if (typeof configValue === 'number' && configValue > 0) {
|
|
31
|
+
return configValue;
|
|
32
|
+
}
|
|
33
|
+
return DEFAULT_CONFLICT_RESOLVE_TIMEOUT_MS;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 调用 Claude Code CLI 以非交互模式解决冲突
|
|
38
|
+
* 使用 execFileSync 数组参数形式避免 shell 注入
|
|
39
|
+
* @param {string} prompt - AI prompt
|
|
40
|
+
* @param {string} cwd - 工作目录
|
|
41
|
+
* @returns {string} Claude Code 的输出
|
|
42
|
+
*/
|
|
43
|
+
export function invokeClaudeForConflictResolve(prompt: string, cwd: string): string {
|
|
44
|
+
const args = ['-p', prompt, '--permission-mode', 'bypassPermissions'];
|
|
45
|
+
|
|
46
|
+
logger.info(`调用 Claude Code 解决冲突,命令: claude -p "..." --permission-mode bypassPermissions`);
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const output = execFileSync('claude', args, {
|
|
50
|
+
cwd,
|
|
51
|
+
encoding: 'utf-8',
|
|
52
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
53
|
+
timeout: getConflictResolveTimeout(),
|
|
54
|
+
});
|
|
55
|
+
return output;
|
|
56
|
+
} catch (error: unknown) {
|
|
57
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
58
|
+
logger.error(`Claude Code 冲突解决失败: ${errMsg}`);
|
|
59
|
+
throw new ClawtError(MESSAGES.MERGE_CONFLICT_AI_FAILED(errMsg));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 执行 AI 辅助冲突解决的完整流程
|
|
65
|
+
* 1. 获取冲突文件列表
|
|
66
|
+
* 2. 构建 AI prompt
|
|
67
|
+
* 3. 调用 Claude Code 解决冲突
|
|
68
|
+
* 4. 检查冲突是否解决
|
|
69
|
+
* 5. git add 已解决的文件并 merge --continue
|
|
70
|
+
* @param {string} currentBranch - 当前分支名
|
|
71
|
+
* @param {string} incomingBranch - 传入分支名
|
|
72
|
+
* @param {string} cwd - 工作目录
|
|
73
|
+
* @returns {boolean} 是否所有冲突都已解决
|
|
74
|
+
*/
|
|
75
|
+
export function resolveConflictsWithAI(
|
|
76
|
+
currentBranch: string,
|
|
77
|
+
incomingBranch: string,
|
|
78
|
+
cwd: string,
|
|
79
|
+
): boolean {
|
|
80
|
+
const conflictFiles = getConflictFiles(cwd);
|
|
81
|
+
if (conflictFiles.length === 0) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
printInfo(MESSAGES.MERGE_CONFLICT_AI_START(conflictFiles.length));
|
|
86
|
+
|
|
87
|
+
// 构建 prompt 并调用 Claude Code,捕获异常提供恢复提示
|
|
88
|
+
const prompt = buildConflictResolvePrompt();
|
|
89
|
+
try {
|
|
90
|
+
invokeClaudeForConflictResolve(prompt, cwd);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
// AI 调用失败时输出友好提示,告知用户可手动解决或重试
|
|
93
|
+
const errMsg = error instanceof ClawtError ? error.message : String(error);
|
|
94
|
+
printWarning(errMsg);
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 检查冲突是否已解决
|
|
99
|
+
const remainingConflicts = getConflictFiles(cwd);
|
|
100
|
+
|
|
101
|
+
if (remainingConflicts.length === 0) {
|
|
102
|
+
// 所有冲突已解决,git add 所有原冲突文件并完成 merge
|
|
103
|
+
gitAddFiles(conflictFiles, cwd);
|
|
104
|
+
gitMergeContinue(cwd);
|
|
105
|
+
printSuccess(MESSAGES.MERGE_CONFLICT_AI_SUCCESS);
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 部分冲突已解决,git add 已解决的文件
|
|
110
|
+
const resolvedFiles = conflictFiles.filter((f) => !remainingConflicts.includes(f));
|
|
111
|
+
if (resolvedFiles.length > 0) {
|
|
112
|
+
gitAddFiles(resolvedFiles, cwd);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
printWarning(MESSAGES.MERGE_CONFLICT_AI_PARTIAL(remainingConflicts.length));
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 根据配置和命令选项判断冲突解决模式
|
|
121
|
+
* @param {boolean} [autoFlag] - --auto 命令行参数
|
|
122
|
+
* @returns {'ask' | 'auto' | 'manual'} 冲突解决模式
|
|
123
|
+
*/
|
|
124
|
+
export function determineConflictResolveMode(autoFlag?: boolean): 'ask' | 'auto' | 'manual' {
|
|
125
|
+
// --auto 命令行参数优先级最高
|
|
126
|
+
if (autoFlag === true) {
|
|
127
|
+
return 'auto';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const configMode = getConfigValue('conflictResolveMode');
|
|
131
|
+
if (configMode === 'auto' || configMode === 'manual') {
|
|
132
|
+
return configMode;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return 'ask';
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* 处理合并冲突的入口函数
|
|
140
|
+
* 根据冲突解决模式决定处理方式:auto 直接 AI 解决,ask 询问用户,manual 抛出错误
|
|
141
|
+
* @param {string} currentBranch - 当前分支名
|
|
142
|
+
* @param {string} incomingBranch - 传入分支名
|
|
143
|
+
* @param {string} cwd - 工作目录
|
|
144
|
+
* @param {boolean} [autoFlag] - --auto 命令行参数
|
|
145
|
+
* @returns {Promise<boolean>} 是否成功解决了所有冲突
|
|
146
|
+
*/
|
|
147
|
+
export async function handleMergeConflict(
|
|
148
|
+
currentBranch: string,
|
|
149
|
+
incomingBranch: string,
|
|
150
|
+
cwd: string,
|
|
151
|
+
autoFlag?: boolean,
|
|
152
|
+
): Promise<boolean> {
|
|
153
|
+
const mode = determineConflictResolveMode(autoFlag);
|
|
154
|
+
|
|
155
|
+
if (mode === 'manual') {
|
|
156
|
+
throw new ClawtError(MESSAGES.MERGE_CONFLICT_MANUAL);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (mode === 'auto') {
|
|
160
|
+
return resolveConflictsWithAI(currentBranch, incomingBranch, cwd);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// mode === 'ask'
|
|
164
|
+
const shouldUseAI = await confirmAction(MESSAGES.MERGE_CONFLICT_ASK_AI);
|
|
165
|
+
if (!shouldUseAI) {
|
|
166
|
+
throw new ClawtError(MESSAGES.MERGE_CONFLICT_MANUAL);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return resolveConflictsWithAI(currentBranch, incomingBranch, cwd);
|
|
170
|
+
}
|
package/src/utils/git-core.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { basename } from 'node:path';
|
|
2
|
-
import { execSync } from 'node:child_process';
|
|
2
|
+
import { execSync, execFileSync } from 'node:child_process';
|
|
3
3
|
import { execCommand, execCommandWithInput } from './shell.js';
|
|
4
4
|
import { logger } from '../logger/index.js';
|
|
5
5
|
|
|
@@ -367,3 +367,51 @@ export function gitApplyCachedCheck(patchContent: Buffer, cwd?: string): boolean
|
|
|
367
367
|
return false;
|
|
368
368
|
}
|
|
369
369
|
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* 获取冲突文件列表
|
|
373
|
+
* 通过 git status --porcelain 解析 UU/AA/DD 等冲突标记
|
|
374
|
+
* @param {string} [cwd] - 工作目录
|
|
375
|
+
* @returns {string[]} 冲突文件路径列表
|
|
376
|
+
*/
|
|
377
|
+
export function getConflictFiles(cwd?: string): string[] {
|
|
378
|
+
const status = getStatusPorcelain(cwd);
|
|
379
|
+
if (!status) return [];
|
|
380
|
+
return status
|
|
381
|
+
.split('\n')
|
|
382
|
+
.filter((line) => /^(UU|AA|DD|DU|UD|AU|UA)/.test(line))
|
|
383
|
+
.map((line) => line.slice(3));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* git add 指定文件
|
|
388
|
+
* 使用 execFileSync 数组参数形式避免文件名中特殊字符导致的注入风险
|
|
389
|
+
* @param {string[]} files - 文件路径列表
|
|
390
|
+
* @param {string} [cwd] - 工作目录
|
|
391
|
+
*/
|
|
392
|
+
export function gitAddFiles(files: string[], cwd?: string): void {
|
|
393
|
+
if (files.length === 0) return;
|
|
394
|
+
const args = ['add', '--', ...files];
|
|
395
|
+
logger.debug(`执行命令: git ${args.join(' ')}${cwd ? ` (cwd: ${cwd})` : ''}`);
|
|
396
|
+
execFileSync('git', args, {
|
|
397
|
+
cwd,
|
|
398
|
+
encoding: 'utf-8',
|
|
399
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* git merge --continue(非交互式)
|
|
405
|
+
* @param {string} [cwd] - 工作目录
|
|
406
|
+
*/
|
|
407
|
+
export function gitMergeContinue(cwd?: string): void {
|
|
408
|
+
execCommand('GIT_EDITOR=true git merge --continue', { cwd });
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* git merge --abort
|
|
413
|
+
* @param {string} [cwd] - 工作目录
|
|
414
|
+
*/
|
|
415
|
+
export function gitMergeAbort(cwd?: string): void {
|
|
416
|
+
execCommand('git merge --abort', { cwd });
|
|
417
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -48,6 +48,10 @@ export {
|
|
|
48
48
|
getBranchCreatedAt,
|
|
49
49
|
gitCheckout,
|
|
50
50
|
createBranch,
|
|
51
|
+
getConflictFiles,
|
|
52
|
+
gitAddFiles,
|
|
53
|
+
gitMergeContinue,
|
|
54
|
+
gitMergeAbort,
|
|
51
55
|
} from './git.js';
|
|
52
56
|
export { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } from './branch.js';
|
|
53
57
|
export { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled, validateHeadExists, validateWorkingDirClean, runPreChecks } from './validation.js';
|
|
@@ -59,7 +63,7 @@ export { ensureDir, removeEmptyDir, calculateDirSize } from './fs.js';
|
|
|
59
63
|
export { multilineInput } from './prompt.js';
|
|
60
64
|
export { launchInteractiveClaude, hasClaudeSessionHistory, launchInteractiveClaudeInNewTerminal } from './claude.js';
|
|
61
65
|
export { getSnapshotPath, hasSnapshot, getSnapshotModifiedTime, readSnapshotTreeHash, readSnapshot, writeSnapshot, removeSnapshot, removeProjectSnapshots, getProjectSnapshotBranches } from './validate-snapshot.js';
|
|
62
|
-
export { findExactMatch, findFuzzyMatches,
|
|
66
|
+
export { findExactMatch, findFuzzyMatches, promptGroupedMultiSelectBranches, resolveTargetWorktree, resolveTargetWorktrees, groupWorktreesByDate, buildGroupedChoices, buildGroupMembershipMap, formatRelativeDate, getWorktreeCreatedDate, getWorktreeCreatedTime } from './worktree-matcher.js';
|
|
63
67
|
export type { WorktreeResolveMessages, WorktreeMultiResolveMessages } from './worktree-matcher.js';
|
|
64
68
|
export { ProgressRenderer } from './progress.js';
|
|
65
69
|
export { parseTaskFile, loadTaskFile, parseTasksFromOptions } from './task-file.js';
|
|
@@ -79,4 +83,5 @@ export { migrateChangesViaPatch, computeCurrentTreeHash, saveCurrentSnapshotTree
|
|
|
79
83
|
export { InteractivePanel } from './interactive-panel.js';
|
|
80
84
|
export { buildPanelFrame, buildGroupedWorktreeLines, buildDisplayOrder, renderDateSeparator, renderWorktreeBlock, renderSnapshotSummary, renderFooter, calculateVisibleRows } from './interactive-panel-render.js';
|
|
81
85
|
export type { PanelLine } from './interactive-panel-render.js';
|
|
86
|
+
export { buildConflictResolvePrompt, invokeClaudeForConflictResolve, resolveConflictsWithAI, determineConflictResolveMode, handleMergeConflict } from './conflict-resolver.js';
|
|
82
87
|
|
|
@@ -67,7 +67,7 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
67
67
|
hasCommitWithMessage: vi.fn(),
|
|
68
68
|
gitMergeBase: vi.fn(),
|
|
69
69
|
gitResetSoftTo: vi.fn(),
|
|
70
|
-
getCurrentBranch: vi.fn(),
|
|
70
|
+
getCurrentBranch: vi.fn().mockReturnValue('main'),
|
|
71
71
|
gitResetHard: vi.fn(),
|
|
72
72
|
gitCleanForce: vi.fn(),
|
|
73
73
|
gitCheckout: vi.fn(),
|
|
@@ -77,6 +77,7 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
77
77
|
ensureOnMainWorkBranch: vi.fn(),
|
|
78
78
|
guardMainWorkBranch: vi.fn().mockResolvedValue(undefined),
|
|
79
79
|
guardMainWorkBranchExists: vi.fn(),
|
|
80
|
+
handleMergeConflict: vi.fn(),
|
|
80
81
|
}));
|
|
81
82
|
|
|
82
83
|
import { registerMergeCommand } from '../../../src/commands/merge.js';
|
|
@@ -101,6 +102,7 @@ import {
|
|
|
101
102
|
cleanupWorktrees,
|
|
102
103
|
hasCommitWithMessage,
|
|
103
104
|
resolveTargetWorktree,
|
|
105
|
+
handleMergeConflict,
|
|
104
106
|
} from '../../../src/utils/index.js';
|
|
105
107
|
|
|
106
108
|
const mockedGetProjectName = vi.mocked(getProjectName);
|
|
@@ -123,6 +125,7 @@ const mockedConfirmAction = vi.mocked(confirmAction);
|
|
|
123
125
|
const mockedCleanupWorktrees = vi.mocked(cleanupWorktrees);
|
|
124
126
|
const mockedHasCommitWithMessage = vi.mocked(hasCommitWithMessage);
|
|
125
127
|
const mockedResolveTargetWorktree = vi.mocked(resolveTargetWorktree);
|
|
128
|
+
const mockedHandleMergeConflict = vi.mocked(handleMergeConflict);
|
|
126
129
|
|
|
127
130
|
const worktree = { path: '/path/feature', branch: 'feature' };
|
|
128
131
|
|
|
@@ -147,6 +150,8 @@ beforeEach(() => {
|
|
|
147
150
|
mockedPrintWarning.mockReset();
|
|
148
151
|
mockedRemoveSnapshot.mockReset();
|
|
149
152
|
mockedCleanupWorktrees.mockReset();
|
|
153
|
+
mockedHandleMergeConflict.mockReset();
|
|
154
|
+
mockedHandleMergeConflict.mockResolvedValue(true); // 默认 AI 解决成功
|
|
150
155
|
});
|
|
151
156
|
|
|
152
157
|
describe('registerMergeCommand', () => {
|
|
@@ -229,11 +234,29 @@ describe('handleMerge', () => {
|
|
|
229
234
|
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
230
235
|
});
|
|
231
236
|
|
|
232
|
-
it('
|
|
237
|
+
it('合并冲突时调用 handleMergeConflict', async () => {
|
|
233
238
|
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
234
239
|
mockedHasLocalCommits.mockReturnValue(true);
|
|
235
240
|
mockedGitMerge.mockImplementation(() => { throw new Error('merge conflict'); });
|
|
236
241
|
mockedHasMergeConflict.mockReturnValue(true);
|
|
242
|
+
mockedHandleMergeConflict.mockResolvedValue(true);
|
|
243
|
+
mockedConfirmAction.mockResolvedValue(false);
|
|
244
|
+
|
|
245
|
+
const program = new Command();
|
|
246
|
+
program.exitOverride();
|
|
247
|
+
registerMergeCommand(program);
|
|
248
|
+
await program.parseAsync(['merge', '-b', 'feature'], { from: 'user' });
|
|
249
|
+
|
|
250
|
+
expect(mockedHandleMergeConflict).toHaveBeenCalledWith('main', 'feature', '/repo', undefined);
|
|
251
|
+
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('合并冲突且 handleMergeConflict 抛出时向上传播错误', async () => {
|
|
255
|
+
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
256
|
+
mockedHasLocalCommits.mockReturnValue(true);
|
|
257
|
+
mockedGitMerge.mockImplementation(() => { throw new Error('merge conflict'); });
|
|
258
|
+
mockedHasMergeConflict.mockReturnValue(true);
|
|
259
|
+
mockedHandleMergeConflict.mockRejectedValue(new Error('请手动解决冲突'));
|
|
237
260
|
|
|
238
261
|
const program = new Command();
|
|
239
262
|
program.exitOverride();
|
|
@@ -244,6 +267,39 @@ describe('handleMerge', () => {
|
|
|
244
267
|
).rejects.toThrow();
|
|
245
268
|
});
|
|
246
269
|
|
|
270
|
+
it('合并冲突且 AI 部分解决时提前返回', async () => {
|
|
271
|
+
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
272
|
+
mockedHasLocalCommits.mockReturnValue(true);
|
|
273
|
+
mockedGitMerge.mockImplementation(() => { throw new Error('merge conflict'); });
|
|
274
|
+
mockedHasMergeConflict.mockReturnValue(true);
|
|
275
|
+
mockedHandleMergeConflict.mockResolvedValue(false);
|
|
276
|
+
|
|
277
|
+
const program = new Command();
|
|
278
|
+
program.exitOverride();
|
|
279
|
+
registerMergeCommand(program);
|
|
280
|
+
await program.parseAsync(['merge', '-b', 'feature'], { from: 'user' });
|
|
281
|
+
|
|
282
|
+
// AI 未完全解决,不应继续到 pull/push 和 success
|
|
283
|
+
expect(mockedGitPull).not.toHaveBeenCalled();
|
|
284
|
+
expect(mockedPrintSuccess).not.toHaveBeenCalled();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it('--auto 参数传递给 handleMergeConflict', async () => {
|
|
288
|
+
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
289
|
+
mockedHasLocalCommits.mockReturnValue(true);
|
|
290
|
+
mockedGitMerge.mockImplementation(() => { throw new Error('merge conflict'); });
|
|
291
|
+
mockedHasMergeConflict.mockReturnValue(true);
|
|
292
|
+
mockedHandleMergeConflict.mockResolvedValue(true);
|
|
293
|
+
mockedConfirmAction.mockResolvedValue(false);
|
|
294
|
+
|
|
295
|
+
const program = new Command();
|
|
296
|
+
program.exitOverride();
|
|
297
|
+
registerMergeCommand(program);
|
|
298
|
+
await program.parseAsync(['merge', '-b', 'feature', '--auto'], { from: 'user' });
|
|
299
|
+
|
|
300
|
+
expect(mockedHandleMergeConflict).toHaveBeenCalledWith('main', 'feature', '/repo', true);
|
|
301
|
+
});
|
|
302
|
+
|
|
247
303
|
it('autoPullPush=true 时执行 pull 和 push', async () => {
|
|
248
304
|
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
249
305
|
mockedHasLocalCommits.mockReturnValue(true);
|
|
@@ -303,7 +359,7 @@ describe('handleMerge', () => {
|
|
|
303
359
|
});
|
|
304
360
|
mockedGitPull.mockImplementation(() => { throw new Error('pull failed'); });
|
|
305
361
|
// merge 成功后的调用顺序:
|
|
306
|
-
// 1. 步骤
|
|
362
|
+
// 1. 步骤 5.5 二次确认 hasMergeConflict → false(merge 成功无冲突)
|
|
307
363
|
// 2. pull catch 中 hasMergeConflict → true(触发 printWarning 并 return)
|
|
308
364
|
mockedHasMergeConflict.mockReturnValueOnce(false)
|
|
309
365
|
.mockReturnValueOnce(true);
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// mock logger
|
|
4
|
+
vi.mock('../../../src/logger/index.js', () => ({
|
|
5
|
+
logger: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
// mock errors
|
|
9
|
+
vi.mock('../../../src/errors/index.js', () => ({
|
|
10
|
+
ClawtError: class ClawtError extends Error {
|
|
11
|
+
exitCode: number;
|
|
12
|
+
constructor(message: string, exitCode = 1) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.exitCode = exitCode;
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
// mock node:child_process
|
|
20
|
+
vi.mock('node:child_process', () => ({
|
|
21
|
+
execFileSync: vi.fn(),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
// mock constants(使用与 src/constants/messages/merge.ts 一致的消息文本)
|
|
25
|
+
vi.mock('../../../src/constants/index.js', async (importOriginal) => {
|
|
26
|
+
const actual = await importOriginal<typeof import('../../../src/constants/index.js')>();
|
|
27
|
+
return {
|
|
28
|
+
...actual,
|
|
29
|
+
MESSAGES: {
|
|
30
|
+
...actual.MESSAGES,
|
|
31
|
+
MERGE_CONFLICT_ASK_AI: '检测到合并冲突,是否使用 Claude Code 自动解决?',
|
|
32
|
+
MERGE_CONFLICT_AI_START: (fileCount: number) =>
|
|
33
|
+
`正在使用 Claude Code 分析并解决 ${fileCount} 个冲突文件...`,
|
|
34
|
+
MERGE_CONFLICT_AI_SUCCESS: '✓ Claude Code 已成功解决所有冲突',
|
|
35
|
+
MERGE_CONFLICT_AI_PARTIAL: (remaining: number) =>
|
|
36
|
+
`Claude Code 已处理冲突文件,但仍有 ${remaining} 个文件存在冲突\n 请手动处理剩余冲突后执行 git add . && git merge --continue`,
|
|
37
|
+
MERGE_CONFLICT_AI_FAILED: (errorMsg: string) =>
|
|
38
|
+
`Claude Code 解决冲突失败: ${errorMsg}\n 请手动处理:\n 解决冲突后执行 git add . && git merge --continue`,
|
|
39
|
+
MERGE_CONFLICT_MANUAL: '合并存在冲突,请手动处理:\n 解决冲突后执行 git add . && git merge --continue',
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// mock config(determineConflictResolveMode 仍需要)
|
|
45
|
+
vi.mock('../../../src/utils/config.js', () => ({
|
|
46
|
+
getConfigValue: vi.fn(),
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
// mock formatter
|
|
50
|
+
vi.mock('../../../src/utils/formatter.js', () => ({
|
|
51
|
+
printInfo: vi.fn(),
|
|
52
|
+
printSuccess: vi.fn(),
|
|
53
|
+
printWarning: vi.fn(),
|
|
54
|
+
confirmAction: vi.fn(),
|
|
55
|
+
}));
|
|
56
|
+
|
|
57
|
+
// mock git
|
|
58
|
+
vi.mock('../../../src/utils/git.js', () => ({
|
|
59
|
+
getConflictFiles: vi.fn(),
|
|
60
|
+
hasMergeConflict: vi.fn(),
|
|
61
|
+
gitAddFiles: vi.fn(),
|
|
62
|
+
gitMergeContinue: vi.fn(),
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
import { execFileSync } from 'node:child_process';
|
|
66
|
+
import {
|
|
67
|
+
buildConflictResolvePrompt,
|
|
68
|
+
invokeClaudeForConflictResolve,
|
|
69
|
+
resolveConflictsWithAI,
|
|
70
|
+
determineConflictResolveMode,
|
|
71
|
+
handleMergeConflict,
|
|
72
|
+
} from '../../../src/utils/conflict-resolver.js';
|
|
73
|
+
import { getConfigValue } from '../../../src/utils/config.js';
|
|
74
|
+
import { confirmAction, printInfo, printSuccess, printWarning } from '../../../src/utils/formatter.js';
|
|
75
|
+
import { getConflictFiles, gitAddFiles, gitMergeContinue } from '../../../src/utils/git.js';
|
|
76
|
+
import { ClawtError } from '../../../src/errors/index.js';
|
|
77
|
+
|
|
78
|
+
const mockedExecFileSync = vi.mocked(execFileSync);
|
|
79
|
+
const mockedGetConfigValue = vi.mocked(getConfigValue);
|
|
80
|
+
const mockedConfirmAction = vi.mocked(confirmAction);
|
|
81
|
+
const mockedGetConflictFiles = vi.mocked(getConflictFiles);
|
|
82
|
+
const mockedGitAddFiles = vi.mocked(gitAddFiles);
|
|
83
|
+
const mockedGitMergeContinue = vi.mocked(gitMergeContinue);
|
|
84
|
+
const mockedPrintInfo = vi.mocked(printInfo);
|
|
85
|
+
const mockedPrintSuccess = vi.mocked(printSuccess);
|
|
86
|
+
const mockedPrintWarning = vi.mocked(printWarning);
|
|
87
|
+
|
|
88
|
+
beforeEach(() => {
|
|
89
|
+
vi.clearAllMocks();
|
|
90
|
+
mockedGetConfigValue.mockReturnValue('ask');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('buildConflictResolvePrompt', () => {
|
|
94
|
+
it('生成纯指令性 prompt(无参数)', () => {
|
|
95
|
+
const prompt = buildConflictResolvePrompt();
|
|
96
|
+
|
|
97
|
+
expect(prompt).toContain('Git 合并冲突解决专家');
|
|
98
|
+
expect(prompt).toContain('git status');
|
|
99
|
+
expect(prompt).toContain('git log');
|
|
100
|
+
expect(prompt).toContain('冲突标记');
|
|
101
|
+
expect(prompt).toContain('请直接开始');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('invokeClaudeForConflictResolve', () => {
|
|
106
|
+
it('成功调用 execFileSync 并返回输出', () => {
|
|
107
|
+
mockedExecFileSync.mockReturnValue('冲突已解决');
|
|
108
|
+
|
|
109
|
+
const result = invokeClaudeForConflictResolve('test prompt', '/repo');
|
|
110
|
+
|
|
111
|
+
expect(result).toBe('冲突已解决');
|
|
112
|
+
expect(mockedExecFileSync).toHaveBeenCalledWith(
|
|
113
|
+
'claude',
|
|
114
|
+
['-p', 'test prompt', '--permission-mode', 'bypassPermissions'],
|
|
115
|
+
expect.objectContaining({ cwd: '/repo' }),
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('Claude Code 执行失败时抛出 ClawtError', () => {
|
|
120
|
+
mockedExecFileSync.mockImplementation(() => { throw new Error('command failed'); });
|
|
121
|
+
|
|
122
|
+
expect(() => invokeClaudeForConflictResolve('test prompt', '/repo')).toThrow(ClawtError);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('resolveConflictsWithAI', () => {
|
|
127
|
+
it('无冲突文件时直接返回 true', () => {
|
|
128
|
+
mockedGetConflictFiles.mockReturnValue([]);
|
|
129
|
+
|
|
130
|
+
const result = resolveConflictsWithAI('main', 'feature', '/repo');
|
|
131
|
+
|
|
132
|
+
expect(result).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('AI 成功解决所有冲突后 git add 并 merge continue', () => {
|
|
136
|
+
mockedGetConflictFiles
|
|
137
|
+
.mockReturnValueOnce(['src/a.ts']) // 初始冲突文件
|
|
138
|
+
.mockReturnValueOnce([]); // AI 解决后无冲突
|
|
139
|
+
mockedExecFileSync.mockReturnValue('resolved');
|
|
140
|
+
|
|
141
|
+
const result = resolveConflictsWithAI('main', 'feature', '/repo');
|
|
142
|
+
|
|
143
|
+
expect(result).toBe(true);
|
|
144
|
+
expect(mockedGitAddFiles).toHaveBeenCalledWith(['src/a.ts'], '/repo');
|
|
145
|
+
expect(mockedGitMergeContinue).toHaveBeenCalledWith('/repo');
|
|
146
|
+
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('AI 部分解决冲突时 git add 已解决的文件并返回 false', () => {
|
|
150
|
+
mockedGetConflictFiles
|
|
151
|
+
.mockReturnValueOnce(['src/a.ts', 'src/b.ts']) // 初始 2 个冲突
|
|
152
|
+
.mockReturnValueOnce(['src/b.ts']); // AI 后还剩 1 个
|
|
153
|
+
mockedExecFileSync.mockReturnValue('partial');
|
|
154
|
+
|
|
155
|
+
const result = resolveConflictsWithAI('main', 'feature', '/repo');
|
|
156
|
+
|
|
157
|
+
expect(result).toBe(false);
|
|
158
|
+
expect(mockedGitAddFiles).toHaveBeenCalledWith(['src/a.ts'], '/repo');
|
|
159
|
+
expect(mockedPrintWarning).toHaveBeenCalled();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('AI 调用失败时输出警告并返回 false', () => {
|
|
163
|
+
mockedGetConflictFiles.mockReturnValueOnce(['src/a.ts']);
|
|
164
|
+
mockedExecFileSync.mockImplementation(() => { throw new Error('timeout'); });
|
|
165
|
+
|
|
166
|
+
const result = resolveConflictsWithAI('main', 'feature', '/repo');
|
|
167
|
+
|
|
168
|
+
expect(result).toBe(false);
|
|
169
|
+
expect(mockedPrintWarning).toHaveBeenCalled();
|
|
170
|
+
expect(mockedGitAddFiles).not.toHaveBeenCalled();
|
|
171
|
+
expect(mockedGitMergeContinue).not.toHaveBeenCalled();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('determineConflictResolveMode', () => {
|
|
176
|
+
it('--auto 参数优先返回 auto', () => {
|
|
177
|
+
mockedGetConfigValue.mockReturnValue('manual');
|
|
178
|
+
|
|
179
|
+
expect(determineConflictResolveMode(true)).toBe('auto');
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('配置为 auto 时返回 auto', () => {
|
|
183
|
+
mockedGetConfigValue.mockReturnValue('auto');
|
|
184
|
+
|
|
185
|
+
expect(determineConflictResolveMode()).toBe('auto');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('配置为 manual 时返回 manual', () => {
|
|
189
|
+
mockedGetConfigValue.mockReturnValue('manual');
|
|
190
|
+
|
|
191
|
+
expect(determineConflictResolveMode()).toBe('manual');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it('配置为 ask 时返回 ask', () => {
|
|
195
|
+
mockedGetConfigValue.mockReturnValue('ask');
|
|
196
|
+
|
|
197
|
+
expect(determineConflictResolveMode()).toBe('ask');
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('默认返回 ask', () => {
|
|
201
|
+
mockedGetConfigValue.mockReturnValue('unknown');
|
|
202
|
+
|
|
203
|
+
expect(determineConflictResolveMode()).toBe('ask');
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('handleMergeConflict', () => {
|
|
208
|
+
it('manual 模式抛出 ClawtError', async () => {
|
|
209
|
+
mockedGetConfigValue.mockReturnValue('manual');
|
|
210
|
+
|
|
211
|
+
await expect(
|
|
212
|
+
handleMergeConflict('main', 'feature', '/repo'),
|
|
213
|
+
).rejects.toThrow(ClawtError);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('auto 模式直接调用 AI 解决', async () => {
|
|
217
|
+
mockedGetConflictFiles
|
|
218
|
+
.mockReturnValueOnce(['src/a.ts'])
|
|
219
|
+
.mockReturnValueOnce([]);
|
|
220
|
+
mockedExecFileSync.mockReturnValue('resolved');
|
|
221
|
+
|
|
222
|
+
const result = await handleMergeConflict('main', 'feature', '/repo', true);
|
|
223
|
+
|
|
224
|
+
expect(result).toBe(true);
|
|
225
|
+
expect(mockedConfirmAction).not.toHaveBeenCalled();
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('ask 模式用户选择使用 AI 时调用 AI 解决', async () => {
|
|
229
|
+
mockedGetConfigValue.mockReturnValue('ask');
|
|
230
|
+
mockedConfirmAction.mockResolvedValue(true);
|
|
231
|
+
mockedGetConflictFiles
|
|
232
|
+
.mockReturnValueOnce(['src/a.ts'])
|
|
233
|
+
.mockReturnValueOnce([]);
|
|
234
|
+
mockedExecFileSync.mockReturnValue('resolved');
|
|
235
|
+
|
|
236
|
+
const result = await handleMergeConflict('main', 'feature', '/repo');
|
|
237
|
+
|
|
238
|
+
expect(result).toBe(true);
|
|
239
|
+
expect(mockedConfirmAction).toHaveBeenCalled();
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('ask 模式用户拒绝 AI 时抛出 ClawtError', async () => {
|
|
243
|
+
mockedGetConfigValue.mockReturnValue('ask');
|
|
244
|
+
mockedConfirmAction.mockResolvedValue(false);
|
|
245
|
+
|
|
246
|
+
await expect(
|
|
247
|
+
handleMergeConflict('main', 'feature', '/repo'),
|
|
248
|
+
).rejects.toThrow(ClawtError);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
/** 提示消息模板 */
|
|
2
|
-
export const MESSAGES = {
|
|
3
|
-
/** 不在主 worktree 根目录 */
|
|
4
|
-
NOT_MAIN_WORKTREE: '请在主 worktree 的根目录下执行 clawt',
|
|
5
|
-
/** Git 未安装 */
|
|
6
|
-
GIT_NOT_INSTALLED: 'Git 未安装或不在 PATH 中,请先安装 Git',
|
|
7
|
-
/** Claude Code CLI 未安装 */
|
|
8
|
-
CLAUDE_NOT_INSTALLED: 'Claude Code CLI 未安装,请先安装:npm install -g @anthropic-ai/claude-code',
|
|
9
|
-
/** 分支已存在 */
|
|
10
|
-
BRANCH_EXISTS: (name: string) => `分支 ${name} 已存在,无法创建`,
|
|
11
|
-
/** 分支已存在时提示使用 resume */
|
|
12
|
-
BRANCH_EXISTS_USE_RESUME: (name: string) =>
|
|
13
|
-
`分支 ${name} 已存在,请使用 clawt resume -b ${name} 恢复会话`,
|
|
14
|
-
/** 分支名清理后为空 */
|
|
15
|
-
BRANCH_NAME_EMPTY: (original: string) =>
|
|
16
|
-
`分支名 "${original}" 中不包含合法字符,无法创建分支`,
|
|
17
|
-
/** 分支名被转换 */
|
|
18
|
-
BRANCH_SANITIZED: (original: string, sanitized: string) =>
|
|
19
|
-
`分支名已转换: ${original} → ${sanitized}`,
|
|
20
|
-
/** worktree 创建成功 */
|
|
21
|
-
WORKTREE_CREATED: (count: number) => `✓ 已创建 ${count} 个 worktree`,
|
|
22
|
-
/** worktree 移除成功 */
|
|
23
|
-
WORKTREE_REMOVED: (path: string) => `✓ 已移除 worktree: ${path}`,
|
|
24
|
-
/** 没有 worktree */
|
|
25
|
-
NO_WORKTREES: '(无 worktree)',
|
|
26
|
-
/** 目标 worktree 不存在 */
|
|
27
|
-
WORKTREE_NOT_FOUND: (name: string) => `worktree ${name} 不存在`,
|
|
28
|
-
/** 主 worktree 有未提交更改 */
|
|
29
|
-
MAIN_WORKTREE_DIRTY: '主 worktree 有未提交的更改,请先处理',
|
|
30
|
-
/** 目标 worktree 无更改 */
|
|
31
|
-
TARGET_WORKTREE_CLEAN: '该 worktree 的分支上没有任何更改,无需验证',
|
|
32
|
-
/** validate 成功 */
|
|
33
|
-
VALIDATE_SUCCESS: (branch: string) =>
|
|
34
|
-
`✓ 已将分支 ${branch} 的变更应用到主 worktree\n 可以开始验证了`,
|
|
35
|
-
/** merge 成功 */
|
|
36
|
-
MERGE_SUCCESS: (branch: string, message: string, pushed: boolean) =>
|
|
37
|
-
`✓ 分支 ${branch} 已成功合并到当前分支\n 提交信息: ${message}${pushed ? '\n 已推送到远程仓库' : ''}`,
|
|
38
|
-
/** merge 成功(无提交信息,目标 worktree 已提交过) */
|
|
39
|
-
MERGE_SUCCESS_NO_MESSAGE: (branch: string, pushed: boolean) =>
|
|
40
|
-
`✓ 分支 ${branch} 已成功合并到当前分支${pushed ? '\n 已推送到远程仓库' : ''}`,
|
|
41
|
-
/** merge 冲突 */
|
|
42
|
-
MERGE_CONFLICT: '合并存在冲突,请手动处理:\n 解决冲突后执行 git add . && git merge --continue',
|
|
43
|
-
/** merge 后清理 worktree 和分支成功 */
|
|
44
|
-
WORKTREE_CLEANED: (branch: string) => `✓ 已清理 worktree 和分支: ${branch}`,
|
|
45
|
-
/** 请提供提交信息 */
|
|
46
|
-
COMMIT_MESSAGE_REQUIRED: '请提供提交信息(-m 参数)',
|
|
47
|
-
/** 目标 worktree 有未提交修改但未指定 -m */
|
|
48
|
-
TARGET_WORKTREE_DIRTY_NO_MESSAGE: (worktreePath: string) =>
|
|
49
|
-
`${worktreePath} 有未提交的修改,请通过 -m 参数提供提交信息`,
|
|
50
|
-
/** 目标 worktree 既干净又无本地提交 */
|
|
51
|
-
TARGET_WORKTREE_NO_CHANGES: '目标 worktree 没有任何可合并的变更(工作区干净且无本地提交)',
|
|
52
|
-
/** 检测到用户中断 */
|
|
53
|
-
INTERRUPTED: '检测到退出指令,已停止 Claude Code 任务',
|
|
54
|
-
/** 中断后自动清理完成 */
|
|
55
|
-
INTERRUPT_AUTO_CLEANED: (count: number) => `✓ 已自动清理 ${count} 个 worktree 和对应分支`,
|
|
56
|
-
/** 中断后手动确认清理 */
|
|
57
|
-
INTERRUPT_CONFIRM_CLEANUP: '是否移除刚刚创建的 worktree 和对应分支?',
|
|
58
|
-
/** 中断后清理完成 */
|
|
59
|
-
INTERRUPT_CLEANED: (count: number) => `✓ 已清理 ${count} 个 worktree 和对应分支`,
|
|
60
|
-
/** 中断后保留 worktree */
|
|
61
|
-
INTERRUPT_KEPT: '已保留 worktree,可稍后使用 clawt remove 手动清理',
|
|
62
|
-
/** 配置文件损坏,已重新生成默认配置 */
|
|
63
|
-
CONFIG_CORRUPTED: '配置文件损坏或无法解析,已重新生成默认配置',
|
|
64
|
-
/** 配置已恢复为默认值 */
|
|
65
|
-
CONFIG_RESET_SUCCESS: '✓ 配置已恢复为默认值',
|
|
66
|
-
/** 分隔线 */
|
|
67
|
-
SEPARATOR: '────────────────────────────────────────',
|
|
68
|
-
/** 粗分隔线 */
|
|
69
|
-
DOUBLE_SEPARATOR: '════════════════════════════════════════',
|
|
70
|
-
/** 创建数量参数无效 */
|
|
71
|
-
INVALID_COUNT: (value: string) => `无效的创建数量: "${value}",请输入正整数`,
|
|
72
|
-
/** worktree 状态获取失败 */
|
|
73
|
-
WORKTREE_STATUS_UNAVAILABLE: '(状态不可用)',
|
|
74
|
-
/** 增量 validate 成功提示 */
|
|
75
|
-
INCREMENTAL_VALIDATE_SUCCESS: (branch: string) =>
|
|
76
|
-
`✓ 已将分支 ${branch} 的最新变更应用到主 worktree(增量模式)\n 暂存区 = 上次快照,工作目录 = 最新变更`,
|
|
77
|
-
/** 增量 validate 降级为全量模式提示 */
|
|
78
|
-
INCREMENTAL_VALIDATE_FALLBACK: '增量对比失败,已降级为全量模式',
|
|
79
|
-
/** validate 状态已清理 */
|
|
80
|
-
VALIDATE_CLEANED: (branch: string) => `✓ 分支 ${branch} 的 validate 状态已清理`,
|
|
81
|
-
/** merge 命令检测到 validate 状态的提示 */
|
|
82
|
-
MERGE_VALIDATE_STATE_HINT: (branch: string) =>
|
|
83
|
-
`主 worktree 可能存在 validate 残留状态,可先执行 clawt validate -b ${branch} --clean 清理`,
|
|
84
|
-
/** sync 自动保存未提交变更 */
|
|
85
|
-
SYNC_AUTO_COMMITTED: (branch: string) =>
|
|
86
|
-
`已自动保存 ${branch} 分支的未提交变更`,
|
|
87
|
-
/** sync 开始合并 */
|
|
88
|
-
SYNC_MERGING: (targetBranch: string, mainBranch: string) =>
|
|
89
|
-
`正在将 ${mainBranch} 合并到 ${targetBranch} ...`,
|
|
90
|
-
/** sync 成功 */
|
|
91
|
-
SYNC_SUCCESS: (targetBranch: string, mainBranch: string) =>
|
|
92
|
-
`✓ 已将 ${mainBranch} 的最新代码同步到 ${targetBranch}`,
|
|
93
|
-
/** sync 冲突 */
|
|
94
|
-
SYNC_CONFLICT: (worktreePath: string) =>
|
|
95
|
-
`合并存在冲突,请进入目标 worktree 手动解决:\n cd ${worktreePath}\n 解决冲突后执行 git add . && git merge --continue\n clawt validate -b <branch> 验证变更`,
|
|
96
|
-
/** validate patch apply 失败,提示用户同步主分支 */
|
|
97
|
-
VALIDATE_PATCH_APPLY_FAILED: (branch: string) =>
|
|
98
|
-
`变更迁移失败:目标分支与主分支差异过大\n 请先执行 clawt sync -b ${branch} 同步主分支后重试`,
|
|
99
|
-
/** merge 检测到 auto-save 提交,提示用户是否压缩 */
|
|
100
|
-
MERGE_SQUASH_PROMPT: '检测到 sync 产生的临时提交,是否将所有提交压缩为一个?\n 压缩后变更将保留在目标worktree的暂存区,需要重新提交(可使用 Claude Code Cli或其他工具生成提交信息)',
|
|
101
|
-
/** squash 完成且通过 -m 直接提交后的提示 */
|
|
102
|
-
MERGE_SQUASH_COMMITTED: (branch: string) =>
|
|
103
|
-
`✓ 已将分支 ${branch} 的所有提交压缩为一个`,
|
|
104
|
-
/** squash 完成但未提供 -m,提示用户自行提交 */
|
|
105
|
-
MERGE_SQUASH_PENDING: (worktreePath: string, branch: string) =>
|
|
106
|
-
`✓ 已将所有提交压缩到暂存区\n 请在目标 worktree 中提交后重新执行 merge:\n cd ${worktreePath}\n 提交完成后执行:clawt merge -b ${branch}`,
|
|
107
|
-
/** 用户取消破坏性操作 */
|
|
108
|
-
DESTRUCTIVE_OP_CANCELLED: '已取消操作',
|
|
109
|
-
/** reset 成功 */
|
|
110
|
-
RESET_SUCCESS: '✓ 主 worktree 工作区和暂存区已重置',
|
|
111
|
-
/** reset 时工作区和暂存区已干净 */
|
|
112
|
-
RESET_ALREADY_CLEAN: '主 worktree 工作区和暂存区已是干净状态,无需重置',
|
|
113
|
-
/** 批量移除部分失败 */
|
|
114
|
-
REMOVE_PARTIAL_FAILURE: (failures: Array<{ path: string; error: string }>) =>
|
|
115
|
-
`以下 worktree 移除失败:\n${failures.map((f) => ` ✗ ${f.path}: ${f.error}`).join('\n')}`,
|
|
116
|
-
/** resume 无可用 worktree */
|
|
117
|
-
RESUME_NO_WORKTREES: '当前项目没有可用的 worktree,请先通过 clawt run 或 clawt create 创建',
|
|
118
|
-
/** resume 模糊匹配无结果,列出可用分支 */
|
|
119
|
-
RESUME_NO_MATCH: (name: string, branches: string[]) =>
|
|
120
|
-
`未找到与 "${name}" 匹配的分支\n 可用分支:\n${branches.map((b) => ` - ${b}`).join('\n')}`,
|
|
121
|
-
/** resume 交互选择提示 */
|
|
122
|
-
RESUME_SELECT_BRANCH: '请选择要恢复的分支',
|
|
123
|
-
/** resume 模糊匹配到多个结果提示 */
|
|
124
|
-
RESUME_MULTIPLE_MATCHES: (name: string) => `"${name}" 匹配到多个分支,请选择:`,
|
|
125
|
-
/** validate 无可用 worktree */
|
|
126
|
-
VALIDATE_NO_WORKTREES: '当前项目没有可用的 worktree,请先通过 clawt run 或 clawt create 创建',
|
|
127
|
-
/** validate 模糊匹配无结果,列出可用分支 */
|
|
128
|
-
VALIDATE_NO_MATCH: (name: string, branches: string[]) =>
|
|
129
|
-
`未找到与 "${name}" 匹配的分支\n 可用分支:\n${branches.map((b) => ` - ${b}`).join('\n')}`,
|
|
130
|
-
/** validate 交互选择提示 */
|
|
131
|
-
VALIDATE_SELECT_BRANCH: '请选择要验证的分支',
|
|
132
|
-
/** validate 模糊匹配到多个结果提示 */
|
|
133
|
-
VALIDATE_MULTIPLE_MATCHES: (name: string) => `"${name}" 匹配到多个分支,请选择:`,
|
|
134
|
-
/** merge 无可用 worktree */
|
|
135
|
-
MERGE_NO_WORKTREES: '当前项目没有可用的 worktree,请先通过 clawt run 或 clawt create 创建',
|
|
136
|
-
/** merge 模糊匹配无结果,列出可用分支 */
|
|
137
|
-
MERGE_NO_MATCH: (name: string, branches: string[]) =>
|
|
138
|
-
`未找到与 "${name}" 匹配的分支\n 可用分支:\n${branches.map((b) => ` - ${b}`).join('\n')}`,
|
|
139
|
-
/** merge 交互选择提示 */
|
|
140
|
-
MERGE_SELECT_BRANCH: '请选择要合并的分支',
|
|
141
|
-
/** merge 模糊匹配到多个结果提示 */
|
|
142
|
-
MERGE_MULTIPLE_MATCHES: (name: string) => `"${name}" 匹配到多个分支,请选择:`,
|
|
143
|
-
/** sync 无可用 worktree */
|
|
144
|
-
SYNC_NO_WORKTREES: '当前项目没有可用的 worktree,请先通过 clawt run 或 clawt create 创建',
|
|
145
|
-
/** sync 模糊匹配无结果,列出可用分支 */
|
|
146
|
-
SYNC_NO_MATCH: (name: string, branches: string[]) =>
|
|
147
|
-
`未找到与 "${name}" 匹配的分支\n 可用分支:\n${branches.map((b) => ` - ${b}`).join('\n')}`,
|
|
148
|
-
/** sync 交互选择提示 */
|
|
149
|
-
SYNC_SELECT_BRANCH: '请选择要同步的分支',
|
|
150
|
-
/** sync 模糊匹配到多个结果提示 */
|
|
151
|
-
SYNC_MULTIPLE_MATCHES: (name: string) => `"${name}" 匹配到多个分支,请选择:`,
|
|
152
|
-
/** status 命令标题 */
|
|
153
|
-
STATUS_TITLE: (projectName: string) => `项目状态总览: ${projectName}`,
|
|
154
|
-
/** status 主 worktree 区块标题 */
|
|
155
|
-
STATUS_MAIN_SECTION: '主 Worktree',
|
|
156
|
-
/** status worktrees 区块标题 */
|
|
157
|
-
STATUS_WORKTREES_SECTION: 'Worktree 列表',
|
|
158
|
-
/** status 快照区块标题 */
|
|
159
|
-
STATUS_SNAPSHOTS_SECTION: '未清理的 Validate 快照',
|
|
160
|
-
/** status 无 worktree */
|
|
161
|
-
STATUS_NO_WORKTREES: '(无活跃 worktree)',
|
|
162
|
-
/** status 无未清理快照 */
|
|
163
|
-
STATUS_NO_SNAPSHOTS: '(无未清理的快照)',
|
|
164
|
-
/** status 变更状态:已提交 */
|
|
165
|
-
STATUS_CHANGE_COMMITTED: '已提交',
|
|
166
|
-
/** status 变更状态:未提交修改 */
|
|
167
|
-
STATUS_CHANGE_UNCOMMITTED: '未提交修改',
|
|
168
|
-
/** status 变更状态:合并冲突 */
|
|
169
|
-
STATUS_CHANGE_CONFLICT: '合并冲突',
|
|
170
|
-
/** status 变更状态:无变更 */
|
|
171
|
-
STATUS_CHANGE_CLEAN: '无变更',
|
|
172
|
-
/** status 快照对应 worktree 已不存在 */
|
|
173
|
-
STATUS_SNAPSHOT_ORPHANED: '(对应 worktree 已不存在)',
|
|
174
|
-
/** merge 后 pull 冲突 */
|
|
175
|
-
PULL_CONFLICT:
|
|
176
|
-
'自动 pull 时发生冲突,merge 已完成但远程同步失败\n 请手动解决冲突:\n 解决冲突后执行 git add . && git commit\n 然后执行 git push 推送到远程',
|
|
177
|
-
/** push 失败 */
|
|
178
|
-
PUSH_FAILED: '自动 push 失败,merge 和 pull 已完成\n 请手动执行 git push',
|
|
179
|
-
} as const;
|