clawt 3.9.0 → 3.9.2
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 +28 -26
- 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/docs/status.md +8 -4
- package/package.json +1 -1
- package/src/commands/cover-validate.ts +17 -24
- package/src/commands/status.ts +5 -0
- package/src/constants/messages/cover-validate.ts +3 -3
- package/src/types/status.ts +4 -0
- package/src/utils/git-core.ts +8 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/interactive-panel-render.ts +17 -2
- package/tests/unit/commands/cover-validate.test.ts +21 -29
- package/tests/unit/commands/status.test.ts +37 -0
- 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
|
/** 覆盖成功 */
|
|
@@ -649,13 +649,6 @@ var PANEL_FOOTER_SHORTCUTS = Object.entries(SHORTCUT_LABELS).map(([key, label])
|
|
|
649
649
|
var PANEL_FOOTER_COUNTDOWN = (seconds) => chalk.gray(`(${seconds}s \u540E\u5237\u65B0)`);
|
|
650
650
|
var PANEL_OVERFLOW_DOWN_HINT = chalk.gray("\u2193 \u66F4\u591A worktree...");
|
|
651
651
|
var PANEL_OVERFLOW_UP_HINT = chalk.gray("\u2191 \u66F4\u591A worktree...");
|
|
652
|
-
var PANEL_SNAPSHOT_SUMMARY = (total, orphaned) => {
|
|
653
|
-
const base = `\u5FEB\u7167: ${total} \u4E2A`;
|
|
654
|
-
if (orphaned > 0) {
|
|
655
|
-
return `${base}\uFF08${chalk.yellow(`${orphaned} \u4E2A\u5B64\u7ACB`)}\uFF09`;
|
|
656
|
-
}
|
|
657
|
-
return base;
|
|
658
|
-
};
|
|
659
652
|
var PANEL_NO_WORKTREES = "(\u65E0\u6D3B\u8DC3 worktree)";
|
|
660
653
|
var PANEL_PRESS_ENTER_TO_RETURN = chalk.gray("\n\u6309 Enter \u8FD4\u56DE\u9762\u677F...");
|
|
661
654
|
var PANEL_NOT_TTY = "\u4EA4\u4E92\u5F0F\u9762\u677F\u9700\u8981 TTY \u7EC8\u7AEF\u73AF\u5883\uFF0C\u8BF7\u76F4\u63A5\u5728\u7EC8\u7AEF\u4E2D\u8FD0\u884C clawt status -i";
|
|
@@ -1252,6 +1245,9 @@ function gitResetHard(cwd) {
|
|
|
1252
1245
|
function gitCleanForce(cwd) {
|
|
1253
1246
|
execCommand("git clean -fd", { cwd });
|
|
1254
1247
|
}
|
|
1248
|
+
function gitCheckoutIndexForce(cwd) {
|
|
1249
|
+
execCommand("git checkout-index -f -a", { cwd });
|
|
1250
|
+
}
|
|
1255
1251
|
function gitStashPush(message, cwd) {
|
|
1256
1252
|
execCommand(`git stash push -m "${message}"`, { cwd });
|
|
1257
1253
|
}
|
|
@@ -3758,7 +3754,7 @@ function buildPanelFrame(statusResult, selectedIndex, scrollOffset, rows, cols,
|
|
|
3758
3754
|
const lines = [];
|
|
3759
3755
|
lines.push(PANEL_TITLE(statusResult.main.projectName));
|
|
3760
3756
|
lines.push(renderConfiguredBranchLine(statusResult.main));
|
|
3761
|
-
lines.push(
|
|
3757
|
+
lines.push(renderMainBranchDiff(statusResult.main));
|
|
3762
3758
|
const visibleRows = calculateVisibleRows(rows);
|
|
3763
3759
|
if (statusResult.worktrees.length === 0) {
|
|
3764
3760
|
lines.push(buildSeparatorWithHint(cols, ""));
|
|
@@ -3893,8 +3889,13 @@ function renderConfiguredBranchLine(main2) {
|
|
|
3893
3889
|
}
|
|
3894
3890
|
return PANEL_CONFIGURED_BRANCH(main2.configuredMainBranch);
|
|
3895
3891
|
}
|
|
3896
|
-
function
|
|
3897
|
-
|
|
3892
|
+
function renderMainBranchDiff(main2) {
|
|
3893
|
+
if (main2.insertions === 0 && main2.deletions === 0) {
|
|
3894
|
+
return `\u5DE5\u4F5C\u533A: ${chalk9.green(MESSAGES.STATUS_CHANGE_CLEAN)}`;
|
|
3895
|
+
}
|
|
3896
|
+
const insertText = chalk9.green(`+${main2.insertions}`);
|
|
3897
|
+
const deleteText = chalk9.red(`-${main2.deletions}`);
|
|
3898
|
+
return `\u5DE5\u4F5C\u533A: ${insertText} ${deleteText}`;
|
|
3898
3899
|
}
|
|
3899
3900
|
function renderFooter(countdown) {
|
|
3900
3901
|
return `${PANEL_FOOTER_SHORTCUTS} ${PANEL_FOOTER_COUNTDOWN(countdown)}`;
|
|
@@ -5150,7 +5151,7 @@ function findTargetWorktreePath(branchName) {
|
|
|
5150
5151
|
}
|
|
5151
5152
|
return match.path;
|
|
5152
5153
|
}
|
|
5153
|
-
function
|
|
5154
|
+
function computeWorktreeTreeHash(mainWorktreePath) {
|
|
5154
5155
|
const savedIndexTreeHash = gitWriteTree(mainWorktreePath);
|
|
5155
5156
|
let currentTreeHash;
|
|
5156
5157
|
try {
|
|
@@ -5159,11 +5160,7 @@ function computeIncrementalPatch(snapshotTreeHash, mainWorktreePath) {
|
|
|
5159
5160
|
} finally {
|
|
5160
5161
|
gitReadTree(savedIndexTreeHash, mainWorktreePath);
|
|
5161
5162
|
}
|
|
5162
|
-
|
|
5163
|
-
return null;
|
|
5164
|
-
}
|
|
5165
|
-
const patch = gitDiffTree(snapshotTreeHash, currentTreeHash, mainWorktreePath);
|
|
5166
|
-
return { patch, currentTreeHash };
|
|
5163
|
+
return currentTreeHash;
|
|
5167
5164
|
}
|
|
5168
5165
|
async function handleCoverValidate() {
|
|
5169
5166
|
await runPreChecks({ requireMainWorktree: true, requireHead: true, requireProjectConfig: true });
|
|
@@ -5185,18 +5182,20 @@ async function handleCoverValidate() {
|
|
|
5185
5182
|
const confirmed = await confirmAction("\u662F\u5426\u7EE7\u7EED\u6267\u884C\u8986\u76D6\uFF1F");
|
|
5186
5183
|
if (!confirmed) return;
|
|
5187
5184
|
}
|
|
5188
|
-
const
|
|
5189
|
-
if (
|
|
5185
|
+
const currentTreeHash = computeWorktreeTreeHash(mainWorktreePath);
|
|
5186
|
+
if (snapshotTreeHash === currentTreeHash) {
|
|
5190
5187
|
printInfo(MESSAGES.COVER_VALIDATE_NO_CHANGES);
|
|
5191
5188
|
return;
|
|
5192
5189
|
}
|
|
5193
5190
|
try {
|
|
5194
|
-
|
|
5191
|
+
gitReadTree(currentTreeHash, targetWorktreePath);
|
|
5192
|
+
gitCheckoutIndexForce(targetWorktreePath);
|
|
5193
|
+
gitCleanForce(targetWorktreePath);
|
|
5195
5194
|
} catch (error) {
|
|
5196
|
-
logger.error(`cover-validate
|
|
5197
|
-
throw new ClawtError(MESSAGES.
|
|
5195
|
+
logger.error(`cover-validate \u8986\u76D6\u5931\u8D25: ${error}`);
|
|
5196
|
+
throw new ClawtError(MESSAGES.COVER_VALIDATE_COVER_FAILED(targetBranchName));
|
|
5198
5197
|
}
|
|
5199
|
-
writeSnapshot(projectName, targetBranchName,
|
|
5198
|
+
writeSnapshot(projectName, targetBranchName, currentTreeHash);
|
|
5200
5199
|
printSuccess(MESSAGES.COVER_VALIDATE_SUCCESS(targetBranchName));
|
|
5201
5200
|
}
|
|
5202
5201
|
|
|
@@ -5478,12 +5477,15 @@ function collectStatus() {
|
|
|
5478
5477
|
const projectConfig = loadProjectConfig();
|
|
5479
5478
|
const configuredMainBranch = projectConfig?.clawtMainWorkBranch || null;
|
|
5480
5479
|
const configuredBranchExists = configuredMainBranch ? checkBranchExists(configuredMainBranch) : null;
|
|
5480
|
+
const { insertions, deletions } = countDiffStat(process.cwd());
|
|
5481
5481
|
const main2 = {
|
|
5482
5482
|
branch: currentBranch,
|
|
5483
5483
|
isClean,
|
|
5484
5484
|
projectName,
|
|
5485
5485
|
configuredMainBranch,
|
|
5486
|
-
configuredBranchExists
|
|
5486
|
+
configuredBranchExists,
|
|
5487
|
+
insertions,
|
|
5488
|
+
deletions
|
|
5487
5489
|
};
|
|
5488
5490
|
const worktrees = getProjectWorktrees();
|
|
5489
5491
|
const worktreeStatuses = worktrees.map((wt) => collectWorktreeDetailedStatus(wt, projectName));
|
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/docs/status.md
CHANGED
|
@@ -131,7 +131,9 @@ clawt status [--json] [-i | --interactive]
|
|
|
131
131
|
"isClean": true,
|
|
132
132
|
"projectName": "main-project",
|
|
133
133
|
"configuredMainBranch": "main",
|
|
134
|
-
"configuredBranchExists": true
|
|
134
|
+
"configuredBranchExists": true,
|
|
135
|
+
"insertions": 185,
|
|
136
|
+
"deletions": 42
|
|
135
137
|
},
|
|
136
138
|
"worktrees": [
|
|
137
139
|
{
|
|
@@ -163,10 +165,12 @@ clawt status [--json] [-i | --interactive]
|
|
|
163
165
|
| `projectName` | `string` | 项目名 |
|
|
164
166
|
| `configuredMainBranch` | `string \| null` | 配置的主工作分支名(项目未初始化时为 null) |
|
|
165
167
|
| `configuredBranchExists`| `boolean \| null` | 配置的主工作分支是否存在(项目未初始化时为 null)|
|
|
168
|
+
| `insertions` | `number` | 工作区和暂存区的新增行数 |
|
|
169
|
+
| `deletions` | `number` | 工作区和暂存区的删除行数 |
|
|
166
170
|
|
|
167
171
|
**实现要点:**
|
|
168
172
|
|
|
169
|
-
- 类型定义在 `src/types/status.ts`:`WorktreeDetailedStatus`(`snapshotTime: string | null`、`createdAt: string | null`)、`MainWorktreeStatus`(包含 `configuredMainBranch
|
|
173
|
+
- 类型定义在 `src/types/status.ts`:`WorktreeDetailedStatus`(`snapshotTime: string | null`、`createdAt: string | null`)、`MainWorktreeStatus`(包含 `configuredMainBranch`、`configuredBranchExists`、`insertions`、`deletions`)、`SnapshotInfo`、`SnapshotSummary`(包含 `total` 和 `orphaned`)、`StatusResult`(`snapshots` 为 `SnapshotSummary` 类型)
|
|
170
174
|
- 消息常量在 `MESSAGES.STATUS_*` 系列:
|
|
171
175
|
- `STATUS_TITLE(projectName)`:标题文本
|
|
172
176
|
- `STATUS_MAIN_SECTION`:主 worktree 区块标题
|
|
@@ -202,7 +206,7 @@ clawt status [--json] [-i | --interactive]
|
|
|
202
206
|
```
|
|
203
207
|
项目状态总览: my-project
|
|
204
208
|
主工作分支: main
|
|
205
|
-
|
|
209
|
+
工作区: +185 -42
|
|
206
210
|
──────── ↑ 更多 worktree... ────────
|
|
207
211
|
════ 2026-03-01(2 天前) ════
|
|
208
212
|
|
|
@@ -239,7 +243,7 @@ clawt status [--json] [-i | --interactive]
|
|
|
239
243
|
- 分支已删除(红色):`✗ 主工作分支: <branchName>(已不存在)`
|
|
240
244
|
- 分支不一致(黄色):`⚠ 主工作分支: <branchName>(不一致)`
|
|
241
245
|
- 未初始化(灰色):`未初始化(执行 clawt init 设置主工作分支)`
|
|
242
|
-
3.
|
|
246
|
+
3. **工作区 diff 信息行**:显示主工作分支的工作区 diff 统计,有变更时格式为 `工作区: +N -M`(新增行数绿色,删除行数红色),无变更时显示 `工作区: 无变更`(绿色)
|
|
243
247
|
4. **顶部分隔线**:当存在向上溢出时,分隔线中间嵌入 `↑ 更多 worktree...` 提示
|
|
244
248
|
5. **Worktree 滚动区域**:按日期分组显示 worktree 列表,支持上下滚动
|
|
245
249
|
6. **底部分隔线**:当存在向下溢出时,分隔线中间嵌入 `↓ 更多 worktree...` 提示
|
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
|
}
|
package/src/commands/status.ts
CHANGED
|
@@ -81,6 +81,9 @@ export function collectStatus(): StatusResult {
|
|
|
81
81
|
const configuredMainBranch = projectConfig?.clawtMainWorkBranch || null;
|
|
82
82
|
const configuredBranchExists = configuredMainBranch ? checkBranchExists(configuredMainBranch) : null;
|
|
83
83
|
|
|
84
|
+
// 主 worktree 的 diff 统计
|
|
85
|
+
const { insertions, deletions } = countDiffStat(process.cwd());
|
|
86
|
+
|
|
84
87
|
// 主 worktree 状态
|
|
85
88
|
const main: MainWorktreeStatus = {
|
|
86
89
|
branch: currentBranch,
|
|
@@ -88,6 +91,8 @@ export function collectStatus(): StatusResult {
|
|
|
88
91
|
projectName,
|
|
89
92
|
configuredMainBranch,
|
|
90
93
|
configuredBranchExists,
|
|
94
|
+
insertions,
|
|
95
|
+
deletions,
|
|
91
96
|
};
|
|
92
97
|
|
|
93
98
|
// 各 worktree 详细状态
|
|
@@ -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/types/status.ts
CHANGED
|
@@ -32,6 +32,10 @@ export interface MainWorktreeStatus {
|
|
|
32
32
|
configuredMainBranch: string | null;
|
|
33
33
|
/** 配置的主工作分支是否存在(项目未初始化时为 null) */
|
|
34
34
|
configuredBranchExists: boolean | null;
|
|
35
|
+
/** 工作区和暂存区的新增行数 */
|
|
36
|
+
insertions: number;
|
|
37
|
+
/** 工作区和暂存区的删除行数 */
|
|
38
|
+
deletions: number;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
/** validate 快照信息 */
|
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
|
@@ -87,8 +87,8 @@ export function buildPanelFrame(
|
|
|
87
87
|
// 配置分支信息行
|
|
88
88
|
lines.push(renderConfiguredBranchLine(statusResult.main));
|
|
89
89
|
|
|
90
|
-
//
|
|
91
|
-
lines.push(
|
|
90
|
+
// 主工作分支 diff 信息行
|
|
91
|
+
lines.push(renderMainBranchDiff(statusResult.main));
|
|
92
92
|
|
|
93
93
|
// 计算可用的 worktree 显示区域行数
|
|
94
94
|
const visibleRows = calculateVisibleRows(rows);
|
|
@@ -315,6 +315,21 @@ function renderConfiguredBranchLine(main: MainWorktreeStatus): string {
|
|
|
315
315
|
return PANEL_CONFIGURED_BRANCH(main.configuredMainBranch);
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
+
/**
|
|
319
|
+
* 渲染主工作分支的 diff 信息行
|
|
320
|
+
* 格式:工作区: +N -M(绿色新增,红色删除)或 工作区: 无变更(绿色)
|
|
321
|
+
* @param {MainWorktreeStatus} main - 主 worktree 状态
|
|
322
|
+
* @returns {string} 格式化的 diff 信息
|
|
323
|
+
*/
|
|
324
|
+
function renderMainBranchDiff(main: MainWorktreeStatus): string {
|
|
325
|
+
if (main.insertions === 0 && main.deletions === 0) {
|
|
326
|
+
return `工作区: ${chalk.green(MESSAGES.STATUS_CHANGE_CLEAN)}`;
|
|
327
|
+
}
|
|
328
|
+
const insertText = chalk.green(`+${main.insertions}`);
|
|
329
|
+
const deleteText = chalk.red(`-${main.deletions}`);
|
|
330
|
+
return `工作区: ${insertText} ${deleteText}`;
|
|
331
|
+
}
|
|
332
|
+
|
|
318
333
|
/**
|
|
319
334
|
* 渲染快照摘要行
|
|
320
335
|
* @param {number} total - 快照总数
|
|
@@ -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
|
});
|
|
@@ -340,4 +340,41 @@ describe('handleStatus', () => {
|
|
|
340
340
|
const unverifiedLine = printedLines.find((line) => line.includes('未验证'));
|
|
341
341
|
expect(unverifiedLine).toBeUndefined();
|
|
342
342
|
});
|
|
343
|
+
|
|
344
|
+
it('主 worktree diff 统计包含在 JSON 输出中', async () => {
|
|
345
|
+
// 模拟主 worktree 有变更
|
|
346
|
+
mockedGetDiffStat.mockReturnValue({ insertions: 185, deletions: 42 });
|
|
347
|
+
|
|
348
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
349
|
+
|
|
350
|
+
const program = new Command();
|
|
351
|
+
program.exitOverride();
|
|
352
|
+
registerStatusCommand(program);
|
|
353
|
+
await program.parseAsync(['status', '--json'], { from: 'user' });
|
|
354
|
+
|
|
355
|
+
const jsonCall = consoleSpy.mock.calls.find((call) => {
|
|
356
|
+
try { JSON.parse(call[0]); return true; } catch { return false; }
|
|
357
|
+
});
|
|
358
|
+
const parsed = JSON.parse(jsonCall![0]);
|
|
359
|
+
expect(parsed.main.insertions).toBe(185);
|
|
360
|
+
expect(parsed.main.deletions).toBe(42);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('主 worktree 无变更时 diff 统计为 0', async () => {
|
|
364
|
+
mockedGetDiffStat.mockReturnValue({ insertions: 0, deletions: 0 });
|
|
365
|
+
|
|
366
|
+
const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
367
|
+
|
|
368
|
+
const program = new Command();
|
|
369
|
+
program.exitOverride();
|
|
370
|
+
registerStatusCommand(program);
|
|
371
|
+
await program.parseAsync(['status', '--json'], { from: 'user' });
|
|
372
|
+
|
|
373
|
+
const jsonCall = consoleSpy.mock.calls.find((call) => {
|
|
374
|
+
try { JSON.parse(call[0]); return true; } catch { return false; }
|
|
375
|
+
});
|
|
376
|
+
const parsed = JSON.parse(jsonCall![0]);
|
|
377
|
+
expect(parsed.main.insertions).toBe(0);
|
|
378
|
+
expect(parsed.main.deletions).toBe(0);
|
|
379
|
+
});
|
|
343
380
|
});
|
|
@@ -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');
|