clawt 1.0.4 → 1.0.6
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 +88 -6
- package/package.json +1 -1
- package/src/commands/create.ts +11 -1
- package/src/commands/list.ts +14 -1
- package/src/constants/messages.ts +6 -0
- package/src/types/command.ts +2 -2
- package/src/types/index.ts +1 -1
- package/src/types/worktree.ts +12 -0
- package/src/utils/config.ts +19 -3
- package/src/utils/formatter.ts +30 -0
- package/src/utils/git.ts +55 -0
- package/src/utils/index.ts +4 -2
- package/src/utils/worktree.ts +21 -2
package/dist/index.js
CHANGED
|
@@ -72,10 +72,16 @@ var MESSAGES = {
|
|
|
72
72
|
INTERRUPT_CLEANED: (count) => `\u2713 \u5DF2\u6E05\u7406 ${count} \u4E2A worktree \u548C\u5BF9\u5E94\u5206\u652F`,
|
|
73
73
|
/** 中断后保留 worktree */
|
|
74
74
|
INTERRUPT_KEPT: "\u5DF2\u4FDD\u7559 worktree\uFF0C\u53EF\u7A0D\u540E\u4F7F\u7528 clawt remove \u624B\u52A8\u6E05\u7406",
|
|
75
|
+
/** 配置文件损坏,已重新生成默认配置 */
|
|
76
|
+
CONFIG_CORRUPTED: "\u914D\u7F6E\u6587\u4EF6\u635F\u574F\u6216\u65E0\u6CD5\u89E3\u6790\uFF0C\u5DF2\u91CD\u65B0\u751F\u6210\u9ED8\u8BA4\u914D\u7F6E",
|
|
75
77
|
/** 分隔线 */
|
|
76
78
|
SEPARATOR: "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500",
|
|
77
79
|
/** 粗分隔线 */
|
|
78
|
-
DOUBLE_SEPARATOR: "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550"
|
|
80
|
+
DOUBLE_SEPARATOR: "\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550",
|
|
81
|
+
/** 创建数量参数无效 */
|
|
82
|
+
INVALID_COUNT: (value) => `\u65E0\u6548\u7684\u521B\u5EFA\u6570\u91CF: "${value}"\uFF0C\u8BF7\u8F93\u5165\u6B63\u6574\u6570`,
|
|
83
|
+
/** worktree 状态获取失败 */
|
|
84
|
+
WORKTREE_STATUS_UNAVAILABLE: "(\u72B6\u6001\u4E0D\u53EF\u7528)"
|
|
79
85
|
};
|
|
80
86
|
|
|
81
87
|
// src/constants/exitCodes.ts
|
|
@@ -262,6 +268,33 @@ function hasLocalCommits(branchName, cwd) {
|
|
|
262
268
|
return false;
|
|
263
269
|
}
|
|
264
270
|
}
|
|
271
|
+
function getCommitCountAhead(branchName, cwd) {
|
|
272
|
+
const output = execCommand(`git rev-list --count HEAD..${branchName}`, { cwd });
|
|
273
|
+
return parseInt(output, 10) || 0;
|
|
274
|
+
}
|
|
275
|
+
function parseShortStat(output) {
|
|
276
|
+
let insertions = 0;
|
|
277
|
+
let deletions = 0;
|
|
278
|
+
const insertMatch = output.match(/(\d+)\s+insertion/);
|
|
279
|
+
if (insertMatch) {
|
|
280
|
+
insertions = parseInt(insertMatch[1], 10);
|
|
281
|
+
}
|
|
282
|
+
const deleteMatch = output.match(/(\d+)\s+deletion/);
|
|
283
|
+
if (deleteMatch) {
|
|
284
|
+
deletions = parseInt(deleteMatch[1], 10);
|
|
285
|
+
}
|
|
286
|
+
return { insertions, deletions };
|
|
287
|
+
}
|
|
288
|
+
function getDiffStat(branchName, worktreePath, cwd) {
|
|
289
|
+
const committedOutput = execCommand(`git diff --shortstat HEAD...${branchName}`, { cwd });
|
|
290
|
+
const committed = parseShortStat(committedOutput);
|
|
291
|
+
const uncommittedOutput = execCommand("git diff --shortstat HEAD", { cwd: worktreePath });
|
|
292
|
+
const uncommitted = parseShortStat(uncommittedOutput);
|
|
293
|
+
return {
|
|
294
|
+
insertions: committed.insertions + uncommitted.insertions,
|
|
295
|
+
deletions: committed.deletions + uncommitted.deletions
|
|
296
|
+
};
|
|
297
|
+
}
|
|
265
298
|
|
|
266
299
|
// src/utils/formatter.ts
|
|
267
300
|
import chalk from "chalk";
|
|
@@ -296,6 +329,22 @@ function confirmAction(question) {
|
|
|
296
329
|
});
|
|
297
330
|
});
|
|
298
331
|
}
|
|
332
|
+
function formatWorktreeStatus(status) {
|
|
333
|
+
const parts = [];
|
|
334
|
+
parts.push(chalk.yellow(`${status.commitCount} \u4E2A\u63D0\u4EA4`));
|
|
335
|
+
if (status.insertions === 0 && status.deletions === 0) {
|
|
336
|
+
parts.push("\u65E0\u53D8\u66F4");
|
|
337
|
+
} else {
|
|
338
|
+
const diffParts = [];
|
|
339
|
+
diffParts.push(chalk.green(`+${status.insertions}`));
|
|
340
|
+
diffParts.push(chalk.red(`-${status.deletions}`));
|
|
341
|
+
parts.push(diffParts.join(" "));
|
|
342
|
+
}
|
|
343
|
+
if (status.hasDirtyFiles) {
|
|
344
|
+
parts.push(chalk.gray("(\u672A\u63D0\u4EA4\u4FEE\u6539)"));
|
|
345
|
+
}
|
|
346
|
+
return parts.join(" ");
|
|
347
|
+
}
|
|
299
348
|
|
|
300
349
|
// src/utils/branch.ts
|
|
301
350
|
function sanitizeBranchName(branchName) {
|
|
@@ -424,15 +473,35 @@ function cleanupWorktrees(worktrees) {
|
|
|
424
473
|
const projectDir = getProjectWorktreeDir();
|
|
425
474
|
removeEmptyDir(projectDir);
|
|
426
475
|
}
|
|
476
|
+
function getWorktreeStatus(worktree) {
|
|
477
|
+
try {
|
|
478
|
+
const commitCount = getCommitCountAhead(worktree.branch);
|
|
479
|
+
const { insertions, deletions } = getDiffStat(worktree.branch, worktree.path);
|
|
480
|
+
const hasDirtyFiles = !isWorkingDirClean(worktree.path);
|
|
481
|
+
return { commitCount, insertions, deletions, hasDirtyFiles };
|
|
482
|
+
} catch (error) {
|
|
483
|
+
logger.error(`\u83B7\u53D6 worktree \u72B6\u6001\u5931\u8D25: ${worktree.path} - ${error}`);
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
427
487
|
|
|
428
488
|
// src/utils/config.ts
|
|
429
|
-
import { existsSync as existsSync4, readFileSync } from "fs";
|
|
489
|
+
import { existsSync as existsSync4, readFileSync, writeFileSync } from "fs";
|
|
430
490
|
function loadConfig() {
|
|
431
491
|
if (!existsSync4(CONFIG_PATH)) {
|
|
432
492
|
return { ...DEFAULT_CONFIG };
|
|
433
493
|
}
|
|
434
|
-
|
|
435
|
-
|
|
494
|
+
try {
|
|
495
|
+
const raw = readFileSync(CONFIG_PATH, "utf-8");
|
|
496
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
|
|
497
|
+
} catch {
|
|
498
|
+
logger.warn(MESSAGES.CONFIG_CORRUPTED);
|
|
499
|
+
writeDefaultConfig();
|
|
500
|
+
return { ...DEFAULT_CONFIG };
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
function writeDefaultConfig() {
|
|
504
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(DEFAULT_CONFIG, null, 2), "utf-8");
|
|
436
505
|
}
|
|
437
506
|
function getConfigValue(key) {
|
|
438
507
|
const config = loadConfig();
|
|
@@ -448,6 +517,7 @@ function ensureClawtDirs() {
|
|
|
448
517
|
import Enquirer from "enquirer";
|
|
449
518
|
|
|
450
519
|
// src/commands/list.ts
|
|
520
|
+
import chalk2 from "chalk";
|
|
451
521
|
function registerListCommand(program2) {
|
|
452
522
|
program2.command("list").description("\u5217\u51FA\u5F53\u524D\u9879\u76EE\u6240\u6709 worktree").action(() => {
|
|
453
523
|
handleList();
|
|
@@ -465,9 +535,15 @@ function handleList() {
|
|
|
465
535
|
} else {
|
|
466
536
|
for (const wt of worktrees) {
|
|
467
537
|
printInfo(` ${wt.path} [${wt.branch}]`);
|
|
538
|
+
const status = getWorktreeStatus(wt);
|
|
539
|
+
if (status) {
|
|
540
|
+
printInfo(` ${formatWorktreeStatus(status)}`);
|
|
541
|
+
} else {
|
|
542
|
+
printInfo(` ${chalk2.yellow(MESSAGES.WORKTREE_STATUS_UNAVAILABLE)}`);
|
|
543
|
+
}
|
|
544
|
+
printInfo("");
|
|
468
545
|
}
|
|
469
|
-
printInfo(`
|
|
470
|
-
\u5171 ${worktrees.length} \u4E2A worktree`);
|
|
546
|
+
printInfo(`\u5171 ${worktrees.length} \u4E2A worktree`);
|
|
471
547
|
}
|
|
472
548
|
}
|
|
473
549
|
|
|
@@ -480,6 +556,12 @@ function registerCreateCommand(program2) {
|
|
|
480
556
|
function handleCreate(options) {
|
|
481
557
|
validateMainWorktree();
|
|
482
558
|
const count = Number(options.number);
|
|
559
|
+
if (!Number.isInteger(count) || count <= 0) {
|
|
560
|
+
throw new ClawtError(
|
|
561
|
+
MESSAGES.INVALID_COUNT(options.number),
|
|
562
|
+
EXIT_CODES.ARGUMENT_ERROR
|
|
563
|
+
);
|
|
564
|
+
}
|
|
483
565
|
logger.info(`create \u547D\u4EE4\u6267\u884C\uFF0C\u5206\u652F: ${options.branch}\uFF0C\u6570\u91CF: ${count}`);
|
|
484
566
|
const worktrees = createWorktrees(options.branch, count);
|
|
485
567
|
printSuccess(MESSAGES.WORKTREE_CREATED(worktrees.length));
|
package/package.json
CHANGED
package/src/commands/create.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
-
import { MESSAGES } from '../constants/index.js';
|
|
2
|
+
import { MESSAGES, EXIT_CODES } from '../constants/index.js';
|
|
3
|
+
import { ClawtError } from '../errors/index.js';
|
|
3
4
|
import { logger } from '../logger/index.js';
|
|
4
5
|
import type { CreateOptions } from '../types/index.js';
|
|
5
6
|
import {
|
|
@@ -33,6 +34,15 @@ function handleCreate(options: CreateOptions): void {
|
|
|
33
34
|
validateMainWorktree();
|
|
34
35
|
|
|
35
36
|
const count = Number(options.number);
|
|
37
|
+
|
|
38
|
+
// 校验创建数量必须为正整数
|
|
39
|
+
if (!Number.isInteger(count) || count <= 0) {
|
|
40
|
+
throw new ClawtError(
|
|
41
|
+
MESSAGES.INVALID_COUNT(options.number),
|
|
42
|
+
EXIT_CODES.ARGUMENT_ERROR,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
36
46
|
logger.info(`create 命令执行,分支: ${options.branch},数量: ${count}`);
|
|
37
47
|
|
|
38
48
|
const worktrees = createWorktrees(options.branch, count);
|
package/src/commands/list.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
+
import chalk from 'chalk';
|
|
2
3
|
import { MESSAGES } from '../constants/index.js';
|
|
3
4
|
import { logger } from '../logger/index.js';
|
|
4
5
|
import {
|
|
5
6
|
validateMainWorktree,
|
|
6
7
|
getProjectName,
|
|
7
8
|
getProjectWorktrees,
|
|
9
|
+
getWorktreeStatus,
|
|
10
|
+
formatWorktreeStatus,
|
|
8
11
|
printInfo,
|
|
9
12
|
} from '../utils/index.js';
|
|
10
13
|
|
|
@@ -39,7 +42,17 @@ function handleList(): void {
|
|
|
39
42
|
} else {
|
|
40
43
|
for (const wt of worktrees) {
|
|
41
44
|
printInfo(` ${wt.path} [${wt.branch}]`);
|
|
45
|
+
|
|
46
|
+
// 获取并展示 worktree 变更状态
|
|
47
|
+
const status = getWorktreeStatus(wt);
|
|
48
|
+
if (status) {
|
|
49
|
+
printInfo(` ${formatWorktreeStatus(status)}`);
|
|
50
|
+
} else {
|
|
51
|
+
printInfo(` ${chalk.yellow(MESSAGES.WORKTREE_STATUS_UNAVAILABLE)}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
printInfo('');
|
|
42
55
|
}
|
|
43
|
-
printInfo(
|
|
56
|
+
printInfo(`共 ${worktrees.length} 个 worktree`);
|
|
44
57
|
}
|
|
45
58
|
}
|
|
@@ -54,8 +54,14 @@ export const MESSAGES = {
|
|
|
54
54
|
INTERRUPT_CLEANED: (count: number) => `✓ 已清理 ${count} 个 worktree 和对应分支`,
|
|
55
55
|
/** 中断后保留 worktree */
|
|
56
56
|
INTERRUPT_KEPT: '已保留 worktree,可稍后使用 clawt remove 手动清理',
|
|
57
|
+
/** 配置文件损坏,已重新生成默认配置 */
|
|
58
|
+
CONFIG_CORRUPTED: '配置文件损坏或无法解析,已重新生成默认配置',
|
|
57
59
|
/** 分隔线 */
|
|
58
60
|
SEPARATOR: '────────────────────────────────────────',
|
|
59
61
|
/** 粗分隔线 */
|
|
60
62
|
DOUBLE_SEPARATOR: '════════════════════════════════════════',
|
|
63
|
+
/** 创建数量参数无效 */
|
|
64
|
+
INVALID_COUNT: (value: string) => `无效的创建数量: "${value}",请输入正整数`,
|
|
65
|
+
/** worktree 状态获取失败 */
|
|
66
|
+
WORKTREE_STATUS_UNAVAILABLE: '(状态不可用)',
|
|
61
67
|
} as const;
|
package/src/types/command.ts
CHANGED
package/src/types/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { ClawtConfig } from './config.js';
|
|
2
2
|
export type { CreateOptions, RunOptions, ValidateOptions, MergeOptions, RemoveOptions } from './command.js';
|
|
3
|
-
export type { WorktreeInfo } from './worktree.js';
|
|
3
|
+
export type { WorktreeInfo, WorktreeStatus } from './worktree.js';
|
|
4
4
|
export type { ClaudeCodeResult } from './claudeCode.js';
|
|
5
5
|
export type { TaskResult, TaskSummary } from './taskResult.js';
|
package/src/types/worktree.ts
CHANGED
|
@@ -5,3 +5,15 @@ export interface WorktreeInfo {
|
|
|
5
5
|
/** 分支名 */
|
|
6
6
|
branch: string;
|
|
7
7
|
}
|
|
8
|
+
|
|
9
|
+
/** worktree 变更统计信息 */
|
|
10
|
+
export interface WorktreeStatus {
|
|
11
|
+
/** 相对于主分支的新增提交数 */
|
|
12
|
+
commitCount: number;
|
|
13
|
+
/** 新增行数 */
|
|
14
|
+
insertions: number;
|
|
15
|
+
/** 删除行数 */
|
|
16
|
+
deletions: number;
|
|
17
|
+
/** 工作区是否有未提交修改 */
|
|
18
|
+
hasDirtyFiles: boolean;
|
|
19
|
+
}
|
package/src/utils/config.ts
CHANGED
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import { CONFIG_PATH, CLAWT_HOME, LOGS_DIR, WORKTREES_DIR, DEFAULT_CONFIG } from '../constants/index.js';
|
|
2
|
+
import { CONFIG_PATH, CLAWT_HOME, LOGS_DIR, WORKTREES_DIR, DEFAULT_CONFIG, MESSAGES } from '../constants/index.js';
|
|
3
3
|
import { ensureDir } from './fs.js';
|
|
4
|
+
import { logger } from '../logger/index.js';
|
|
4
5
|
import type { ClawtConfig } from '../types/index.js';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* 加载全局配置,不存在则返回默认配置
|
|
9
|
+
* 配置文件损坏或无法解析时,视为不存在,重新生成默认配置
|
|
8
10
|
* @returns {ClawtConfig} 配置对象
|
|
9
11
|
*/
|
|
10
12
|
export function loadConfig(): ClawtConfig {
|
|
11
13
|
if (!existsSync(CONFIG_PATH)) {
|
|
12
14
|
return { ...DEFAULT_CONFIG };
|
|
13
15
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
try {
|
|
17
|
+
const raw = readFileSync(CONFIG_PATH, 'utf-8');
|
|
18
|
+
return { ...DEFAULT_CONFIG, ...JSON.parse(raw) };
|
|
19
|
+
} catch {
|
|
20
|
+
// 配置文件损坏或无法解析时,重新生成默认配置
|
|
21
|
+
logger.warn(MESSAGES.CONFIG_CORRUPTED);
|
|
22
|
+
writeDefaultConfig();
|
|
23
|
+
return { ...DEFAULT_CONFIG };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 将默认配置写入配置文件
|
|
29
|
+
*/
|
|
30
|
+
function writeDefaultConfig(): void {
|
|
31
|
+
writeFileSync(CONFIG_PATH, JSON.stringify(DEFAULT_CONFIG, null, 2), 'utf-8');
|
|
16
32
|
}
|
|
17
33
|
|
|
18
34
|
/**
|
package/src/utils/formatter.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { MESSAGES } from '../constants/index.js';
|
|
3
3
|
import { createInterface } from 'node:readline';
|
|
4
|
+
import type { WorktreeStatus } from '../types/index.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* 输出成功信息
|
|
@@ -65,3 +66,32 @@ export function confirmAction(question: string): Promise<boolean> {
|
|
|
65
66
|
});
|
|
66
67
|
});
|
|
67
68
|
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 将 WorktreeStatus 格式化为带颜色的字符串
|
|
72
|
+
* @param {WorktreeStatus} status - worktree 变更统计信息
|
|
73
|
+
* @returns {string} 格式化后的状态字符串
|
|
74
|
+
*/
|
|
75
|
+
export function formatWorktreeStatus(status: WorktreeStatus): string {
|
|
76
|
+
const parts: string[] = [];
|
|
77
|
+
|
|
78
|
+
// 提交数(黄色)
|
|
79
|
+
parts.push(chalk.yellow(`${status.commitCount} 个提交`));
|
|
80
|
+
|
|
81
|
+
// 变更统计
|
|
82
|
+
if (status.insertions === 0 && status.deletions === 0) {
|
|
83
|
+
parts.push('无变更');
|
|
84
|
+
} else {
|
|
85
|
+
const diffParts: string[] = [];
|
|
86
|
+
diffParts.push(chalk.green(`+${status.insertions}`));
|
|
87
|
+
diffParts.push(chalk.red(`-${status.deletions}`));
|
|
88
|
+
parts.push(diffParts.join(' '));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 未提交修改提示(灰色)
|
|
92
|
+
if (status.hasDirtyFiles) {
|
|
93
|
+
parts.push(chalk.gray('(未提交修改)'));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return parts.join(' ');
|
|
97
|
+
}
|
package/src/utils/git.ts
CHANGED
|
@@ -241,3 +241,58 @@ export function hasLocalCommits(branchName: string, cwd?: string): boolean {
|
|
|
241
241
|
return false;
|
|
242
242
|
}
|
|
243
243
|
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 获取目标分支相对于当前分支的新增提交数
|
|
247
|
+
* @param {string} branchName - 目标分支名
|
|
248
|
+
* @param {string} [cwd] - 工作目录
|
|
249
|
+
* @returns {number} 新增提交数
|
|
250
|
+
*/
|
|
251
|
+
export function getCommitCountAhead(branchName: string, cwd?: string): number {
|
|
252
|
+
const output = execCommand(`git rev-list --count HEAD..${branchName}`, { cwd });
|
|
253
|
+
return parseInt(output, 10) || 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 解析 git diff --shortstat 输出,提取新增行数和删除行数
|
|
258
|
+
* @param {string} output - shortstat 输出字符串
|
|
259
|
+
* @returns {{ insertions: number; deletions: number }} 新增和删除行数
|
|
260
|
+
*/
|
|
261
|
+
function parseShortStat(output: string): { insertions: number; deletions: number } {
|
|
262
|
+
let insertions = 0;
|
|
263
|
+
let deletions = 0;
|
|
264
|
+
|
|
265
|
+
const insertMatch = output.match(/(\d+)\s+insertion/);
|
|
266
|
+
if (insertMatch) {
|
|
267
|
+
insertions = parseInt(insertMatch[1], 10);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const deleteMatch = output.match(/(\d+)\s+deletion/);
|
|
271
|
+
if (deleteMatch) {
|
|
272
|
+
deletions = parseInt(deleteMatch[1], 10);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return { insertions, deletions };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* 获取目标分支的变更统计(已提交 + 未提交)
|
|
280
|
+
* @param {string} branchName - 目标分支名
|
|
281
|
+
* @param {string} worktreePath - worktree 目录路径
|
|
282
|
+
* @param {string} [cwd] - 执行 git diff HEAD...branch 的工作目录
|
|
283
|
+
* @returns {{ insertions: number; deletions: number }} 聚合后的新增和删除行数
|
|
284
|
+
*/
|
|
285
|
+
export function getDiffStat(branchName: string, worktreePath: string, cwd?: string): { insertions: number; deletions: number } {
|
|
286
|
+
// 已提交的变更(当前分支与目标分支的差异)
|
|
287
|
+
const committedOutput = execCommand(`git diff --shortstat HEAD...${branchName}`, { cwd });
|
|
288
|
+
const committed = parseShortStat(committedOutput);
|
|
289
|
+
|
|
290
|
+
// 未提交的变更(在 worktree 内执行)
|
|
291
|
+
const uncommittedOutput = execCommand('git diff --shortstat HEAD', { cwd: worktreePath });
|
|
292
|
+
const uncommitted = parseShortStat(uncommittedOutput);
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
insertions: committed.insertions + uncommitted.insertions,
|
|
296
|
+
deletions: committed.deletions + uncommitted.deletions,
|
|
297
|
+
};
|
|
298
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -25,11 +25,13 @@ export {
|
|
|
25
25
|
gitWorktreeList,
|
|
26
26
|
gitWorktreePrune,
|
|
27
27
|
hasLocalCommits,
|
|
28
|
+
getCommitCountAhead,
|
|
29
|
+
getDiffStat,
|
|
28
30
|
} from './git.js';
|
|
29
31
|
export { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } from './branch.js';
|
|
30
32
|
export { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled } from './validation.js';
|
|
31
|
-
export { createWorktrees, getProjectWorktrees, getProjectWorktreeDir, cleanupWorktrees } from './worktree.js';
|
|
33
|
+
export { createWorktrees, getProjectWorktrees, getProjectWorktreeDir, cleanupWorktrees, getWorktreeStatus } from './worktree.js';
|
|
32
34
|
export { loadConfig, getConfigValue, ensureClawtDirs } from './config.js';
|
|
33
|
-
export { printSuccess, printError, printWarning, printInfo, printSeparator, printDoubleSeparator, confirmAction } from './formatter.js';
|
|
35
|
+
export { printSuccess, printError, printWarning, printInfo, printSeparator, printDoubleSeparator, confirmAction, formatWorktreeStatus } from './formatter.js';
|
|
34
36
|
export { ensureDir, removeEmptyDir } from './fs.js';
|
|
35
37
|
export { multilineInput } from './prompt.js';
|
package/src/utils/worktree.ts
CHANGED
|
@@ -2,10 +2,10 @@ import { join } from 'node:path';
|
|
|
2
2
|
import { existsSync, readdirSync } from 'node:fs';
|
|
3
3
|
import { WORKTREES_DIR } from '../constants/index.js';
|
|
4
4
|
import { logger } from '../logger/index.js';
|
|
5
|
-
import { createWorktree as gitCreateWorktree, getProjectName, gitWorktreeList, removeWorktreeByPath, deleteBranch, gitWorktreePrune } from './git.js';
|
|
5
|
+
import { createWorktree as gitCreateWorktree, getProjectName, gitWorktreeList, removeWorktreeByPath, deleteBranch, gitWorktreePrune, getCommitCountAhead, getDiffStat, isWorkingDirClean } from './git.js';
|
|
6
6
|
import { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } from './branch.js';
|
|
7
7
|
import { ensureDir, removeEmptyDir } from './fs.js';
|
|
8
|
-
import type { WorktreeInfo } from '../types/index.js';
|
|
8
|
+
import type { WorktreeInfo, WorktreeStatus } from '../types/index.js';
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* 获取当前项目的 worktree 存放目录
|
|
@@ -105,3 +105,22 @@ export function cleanupWorktrees(worktrees: WorktreeInfo[]): void {
|
|
|
105
105
|
const projectDir = getProjectWorktreeDir();
|
|
106
106
|
removeEmptyDir(projectDir);
|
|
107
107
|
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 获取 worktree 的变更统计信息
|
|
111
|
+
* 聚合提交数、变更行数、未提交修改状态
|
|
112
|
+
* @param {WorktreeInfo} worktree - worktree 信息
|
|
113
|
+
* @returns {WorktreeStatus | null} 变更统计信息,获取失败时返回 null
|
|
114
|
+
*/
|
|
115
|
+
export function getWorktreeStatus(worktree: WorktreeInfo): WorktreeStatus | null {
|
|
116
|
+
try {
|
|
117
|
+
const commitCount = getCommitCountAhead(worktree.branch);
|
|
118
|
+
const { insertions, deletions } = getDiffStat(worktree.branch, worktree.path);
|
|
119
|
+
const hasDirtyFiles = !isWorkingDirClean(worktree.path);
|
|
120
|
+
|
|
121
|
+
return { commitCount, insertions, deletions, hasDirtyFiles };
|
|
122
|
+
} catch (error) {
|
|
123
|
+
logger.error(`获取 worktree 状态失败: ${worktree.path} - ${error}`);
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
}
|