clawt 3.9.0 → 3.9.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/README.md +1 -1
- package/dist/index.js +16 -15
- package/dist/postinstall.js +3 -3
- package/docs/config-file.md +1 -1
- package/docs/cover-validate.md +21 -12
- package/docs/resume.md +6 -5
- package/package.json +1 -1
- package/src/commands/cover-validate.ts +17 -24
- package/src/constants/messages/cover-validate.ts +3 -3
- package/src/utils/git-core.ts +8 -0
- package/src/utils/index.ts +1 -0
- package/tests/unit/commands/cover-validate.test.ts +21 -29
- package/tests/unit/utils/git.test.ts +8 -0
package/README.md
CHANGED
|
@@ -338,7 +338,7 @@ Configuration file is located at `~/.clawt/config.json`, auto-generated after in
|
|
|
338
338
|
| `autoPullPush` | `false` | Auto pull/push after merge |
|
|
339
339
|
| `confirmDestructiveOps` | `true` | Confirm before destructive operations |
|
|
340
340
|
| `maxConcurrency` | `0` | Max concurrency for run command, `0` means unlimited |
|
|
341
|
-
| `terminalApp` | `"auto"` | Terminal for batch resume: `auto` / `iterm2` / `terminal` |
|
|
341
|
+
| `terminalApp` | `"auto"` | Terminal for batch resume: `auto` / `iterm2` / `terminal` / `cmux` |
|
|
342
342
|
| `resumeInPlace` | `false` | Resume single selection in current terminal; `false` opens in new tab |
|
|
343
343
|
| `aliases` | `{}` | Command alias mapping (e.g., `{"l": "list", "r": "run"}`) |
|
|
344
344
|
| `autoUpdate` | `true` | Auto-check for new versions (checks npm registry every 24 hours) |
|
package/dist/index.js
CHANGED
|
@@ -545,9 +545,9 @@ var COVER_VALIDATE_MESSAGES = {
|
|
|
545
545
|
/** 无快照,提示先执行 validate */
|
|
546
546
|
COVER_VALIDATE_NO_SNAPSHOT: (branch) => `\u672A\u627E\u5230\u5206\u652F ${branch} \u7684 validate \u5FEB\u7167
|
|
547
547
|
\u8BF7\u5148\u6267\u884C clawt validate -b ${branch} \u521B\u5EFA\u5FEB\u7167`,
|
|
548
|
-
/**
|
|
549
|
-
|
|
550
|
-
\u8BF7\u68C0\u67E5\u76EE\u6807 worktree \
|
|
548
|
+
/** 覆盖失败(tree checkout/clean 失败) */
|
|
549
|
+
COVER_VALIDATE_COVER_FAILED: (branch) => `\u8986\u76D6\u53D8\u66F4\u5230 worktree ${branch} \u5931\u8D25\uFF1Atree checkout \u6216\u6E05\u7406\u64CD\u4F5C\u51FA\u9519
|
|
550
|
+
\u8BF7\u68C0\u67E5\u76EE\u6807 worktree \u72B6\u6001\u540E\u91CD\u8BD5`,
|
|
551
551
|
/** 工作区和暂存区无修改,可能为误操作 */
|
|
552
552
|
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",
|
|
553
553
|
/** 覆盖成功 */
|
|
@@ -1252,6 +1252,9 @@ function gitResetHard(cwd) {
|
|
|
1252
1252
|
function gitCleanForce(cwd) {
|
|
1253
1253
|
execCommand("git clean -fd", { cwd });
|
|
1254
1254
|
}
|
|
1255
|
+
function gitCheckoutIndexForce(cwd) {
|
|
1256
|
+
execCommand("git checkout-index -f -a", { cwd });
|
|
1257
|
+
}
|
|
1255
1258
|
function gitStashPush(message, cwd) {
|
|
1256
1259
|
execCommand(`git stash push -m "${message}"`, { cwd });
|
|
1257
1260
|
}
|
|
@@ -5150,7 +5153,7 @@ function findTargetWorktreePath(branchName) {
|
|
|
5150
5153
|
}
|
|
5151
5154
|
return match.path;
|
|
5152
5155
|
}
|
|
5153
|
-
function
|
|
5156
|
+
function computeWorktreeTreeHash(mainWorktreePath) {
|
|
5154
5157
|
const savedIndexTreeHash = gitWriteTree(mainWorktreePath);
|
|
5155
5158
|
let currentTreeHash;
|
|
5156
5159
|
try {
|
|
@@ -5159,11 +5162,7 @@ function computeIncrementalPatch(snapshotTreeHash, mainWorktreePath) {
|
|
|
5159
5162
|
} finally {
|
|
5160
5163
|
gitReadTree(savedIndexTreeHash, mainWorktreePath);
|
|
5161
5164
|
}
|
|
5162
|
-
|
|
5163
|
-
return null;
|
|
5164
|
-
}
|
|
5165
|
-
const patch = gitDiffTree(snapshotTreeHash, currentTreeHash, mainWorktreePath);
|
|
5166
|
-
return { patch, currentTreeHash };
|
|
5165
|
+
return currentTreeHash;
|
|
5167
5166
|
}
|
|
5168
5167
|
async function handleCoverValidate() {
|
|
5169
5168
|
await runPreChecks({ requireMainWorktree: true, requireHead: true, requireProjectConfig: true });
|
|
@@ -5185,18 +5184,20 @@ async function handleCoverValidate() {
|
|
|
5185
5184
|
const confirmed = await confirmAction("\u662F\u5426\u7EE7\u7EED\u6267\u884C\u8986\u76D6\uFF1F");
|
|
5186
5185
|
if (!confirmed) return;
|
|
5187
5186
|
}
|
|
5188
|
-
const
|
|
5189
|
-
if (
|
|
5187
|
+
const currentTreeHash = computeWorktreeTreeHash(mainWorktreePath);
|
|
5188
|
+
if (snapshotTreeHash === currentTreeHash) {
|
|
5190
5189
|
printInfo(MESSAGES.COVER_VALIDATE_NO_CHANGES);
|
|
5191
5190
|
return;
|
|
5192
5191
|
}
|
|
5193
5192
|
try {
|
|
5194
|
-
|
|
5193
|
+
gitReadTree(currentTreeHash, targetWorktreePath);
|
|
5194
|
+
gitCheckoutIndexForce(targetWorktreePath);
|
|
5195
|
+
gitCleanForce(targetWorktreePath);
|
|
5195
5196
|
} catch (error) {
|
|
5196
|
-
logger.error(`cover-validate
|
|
5197
|
-
throw new ClawtError(MESSAGES.
|
|
5197
|
+
logger.error(`cover-validate \u8986\u76D6\u5931\u8D25: ${error}`);
|
|
5198
|
+
throw new ClawtError(MESSAGES.COVER_VALIDATE_COVER_FAILED(targetBranchName));
|
|
5198
5199
|
}
|
|
5199
|
-
writeSnapshot(projectName, targetBranchName,
|
|
5200
|
+
writeSnapshot(projectName, targetBranchName, currentTreeHash);
|
|
5200
5201
|
printSuccess(MESSAGES.COVER_VALIDATE_SUCCESS(targetBranchName));
|
|
5201
5202
|
}
|
|
5202
5203
|
|
package/dist/postinstall.js
CHANGED
|
@@ -522,9 +522,9 @@ var COVER_VALIDATE_MESSAGES = {
|
|
|
522
522
|
/** 无快照,提示先执行 validate */
|
|
523
523
|
COVER_VALIDATE_NO_SNAPSHOT: (branch) => `\u672A\u627E\u5230\u5206\u652F ${branch} \u7684 validate \u5FEB\u7167
|
|
524
524
|
\u8BF7\u5148\u6267\u884C clawt validate -b ${branch} \u521B\u5EFA\u5FEB\u7167`,
|
|
525
|
-
/**
|
|
526
|
-
|
|
527
|
-
\u8BF7\u68C0\u67E5\u76EE\u6807 worktree \
|
|
525
|
+
/** 覆盖失败(tree checkout/clean 失败) */
|
|
526
|
+
COVER_VALIDATE_COVER_FAILED: (branch) => `\u8986\u76D6\u53D8\u66F4\u5230 worktree ${branch} \u5931\u8D25\uFF1Atree checkout \u6216\u6E05\u7406\u64CD\u4F5C\u51FA\u9519
|
|
527
|
+
\u8BF7\u68C0\u67E5\u76EE\u6807 worktree \u72B6\u6001\u540E\u91CD\u8BD5`,
|
|
528
528
|
/** 工作区和暂存区无修改,可能为误操作 */
|
|
529
529
|
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",
|
|
530
530
|
/** 覆盖成功 */
|
package/docs/config-file.md
CHANGED
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
| `autoPullPush` | `boolean` | `false` | merge 成功后是否自动执行 git pull 和 git push |
|
|
40
40
|
| `confirmDestructiveOps` | `boolean` | `true` | 执行破坏性操作(reset、validate --clean)前是否提示确认 |
|
|
41
41
|
| `maxConcurrency` | `number` | `0` | run 命令默认最大并发数,`0` 表示不限制 |
|
|
42
|
-
| `terminalApp` | `string` | `"auto"` | 批量 resume 使用的终端应用:`auto`(自动检测)、`iterm2`、`terminal`(macOS) |
|
|
42
|
+
| `terminalApp` | `string` | `"auto"` | 批量 resume 使用的终端应用:`auto`(自动检测)、`iterm2`、`terminal`、`cmux`(macOS) |
|
|
43
43
|
| `resumeInPlace` | `boolean` | `false` | resume 单选时是否在当前终端就地打开,`false` 则通过 `terminalApp` 在新 Tab 中打开 |
|
|
44
44
|
| `aliases` | `Record<string, string>` | `{}` | 命令别名映射,键为别名,值为目标内置命令名 |
|
|
45
45
|
| `autoUpdate` | `boolean` | `true` | 是否启用自动更新检查(每 24 小时通过 npm registry 检查一次新版本) |
|
package/docs/cover-validate.md
CHANGED
|
@@ -44,20 +44,25 @@ clawt cover
|
|
|
44
44
|
|
|
45
45
|
> 工作区干净时通常意味着用户没有在验证分支上做任何修改就执行了 cover,这大概率是误操作。增加确认提示可以避免不必要的覆盖操作。
|
|
46
46
|
|
|
47
|
-
##### 步骤 4
|
|
47
|
+
##### 步骤 4:计算当前 tree hash
|
|
48
48
|
|
|
49
|
-
通过 `
|
|
49
|
+
通过 `computeWorktreeTreeHash()` 计算验证分支当前的完整 tree hash:
|
|
50
50
|
|
|
51
|
-
1. 保存当前暂存区的 tree hash
|
|
52
|
-
2. `git add .` + `git write-tree` 获取当前工作区的完整 tree hash
|
|
53
|
-
3. 通过 `git read-tree`
|
|
54
|
-
4. 比较 `snapshotTreeHash` 与 `currentTreeHash`:
|
|
55
|
-
- **相同** → 无增量变更,输出提示后返回
|
|
56
|
-
- **不同** → 通过 `git diff-tree` 生成 patch
|
|
51
|
+
1. 保存当前暂存区的 tree hash,用于后续恢复
|
|
52
|
+
2. `git add .` + `git write-tree` 获取当前工作区的完整 tree hash
|
|
53
|
+
3. 通过 `git read-tree` 恢复原始暂存区状态
|
|
57
54
|
|
|
58
|
-
|
|
55
|
+
比较 snapshotTreeHash 与 currentTreeHash,如果相同则无变更,输出提示后返回。
|
|
59
56
|
|
|
60
|
-
|
|
57
|
+
##### 步骤 5:直接覆盖目标 worktree
|
|
58
|
+
|
|
59
|
+
采用 **直接 checkout tree** 方式,实现真正的覆盖语义:
|
|
60
|
+
|
|
61
|
+
1. **写入暂存区**:通过 `git read-tree <currentTreeHash>` 将验证分支的完整 tree 写入目标 worktree 的暂存区
|
|
62
|
+
2. **强制检出工作区**:通过 `git checkout-index -f -a` 将暂存区内容强制写入工作区
|
|
63
|
+
3. **清理残留文件**:通过 `git clean -fd` 删除目标 worktree 中未跟踪文件
|
|
64
|
+
|
|
65
|
+
> **关键优势**:无基准依赖,无条件覆盖目标 worktree,符合 cover 的语义。
|
|
61
66
|
|
|
62
67
|
##### 步骤 6:更新快照
|
|
63
68
|
|
|
@@ -78,7 +83,7 @@ clawt cover
|
|
|
78
83
|
| `COVER_VALIDATE_NO_SNAPSHOT` | 无快照 | 提示先执行 `clawt validate -b <branch>` 创建快照 |
|
|
79
84
|
| `COVER_VALIDATE_NO_CHANGES` | 无增量变更 | 提示无需覆盖 |
|
|
80
85
|
| `COVER_VALIDATE_WORKING_DIR_CLEAN` | 工作区干净 | 提示可能为误操作,需确认是否继续 |
|
|
81
|
-
| `
|
|
86
|
+
| `COVER_VALIDATE_COVER_FAILED` | tree checkout/clean 失败 | 提示检查目标 worktree 状态后重试 |
|
|
82
87
|
|
|
83
88
|
**实现要点:**
|
|
84
89
|
|
|
@@ -87,7 +92,11 @@ clawt cover
|
|
|
87
92
|
- 辅助函数:
|
|
88
93
|
- `extractTargetBranchName()`:从验证分支名提取目标分支名
|
|
89
94
|
- `findTargetWorktreePath()`:查找目标 worktree 路径
|
|
90
|
-
- `
|
|
95
|
+
- `computeWorktreeTreeHash()`:计算当前工作区 tree hash(保存并恢复暂存区状态)
|
|
96
|
+
- Git 工具函数:
|
|
97
|
+
- `gitReadTree()`:将 tree 写入暂存区
|
|
98
|
+
- `gitCheckoutIndexForce()`:强制检出暂存区到工作区
|
|
99
|
+
- `gitCleanForce()`:清理未跟踪文件
|
|
91
100
|
- 消息常量:`COVER_VALIDATE_MESSAGES`(`src/constants/messages/cover-validate.ts`)
|
|
92
101
|
- `writeSnapshot` 调用时只传 `treeHash`,利用其可选参数特性保留磁盘上的 `headCommitHash` 和 `stagedTreeHash` 原值
|
|
93
102
|
|
package/docs/resume.md
CHANGED
|
@@ -88,17 +88,18 @@ clawt resume -f tasks.md -c 2
|
|
|
88
88
|
|
|
89
89
|
| 配置值 | 行为 |
|
|
90
90
|
| ---------- | ------------------------------------------------------------ |
|
|
91
|
-
| `auto` | 自动检测:优先检测 cmux 环境(通过 `
|
|
92
|
-
| `cmux` | 在当前 cmux
|
|
91
|
+
| `auto` | 自动检测:优先检测 cmux 环境(通过 `CMUX_WORKSPACE_ID` 环境变量),在 cmux 环境时在当前 workspace 创建新 surface;否则检测 iTerm2 是否已安装,已安装则使用 iTerm2,否则降级到 Terminal.app |
|
|
92
|
+
| `cmux` | 在当前 cmux workspace 中创建新 surface 执行命令 |
|
|
93
93
|
| `iterm2` | 强制使用 iTerm2 创建新 Tab |
|
|
94
94
|
| `terminal` | 强制使用 Terminal.app 创建新 Tab |
|
|
95
95
|
|
|
96
96
|
**平台限制:** 批量恢复目前仅支持 macOS 平台。非 macOS 平台会抛出错误。
|
|
97
97
|
|
|
98
98
|
**cmux 集成说明:**
|
|
99
|
-
-
|
|
100
|
-
-
|
|
101
|
-
-
|
|
99
|
+
- 环境检测:通过 `CMUX_WORKSPACE_ID` 环境变量判断是否在 cmux 环境中
|
|
100
|
+
- 创建 surface:使用 `cmux new-split right` 在右侧创建新 surface
|
|
101
|
+
- 执行命令:通过 `cmux send --surface <surface-id> <command>\n` 发送命令(`\n` 触发自动执行)
|
|
102
|
+
- 输出解析:支持简短格式 `surface:24` 和完整格式 `OK surface:24 pane:14 workspace:5`
|
|
102
103
|
|
|
103
104
|
**权限要求:** Terminal.app 通过 System Events 模拟键盘操作(`Cmd+T`)新建 Tab,需要在「系统设置 → 隐私与安全性 → 辅助功能」中授权终端应用。iTerm2 使用原生 AppleScript 接口,无需辅助功能权限。cmux 通过 CLI 命令操作,无需特殊权限。
|
|
104
105
|
|
package/package.json
CHANGED
|
@@ -15,8 +15,8 @@ import {
|
|
|
15
15
|
gitAddAll,
|
|
16
16
|
gitWriteTree,
|
|
17
17
|
gitReadTree,
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
gitCheckoutIndexForce,
|
|
19
|
+
gitCleanForce,
|
|
20
20
|
printSuccess,
|
|
21
21
|
printInfo,
|
|
22
22
|
isWorkingDirClean,
|
|
@@ -62,15 +62,12 @@ export function findTargetWorktreePath(branchName: string): string {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
/**
|
|
65
|
-
*
|
|
65
|
+
* 计算验证分支当前的 tree hash(保存并恢复暂存区状态)
|
|
66
66
|
* 操作序列:git write-tree(保存暂存区)→ git add . → git write-tree(获取工作区 tree)→ git read-tree(恢复暂存区)
|
|
67
|
-
* 当 snapshotTreeHash 与当前 tree hash 相同时返回 null 表示无变更
|
|
68
|
-
* @param {string} snapshotTreeHash - 快照中记录的 tree hash
|
|
69
67
|
* @param {string} mainWorktreePath - 主 worktree 路径
|
|
70
|
-
* @returns {
|
|
68
|
+
* @returns {string} 当前工作区对应的 tree hash
|
|
71
69
|
*/
|
|
72
|
-
export function
|
|
73
|
-
// 先保存当前暂存区的 tree hash,用于后续恢复
|
|
70
|
+
export function computeWorktreeTreeHash(mainWorktreePath: string): string {
|
|
74
71
|
const savedIndexTreeHash = gitWriteTree(mainWorktreePath);
|
|
75
72
|
let currentTreeHash: string;
|
|
76
73
|
|
|
@@ -78,17 +75,10 @@ export function computeIncrementalPatch(snapshotTreeHash: string, mainWorktreePa
|
|
|
78
75
|
gitAddAll(mainWorktreePath);
|
|
79
76
|
currentTreeHash = gitWriteTree(mainWorktreePath);
|
|
80
77
|
} finally {
|
|
81
|
-
// 无论成功或失败,都通过 git read-tree 恢复原始暂存区状态
|
|
82
78
|
gitReadTree(savedIndexTreeHash, mainWorktreePath);
|
|
83
79
|
}
|
|
84
80
|
|
|
85
|
-
|
|
86
|
-
if (snapshotTreeHash === currentTreeHash) {
|
|
87
|
-
return null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const patch = gitDiffTree(snapshotTreeHash, currentTreeHash, mainWorktreePath);
|
|
91
|
-
return { patch, currentTreeHash };
|
|
81
|
+
return currentTreeHash;
|
|
92
82
|
}
|
|
93
83
|
|
|
94
84
|
/**
|
|
@@ -127,23 +117,26 @@ async function handleCoverValidate(): Promise<void> {
|
|
|
127
117
|
if (!confirmed) return;
|
|
128
118
|
}
|
|
129
119
|
|
|
130
|
-
// 步骤 4
|
|
131
|
-
const
|
|
132
|
-
|
|
120
|
+
// 步骤 4:计算当前 tree hash
|
|
121
|
+
const currentTreeHash = computeWorktreeTreeHash(mainWorktreePath);
|
|
122
|
+
|
|
123
|
+
if (snapshotTreeHash === currentTreeHash) {
|
|
133
124
|
printInfo(MESSAGES.COVER_VALIDATE_NO_CHANGES);
|
|
134
125
|
return;
|
|
135
126
|
}
|
|
136
127
|
|
|
137
|
-
// 步骤 5
|
|
128
|
+
// 步骤 5:直接将 tree 应用到目标 worktree
|
|
138
129
|
try {
|
|
139
|
-
|
|
130
|
+
gitReadTree(currentTreeHash, targetWorktreePath);
|
|
131
|
+
gitCheckoutIndexForce(targetWorktreePath);
|
|
132
|
+
gitCleanForce(targetWorktreePath);
|
|
140
133
|
} catch (error) {
|
|
141
|
-
logger.error(`cover-validate
|
|
142
|
-
throw new ClawtError(MESSAGES.
|
|
134
|
+
logger.error(`cover-validate 覆盖失败: ${error}`);
|
|
135
|
+
throw new ClawtError(MESSAGES.COVER_VALIDATE_COVER_FAILED(targetBranchName));
|
|
143
136
|
}
|
|
144
137
|
|
|
145
138
|
// 步骤 6:更新快照 treeHash(使后续再次 cover 的基准正确),HEAD 和 stagedTreeHash 不变
|
|
146
|
-
writeSnapshot(projectName, targetBranchName,
|
|
139
|
+
writeSnapshot(projectName, targetBranchName, currentTreeHash);
|
|
147
140
|
|
|
148
141
|
printSuccess(MESSAGES.COVER_VALIDATE_SUCCESS(targetBranchName));
|
|
149
142
|
}
|
|
@@ -10,9 +10,9 @@ export const COVER_VALIDATE_MESSAGES = {
|
|
|
10
10
|
/** 无快照,提示先执行 validate */
|
|
11
11
|
COVER_VALIDATE_NO_SNAPSHOT: (branch: string) =>
|
|
12
12
|
`未找到分支 ${branch} 的 validate 快照\n 请先执行 clawt validate -b ${branch} 创建快照`,
|
|
13
|
-
/**
|
|
14
|
-
|
|
15
|
-
`覆盖变更到 worktree ${branch} 失败:
|
|
13
|
+
/** 覆盖失败(tree checkout/clean 失败) */
|
|
14
|
+
COVER_VALIDATE_COVER_FAILED: (branch: string) =>
|
|
15
|
+
`覆盖变更到 worktree ${branch} 失败:tree checkout 或清理操作出错\n 请检查目标 worktree 状态后重试`,
|
|
16
16
|
/** 工作区和暂存区无修改,可能为误操作 */
|
|
17
17
|
COVER_VALIDATE_WORKING_DIR_CLEAN: '当前验证分支的工作区和暂存区没有任何修改,可能为误操作',
|
|
18
18
|
/** 覆盖成功 */
|
package/src/utils/git-core.ts
CHANGED
|
@@ -159,6 +159,14 @@ export function gitCleanForce(cwd?: string): void {
|
|
|
159
159
|
execCommand('git clean -fd', { cwd });
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
+
/**
|
|
163
|
+
* git checkout-index -f -a,将暂存区内容强制写入工作区(覆盖工作区文件)
|
|
164
|
+
* @param {string} [cwd] - 工作目录
|
|
165
|
+
*/
|
|
166
|
+
export function gitCheckoutIndexForce(cwd?: string): void {
|
|
167
|
+
execCommand('git checkout-index -f -a', { cwd });
|
|
168
|
+
}
|
|
169
|
+
|
|
162
170
|
/**
|
|
163
171
|
* git stash push -m <message>
|
|
164
172
|
* @param {string} message - stash 消息
|
package/src/utils/index.ts
CHANGED
|
@@ -21,7 +21,7 @@ vi.mock('../../../src/constants/index.js', () => ({
|
|
|
21
21
|
COVER_VALIDATE_NO_CHANGES: '验证分支上没有相对于快照的增量修改,无需覆盖',
|
|
22
22
|
COVER_VALIDATE_TARGET_NOT_FOUND: (branch: string) => `未找到分支 ${branch} 对应的 worktree`,
|
|
23
23
|
COVER_VALIDATE_NO_SNAPSHOT: (branch: string) => `未找到分支 ${branch} 的 validate 快照`,
|
|
24
|
-
|
|
24
|
+
COVER_VALIDATE_COVER_FAILED: (branch: string) => `覆盖变更到 worktree ${branch} 失败`,
|
|
25
25
|
COVER_VALIDATE_SUCCESS: (branch: string) => `✓ 已将验证分支上的修改覆盖到 worktree => ${branch}`,
|
|
26
26
|
COVER_VALIDATE_WORKING_DIR_CLEAN: '当前验证分支的工作区和暂存区没有任何修改,可能为误操作',
|
|
27
27
|
},
|
|
@@ -42,8 +42,8 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
42
42
|
gitAddAll: vi.fn(),
|
|
43
43
|
gitWriteTree: vi.fn().mockReturnValue('current-tree-hash'),
|
|
44
44
|
gitReadTree: vi.fn(),
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
gitCheckoutIndexForce: vi.fn(),
|
|
46
|
+
gitCleanForce: vi.fn(),
|
|
47
47
|
printSuccess: vi.fn(),
|
|
48
48
|
printInfo: vi.fn(),
|
|
49
49
|
isWorkingDirClean: vi.fn().mockReturnValue(false),
|
|
@@ -53,7 +53,7 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
53
53
|
isNonInteractive: vi.fn().mockReturnValue(false),
|
|
54
54
|
}));
|
|
55
55
|
|
|
56
|
-
import { registerCoverValidateCommand, extractTargetBranchName, findTargetWorktreePath,
|
|
56
|
+
import { registerCoverValidateCommand, extractTargetBranchName, findTargetWorktreePath, computeWorktreeTreeHash } from '../../../src/commands/cover-validate.js';
|
|
57
57
|
import {
|
|
58
58
|
getCurrentBranch,
|
|
59
59
|
getProjectWorktrees,
|
|
@@ -64,8 +64,8 @@ import {
|
|
|
64
64
|
gitAddAll,
|
|
65
65
|
gitWriteTree,
|
|
66
66
|
gitReadTree,
|
|
67
|
-
|
|
68
|
-
|
|
67
|
+
gitCheckoutIndexForce,
|
|
68
|
+
gitCleanForce,
|
|
69
69
|
printSuccess,
|
|
70
70
|
printInfo,
|
|
71
71
|
isWorkingDirClean,
|
|
@@ -81,8 +81,8 @@ const mockedWriteSnapshot = vi.mocked(writeSnapshot);
|
|
|
81
81
|
const mockedGitAddAll = vi.mocked(gitAddAll);
|
|
82
82
|
const mockedGitWriteTree = vi.mocked(gitWriteTree);
|
|
83
83
|
const mockedGitReadTree = vi.mocked(gitReadTree);
|
|
84
|
-
const
|
|
85
|
-
const
|
|
84
|
+
const mockedGitCheckoutIndexForce = vi.mocked(gitCheckoutIndexForce);
|
|
85
|
+
const mockedGitCleanForce = vi.mocked(gitCleanForce);
|
|
86
86
|
const mockedPrintSuccess = vi.mocked(printSuccess);
|
|
87
87
|
const mockedPrintInfo = vi.mocked(printInfo);
|
|
88
88
|
const mockedIsWorkingDirClean = vi.mocked(isWorkingDirClean);
|
|
@@ -99,7 +99,6 @@ beforeEach(() => {
|
|
|
99
99
|
mockedIsWorkingDirClean.mockReturnValue(false);
|
|
100
100
|
mockedConfirmAction.mockResolvedValue(true);
|
|
101
101
|
mockedGitWriteTree.mockReturnValue('current-tree-hash');
|
|
102
|
-
mockedGitDiffTree.mockReturnValue(Buffer.from('fake-patch'));
|
|
103
102
|
});
|
|
104
103
|
|
|
105
104
|
describe('registerCoverValidateCommand', () => {
|
|
@@ -130,27 +129,16 @@ describe('findTargetWorktreePath', () => {
|
|
|
130
129
|
});
|
|
131
130
|
});
|
|
132
131
|
|
|
133
|
-
describe('
|
|
134
|
-
it('
|
|
132
|
+
describe('computeWorktreeTreeHash', () => {
|
|
133
|
+
it('返回当前工作区的 tree hash', () => {
|
|
135
134
|
mockedGitWriteTree
|
|
136
135
|
.mockReturnValueOnce('saved-index-tree') // 保存暂存区
|
|
137
136
|
.mockReturnValueOnce('new-tree-hash'); // git add . 后的 tree
|
|
138
137
|
|
|
139
|
-
const result =
|
|
140
|
-
expect(result).
|
|
141
|
-
expect(result!.currentTreeHash).toBe('new-tree-hash');
|
|
138
|
+
const result = computeWorktreeTreeHash('/repo');
|
|
139
|
+
expect(result).toBe('new-tree-hash');
|
|
142
140
|
expect(mockedGitAddAll).toHaveBeenCalledWith('/repo');
|
|
143
141
|
expect(mockedGitReadTree).toHaveBeenCalledWith('saved-index-tree', '/repo');
|
|
144
|
-
expect(mockedGitDiffTree).toHaveBeenCalledWith('snapshot-tree-hash', 'new-tree-hash', '/repo');
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('无增量变更时返回 null', () => {
|
|
148
|
-
mockedGitWriteTree
|
|
149
|
-
.mockReturnValueOnce('saved-index-tree')
|
|
150
|
-
.mockReturnValueOnce('snapshot-tree-hash'); // 与快照相同
|
|
151
|
-
|
|
152
|
-
const result = computeIncrementalPatch('snapshot-tree-hash', '/repo');
|
|
153
|
-
expect(result).toBeNull();
|
|
154
142
|
});
|
|
155
143
|
});
|
|
156
144
|
|
|
@@ -163,7 +151,7 @@ describe('handleCoverValidate - 工作区干净检查', () => {
|
|
|
163
151
|
await program.parseAsync(['cover'], { from: 'user' });
|
|
164
152
|
}
|
|
165
153
|
|
|
166
|
-
it('
|
|
154
|
+
it('工作区干净且用户取消时不执行覆盖', async () => {
|
|
167
155
|
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
168
156
|
mockedConfirmAction.mockResolvedValue(false);
|
|
169
157
|
|
|
@@ -173,14 +161,14 @@ describe('handleCoverValidate - 工作区干净检查', () => {
|
|
|
173
161
|
expect(mockedConfirmAction).toHaveBeenCalledWith('是否继续执行覆盖?');
|
|
174
162
|
// 用户取消后不应继续执行后续逻辑
|
|
175
163
|
expect(mockedGitAddAll).not.toHaveBeenCalled();
|
|
176
|
-
expect(
|
|
164
|
+
expect(mockedGitCheckoutIndexForce).not.toHaveBeenCalled();
|
|
165
|
+
expect(mockedGitCleanForce).not.toHaveBeenCalled();
|
|
177
166
|
expect(mockedPrintSuccess).not.toHaveBeenCalled();
|
|
178
167
|
});
|
|
179
168
|
|
|
180
169
|
it('工作区干净且用户确认继续时正常执行', async () => {
|
|
181
170
|
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
182
171
|
mockedConfirmAction.mockResolvedValue(true);
|
|
183
|
-
// computeIncrementalPatch 返回有 patch 的结果
|
|
184
172
|
mockedGitWriteTree
|
|
185
173
|
.mockReturnValueOnce('saved-index-tree')
|
|
186
174
|
.mockReturnValueOnce('new-tree-hash');
|
|
@@ -188,7 +176,9 @@ describe('handleCoverValidate - 工作区干净检查', () => {
|
|
|
188
176
|
await runCover();
|
|
189
177
|
|
|
190
178
|
expect(mockedConfirmAction).toHaveBeenCalledWith('是否继续执行覆盖?');
|
|
191
|
-
expect(
|
|
179
|
+
expect(mockedGitReadTree).toHaveBeenCalledWith('new-tree-hash', '/path/feature');
|
|
180
|
+
expect(mockedGitCheckoutIndexForce).toHaveBeenCalledWith('/path/feature');
|
|
181
|
+
expect(mockedGitCleanForce).toHaveBeenCalledWith('/path/feature');
|
|
192
182
|
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
193
183
|
});
|
|
194
184
|
|
|
@@ -201,7 +191,9 @@ describe('handleCoverValidate - 工作区干净检查', () => {
|
|
|
201
191
|
await runCover();
|
|
202
192
|
|
|
203
193
|
expect(mockedConfirmAction).not.toHaveBeenCalled();
|
|
204
|
-
expect(
|
|
194
|
+
expect(mockedGitReadTree).toHaveBeenCalledWith('new-tree-hash', '/path/feature');
|
|
195
|
+
expect(mockedGitCheckoutIndexForce).toHaveBeenCalledWith('/path/feature');
|
|
196
|
+
expect(mockedGitCleanForce).toHaveBeenCalledWith('/path/feature');
|
|
205
197
|
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
206
198
|
});
|
|
207
199
|
});
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
gitPush,
|
|
37
37
|
gitResetHard,
|
|
38
38
|
gitCleanForce,
|
|
39
|
+
gitCheckoutIndexForce,
|
|
39
40
|
gitStashPush,
|
|
40
41
|
gitStashApply,
|
|
41
42
|
gitStashPop,
|
|
@@ -251,6 +252,13 @@ describe('gitCleanForce', () => {
|
|
|
251
252
|
});
|
|
252
253
|
});
|
|
253
254
|
|
|
255
|
+
describe('gitCheckoutIndexForce', () => {
|
|
256
|
+
it('执行 git checkout-index -f -a', () => {
|
|
257
|
+
gitCheckoutIndexForce('/repo');
|
|
258
|
+
expect(mockedExecCommand).toHaveBeenCalledWith('git checkout-index -f -a', { cwd: '/repo' });
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
254
262
|
describe('gitStashPush', () => {
|
|
255
263
|
it('执行 git stash push -m', () => {
|
|
256
264
|
gitStashPush('auto-stash', '/repo');
|