clawt 1.2.0 → 1.4.0
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/.claude/agent-memory/docs-sync-updater/MEMORY.md +16 -0
- package/CLAUDE.md +22 -6
- package/README.md +31 -2
- package/dist/index.js +279 -43
- package/dist/postinstall.js +1 -0
- package/docs/spec.md +162 -26
- package/package.json +1 -1
- package/src/commands/merge.ts +15 -0
- package/src/commands/sync.ts +116 -0
- package/src/commands/validate.ts +194 -26
- package/src/constants/index.ts +1 -1
- package/src/constants/messages.ts +25 -2
- package/src/constants/paths.ts +3 -0
- package/src/index.ts +2 -0
- package/src/types/command.ts +8 -0
- package/src/types/index.ts +1 -1
- package/src/utils/git.ts +85 -1
- package/src/utils/index.ts +10 -1
- package/src/utils/shell.ts +22 -1
- package/src/utils/validate-snapshot.ts +123 -0
package/src/commands/validate.ts
CHANGED
|
@@ -13,16 +13,24 @@ import {
|
|
|
13
13
|
getProjectWorktreeDir,
|
|
14
14
|
isWorkingDirClean,
|
|
15
15
|
gitAddAll,
|
|
16
|
+
gitCommit,
|
|
16
17
|
gitStashPush,
|
|
17
|
-
gitStashApply,
|
|
18
|
-
gitStashPop,
|
|
19
|
-
gitStashList,
|
|
20
18
|
gitRestoreStaged,
|
|
21
19
|
gitResetHard,
|
|
22
20
|
gitCleanForce,
|
|
23
|
-
|
|
21
|
+
gitDiffCachedBinary,
|
|
22
|
+
gitApplyCachedFromStdin,
|
|
23
|
+
gitDiffBinaryAgainstBranch,
|
|
24
|
+
gitApplyFromStdin,
|
|
25
|
+
gitResetSoft,
|
|
26
|
+
getHeadCommitHash,
|
|
27
|
+
hasLocalCommits,
|
|
28
|
+
hasSnapshot,
|
|
29
|
+
readSnapshot,
|
|
30
|
+
readSnapshotHead,
|
|
31
|
+
writeSnapshot,
|
|
32
|
+
removeSnapshot,
|
|
24
33
|
printSuccess,
|
|
25
|
-
printError,
|
|
26
34
|
printWarning,
|
|
27
35
|
printInfo,
|
|
28
36
|
} from '../utils/index.js';
|
|
@@ -36,13 +44,14 @@ export function registerValidateCommand(program: Command): void {
|
|
|
36
44
|
.command('validate')
|
|
37
45
|
.description('在主 worktree 验证某个 worktree 分支的变更')
|
|
38
46
|
.requiredOption('-b, --branch <branchName>', '要验证的分支名')
|
|
47
|
+
.option('--clean', '清理 validate 状态(重置主 worktree 并删除快照)')
|
|
39
48
|
.action(async (options: ValidateOptions) => {
|
|
40
49
|
await handleValidate(options);
|
|
41
50
|
});
|
|
42
51
|
}
|
|
43
52
|
|
|
44
53
|
/**
|
|
45
|
-
* 处理主 worktree
|
|
54
|
+
* 处理主 worktree 工作区有未提交更改的情况(首次 validate 时使用)
|
|
46
55
|
* @param {string} mainWorktreePath - 主 worktree 路径
|
|
47
56
|
*/
|
|
48
57
|
async function handleDirtyMainWorktree(mainWorktreePath: string): Promise<void> {
|
|
@@ -86,11 +95,162 @@ async function handleDirtyMainWorktree(mainWorktreePath: string): Promise<void>
|
|
|
86
95
|
}
|
|
87
96
|
}
|
|
88
97
|
|
|
98
|
+
/**
|
|
99
|
+
* 通过 patch 将目标分支的全量变更(已提交 + 未提交)迁移到主 worktree
|
|
100
|
+
* 使用 git diff HEAD...branch --binary 获取变更,避免 stash 方式无法检测已提交 commit 的问题
|
|
101
|
+
* @param {string} targetWorktreePath - 目标 worktree 路径
|
|
102
|
+
* @param {string} mainWorktreePath - 主 worktree 路径
|
|
103
|
+
* @param {string} branchName - 分支名
|
|
104
|
+
* @param {boolean} hasUncommitted - 目标 worktree 是否有未提交修改
|
|
105
|
+
*/
|
|
106
|
+
function migrateChangesViaPatch(targetWorktreePath: string, mainWorktreePath: string, branchName: string, hasUncommitted: boolean): void {
|
|
107
|
+
let didTempCommit = false;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
// 如果有未提交修改,先做临时 commit 以便 diff 能捕获全部变更
|
|
111
|
+
if (hasUncommitted) {
|
|
112
|
+
gitAddAll(targetWorktreePath);
|
|
113
|
+
gitCommit('clawt:temp-commit-for-validate', targetWorktreePath);
|
|
114
|
+
didTempCommit = true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 在主 worktree 执行三点 diff,获取目标分支自分叉点以来的全量变更
|
|
118
|
+
const patch = gitDiffBinaryAgainstBranch(branchName, mainWorktreePath);
|
|
119
|
+
|
|
120
|
+
// 应用 patch 到主 worktree 工作目录
|
|
121
|
+
if (patch.length > 0) {
|
|
122
|
+
try {
|
|
123
|
+
gitApplyFromStdin(patch, mainWorktreePath);
|
|
124
|
+
} catch (error) {
|
|
125
|
+
logger.warn(`patch apply 失败: ${error}`);
|
|
126
|
+
printWarning(MESSAGES.VALIDATE_PATCH_APPLY_FAILED(branchName));
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} finally {
|
|
131
|
+
// 确保临时 commit 一定会被撤销,恢复目标 worktree 原状
|
|
132
|
+
if (didTempCommit) {
|
|
133
|
+
gitResetSoft(1, targetWorktreePath);
|
|
134
|
+
gitRestoreStaged(targetWorktreePath);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 保存当前主 worktree 工作目录变更为纯净快照 patch
|
|
141
|
+
* 操作序列:git add . → git diff --cached --binary → git restore --staged .
|
|
142
|
+
* 同时记录主分支 HEAD hash,用于增量 validate 一致性校验
|
|
143
|
+
* @param {string} mainWorktreePath - 主 worktree 路径
|
|
144
|
+
* @param {string} projectName - 项目名
|
|
145
|
+
* @param {string} branchName - 分支名
|
|
146
|
+
* @returns {Buffer} 生成的 patch 内容
|
|
147
|
+
*/
|
|
148
|
+
function saveCurrentSnapshotPatch(mainWorktreePath: string, projectName: string, branchName: string): Buffer {
|
|
149
|
+
gitAddAll(mainWorktreePath);
|
|
150
|
+
const patch = gitDiffCachedBinary(mainWorktreePath);
|
|
151
|
+
gitRestoreStaged(mainWorktreePath);
|
|
152
|
+
const headHash = getHeadCommitHash(mainWorktreePath);
|
|
153
|
+
writeSnapshot(projectName, branchName, patch, headHash);
|
|
154
|
+
return patch;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* 处理 --clean 选项:清理 validate 状态
|
|
159
|
+
* @param {ValidateOptions} options - 命令选项
|
|
160
|
+
*/
|
|
161
|
+
function handleValidateClean(options: ValidateOptions): void {
|
|
162
|
+
validateMainWorktree();
|
|
163
|
+
|
|
164
|
+
const projectName = getProjectName();
|
|
165
|
+
const mainWorktreePath = getGitTopLevel();
|
|
166
|
+
|
|
167
|
+
logger.info(`validate --clean 执行,分支: ${options.branch}`);
|
|
168
|
+
|
|
169
|
+
// 清空主 worktree
|
|
170
|
+
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
171
|
+
gitResetHard(mainWorktreePath);
|
|
172
|
+
gitCleanForce(mainWorktreePath);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 删除对应的 patch 文件
|
|
176
|
+
removeSnapshot(projectName, options.branch);
|
|
177
|
+
|
|
178
|
+
printSuccess(MESSAGES.VALIDATE_CLEANED(options.branch));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* 首次 validate 逻辑(无历史快照)
|
|
183
|
+
* @param {string} targetWorktreePath - 目标 worktree 路径
|
|
184
|
+
* @param {string} mainWorktreePath - 主 worktree 路径
|
|
185
|
+
* @param {string} projectName - 项目名
|
|
186
|
+
* @param {string} branchName - 分支名
|
|
187
|
+
* @param {boolean} hasUncommitted - 目标 worktree 是否有未提交修改
|
|
188
|
+
*/
|
|
189
|
+
function handleFirstValidate(targetWorktreePath: string, mainWorktreePath: string, projectName: string, branchName: string, hasUncommitted: boolean): void {
|
|
190
|
+
// 通过 patch 迁移目标分支全量变更到主 worktree
|
|
191
|
+
migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
|
|
192
|
+
|
|
193
|
+
// 保存纯净快照到 patch 文件
|
|
194
|
+
saveCurrentSnapshotPatch(mainWorktreePath, projectName, branchName);
|
|
195
|
+
|
|
196
|
+
// 结果:暂存区=空,工作目录=全量变更
|
|
197
|
+
printSuccess(MESSAGES.VALIDATE_SUCCESS(branchName));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* 增量 validate 逻辑(存在历史快照)
|
|
202
|
+
* @param {string} targetWorktreePath - 目标 worktree 路径
|
|
203
|
+
* @param {string} mainWorktreePath - 主 worktree 路径
|
|
204
|
+
* @param {string} projectName - 项目名
|
|
205
|
+
* @param {string} branchName - 分支名
|
|
206
|
+
* @param {boolean} hasUncommitted - 目标 worktree 是否有未提交修改
|
|
207
|
+
*/
|
|
208
|
+
function handleIncrementalValidate(targetWorktreePath: string, mainWorktreePath: string, projectName: string, branchName: string, hasUncommitted: boolean): void {
|
|
209
|
+
// 步骤 1:读取旧 patch(在清空前读取)
|
|
210
|
+
const oldPatch = readSnapshot(projectName, branchName);
|
|
211
|
+
|
|
212
|
+
// 步骤 2:确保主 worktree 干净(调用方已通过 handleDirtyMainWorktree 处理)
|
|
213
|
+
// 这里做兜底清理,防止 handleDirtyMainWorktree 之后仍有残留
|
|
214
|
+
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
215
|
+
gitResetHard(mainWorktreePath);
|
|
216
|
+
gitCleanForce(mainWorktreePath);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 步骤 3:通过 patch 从目标分支获取最新全量变更
|
|
220
|
+
migrateChangesViaPatch(targetWorktreePath, mainWorktreePath, branchName, hasUncommitted);
|
|
221
|
+
|
|
222
|
+
// 步骤 4:保存最新快照
|
|
223
|
+
saveCurrentSnapshotPatch(mainWorktreePath, projectName, branchName);
|
|
224
|
+
|
|
225
|
+
// 步骤 5:将旧 patch 应用到暂存区
|
|
226
|
+
if (oldPatch.length > 0) {
|
|
227
|
+
try {
|
|
228
|
+
gitApplyCachedFromStdin(oldPatch, mainWorktreePath);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
// 旧 patch 无法应用(可能文件结构变化太大),降级为全量模式
|
|
231
|
+
logger.warn(`增量 apply 失败: ${error}`);
|
|
232
|
+
printWarning(MESSAGES.INCREMENTAL_VALIDATE_FALLBACK);
|
|
233
|
+
// 降级后暂存区保持为空,工作目录为最新全量变更,与首次 validate 一致
|
|
234
|
+
printSuccess(MESSAGES.VALIDATE_SUCCESS(branchName));
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 结果:暂存区=上次快照,工作目录=最新全量变更
|
|
240
|
+
printSuccess(MESSAGES.INCREMENTAL_VALIDATE_SUCCESS(branchName));
|
|
241
|
+
}
|
|
242
|
+
|
|
89
243
|
/**
|
|
90
244
|
* 执行 validate 命令的核心逻辑
|
|
91
245
|
* @param {ValidateOptions} options - 命令选项
|
|
92
246
|
*/
|
|
93
247
|
async function handleValidate(options: ValidateOptions): Promise<void> {
|
|
248
|
+
// 处理 --clean 选项
|
|
249
|
+
if (options.clean) {
|
|
250
|
+
handleValidateClean(options);
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
94
254
|
validateMainWorktree();
|
|
95
255
|
|
|
96
256
|
const projectName = getProjectName();
|
|
@@ -105,33 +265,41 @@ async function handleValidate(options: ValidateOptions): Promise<void> {
|
|
|
105
265
|
throw new ClawtError(MESSAGES.WORKTREE_NOT_FOUND(options.branch));
|
|
106
266
|
}
|
|
107
267
|
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
268
|
+
// 统一检测未提交修改 + 已提交 commit
|
|
269
|
+
const hasUncommitted = !isWorkingDirClean(targetWorktreePath);
|
|
270
|
+
const hasCommitted = hasLocalCommits(options.branch, mainWorktreePath);
|
|
112
271
|
|
|
113
|
-
|
|
114
|
-
if (isWorkingDirClean(targetWorktreePath)) {
|
|
272
|
+
if (!hasUncommitted && !hasCommitted) {
|
|
115
273
|
printInfo(MESSAGES.TARGET_WORKTREE_CLEAN);
|
|
116
274
|
return;
|
|
117
275
|
}
|
|
118
276
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
gitStashPush(stashMessage, targetWorktreePath);
|
|
122
|
-
gitStashApply(targetWorktreePath);
|
|
123
|
-
gitRestoreStaged(targetWorktreePath);
|
|
124
|
-
|
|
125
|
-
// 步骤 3:在主 worktree 应用 stash
|
|
126
|
-
const stashList = gitStashList(mainWorktreePath);
|
|
127
|
-
const firstLine = stashList.split('\n')[0] || '';
|
|
277
|
+
// 判断是否为增量 validate
|
|
278
|
+
let isIncremental = hasSnapshot(projectName, options.branch);
|
|
128
279
|
|
|
129
|
-
|
|
130
|
-
|
|
280
|
+
// 主分支 HEAD 发生变化或旧快照无 .head 记录时,清除后走首次全量模式
|
|
281
|
+
if (isIncremental) {
|
|
282
|
+
const savedHead = readSnapshotHead(projectName, options.branch);
|
|
283
|
+
const currentHead = getHeadCommitHash(mainWorktreePath);
|
|
284
|
+
if (!savedHead || savedHead !== currentHead) {
|
|
285
|
+
logger.info(`主分支 HEAD 不匹配 (${savedHead ?? 'null'} → ${currentHead}),清除旧快照`);
|
|
286
|
+
removeSnapshot(projectName, options.branch);
|
|
287
|
+
isIncremental = false;
|
|
288
|
+
}
|
|
131
289
|
}
|
|
132
290
|
|
|
133
|
-
|
|
291
|
+
if (isIncremental) {
|
|
292
|
+
// 增量模式:主 worktree 有残留状态时让用户选择处理方式
|
|
293
|
+
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
294
|
+
await handleDirtyMainWorktree(mainWorktreePath);
|
|
295
|
+
}
|
|
296
|
+
handleIncrementalValidate(targetWorktreePath, mainWorktreePath, projectName, options.branch, hasUncommitted);
|
|
297
|
+
} else {
|
|
298
|
+
// 首次模式:先确保主 worktree 干净
|
|
299
|
+
if (!isWorkingDirClean(mainWorktreePath)) {
|
|
300
|
+
await handleDirtyMainWorktree(mainWorktreePath);
|
|
301
|
+
}
|
|
134
302
|
|
|
135
|
-
|
|
136
|
-
|
|
303
|
+
handleFirstValidate(targetWorktreePath, mainWorktreePath, projectName, options.branch, hasUncommitted);
|
|
304
|
+
}
|
|
137
305
|
}
|
package/src/constants/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { CLAWT_HOME, CONFIG_PATH, LOGS_DIR, WORKTREES_DIR } from './paths.js';
|
|
1
|
+
export { CLAWT_HOME, CONFIG_PATH, LOGS_DIR, WORKTREES_DIR, VALIDATE_SNAPSHOTS_DIR } from './paths.js';
|
|
2
2
|
export { INVALID_BRANCH_CHARS } from './branch.js';
|
|
3
3
|
export { MESSAGES } from './messages.js';
|
|
4
4
|
export { EXIT_CODES } from './exitCodes.js';
|
|
@@ -23,8 +23,6 @@ export const MESSAGES = {
|
|
|
23
23
|
MAIN_WORKTREE_DIRTY: '主 worktree 有未提交的更改,请先处理',
|
|
24
24
|
/** 目标 worktree 无更改 */
|
|
25
25
|
TARGET_WORKTREE_CLEAN: '该 worktree 的分支上没有任何更改,无需验证',
|
|
26
|
-
/** stash 已变更 */
|
|
27
|
-
STASH_CHANGED: 'git stash list 已变更,请重新执行',
|
|
28
26
|
/** validate 成功 */
|
|
29
27
|
VALIDATE_SUCCESS: (branch: string) =>
|
|
30
28
|
`✓ 已将分支 ${branch} 的变更应用到主 worktree\n 可以开始验证了`,
|
|
@@ -64,4 +62,29 @@ export const MESSAGES = {
|
|
|
64
62
|
INVALID_COUNT: (value: string) => `无效的创建数量: "${value}",请输入正整数`,
|
|
65
63
|
/** worktree 状态获取失败 */
|
|
66
64
|
WORKTREE_STATUS_UNAVAILABLE: '(状态不可用)',
|
|
65
|
+
/** 增量 validate 成功提示 */
|
|
66
|
+
INCREMENTAL_VALIDATE_SUCCESS: (branch: string) =>
|
|
67
|
+
`✓ 已将分支 ${branch} 的最新变更应用到主 worktree(增量模式)\n 暂存区 = 上次快照,工作目录 = 最新变更`,
|
|
68
|
+
/** 增量 validate 降级为全量模式提示 */
|
|
69
|
+
INCREMENTAL_VALIDATE_FALLBACK: '增量对比失败,已降级为全量模式',
|
|
70
|
+
/** validate 状态已清理 */
|
|
71
|
+
VALIDATE_CLEANED: (branch: string) => `✓ 分支 ${branch} 的 validate 状态已清理`,
|
|
72
|
+
/** merge 命令检测到 validate 状态的提示 */
|
|
73
|
+
MERGE_VALIDATE_STATE_HINT: (branch: string) =>
|
|
74
|
+
`主 worktree 可能存在 validate 残留状态,可先执行 clawt validate -b ${branch} --clean 清理`,
|
|
75
|
+
/** sync 自动保存未提交变更 */
|
|
76
|
+
SYNC_AUTO_COMMITTED: (branch: string) =>
|
|
77
|
+
`已自动保存 ${branch} 分支的未提交变更`,
|
|
78
|
+
/** sync 开始合并 */
|
|
79
|
+
SYNC_MERGING: (targetBranch: string, mainBranch: string) =>
|
|
80
|
+
`正在将 ${mainBranch} 合并到 ${targetBranch} ...`,
|
|
81
|
+
/** sync 成功 */
|
|
82
|
+
SYNC_SUCCESS: (targetBranch: string, mainBranch: string) =>
|
|
83
|
+
`✓ 已将 ${mainBranch} 的最新代码同步到 ${targetBranch}`,
|
|
84
|
+
/** sync 冲突 */
|
|
85
|
+
SYNC_CONFLICT: (worktreePath: string) =>
|
|
86
|
+
`合并存在冲突,请进入目标 worktree 手动解决:\n cd ${worktreePath}\n 解决冲突后执行 git add . && git merge --continue`,
|
|
87
|
+
/** validate patch apply 失败,提示用户同步主分支 */
|
|
88
|
+
VALIDATE_PATCH_APPLY_FAILED: (branch: string) =>
|
|
89
|
+
`变更迁移失败:目标分支与主分支差异过大\n 请先执行 clawt sync -b ${branch} 同步主分支后重试`,
|
|
67
90
|
} as const;
|
package/src/constants/paths.ts
CHANGED
|
@@ -12,3 +12,6 @@ export const LOGS_DIR = join(CLAWT_HOME, 'logs');
|
|
|
12
12
|
|
|
13
13
|
/** worktree 统一存放目录 ~/.clawt/worktrees/ */
|
|
14
14
|
export const WORKTREES_DIR = join(CLAWT_HOME, 'worktrees');
|
|
15
|
+
|
|
16
|
+
/** validate 快照目录 ~/.clawt/validate-snapshots/ */
|
|
17
|
+
export const VALIDATE_SNAPSHOTS_DIR = join(CLAWT_HOME, 'validate-snapshots');
|
package/src/index.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { registerResumeCommand } from './commands/resume.js';
|
|
|
12
12
|
import { registerValidateCommand } from './commands/validate.js';
|
|
13
13
|
import { registerMergeCommand } from './commands/merge.js';
|
|
14
14
|
import { registerConfigCommand } from './commands/config.js';
|
|
15
|
+
import { registerSyncCommand } from './commands/sync.js';
|
|
15
16
|
|
|
16
17
|
// 从 package.json 读取版本号,避免硬编码
|
|
17
18
|
const require = createRequire(import.meta.url);
|
|
@@ -36,6 +37,7 @@ registerResumeCommand(program);
|
|
|
36
37
|
registerValidateCommand(program);
|
|
37
38
|
registerMergeCommand(program);
|
|
38
39
|
registerConfigCommand(program);
|
|
40
|
+
registerSyncCommand(program);
|
|
39
41
|
|
|
40
42
|
// 全局未捕获异常处理
|
|
41
43
|
process.on('uncaughtException', (error) => {
|
package/src/types/command.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface RunOptions {
|
|
|
18
18
|
export interface ValidateOptions {
|
|
19
19
|
/** 要验证的分支名 */
|
|
20
20
|
branch: string;
|
|
21
|
+
/** 清理 validate 状态 */
|
|
22
|
+
clean?: boolean;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
25
|
/** merge 命令选项 */
|
|
@@ -43,3 +45,9 @@ export interface ResumeOptions {
|
|
|
43
45
|
/** 要恢复的分支名 */
|
|
44
46
|
branch: string;
|
|
45
47
|
}
|
|
48
|
+
|
|
49
|
+
/** sync 命令选项 */
|
|
50
|
+
export interface SyncOptions {
|
|
51
|
+
/** 要同步的分支名 */
|
|
52
|
+
branch: string;
|
|
53
|
+
}
|
package/src/types/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { ClawtConfig, ConfigItemDefinition, ConfigDefinitions } from './config.js';
|
|
2
|
-
export type { CreateOptions, RunOptions, ValidateOptions, MergeOptions, RemoveOptions, ResumeOptions } from './command.js';
|
|
2
|
+
export type { CreateOptions, RunOptions, ValidateOptions, MergeOptions, RemoveOptions, ResumeOptions, SyncOptions } from './command.js';
|
|
3
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/utils/git.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { basename } from 'node:path';
|
|
2
|
-
import {
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
import { execCommand, execCommandWithInput } from './shell.js';
|
|
3
4
|
import { logger } from '../logger/index.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -189,6 +190,15 @@ export function gitStashPop(index: number = 0, cwd?: string): void {
|
|
|
189
190
|
execCommand(`git stash pop stash@{${index}}`, { cwd });
|
|
190
191
|
}
|
|
191
192
|
|
|
193
|
+
/**
|
|
194
|
+
* git stash drop stash@{index}
|
|
195
|
+
* @param {number} index - stash 索引
|
|
196
|
+
* @param {string} [cwd] - 工作目录
|
|
197
|
+
*/
|
|
198
|
+
export function gitStashDrop(index: number = 0, cwd?: string): void {
|
|
199
|
+
execCommand(`git stash drop stash@{${index}}`, { cwd });
|
|
200
|
+
}
|
|
201
|
+
|
|
192
202
|
/**
|
|
193
203
|
* git stash list
|
|
194
204
|
* @param {string} cwd - 工作目录
|
|
@@ -296,3 +306,77 @@ export function getDiffStat(branchName: string, worktreePath: string, cwd?: stri
|
|
|
296
306
|
deletions: committed.deletions + uncommitted.deletions,
|
|
297
307
|
};
|
|
298
308
|
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* 获取暂存区相对于 HEAD 的完整 diff(含二进制文件)
|
|
312
|
+
* 注意:返回原始输出不做 trim,保留 patch 格式完整性
|
|
313
|
+
* @param {string} [cwd] - 工作目录
|
|
314
|
+
* @returns {Buffer} diff 原始输出(Buffer 格式,保留二进制数据完整性)
|
|
315
|
+
*/
|
|
316
|
+
export function gitDiffCachedBinary(cwd?: string): Buffer {
|
|
317
|
+
logger.debug(`执行命令: git diff --cached --binary${cwd ? ` (cwd: ${cwd})` : ''}`);
|
|
318
|
+
return execSync('git diff --cached --binary', {
|
|
319
|
+
cwd,
|
|
320
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* 将 patch 内容通过 stdin 应用到暂存区
|
|
326
|
+
* @param {Buffer} patchContent - patch 内容(Buffer 格式)
|
|
327
|
+
* @param {string} [cwd] - 工作目录
|
|
328
|
+
*/
|
|
329
|
+
export function gitApplyCachedFromStdin(patchContent: Buffer, cwd?: string): void {
|
|
330
|
+
execCommandWithInput('git', ['apply', '--cached'], { input: patchContent, cwd });
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* 获取当前分支名
|
|
335
|
+
* @param {string} [cwd] - 工作目录
|
|
336
|
+
* @returns {string} 当前分支名
|
|
337
|
+
*/
|
|
338
|
+
export function getCurrentBranch(cwd?: string): string {
|
|
339
|
+
return execCommand('git rev-parse --abbrev-ref HEAD', { cwd });
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* 获取当前 HEAD 的 commit hash
|
|
344
|
+
* @param {string} [cwd] - 工作目录
|
|
345
|
+
* @returns {string} commit hash
|
|
346
|
+
*/
|
|
347
|
+
export function getHeadCommitHash(cwd?: string): string {
|
|
348
|
+
return execCommand('git rev-parse HEAD', { cwd });
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* 获取目标分支相对于当前分支的已提交变更(含二进制文件)
|
|
353
|
+
* 使用三点 diff(HEAD...branchName)获取自分叉点以来的变更
|
|
354
|
+
* @param {string} branchName - 目标分支名
|
|
355
|
+
* @param {string} [cwd] - 工作目录(应在主 worktree 中执行)
|
|
356
|
+
* @returns {Buffer} diff 原始输出
|
|
357
|
+
*/
|
|
358
|
+
export function gitDiffBinaryAgainstBranch(branchName: string, cwd?: string): Buffer {
|
|
359
|
+
logger.debug(`执行命令: git diff HEAD...${branchName} --binary${cwd ? ` (cwd: ${cwd})` : ''}`);
|
|
360
|
+
return execSync(`git diff HEAD...${branchName} --binary`, {
|
|
361
|
+
cwd,
|
|
362
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* 将 patch 内容通过 stdin 应用到工作目录(不带 --cached)
|
|
368
|
+
* @param {Buffer} patchContent - patch 内容
|
|
369
|
+
* @param {string} [cwd] - 工作目录
|
|
370
|
+
*/
|
|
371
|
+
export function gitApplyFromStdin(patchContent: Buffer, cwd?: string): void {
|
|
372
|
+
execCommandWithInput('git', ['apply'], { input: patchContent, cwd });
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* git reset --soft HEAD~<count>,撤销 commit 但保留变更在暂存区
|
|
377
|
+
* @param {number} count - 撤销的 commit 数量
|
|
378
|
+
* @param {string} [cwd] - 工作目录
|
|
379
|
+
*/
|
|
380
|
+
export function gitResetSoft(count: number = 1, cwd?: string): void {
|
|
381
|
+
execCommand(`git reset --soft HEAD~${count}`, { cwd });
|
|
382
|
+
}
|
package/src/utils/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { execCommand, spawnProcess, killAllChildProcesses } from './shell.js';
|
|
1
|
+
export { execCommand, spawnProcess, killAllChildProcesses, execCommandWithInput } from './shell.js';
|
|
2
2
|
export {
|
|
3
3
|
getGitCommonDir,
|
|
4
4
|
getGitTopLevel,
|
|
@@ -20,6 +20,7 @@ export {
|
|
|
20
20
|
gitStashPush,
|
|
21
21
|
gitStashApply,
|
|
22
22
|
gitStashPop,
|
|
23
|
+
gitStashDrop,
|
|
23
24
|
gitStashList,
|
|
24
25
|
gitRestoreStaged,
|
|
25
26
|
gitWorktreeList,
|
|
@@ -27,6 +28,13 @@ export {
|
|
|
27
28
|
hasLocalCommits,
|
|
28
29
|
getCommitCountAhead,
|
|
29
30
|
getDiffStat,
|
|
31
|
+
gitDiffCachedBinary,
|
|
32
|
+
gitApplyCachedFromStdin,
|
|
33
|
+
getCurrentBranch,
|
|
34
|
+
getHeadCommitHash,
|
|
35
|
+
gitDiffBinaryAgainstBranch,
|
|
36
|
+
gitApplyFromStdin,
|
|
37
|
+
gitResetSoft,
|
|
30
38
|
} from './git.js';
|
|
31
39
|
export { sanitizeBranchName, generateBranchNames, validateBranchesNotExist } from './branch.js';
|
|
32
40
|
export { validateMainWorktree, validateGitInstalled, validateClaudeCodeInstalled } from './validation.js';
|
|
@@ -36,3 +44,4 @@ export { printSuccess, printError, printWarning, printInfo, printSeparator, prin
|
|
|
36
44
|
export { ensureDir, removeEmptyDir } from './fs.js';
|
|
37
45
|
export { multilineInput } from './prompt.js';
|
|
38
46
|
export { launchInteractiveClaude } from './claude.js';
|
|
47
|
+
export { getSnapshotPath, hasSnapshot, readSnapshot, writeSnapshot, removeSnapshot, removeProjectSnapshots, readSnapshotHead } from './validate-snapshot.js';
|
package/src/utils/shell.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { execSync, spawn, type ChildProcess, type StdioOptions } from 'node:child_process';
|
|
1
|
+
import { execSync, execFileSync, spawn, type ChildProcess, type StdioOptions } from 'node:child_process';
|
|
2
2
|
import { logger } from '../logger/index.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -51,3 +51,24 @@ export function killAllChildProcesses(children: ChildProcess[]): void {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 同步执行命令,通过 stdin 传入数据
|
|
57
|
+
* @param {string} command - 要执行的命令
|
|
58
|
+
* @param {string[]} args - 命令参数
|
|
59
|
+
* @param {object} options - 配置
|
|
60
|
+
* @param {Buffer} options.input - 通过 stdin 传入的数据(Buffer 格式,保留二进制完整性)
|
|
61
|
+
* @param {string} [options.cwd] - 工作目录
|
|
62
|
+
* @returns {string} 命令的标准输出(已 trim)
|
|
63
|
+
* @throws {Error} 命令执行失败时抛出
|
|
64
|
+
*/
|
|
65
|
+
export function execCommandWithInput(command: string, args: string[], options: { input: Buffer; cwd?: string }): string {
|
|
66
|
+
logger.debug(`执行命令(stdin): ${command} ${args.join(' ')}${options.cwd ? ` (cwd: ${options.cwd})` : ''}`);
|
|
67
|
+
const result = execFileSync(command, args, {
|
|
68
|
+
cwd: options.cwd,
|
|
69
|
+
input: options.input,
|
|
70
|
+
encoding: 'utf-8',
|
|
71
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
72
|
+
});
|
|
73
|
+
return result.trim();
|
|
74
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync, readdirSync, rmdirSync } from 'node:fs';
|
|
3
|
+
import { VALIDATE_SNAPSHOTS_DIR } from '../constants/index.js';
|
|
4
|
+
import { ensureDir } from './fs.js';
|
|
5
|
+
import { logger } from '../logger/index.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 获取指定项目和分支的 validate 快照文件路径
|
|
9
|
+
* @param {string} projectName - 项目名
|
|
10
|
+
* @param {string} branchName - 分支名
|
|
11
|
+
* @returns {string} patch 文件的绝对路径
|
|
12
|
+
*/
|
|
13
|
+
export function getSnapshotPath(projectName: string, branchName: string): string {
|
|
14
|
+
return join(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.patch`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 获取指定项目和分支的快照 HEAD hash 文件路径
|
|
19
|
+
* @param {string} projectName - 项目名
|
|
20
|
+
* @param {string} branchName - 分支名
|
|
21
|
+
* @returns {string} head 文件的绝对路径
|
|
22
|
+
*/
|
|
23
|
+
function getSnapshotHeadPath(projectName: string, branchName: string): string {
|
|
24
|
+
return join(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.head`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 判断指定项目和分支是否存在 validate 快照
|
|
29
|
+
* @param {string} projectName - 项目名
|
|
30
|
+
* @param {string} branchName - 分支名
|
|
31
|
+
* @returns {boolean} 快照是否存在
|
|
32
|
+
*/
|
|
33
|
+
export function hasSnapshot(projectName: string, branchName: string): boolean {
|
|
34
|
+
return existsSync(getSnapshotPath(projectName, branchName));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 读取指定项目和分支的 validate 快照内容
|
|
39
|
+
* @param {string} projectName - 项目名
|
|
40
|
+
* @param {string} branchName - 分支名
|
|
41
|
+
* @returns {Buffer} patch 文件内容(Buffer 格式,保留二进制完整性)
|
|
42
|
+
*/
|
|
43
|
+
export function readSnapshot(projectName: string, branchName: string): Buffer {
|
|
44
|
+
const snapshotPath = getSnapshotPath(projectName, branchName);
|
|
45
|
+
logger.debug(`读取 validate 快照: ${snapshotPath}`);
|
|
46
|
+
return readFileSync(snapshotPath);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 写入 validate 快照内容(自动创建目录)
|
|
51
|
+
* @param {string} projectName - 项目名
|
|
52
|
+
* @param {string} branchName - 分支名
|
|
53
|
+
* @param {Buffer} patch - patch 内容(Buffer 格式)
|
|
54
|
+
* @param {string} [headHash] - 主分支 HEAD commit hash(用于增量 validate 一致性校验)
|
|
55
|
+
*/
|
|
56
|
+
export function writeSnapshot(projectName: string, branchName: string, patch: Buffer, headHash?: string): void {
|
|
57
|
+
const snapshotPath = getSnapshotPath(projectName, branchName);
|
|
58
|
+
const snapshotDir = join(VALIDATE_SNAPSHOTS_DIR, projectName);
|
|
59
|
+
ensureDir(snapshotDir);
|
|
60
|
+
writeFileSync(snapshotPath, patch);
|
|
61
|
+
// 保存主分支 HEAD hash,用于下次增量 validate 时校验一致性
|
|
62
|
+
if (headHash) {
|
|
63
|
+
writeFileSync(getSnapshotHeadPath(projectName, branchName), headHash, 'utf-8');
|
|
64
|
+
}
|
|
65
|
+
logger.info(`已保存 validate 快照: ${snapshotPath}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 删除指定项目和分支的 validate 快照
|
|
70
|
+
* @param {string} projectName - 项目名
|
|
71
|
+
* @param {string} branchName - 分支名
|
|
72
|
+
*/
|
|
73
|
+
export function removeSnapshot(projectName: string, branchName: string): void {
|
|
74
|
+
const snapshotPath = getSnapshotPath(projectName, branchName);
|
|
75
|
+
if (existsSync(snapshotPath)) {
|
|
76
|
+
unlinkSync(snapshotPath);
|
|
77
|
+
logger.info(`已删除 validate 快照: ${snapshotPath}`);
|
|
78
|
+
}
|
|
79
|
+
// 同时删除对应的 .head 文件
|
|
80
|
+
const headPath = getSnapshotHeadPath(projectName, branchName);
|
|
81
|
+
if (existsSync(headPath)) {
|
|
82
|
+
unlinkSync(headPath);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* 读取快照保存时的主分支 HEAD commit hash
|
|
88
|
+
* @param {string} projectName - 项目名
|
|
89
|
+
* @param {string} branchName - 分支名
|
|
90
|
+
* @returns {string | null} HEAD hash,不存在则返回 null
|
|
91
|
+
*/
|
|
92
|
+
export function readSnapshotHead(projectName: string, branchName: string): string | null {
|
|
93
|
+
const headPath = getSnapshotHeadPath(projectName, branchName);
|
|
94
|
+
if (!existsSync(headPath)) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
return readFileSync(headPath, 'utf-8').trim();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 删除指定项目的所有 validate 快照
|
|
102
|
+
* @param {string} projectName - 项目名
|
|
103
|
+
*/
|
|
104
|
+
export function removeProjectSnapshots(projectName: string): void {
|
|
105
|
+
const projectDir = join(VALIDATE_SNAPSHOTS_DIR, projectName);
|
|
106
|
+
if (!existsSync(projectDir)) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const files = readdirSync(projectDir);
|
|
111
|
+
for (const file of files) {
|
|
112
|
+
unlinkSync(join(projectDir, file));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 尝试删除空目录
|
|
116
|
+
try {
|
|
117
|
+
rmdirSync(projectDir);
|
|
118
|
+
} catch {
|
|
119
|
+
// 目录非空或其他原因,忽略
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
logger.info(`已删除项目 ${projectName} 的所有 validate 快照`);
|
|
123
|
+
}
|