sync-worktrees 2.0.0 → 2.2.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/dist/index.js +1835 -354
- package/dist/index.js.map +4 -4
- package/package.json +4 -1
package/dist/index.js
CHANGED
|
@@ -1,9 +1,99 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import * as
|
|
4
|
+
import * as path10 from "path";
|
|
5
5
|
import { confirm as confirm2 } from "@inquirer/prompts";
|
|
6
6
|
import * as cron2 from "node-cron";
|
|
7
|
+
import pLimit2 from "p-limit";
|
|
8
|
+
|
|
9
|
+
// src/constants.ts
|
|
10
|
+
var GIT_CONSTANTS = {
|
|
11
|
+
REMOTE_PREFIX: "origin/",
|
|
12
|
+
REMOTE_NAME: "origin",
|
|
13
|
+
HEAD_REF: "/HEAD",
|
|
14
|
+
DEFAULT_BRANCH: "main",
|
|
15
|
+
COMMON_DEFAULT_BRANCHES: ["main", "master", "develop", "trunk"],
|
|
16
|
+
BARE_DIR_NAME: ".bare",
|
|
17
|
+
DIVERGED_DIR_NAME: ".diverged",
|
|
18
|
+
LFS_HEADER: "version https://git-lfs.github.com/spec/",
|
|
19
|
+
SUBMODULE_STATUS_ADDED: "+",
|
|
20
|
+
SUBMODULE_STATUS_REMOVED: "-",
|
|
21
|
+
GITDIR_PREFIX: "gitdir:",
|
|
22
|
+
GIT_CHECK_IGNORE_NO_MATCH: "exit code: 1",
|
|
23
|
+
REFS: {
|
|
24
|
+
HEADS: "refs/heads/",
|
|
25
|
+
REMOTES: "refs/remotes/origin",
|
|
26
|
+
REMOTES_ORIGIN: "refs/remotes/origin/*"
|
|
27
|
+
},
|
|
28
|
+
FETCH_CONFIG: "+refs/heads/*:refs/remotes/origin/*"
|
|
29
|
+
};
|
|
30
|
+
var GIT_OPERATIONS = {
|
|
31
|
+
MERGE_HEAD: "MERGE_HEAD",
|
|
32
|
+
CHERRY_PICK_HEAD: "CHERRY_PICK_HEAD",
|
|
33
|
+
REVERT_HEAD: "REVERT_HEAD",
|
|
34
|
+
BISECT_LOG: "BISECT_LOG",
|
|
35
|
+
REBASE_MERGE: "rebase-merge",
|
|
36
|
+
REBASE_APPLY: "rebase-apply"
|
|
37
|
+
};
|
|
38
|
+
var DEFAULT_CONFIG = {
|
|
39
|
+
CRON_SCHEDULE: "0 * * * *",
|
|
40
|
+
RETRY: {
|
|
41
|
+
MAX_ATTEMPTS: 3,
|
|
42
|
+
MAX_LFS_RETRIES: 2,
|
|
43
|
+
INITIAL_DELAY_MS: 1e3,
|
|
44
|
+
MAX_DELAY_MS: 3e4,
|
|
45
|
+
BACKOFF_MULTIPLIER: 2,
|
|
46
|
+
JITTER_MS: 500
|
|
47
|
+
},
|
|
48
|
+
PARALLELISM: {
|
|
49
|
+
MAX_REPOSITORIES: 2,
|
|
50
|
+
MAX_WORKTREE_CREATION: 1,
|
|
51
|
+
MAX_WORKTREE_UPDATES: 3,
|
|
52
|
+
MAX_WORKTREE_REMOVAL: 3,
|
|
53
|
+
MAX_STATUS_CHECKS: 20,
|
|
54
|
+
MAX_SAFE_TOTAL_CONCURRENT_OPS: 100
|
|
55
|
+
},
|
|
56
|
+
UPDATE_EXISTING_WORKTREES: true
|
|
57
|
+
};
|
|
58
|
+
var ERROR_MESSAGES = {
|
|
59
|
+
GIT_NOT_INITIALIZED: "Git service not initialized. Call initialize() first.",
|
|
60
|
+
ALREADY_EXISTS: "already exists",
|
|
61
|
+
ALREADY_REGISTERED: "already registered worktree",
|
|
62
|
+
FAST_FORWARD_FAILED: [
|
|
63
|
+
"Not possible to fast-forward",
|
|
64
|
+
"fatal: Not possible to fast-forward, aborting",
|
|
65
|
+
"cannot fast-forward"
|
|
66
|
+
],
|
|
67
|
+
NO_UPSTREAM: [
|
|
68
|
+
"fatal: no upstream configured",
|
|
69
|
+
"no upstream configured for branch",
|
|
70
|
+
"fatal: ambiguous argument",
|
|
71
|
+
"unknown revision or path"
|
|
72
|
+
],
|
|
73
|
+
LFS_ERROR: ["smudge filter lfs failed", "git-lfs", "LFS"],
|
|
74
|
+
EXDEV: "EXDEV"
|
|
75
|
+
};
|
|
76
|
+
var ENV_CONSTANTS = {
|
|
77
|
+
GIT_LFS_SKIP_SMUDGE: "GIT_LFS_SKIP_SMUDGE",
|
|
78
|
+
NODE_ENV_TEST: "test"
|
|
79
|
+
};
|
|
80
|
+
var PATH_CONSTANTS = {
|
|
81
|
+
GIT_DIR: ".git",
|
|
82
|
+
README: "README"
|
|
83
|
+
};
|
|
84
|
+
var CONFIG_CONSTANTS = {
|
|
85
|
+
WILDCARD_PATTERN: ".*"
|
|
86
|
+
};
|
|
87
|
+
var METADATA_CONSTANTS = {
|
|
88
|
+
MAX_HISTORY_ENTRIES: 10,
|
|
89
|
+
METADATA_FILENAME: "sync-metadata.json",
|
|
90
|
+
WORKTREE_METADATA_PATH: ".git/worktrees",
|
|
91
|
+
DIVERGED_INFO_FILE: ".diverged-info.json",
|
|
92
|
+
DIVERGED_REASON: "diverged-history-with-changes",
|
|
93
|
+
ACTION_CREATED: "created",
|
|
94
|
+
ACTION_UPDATED: "updated",
|
|
95
|
+
ACTION_FETCHED: "fetched"
|
|
96
|
+
};
|
|
7
97
|
|
|
8
98
|
// src/services/config-loader.service.ts
|
|
9
99
|
import * as fs from "fs/promises";
|
|
@@ -73,6 +163,9 @@ var ConfigLoaderService = class {
|
|
|
73
163
|
if (repoObj.runOnce !== void 0 && typeof repoObj.runOnce !== "boolean") {
|
|
74
164
|
throw new Error(`Repository '${repoObj.name}' has invalid 'runOnce' property`);
|
|
75
165
|
}
|
|
166
|
+
if (repoObj.filesToCopyOnBranchCreate !== void 0) {
|
|
167
|
+
this.validateFilesToCopyConfig(repoObj.filesToCopyOnBranchCreate, `Repository '${repoObj.name}'`);
|
|
168
|
+
}
|
|
76
169
|
});
|
|
77
170
|
if (configObj.defaults) {
|
|
78
171
|
if (typeof configObj.defaults !== "object") {
|
|
@@ -88,6 +181,9 @@ var ConfigLoaderService = class {
|
|
|
88
181
|
if (defaults.retry !== void 0 && typeof defaults.retry !== "object") {
|
|
89
182
|
throw new Error("Invalid 'retry' in defaults");
|
|
90
183
|
}
|
|
184
|
+
if (defaults.filesToCopyOnBranchCreate !== void 0) {
|
|
185
|
+
this.validateFilesToCopyConfig(defaults.filesToCopyOnBranchCreate, "defaults");
|
|
186
|
+
}
|
|
91
187
|
}
|
|
92
188
|
if (configObj.retry !== void 0) {
|
|
93
189
|
if (typeof configObj.retry !== "object") {
|
|
@@ -114,13 +210,79 @@ var ConfigLoaderService = class {
|
|
|
114
210
|
throw new Error("Invalid 'backoffMultiplier' in retry config");
|
|
115
211
|
}
|
|
116
212
|
}
|
|
213
|
+
if (configObj.parallelism !== void 0) {
|
|
214
|
+
this.validateParallelismConfig(configObj.parallelism, "global");
|
|
215
|
+
}
|
|
216
|
+
if (configObj.defaults && typeof configObj.defaults === "object") {
|
|
217
|
+
const defaults = configObj.defaults;
|
|
218
|
+
if (defaults.parallelism !== void 0) {
|
|
219
|
+
this.validateParallelismConfig(defaults.parallelism, "defaults");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
validateParallelismConfig(parallelism, context) {
|
|
224
|
+
if (typeof parallelism !== "object" || parallelism === null) {
|
|
225
|
+
throw new Error(`'parallelism' in ${context} must be an object`);
|
|
226
|
+
}
|
|
227
|
+
const config = parallelism;
|
|
228
|
+
if (config.maxRepositories !== void 0) {
|
|
229
|
+
if (typeof config.maxRepositories !== "number" || config.maxRepositories < 1) {
|
|
230
|
+
throw new Error(`Invalid 'maxRepositories' in ${context} parallelism config. Must be a positive number`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
if (config.maxWorktreeCreation !== void 0) {
|
|
234
|
+
if (typeof config.maxWorktreeCreation !== "number" || config.maxWorktreeCreation < 1) {
|
|
235
|
+
throw new Error(`Invalid 'maxWorktreeCreation' in ${context} parallelism config. Must be a positive number`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
if (config.maxWorktreeUpdates !== void 0) {
|
|
239
|
+
if (typeof config.maxWorktreeUpdates !== "number" || config.maxWorktreeUpdates < 1) {
|
|
240
|
+
throw new Error(`Invalid 'maxWorktreeUpdates' in ${context} parallelism config. Must be a positive number`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (config.maxWorktreeRemoval !== void 0) {
|
|
244
|
+
if (typeof config.maxWorktreeRemoval !== "number" || config.maxWorktreeRemoval < 1) {
|
|
245
|
+
throw new Error(`Invalid 'maxWorktreeRemoval' in ${context} parallelism config. Must be a positive number`);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (config.maxStatusChecks !== void 0) {
|
|
249
|
+
if (typeof config.maxStatusChecks !== "number" || config.maxStatusChecks < 1) {
|
|
250
|
+
throw new Error(`Invalid 'maxStatusChecks' in ${context} parallelism config. Must be a positive number`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const maxRepos = config.maxRepositories ?? DEFAULT_CONFIG.PARALLELISM.MAX_REPOSITORIES;
|
|
254
|
+
const maxCreation = config.maxWorktreeCreation ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_CREATION;
|
|
255
|
+
const maxUpdates = config.maxWorktreeUpdates ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_UPDATES;
|
|
256
|
+
const maxRemoval = config.maxWorktreeRemoval ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_REMOVAL;
|
|
257
|
+
const maxStatus = config.maxStatusChecks ?? DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS;
|
|
258
|
+
const maxPerRepoOps = maxCreation + maxUpdates + maxRemoval + maxStatus;
|
|
259
|
+
const totalMaxConcurrent = maxRepos * maxPerRepoOps;
|
|
260
|
+
if (totalMaxConcurrent > DEFAULT_CONFIG.PARALLELISM.MAX_SAFE_TOTAL_CONCURRENT_OPS) {
|
|
261
|
+
const safeMaxRepos = Math.floor(DEFAULT_CONFIG.PARALLELISM.MAX_SAFE_TOTAL_CONCURRENT_OPS / maxPerRepoOps);
|
|
262
|
+
throw new Error(
|
|
263
|
+
`Total concurrent operations (${totalMaxConcurrent}) exceeds safe limit (${DEFAULT_CONFIG.PARALLELISM.MAX_SAFE_TOTAL_CONCURRENT_OPS}). With current per-repository limits (creation: ${maxCreation}, updates: ${maxUpdates}, removal: ${maxRemoval}, status: ${maxStatus}), maximum safe maxRepositories is ${safeMaxRepos}. Consider reducing maxRepositories or lowering per-operation limits.`
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
validateFilesToCopyConfig(filesToCopy, context) {
|
|
268
|
+
if (!Array.isArray(filesToCopy)) {
|
|
269
|
+
throw new Error(`'filesToCopyOnBranchCreate' in ${context} must be an array`);
|
|
270
|
+
}
|
|
271
|
+
for (let i = 0; i < filesToCopy.length; i++) {
|
|
272
|
+
const pattern = filesToCopy[i];
|
|
273
|
+
if (typeof pattern !== "string" || pattern.trim() === "") {
|
|
274
|
+
throw new Error(
|
|
275
|
+
`'filesToCopyOnBranchCreate' in ${context} must contain only non-empty strings (invalid at index ${i})`
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
117
279
|
}
|
|
118
280
|
resolveRepositoryConfig(repo, defaults, configDir, globalRetry) {
|
|
119
281
|
const resolved = {
|
|
120
282
|
name: repo.name,
|
|
121
283
|
repoUrl: repo.repoUrl,
|
|
122
284
|
worktreeDir: this.resolvePath(repo.worktreeDir, configDir),
|
|
123
|
-
cronSchedule: repo.cronSchedule ?? defaults?.cronSchedule ??
|
|
285
|
+
cronSchedule: repo.cronSchedule ?? defaults?.cronSchedule ?? DEFAULT_CONFIG.CRON_SCHEDULE,
|
|
124
286
|
runOnce: repo.runOnce ?? defaults?.runOnce ?? false
|
|
125
287
|
};
|
|
126
288
|
if (repo.bareRepoDir) {
|
|
@@ -139,9 +301,18 @@ var ConfigLoaderService = class {
|
|
|
139
301
|
...repo.retry || {}
|
|
140
302
|
};
|
|
141
303
|
}
|
|
304
|
+
if (repo.parallelism || defaults?.parallelism) {
|
|
305
|
+
resolved.parallelism = {
|
|
306
|
+
...defaults?.parallelism || {},
|
|
307
|
+
...repo.parallelism || {}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
142
310
|
if (repo.updateExistingWorktrees !== void 0 || defaults?.updateExistingWorktrees !== void 0) {
|
|
143
311
|
resolved.updateExistingWorktrees = repo.updateExistingWorktrees ?? defaults?.updateExistingWorktrees ?? true;
|
|
144
312
|
}
|
|
313
|
+
if (repo.filesToCopyOnBranchCreate || defaults?.filesToCopyOnBranchCreate) {
|
|
314
|
+
resolved.filesToCopyOnBranchCreate = repo.filesToCopyOnBranchCreate ?? defaults?.filesToCopyOnBranchCreate;
|
|
315
|
+
}
|
|
145
316
|
return resolved;
|
|
146
317
|
}
|
|
147
318
|
resolvePath(inputPath, baseDir) {
|
|
@@ -158,7 +329,7 @@ var ConfigLoaderService = class {
|
|
|
158
329
|
return repositories.filter((repo) => {
|
|
159
330
|
return patterns.some((pattern) => {
|
|
160
331
|
if (pattern.includes("*")) {
|
|
161
|
-
const regex = new RegExp("^" + pattern.replace(/\*/g,
|
|
332
|
+
const regex = new RegExp("^" + pattern.replace(/\*/g, CONFIG_CONSTANTS.WILDCARD_PATTERN) + "$");
|
|
162
333
|
return regex.test(repo.name);
|
|
163
334
|
}
|
|
164
335
|
return repo.name === pattern;
|
|
@@ -168,13 +339,15 @@ var ConfigLoaderService = class {
|
|
|
168
339
|
};
|
|
169
340
|
|
|
170
341
|
// src/services/InteractiveUIService.tsx
|
|
171
|
-
import
|
|
342
|
+
import React7 from "react";
|
|
343
|
+
import * as path7 from "path";
|
|
172
344
|
import { render } from "ink";
|
|
173
345
|
import * as cron from "node-cron";
|
|
346
|
+
import { spawn } from "child_process";
|
|
174
347
|
|
|
175
348
|
// src/components/App.tsx
|
|
176
|
-
import
|
|
177
|
-
import { Box as
|
|
349
|
+
import React6, { useState as useState5, useEffect as useEffect5, useCallback as useCallback3, useRef as useRef3 } from "react";
|
|
350
|
+
import { Box as Box6, useInput as useInput5, useStdout } from "ink";
|
|
178
351
|
|
|
179
352
|
// src/components/StatusBar.tsx
|
|
180
353
|
import React, { useState, useEffect } from "react";
|
|
@@ -210,7 +383,7 @@ var StatusBar = ({ status, repositoryCount, lastSyncTime, cronSchedule, diskSpac
|
|
|
210
383
|
const getStatusIcon = () => {
|
|
211
384
|
return status === "syncing" ? "\u27F3" : "\u2713";
|
|
212
385
|
};
|
|
213
|
-
return /* @__PURE__ */ React.createElement(Box, { borderStyle: "single", paddingX: 1 }, /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", width: "100%" }, /* @__PURE__ */ React.createElement(Box, { justifyContent: "space-between" }, /* @__PURE__ */ React.createElement(Text, { bold: true }, getStatusIcon(), " Status:", " ", /* @__PURE__ */ React.createElement(Text, { color: getStatusColor() }, status === "syncing" ? "Syncing..." : "Running")), /* @__PURE__ */ React.createElement(Text, null, "Repositories: ", /* @__PURE__ */ React.createElement(Text, { bold: true, color: "cyan" }, repositoryCount))), /* @__PURE__ */ React.createElement(Box, { justifyContent: "space-between" }, /* @__PURE__ */ React.createElement(Text, null, "Last Sync: ", /* @__PURE__ */ React.createElement(Text, { color: "gray" }, formatTime(lastSyncTime))), cronSchedule && /* @__PURE__ */ React.createElement(Text, null, "Next Sync: ", /* @__PURE__ */ React.createElement(Text, { color: "gray" }, formatTime(nextSyncTime)))), /* @__PURE__ */ React.createElement(Box, { justifyContent: "space-between" }, /* @__PURE__ */ React.createElement(Text, null, "Disk Space: ", /* @__PURE__ */ React.createElement(Text, { color: "magenta" }, diskSpaceUsed || "Calculating...")))));
|
|
386
|
+
return /* @__PURE__ */ React.createElement(Box, { borderStyle: "single", paddingX: 1 }, /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", width: "100%" }, /* @__PURE__ */ React.createElement(Box, { justifyContent: "space-between" }, /* @__PURE__ */ React.createElement(Text, { bold: true }, getStatusIcon(), " Status:", " ", /* @__PURE__ */ React.createElement(Text, { color: getStatusColor() }, status === "syncing" ? "Syncing..." : "Running")), /* @__PURE__ */ React.createElement(Text, null, "Repositories: ", /* @__PURE__ */ React.createElement(Text, { bold: true, color: "cyan" }, repositoryCount))), /* @__PURE__ */ React.createElement(Box, { justifyContent: "space-between" }, /* @__PURE__ */ React.createElement(Text, null, "Last Sync: ", /* @__PURE__ */ React.createElement(Text, { color: "gray" }, formatTime(lastSyncTime))), cronSchedule && /* @__PURE__ */ React.createElement(Text, null, "Next Sync: ", /* @__PURE__ */ React.createElement(Text, { color: "gray" }, formatTime(nextSyncTime)))), /* @__PURE__ */ React.createElement(Box, { justifyContent: "space-between" }, /* @__PURE__ */ React.createElement(Text, null, "Disk Space: ", /* @__PURE__ */ React.createElement(Text, { color: "magenta" }, diskSpaceUsed || "Calculating...")), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "s"), "ync", " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "c"), "reate", " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "o"), "pen", " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "r"), "eload", " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "?"), "help", " ", /* @__PURE__ */ React.createElement(Text, { color: "yellow" }, "q"), "uit"))));
|
|
214
387
|
};
|
|
215
388
|
var StatusBar_default = StatusBar;
|
|
216
389
|
|
|
@@ -221,27 +394,632 @@ var HelpModal = ({ onClose }) => {
|
|
|
221
394
|
useInput(() => {
|
|
222
395
|
onClose();
|
|
223
396
|
});
|
|
224
|
-
return /* @__PURE__ */ React2.createElement(Box2, { justifyContent: "center", alignItems: "center", flexDirection: "column", marginTop: 2, marginBottom: 2 }, /* @__PURE__ */ React2.createElement(Box2, { borderStyle: "double", borderColor: "cyan", paddingX: 2, paddingY: 1, flexDirection: "column", width: 60 }, /* @__PURE__ */ React2.createElement(Box2, { justifyContent: "center", marginBottom: 1 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "cyan" }, "\u{1F333} sync-worktrees - Keyboard Shortcuts")), /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", gap: 0 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "
|
|
397
|
+
return /* @__PURE__ */ React2.createElement(Box2, { justifyContent: "center", alignItems: "center", flexDirection: "column", marginTop: 2, marginBottom: 2 }, /* @__PURE__ */ React2.createElement(Box2, { borderStyle: "double", borderColor: "cyan", paddingX: 2, paddingY: 1, flexDirection: "column", width: 60 }, /* @__PURE__ */ React2.createElement(Box2, { justifyContent: "center", marginBottom: 1 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "cyan" }, "\u{1F333} sync-worktrees - Keyboard Shortcuts")), /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", gap: 0 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "green", dimColor: true }, "Navigation"), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "j"), /* @__PURE__ */ React2.createElement(Text2, null, " / "), /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "\u2193")), /* @__PURE__ */ React2.createElement(Text2, null, "Scroll down one line")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "k"), /* @__PURE__ */ React2.createElement(Text2, null, " / "), /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "\u2191")), /* @__PURE__ */ React2.createElement(Text2, null, "Scroll up one line")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "gg")), /* @__PURE__ */ React2.createElement(Text2, null, "Jump to top")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "G")), /* @__PURE__ */ React2.createElement(Text2, null, "Jump to bottom (re-enables auto-scroll)")), /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "green", dimColor: true }, "Actions")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "s")), /* @__PURE__ */ React2.createElement(Text2, null, "Manually trigger sync for all repositories")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "c")), /* @__PURE__ */ React2.createElement(Text2, null, "Create a new branch")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "o")), /* @__PURE__ */ React2.createElement(Text2, null, "Open editor in worktree")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "r")), /* @__PURE__ */ React2.createElement(Text2, null, "Reload configuration and re-sync all repos")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "?"), /* @__PURE__ */ React2.createElement(Text2, null, " / "), /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "h")), /* @__PURE__ */ React2.createElement(Text2, null, "Toggle this help screen")), /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Box2, { width: 15 }, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "q"), /* @__PURE__ */ React2.createElement(Text2, null, " / "), /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "yellow" }, "Esc")), /* @__PURE__ */ React2.createElement(Text2, null, "Gracefully quit"))), /* @__PURE__ */ React2.createElement(Box2, { justifyContent: "center", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "Press any key to close"))));
|
|
225
398
|
};
|
|
226
399
|
var HelpModal_default = HelpModal;
|
|
227
400
|
|
|
228
|
-
// src/components/
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
401
|
+
// src/components/BranchCreationWizard.tsx
|
|
402
|
+
import React3, { useState as useState2, useEffect as useEffect2, useCallback } from "react";
|
|
403
|
+
import { Box as Box3, Text as Text3, useInput as useInput2 } from "ink";
|
|
404
|
+
var isValidGitBranchName = (name) => {
|
|
405
|
+
if (!name.trim()) {
|
|
406
|
+
return { valid: false, error: "Branch name cannot be empty" };
|
|
407
|
+
}
|
|
408
|
+
if (name.startsWith("-")) {
|
|
409
|
+
return { valid: false, error: "Branch name cannot start with '-'" };
|
|
410
|
+
}
|
|
411
|
+
if (name.endsWith(".lock")) {
|
|
412
|
+
return { valid: false, error: "Branch name cannot end with '.lock'" };
|
|
413
|
+
}
|
|
414
|
+
if (name.includes("..")) {
|
|
415
|
+
return { valid: false, error: "Branch name cannot contain '..'" };
|
|
416
|
+
}
|
|
417
|
+
if (name.includes("@{")) {
|
|
418
|
+
return { valid: false, error: "Branch name cannot contain '@{'" };
|
|
419
|
+
}
|
|
420
|
+
if (name.startsWith(".") || name.endsWith(".")) {
|
|
421
|
+
return { valid: false, error: "Branch name cannot start or end with '.'" };
|
|
422
|
+
}
|
|
423
|
+
if (name.includes("//")) {
|
|
424
|
+
return { valid: false, error: "Branch name cannot contain consecutive slashes" };
|
|
425
|
+
}
|
|
426
|
+
if (/[\x00-\x1f\x7f~^:?*\[\\]/.test(name)) {
|
|
427
|
+
return { valid: false, error: "Branch name contains invalid characters" };
|
|
428
|
+
}
|
|
429
|
+
return { valid: true };
|
|
430
|
+
};
|
|
431
|
+
var BranchCreationWizard = ({
|
|
432
|
+
repositories,
|
|
433
|
+
getBranchesForRepo,
|
|
434
|
+
getDefaultBranchForRepo,
|
|
435
|
+
createAndPushBranch,
|
|
436
|
+
onClose,
|
|
437
|
+
onComplete
|
|
438
|
+
}) => {
|
|
439
|
+
const [step, setStep] = useState2(repositories.length > 1 ? "SELECT_PROJECT" : "SELECT_BRANCH");
|
|
440
|
+
const [selectedProjectIndex, setSelectedProjectIndex] = useState2(repositories.length === 1 ? 0 : 0);
|
|
441
|
+
const [branches, setBranches] = useState2([]);
|
|
442
|
+
const [defaultBranch, setDefaultBranch] = useState2("");
|
|
443
|
+
const [selectedBranchIndex, setSelectedBranchIndex] = useState2(0);
|
|
444
|
+
const [branchName, setBranchName] = useState2("");
|
|
445
|
+
const [existingSuffix, setExistingSuffix] = useState2(null);
|
|
446
|
+
const [validationError, setValidationError] = useState2(null);
|
|
447
|
+
const [result, setResult] = useState2(null);
|
|
448
|
+
const [loading, setLoading] = useState2(false);
|
|
449
|
+
const loadBranches = useCallback(
|
|
450
|
+
async (repoIndex) => {
|
|
451
|
+
setLoading(true);
|
|
452
|
+
try {
|
|
453
|
+
const branchList = await getBranchesForRepo(repoIndex);
|
|
454
|
+
const defaultBr = getDefaultBranchForRepo(repoIndex);
|
|
455
|
+
setBranches(branchList);
|
|
456
|
+
setDefaultBranch(defaultBr);
|
|
457
|
+
const defaultIndex = branchList.indexOf(defaultBr);
|
|
458
|
+
setSelectedBranchIndex(defaultIndex >= 0 ? defaultIndex : 0);
|
|
459
|
+
} catch {
|
|
460
|
+
setBranches([]);
|
|
461
|
+
}
|
|
462
|
+
setLoading(false);
|
|
463
|
+
},
|
|
464
|
+
[getBranchesForRepo, getDefaultBranchForRepo]
|
|
465
|
+
);
|
|
466
|
+
const checkBranchExists = useCallback(
|
|
467
|
+
(name) => {
|
|
468
|
+
if (!name.trim()) {
|
|
469
|
+
setExistingSuffix(null);
|
|
470
|
+
setValidationError(null);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const validation = isValidGitBranchName(name);
|
|
474
|
+
if (!validation.valid) {
|
|
475
|
+
setValidationError(validation.error ?? null);
|
|
476
|
+
setExistingSuffix(null);
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
setValidationError(null);
|
|
480
|
+
let suffix = 0;
|
|
481
|
+
let testName = name;
|
|
482
|
+
while (branches.includes(testName)) {
|
|
483
|
+
suffix++;
|
|
484
|
+
testName = `${name}-${suffix}`;
|
|
485
|
+
}
|
|
486
|
+
setExistingSuffix(suffix > 0 ? suffix : null);
|
|
487
|
+
},
|
|
488
|
+
[branches]
|
|
489
|
+
);
|
|
490
|
+
useEffect2(() => {
|
|
491
|
+
if (step === "SELECT_BRANCH" && branches.length === 0 && !loading) {
|
|
492
|
+
loadBranches(selectedProjectIndex);
|
|
493
|
+
}
|
|
494
|
+
}, [step, selectedProjectIndex, branches.length, loading, loadBranches]);
|
|
495
|
+
useEffect2(() => {
|
|
496
|
+
if (step === "ENTER_NAME") {
|
|
497
|
+
checkBranchExists(branchName);
|
|
498
|
+
}
|
|
499
|
+
}, [branchName, step, checkBranchExists]);
|
|
500
|
+
const handleCreateBranch = async () => {
|
|
501
|
+
const trimmedName = branchName.trim();
|
|
502
|
+
if (!trimmedName) return;
|
|
503
|
+
const validation = isValidGitBranchName(trimmedName);
|
|
504
|
+
if (!validation.valid) {
|
|
505
|
+
setValidationError(validation.error ?? null);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
setStep("CREATING");
|
|
509
|
+
const baseBranch = branches[selectedBranchIndex];
|
|
510
|
+
const createResult = await createAndPushBranch(selectedProjectIndex, baseBranch, trimmedName);
|
|
511
|
+
setResult(createResult);
|
|
512
|
+
setStep("RESULT");
|
|
513
|
+
};
|
|
234
514
|
useInput2((input2, key) => {
|
|
515
|
+
if (step === "CREATING") return;
|
|
516
|
+
if (key.escape) {
|
|
517
|
+
if (step === "SELECT_PROJECT") {
|
|
518
|
+
onClose();
|
|
519
|
+
} else if (step === "SELECT_BRANCH") {
|
|
520
|
+
if (repositories.length > 1) {
|
|
521
|
+
setBranches([]);
|
|
522
|
+
setStep("SELECT_PROJECT");
|
|
523
|
+
} else {
|
|
524
|
+
onClose();
|
|
525
|
+
}
|
|
526
|
+
} else if (step === "ENTER_NAME") {
|
|
527
|
+
setBranchName("");
|
|
528
|
+
setExistingSuffix(null);
|
|
529
|
+
setStep("SELECT_BRANCH");
|
|
530
|
+
} else if (step === "RESULT") {
|
|
531
|
+
const context = result?.success ? {
|
|
532
|
+
repoIndex: selectedProjectIndex,
|
|
533
|
+
baseBranch: branches[selectedBranchIndex],
|
|
534
|
+
newBranch: result.finalName
|
|
535
|
+
} : void 0;
|
|
536
|
+
onComplete(result?.success ?? false, context);
|
|
537
|
+
}
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (step === "SELECT_PROJECT") {
|
|
541
|
+
if (key.upArrow) {
|
|
542
|
+
setSelectedProjectIndex((prev) => Math.max(0, prev - 1));
|
|
543
|
+
} else if (key.downArrow) {
|
|
544
|
+
setSelectedProjectIndex((prev) => Math.min(repositories.length - 1, prev + 1));
|
|
545
|
+
} else if (key.return) {
|
|
546
|
+
loadBranches(selectedProjectIndex);
|
|
547
|
+
setStep("SELECT_BRANCH");
|
|
548
|
+
}
|
|
549
|
+
} else if (step === "SELECT_BRANCH") {
|
|
550
|
+
if (key.upArrow) {
|
|
551
|
+
setSelectedBranchIndex((prev) => Math.max(0, prev - 1));
|
|
552
|
+
} else if (key.downArrow) {
|
|
553
|
+
setSelectedBranchIndex((prev) => Math.min(branches.length - 1, prev + 1));
|
|
554
|
+
} else if (key.return && branches.length > 0) {
|
|
555
|
+
setStep("ENTER_NAME");
|
|
556
|
+
}
|
|
557
|
+
} else if (step === "ENTER_NAME") {
|
|
558
|
+
if (key.return && branchName.trim()) {
|
|
559
|
+
void handleCreateBranch();
|
|
560
|
+
} else if (key.backspace || key.delete) {
|
|
561
|
+
setBranchName((prev) => prev.slice(0, -1));
|
|
562
|
+
} else if (input2 && !key.ctrl && !key.meta) {
|
|
563
|
+
const validChar = /^[a-zA-Z0-9/_-]$/.test(input2);
|
|
564
|
+
if (validChar) {
|
|
565
|
+
setBranchName((prev) => prev + input2);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
} else if (step === "RESULT") {
|
|
569
|
+
const context = result?.success ? {
|
|
570
|
+
repoIndex: selectedProjectIndex,
|
|
571
|
+
baseBranch: branches[selectedBranchIndex],
|
|
572
|
+
newBranch: result.finalName
|
|
573
|
+
} : void 0;
|
|
574
|
+
onComplete(result?.success ?? false, context);
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
const getStepNumber = () => {
|
|
578
|
+
if (repositories.length === 1) {
|
|
579
|
+
if (step === "SELECT_BRANCH") return 1;
|
|
580
|
+
if (step === "ENTER_NAME") return 2;
|
|
581
|
+
return 2;
|
|
582
|
+
}
|
|
583
|
+
if (step === "SELECT_PROJECT") return 1;
|
|
584
|
+
if (step === "SELECT_BRANCH") return 2;
|
|
585
|
+
if (step === "ENTER_NAME") return 3;
|
|
586
|
+
return 3;
|
|
587
|
+
};
|
|
588
|
+
const getTotalSteps = () => repositories.length === 1 ? 2 : 3;
|
|
589
|
+
const renderProjectSelection = () => /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React3.createElement(Text3, null, "Select repository:"), /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, repositories.map((repo, idx) => /* @__PURE__ */ React3.createElement(Box3, { key: repo.index }, /* @__PURE__ */ React3.createElement(Text3, { color: idx === selectedProjectIndex ? "cyan" : void 0 }, idx === selectedProjectIndex ? "> " : " ", repo.name)))));
|
|
590
|
+
const renderBranchSelection = () => {
|
|
591
|
+
if (loading) {
|
|
592
|
+
return /* @__PURE__ */ React3.createElement(Text3, { color: "yellow" }, "Loading branches...");
|
|
593
|
+
}
|
|
594
|
+
if (branches.length === 0) {
|
|
595
|
+
return /* @__PURE__ */ React3.createElement(Text3, { color: "red" }, "No branches found");
|
|
596
|
+
}
|
|
597
|
+
const visibleCount = 8;
|
|
598
|
+
const halfVisible = Math.floor(visibleCount / 2);
|
|
599
|
+
let startIdx = Math.max(0, selectedBranchIndex - halfVisible);
|
|
600
|
+
const endIdx = Math.min(branches.length, startIdx + visibleCount);
|
|
601
|
+
if (endIdx - startIdx < visibleCount) {
|
|
602
|
+
startIdx = Math.max(0, endIdx - visibleCount);
|
|
603
|
+
}
|
|
604
|
+
const visibleBranches = branches.slice(startIdx, endIdx);
|
|
605
|
+
return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React3.createElement(Text3, null, "Select base branch:"), /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column" }, startIdx > 0 && /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, " ..."), visibleBranches.map((branch, idx) => {
|
|
606
|
+
const actualIdx = startIdx + idx;
|
|
607
|
+
const isSelected = actualIdx === selectedBranchIndex;
|
|
608
|
+
const isDefault = branch === defaultBranch;
|
|
609
|
+
return /* @__PURE__ */ React3.createElement(Box3, { key: branch }, /* @__PURE__ */ React3.createElement(Text3, { color: isSelected ? "cyan" : void 0 }, isSelected ? "> " : " ", branch, isDefault && /* @__PURE__ */ React3.createElement(Text3, { color: "green" }, " (default)")));
|
|
610
|
+
}), endIdx < branches.length && /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, " ...")));
|
|
611
|
+
};
|
|
612
|
+
const renderNameInput = () => {
|
|
613
|
+
const baseBranch = branches[selectedBranchIndex] || "";
|
|
614
|
+
const finalName = existingSuffix !== null ? `${branchName}-${existingSuffix}` : branchName;
|
|
615
|
+
const endsWithSlash = branchName.endsWith("/");
|
|
616
|
+
return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React3.createElement(Text3, null, "Base branch: ", /* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, baseBranch)), /* @__PURE__ */ React3.createElement(Text3, null, "Enter new branch name:"), /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, "> "), /* @__PURE__ */ React3.createElement(Text3, null, branchName), /* @__PURE__ */ React3.createElement(Text3, { color: "gray" }, "|")), validationError && /* @__PURE__ */ React3.createElement(Text3, { color: "red" }, validationError), !validationError && endsWithSlash && /* @__PURE__ */ React3.createElement(Text3, { color: "yellow", dimColor: true }, "Hint: consecutive slashes (//) are not allowed"), !validationError && !endsWithSlash && existingSuffix !== null && branchName && /* @__PURE__ */ React3.createElement(Text3, { color: "yellow" }, "Name exists, will create: ", /* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, finalName)));
|
|
617
|
+
};
|
|
618
|
+
const renderCreating = () => /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: "yellow" }, "Creating branch..."), /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "Please wait while the branch is created and pushed to remote."));
|
|
619
|
+
const renderResult = () => {
|
|
620
|
+
if (!result) return null;
|
|
621
|
+
if (result.success) {
|
|
622
|
+
return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: "green" }, "Branch created successfully!"), /* @__PURE__ */ React3.createElement(Text3, null, "Created: ", /* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, result.finalName)), /* @__PURE__ */ React3.createElement(Text3, null, "From: ", /* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, branches[selectedBranchIndex])), /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "Syncing now to create the worktree..."));
|
|
623
|
+
}
|
|
624
|
+
return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: "red" }, "Failed to create branch"), /* @__PURE__ */ React3.createElement(Text3, { color: "red" }, result.error));
|
|
625
|
+
};
|
|
626
|
+
const renderContent = () => {
|
|
627
|
+
switch (step) {
|
|
628
|
+
case "SELECT_PROJECT":
|
|
629
|
+
return renderProjectSelection();
|
|
630
|
+
case "SELECT_BRANCH":
|
|
631
|
+
return renderBranchSelection();
|
|
632
|
+
case "ENTER_NAME":
|
|
633
|
+
return renderNameInput();
|
|
634
|
+
case "CREATING":
|
|
635
|
+
return renderCreating();
|
|
636
|
+
case "RESULT":
|
|
637
|
+
return renderResult();
|
|
638
|
+
}
|
|
639
|
+
};
|
|
640
|
+
const renderFooter = () => {
|
|
641
|
+
if (step === "CREATING") return null;
|
|
642
|
+
if (step === "RESULT") {
|
|
643
|
+
return /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "Press any key to continue");
|
|
644
|
+
}
|
|
645
|
+
if (step === "ENTER_NAME") {
|
|
646
|
+
return /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "Enter to create \u2022 ESC to go back");
|
|
647
|
+
}
|
|
648
|
+
return /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "\u2191/\u2193 to navigate \u2022 Enter to select \u2022 ESC to cancel");
|
|
649
|
+
};
|
|
650
|
+
return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", marginTop: 1, marginBottom: 1 }, /* @__PURE__ */ React3.createElement(Box3, { borderStyle: "round", borderColor: "green", paddingX: 2, paddingY: 1, flexDirection: "column", width: 60 }, /* @__PURE__ */ React3.createElement(Box3, { marginBottom: 1 }, /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: "green" }, "\u{1F33F} Create New Branch", " ", step !== "CREATING" && step !== "RESULT" && /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "(Step ", getStepNumber(), "/", getTotalSteps(), ")"))), repositories.length > 1 && step !== "SELECT_PROJECT" && step !== "CREATING" && step !== "RESULT" && /* @__PURE__ */ React3.createElement(Box3, { marginBottom: 1 }, /* @__PURE__ */ React3.createElement(Text3, null, "Repository: ", /* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, repositories[selectedProjectIndex].name))), renderContent(), /* @__PURE__ */ React3.createElement(Box3, { marginTop: 1 }, renderFooter())));
|
|
651
|
+
};
|
|
652
|
+
var BranchCreationWizard_default = BranchCreationWizard;
|
|
653
|
+
|
|
654
|
+
// src/components/OpenEditorWizard.tsx
|
|
655
|
+
import React4, { useState as useState3, useEffect as useEffect3, useMemo, useCallback as useCallback2, useRef } from "react";
|
|
656
|
+
import { Box as Box4, Text as Text4, useInput as useInput3 } from "ink";
|
|
657
|
+
var OpenEditorWizard = ({
|
|
658
|
+
repositories,
|
|
659
|
+
getWorktreesForRepo,
|
|
660
|
+
openEditorInWorktree,
|
|
661
|
+
onClose
|
|
662
|
+
}) => {
|
|
663
|
+
const [step, setStep] = useState3(repositories.length > 1 ? "SELECT_PROJECT" : "SELECT_WORKTREE");
|
|
664
|
+
const [selectedProjectIndex, setSelectedProjectIndex] = useState3(0);
|
|
665
|
+
const [projectFilter, setProjectFilter] = useState3("");
|
|
666
|
+
const selectedRepoIndexRef = useRef(repositories.length === 1 ? 0 : -1);
|
|
667
|
+
const [worktrees, setWorktrees] = useState3([]);
|
|
668
|
+
const [selectedWorktreeIndex, setSelectedWorktreeIndex] = useState3(0);
|
|
669
|
+
const [worktreeFilter, setWorktreeFilter] = useState3("");
|
|
670
|
+
const [loading, setLoading] = useState3(false);
|
|
671
|
+
const [error, setError] = useState3(null);
|
|
672
|
+
const filteredProjects = useMemo(() => {
|
|
673
|
+
if (!projectFilter) return repositories;
|
|
674
|
+
const lowerFilter = projectFilter.toLowerCase();
|
|
675
|
+
return repositories.filter((repo) => repo.name.toLowerCase().includes(lowerFilter));
|
|
676
|
+
}, [repositories, projectFilter]);
|
|
677
|
+
const filteredWorktrees = useMemo(() => {
|
|
678
|
+
if (!worktreeFilter) return worktrees;
|
|
679
|
+
const lowerFilter = worktreeFilter.toLowerCase();
|
|
680
|
+
return worktrees.filter((wt) => wt.branch.toLowerCase().includes(lowerFilter));
|
|
681
|
+
}, [worktrees, worktreeFilter]);
|
|
682
|
+
const loadWorktrees = useCallback2(
|
|
683
|
+
async (repoIndex) => {
|
|
684
|
+
setLoading(true);
|
|
685
|
+
try {
|
|
686
|
+
const wts = await getWorktreesForRepo(repoIndex);
|
|
687
|
+
setWorktrees(wts);
|
|
688
|
+
setSelectedWorktreeIndex(0);
|
|
689
|
+
} catch (err) {
|
|
690
|
+
setError(`Failed to load worktrees: ${err}`);
|
|
691
|
+
setStep("ERROR");
|
|
692
|
+
}
|
|
693
|
+
setLoading(false);
|
|
694
|
+
},
|
|
695
|
+
[getWorktreesForRepo]
|
|
696
|
+
);
|
|
697
|
+
useEffect3(() => {
|
|
698
|
+
if (step === "SELECT_WORKTREE" && worktrees.length === 0 && !loading && selectedRepoIndexRef.current >= 0) {
|
|
699
|
+
loadWorktrees(selectedRepoIndexRef.current);
|
|
700
|
+
}
|
|
701
|
+
}, [step, worktrees.length, loading, loadWorktrees]);
|
|
702
|
+
const handleOpenEditor = () => {
|
|
703
|
+
const worktree = filteredWorktrees[selectedWorktreeIndex];
|
|
704
|
+
if (!worktree) return;
|
|
705
|
+
setStep("OPENING");
|
|
706
|
+
const result = openEditorInWorktree(worktree.path);
|
|
707
|
+
if (result.success) {
|
|
708
|
+
onClose();
|
|
709
|
+
} else {
|
|
710
|
+
setError(result.error || "Failed to open editor");
|
|
711
|
+
setStep("ERROR");
|
|
712
|
+
}
|
|
713
|
+
};
|
|
714
|
+
useInput3((input2, key) => {
|
|
715
|
+
if (step === "OPENING") return;
|
|
716
|
+
if (key.escape) {
|
|
717
|
+
if (step === "SELECT_PROJECT") {
|
|
718
|
+
onClose();
|
|
719
|
+
} else if (step === "SELECT_WORKTREE") {
|
|
720
|
+
if (repositories.length > 1) {
|
|
721
|
+
setWorktrees([]);
|
|
722
|
+
setWorktreeFilter("");
|
|
723
|
+
selectedRepoIndexRef.current = -1;
|
|
724
|
+
setStep("SELECT_PROJECT");
|
|
725
|
+
} else {
|
|
726
|
+
onClose();
|
|
727
|
+
}
|
|
728
|
+
} else if (step === "ERROR") {
|
|
729
|
+
onClose();
|
|
730
|
+
}
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
733
|
+
if (step === "SELECT_PROJECT") {
|
|
734
|
+
if (key.upArrow) {
|
|
735
|
+
setSelectedProjectIndex((prev) => Math.max(0, prev - 1));
|
|
736
|
+
} else if (key.downArrow) {
|
|
737
|
+
setSelectedProjectIndex((prev) => Math.min(filteredProjects.length - 1, prev + 1));
|
|
738
|
+
} else if (key.return && filteredProjects.length > 0) {
|
|
739
|
+
const selectedRepo = filteredProjects[selectedProjectIndex];
|
|
740
|
+
if (selectedRepo) {
|
|
741
|
+
selectedRepoIndexRef.current = selectedRepo.index;
|
|
742
|
+
setStep("SELECT_WORKTREE");
|
|
743
|
+
loadWorktrees(selectedRepo.index);
|
|
744
|
+
}
|
|
745
|
+
} else if (key.backspace || key.delete) {
|
|
746
|
+
setProjectFilter((prev) => prev.slice(0, -1));
|
|
747
|
+
setSelectedProjectIndex(0);
|
|
748
|
+
} else if (input2 && !key.ctrl && !key.meta) {
|
|
749
|
+
setProjectFilter((prev) => prev + input2);
|
|
750
|
+
setSelectedProjectIndex(0);
|
|
751
|
+
}
|
|
752
|
+
} else if (step === "SELECT_WORKTREE") {
|
|
753
|
+
if (key.upArrow) {
|
|
754
|
+
setSelectedWorktreeIndex((prev) => Math.max(0, prev - 1));
|
|
755
|
+
} else if (key.downArrow) {
|
|
756
|
+
setSelectedWorktreeIndex((prev) => Math.min(filteredWorktrees.length - 1, prev + 1));
|
|
757
|
+
} else if (key.return && filteredWorktrees.length > 0) {
|
|
758
|
+
handleOpenEditor();
|
|
759
|
+
} else if (key.backspace || key.delete) {
|
|
760
|
+
setWorktreeFilter((prev) => prev.slice(0, -1));
|
|
761
|
+
setSelectedWorktreeIndex(0);
|
|
762
|
+
} else if (input2 && !key.ctrl && !key.meta) {
|
|
763
|
+
setWorktreeFilter((prev) => prev + input2);
|
|
764
|
+
setSelectedWorktreeIndex(0);
|
|
765
|
+
}
|
|
766
|
+
} else if (step === "ERROR") {
|
|
767
|
+
onClose();
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
const getStepNumber = () => {
|
|
771
|
+
if (repositories.length === 1) {
|
|
772
|
+
return 1;
|
|
773
|
+
}
|
|
774
|
+
return step === "SELECT_PROJECT" ? 1 : 2;
|
|
775
|
+
};
|
|
776
|
+
const getTotalSteps = () => repositories.length === 1 ? 1 : 2;
|
|
777
|
+
const renderProjectSelection = () => {
|
|
778
|
+
const visibleCount = 8;
|
|
779
|
+
const halfVisible = Math.floor(visibleCount / 2);
|
|
780
|
+
let startIdx = Math.max(0, selectedProjectIndex - halfVisible);
|
|
781
|
+
const endIdx = Math.min(filteredProjects.length, startIdx + visibleCount);
|
|
782
|
+
if (endIdx - startIdx < visibleCount) {
|
|
783
|
+
startIdx = Math.max(0, endIdx - visibleCount);
|
|
784
|
+
}
|
|
785
|
+
const visibleProjects = filteredProjects.slice(startIdx, endIdx);
|
|
786
|
+
return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React4.createElement(Text4, null, "Select repository:"), /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Text4, null, "Filter: "), /* @__PURE__ */ React4.createElement(Text4, { color: "cyan" }, projectFilter || "_"), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " ", "(", filteredProjects.length, "/", repositories.length, " matches)")), /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column" }, filteredProjects.length === 0 ? /* @__PURE__ */ React4.createElement(Text4, { color: "yellow" }, "No matches") : /* @__PURE__ */ React4.createElement(React4.Fragment, null, startIdx > 0 && /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " ..."), visibleProjects.map((repo, idx) => {
|
|
787
|
+
const actualIdx = startIdx + idx;
|
|
788
|
+
const isSelected = actualIdx === selectedProjectIndex;
|
|
789
|
+
return /* @__PURE__ */ React4.createElement(Box4, { key: repo.index }, /* @__PURE__ */ React4.createElement(Text4, { color: isSelected ? "cyan" : void 0 }, isSelected ? "> " : " ", repo.name));
|
|
790
|
+
}), endIdx < filteredProjects.length && /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " ..."))));
|
|
791
|
+
};
|
|
792
|
+
const renderWorktreeSelection = () => {
|
|
793
|
+
if (loading) {
|
|
794
|
+
return /* @__PURE__ */ React4.createElement(Text4, { color: "yellow" }, "Loading worktrees...");
|
|
795
|
+
}
|
|
796
|
+
if (worktrees.length === 0) {
|
|
797
|
+
return /* @__PURE__ */ React4.createElement(Text4, { color: "red" }, "No worktrees found");
|
|
798
|
+
}
|
|
799
|
+
const visibleCount = 8;
|
|
800
|
+
const halfVisible = Math.floor(visibleCount / 2);
|
|
801
|
+
let startIdx = Math.max(0, selectedWorktreeIndex - halfVisible);
|
|
802
|
+
const endIdx = Math.min(filteredWorktrees.length, startIdx + visibleCount);
|
|
803
|
+
if (endIdx - startIdx < visibleCount) {
|
|
804
|
+
startIdx = Math.max(0, endIdx - visibleCount);
|
|
805
|
+
}
|
|
806
|
+
const visibleWorktrees = filteredWorktrees.slice(startIdx, endIdx);
|
|
807
|
+
return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React4.createElement(Text4, null, "Select worktree:"), /* @__PURE__ */ React4.createElement(Box4, null, /* @__PURE__ */ React4.createElement(Text4, null, "Filter: "), /* @__PURE__ */ React4.createElement(Text4, { color: "cyan" }, worktreeFilter || "_"), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " ", "(", filteredWorktrees.length, "/", worktrees.length, " matches)")), /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column" }, filteredWorktrees.length === 0 ? /* @__PURE__ */ React4.createElement(Text4, { color: "yellow" }, "No matches") : /* @__PURE__ */ React4.createElement(React4.Fragment, null, startIdx > 0 && /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " ..."), visibleWorktrees.map((wt, idx) => {
|
|
808
|
+
const actualIdx = startIdx + idx;
|
|
809
|
+
const isSelected = actualIdx === selectedWorktreeIndex;
|
|
810
|
+
return /* @__PURE__ */ React4.createElement(Box4, { key: wt.path }, /* @__PURE__ */ React4.createElement(Text4, { color: isSelected ? "cyan" : void 0 }, isSelected ? "> " : " ", wt.branch));
|
|
811
|
+
}), endIdx < filteredWorktrees.length && /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, " ..."))));
|
|
812
|
+
};
|
|
813
|
+
const renderOpening = () => /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React4.createElement(Text4, { color: "yellow" }, "Opening editor..."));
|
|
814
|
+
const renderError = () => /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", gap: 1 }, /* @__PURE__ */ React4.createElement(Text4, { color: "red" }, "Error: ", error), /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "Press any key to close"));
|
|
815
|
+
const renderContent = () => {
|
|
816
|
+
switch (step) {
|
|
817
|
+
case "SELECT_PROJECT":
|
|
818
|
+
return renderProjectSelection();
|
|
819
|
+
case "SELECT_WORKTREE":
|
|
820
|
+
return renderWorktreeSelection();
|
|
821
|
+
case "OPENING":
|
|
822
|
+
return renderOpening();
|
|
823
|
+
case "ERROR":
|
|
824
|
+
return renderError();
|
|
825
|
+
}
|
|
826
|
+
};
|
|
827
|
+
const renderFooter = () => {
|
|
828
|
+
if (step === "OPENING") return null;
|
|
829
|
+
if (step === "ERROR") return null;
|
|
830
|
+
return /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "\u2191/\u2193 navigate \u2022 Type to filter \u2022 Enter to select \u2022 ESC to cancel");
|
|
831
|
+
};
|
|
832
|
+
return /* @__PURE__ */ React4.createElement(Box4, { flexDirection: "column", marginTop: 1, marginBottom: 1 }, /* @__PURE__ */ React4.createElement(Box4, { borderStyle: "round", borderColor: "blue", paddingX: 2, paddingY: 1, flexDirection: "column", width: 60 }, /* @__PURE__ */ React4.createElement(Box4, { marginBottom: 1 }, /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: "blue" }, "\u{1F4C2} Open in Editor", " ", step !== "OPENING" && step !== "ERROR" && /* @__PURE__ */ React4.createElement(Text4, { dimColor: true }, "(Step ", getStepNumber(), "/", getTotalSteps(), ")"))), repositories.length > 1 && step === "SELECT_WORKTREE" && !loading && selectedRepoIndexRef.current >= 0 && /* @__PURE__ */ React4.createElement(Box4, { marginBottom: 1 }, /* @__PURE__ */ React4.createElement(Text4, null, "Repository: ", /* @__PURE__ */ React4.createElement(Text4, { color: "cyan" }, repositories.find((r) => r.index === selectedRepoIndexRef.current)?.name))), renderContent(), /* @__PURE__ */ React4.createElement(Box4, { marginTop: 1 }, renderFooter())));
|
|
833
|
+
};
|
|
834
|
+
var OpenEditorWizard_default = OpenEditorWizard;
|
|
835
|
+
|
|
836
|
+
// src/components/LogPanel.tsx
|
|
837
|
+
import React5, { useState as useState4, useEffect as useEffect4, useRef as useRef2 } from "react";
|
|
838
|
+
import { Box as Box5, Text as Text5, useInput as useInput4 } from "ink";
|
|
839
|
+
var LogPanel = ({ logs, height, isActive }) => {
|
|
840
|
+
const [scrollOffset, setScrollOffset] = useState4(0);
|
|
841
|
+
const [autoScroll, setAutoScroll] = useState4(true);
|
|
842
|
+
const [pendingG, setPendingG] = useState4(false);
|
|
843
|
+
const gTimeoutRef = useRef2(null);
|
|
844
|
+
const borderLines = 2;
|
|
845
|
+
const headerLine = 1;
|
|
846
|
+
const visibleLines = Math.max(1, height - borderLines - headerLine);
|
|
847
|
+
const maxOffset = Math.max(0, logs.length - visibleLines);
|
|
848
|
+
useEffect4(() => {
|
|
849
|
+
if (autoScroll) {
|
|
850
|
+
setScrollOffset(maxOffset);
|
|
851
|
+
}
|
|
852
|
+
}, [logs.length, maxOffset, autoScroll]);
|
|
853
|
+
useEffect4(() => {
|
|
854
|
+
return () => {
|
|
855
|
+
if (gTimeoutRef.current) {
|
|
856
|
+
clearTimeout(gTimeoutRef.current);
|
|
857
|
+
}
|
|
858
|
+
};
|
|
859
|
+
}, []);
|
|
860
|
+
useInput4(
|
|
861
|
+
(input2, key) => {
|
|
862
|
+
if (!isActive) return;
|
|
863
|
+
if (key.upArrow || input2 === "k") {
|
|
864
|
+
setScrollOffset((prev) => Math.max(0, prev - 1));
|
|
865
|
+
setAutoScroll(false);
|
|
866
|
+
setPendingG(false);
|
|
867
|
+
} else if (key.downArrow || input2 === "j") {
|
|
868
|
+
setScrollOffset((prev) => {
|
|
869
|
+
const newOffset = Math.min(maxOffset, prev + 1);
|
|
870
|
+
if (newOffset >= maxOffset) {
|
|
871
|
+
setAutoScroll(true);
|
|
872
|
+
}
|
|
873
|
+
return newOffset;
|
|
874
|
+
});
|
|
875
|
+
setPendingG(false);
|
|
876
|
+
} else if (key.pageUp) {
|
|
877
|
+
setScrollOffset((prev) => Math.max(0, prev - visibleLines));
|
|
878
|
+
setAutoScroll(false);
|
|
879
|
+
setPendingG(false);
|
|
880
|
+
} else if (key.pageDown) {
|
|
881
|
+
setScrollOffset((prev) => {
|
|
882
|
+
const newOffset = Math.min(maxOffset, prev + visibleLines);
|
|
883
|
+
if (newOffset >= maxOffset) {
|
|
884
|
+
setAutoScroll(true);
|
|
885
|
+
}
|
|
886
|
+
return newOffset;
|
|
887
|
+
});
|
|
888
|
+
setPendingG(false);
|
|
889
|
+
} else if (input2 === "g") {
|
|
890
|
+
if (pendingG) {
|
|
891
|
+
setScrollOffset(0);
|
|
892
|
+
setAutoScroll(false);
|
|
893
|
+
setPendingG(false);
|
|
894
|
+
if (gTimeoutRef.current) {
|
|
895
|
+
clearTimeout(gTimeoutRef.current);
|
|
896
|
+
gTimeoutRef.current = null;
|
|
897
|
+
}
|
|
898
|
+
} else {
|
|
899
|
+
setPendingG(true);
|
|
900
|
+
gTimeoutRef.current = setTimeout(() => {
|
|
901
|
+
setPendingG(false);
|
|
902
|
+
}, 500);
|
|
903
|
+
}
|
|
904
|
+
} else if (input2 === "G") {
|
|
905
|
+
setScrollOffset(maxOffset);
|
|
906
|
+
setAutoScroll(true);
|
|
907
|
+
setPendingG(false);
|
|
908
|
+
}
|
|
909
|
+
},
|
|
910
|
+
{ isActive }
|
|
911
|
+
);
|
|
912
|
+
const getLogColor = (level) => {
|
|
913
|
+
switch (level) {
|
|
914
|
+
case "error":
|
|
915
|
+
return "red";
|
|
916
|
+
case "warn":
|
|
917
|
+
return "yellow";
|
|
918
|
+
default:
|
|
919
|
+
return void 0;
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
const visibleLogs = logs.slice(scrollOffset, scrollOffset + visibleLines);
|
|
923
|
+
const hasMoreAbove = scrollOffset > 0;
|
|
924
|
+
const hasMoreBelow = scrollOffset + visibleLines < logs.length;
|
|
925
|
+
const aboveCount = scrollOffset;
|
|
926
|
+
const belowCount = logs.length - scrollOffset - visibleLines;
|
|
927
|
+
const emptyLines = Math.max(0, visibleLines - visibleLogs.length);
|
|
928
|
+
return /* @__PURE__ */ React5.createElement(Box5, { borderStyle: "single", flexDirection: "column", flexGrow: 1, paddingX: 1 }, /* @__PURE__ */ React5.createElement(Box5, { justifyContent: "space-between" }, /* @__PURE__ */ React5.createElement(Text5, { bold: true }, "\u{1F4CB} Logs ", logs.length > 0 && /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "(", logs.length, " entries)")), isActive && /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, hasMoreAbove || hasMoreBelow ? "\u2191/\u2193 scroll" : "", " ", autoScroll ? "(auto)" : "")), hasMoreAbove && /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "\u2191 ", aboveCount, " more above"), visibleLogs.map((log) => /* @__PURE__ */ React5.createElement(Text5, { key: log.id, color: getLogColor(log.level), wrap: "truncate" }, log.message)), Array.from({ length: emptyLines }).map((_, i) => /* @__PURE__ */ React5.createElement(Text5, { key: `empty-${i}` }, " ")), hasMoreBelow && /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "\u2193 ", belowCount, " more below"));
|
|
929
|
+
};
|
|
930
|
+
var LogPanel_default = LogPanel;
|
|
931
|
+
|
|
932
|
+
// src/utils/app-events.ts
|
|
933
|
+
var AppEventEmitter = class {
|
|
934
|
+
listeners = /* @__PURE__ */ new Map();
|
|
935
|
+
on(event, callback) {
|
|
936
|
+
if (!this.listeners.has(event)) {
|
|
937
|
+
this.listeners.set(event, /* @__PURE__ */ new Set());
|
|
938
|
+
}
|
|
939
|
+
this.listeners.get(event).add(callback);
|
|
940
|
+
return () => {
|
|
941
|
+
this.listeners.get(event)?.delete(callback);
|
|
942
|
+
};
|
|
943
|
+
}
|
|
944
|
+
emit(event, ...args) {
|
|
945
|
+
const callbacks = this.listeners.get(event);
|
|
946
|
+
if (callbacks) {
|
|
947
|
+
for (const callback of callbacks) {
|
|
948
|
+
try {
|
|
949
|
+
callback(args[0]);
|
|
950
|
+
} catch {
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
removeAllListeners() {
|
|
956
|
+
this.listeners.clear();
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
var appEvents = new AppEventEmitter();
|
|
960
|
+
|
|
961
|
+
// src/components/App.tsx
|
|
962
|
+
var MAX_LOG_ENTRIES = 5e3;
|
|
963
|
+
var App = ({
|
|
964
|
+
repositoryCount,
|
|
965
|
+
cronSchedule,
|
|
966
|
+
onManualSync,
|
|
967
|
+
onReload,
|
|
968
|
+
onQuit,
|
|
969
|
+
getRepositoryList,
|
|
970
|
+
getBranchesForRepo,
|
|
971
|
+
getDefaultBranchForRepo,
|
|
972
|
+
createAndPushBranch,
|
|
973
|
+
getWorktreesForRepo,
|
|
974
|
+
openEditorInWorktree,
|
|
975
|
+
copyBranchFiles,
|
|
976
|
+
createWorktreeForBranch
|
|
977
|
+
}) => {
|
|
978
|
+
const [showHelp, setShowHelp] = useState5(false);
|
|
979
|
+
const [showBranchWizard, setShowBranchWizard] = useState5(false);
|
|
980
|
+
const [showOpenEditorWizard, setShowOpenEditorWizard] = useState5(false);
|
|
981
|
+
const [status, setStatus] = useState5("idle");
|
|
982
|
+
const [lastSyncTime, setLastSyncTime] = useState5(null);
|
|
983
|
+
const [diskSpaceUsed, setDiskSpaceUsed] = useState5(null);
|
|
984
|
+
const [logs, setLogs] = useState5([]);
|
|
985
|
+
const { stdout } = useStdout();
|
|
986
|
+
const addLog = useCallback3((message, level = "info") => {
|
|
987
|
+
setLogs((prev) => {
|
|
988
|
+
const newLogs = [
|
|
989
|
+
...prev,
|
|
990
|
+
{
|
|
991
|
+
id: `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
|
992
|
+
message,
|
|
993
|
+
level,
|
|
994
|
+
timestamp: /* @__PURE__ */ new Date()
|
|
995
|
+
}
|
|
996
|
+
];
|
|
997
|
+
if (newLogs.length > MAX_LOG_ENTRIES) {
|
|
998
|
+
return newLogs.slice(-MAX_LOG_ENTRIES);
|
|
999
|
+
}
|
|
1000
|
+
return newLogs;
|
|
1001
|
+
});
|
|
1002
|
+
}, []);
|
|
1003
|
+
const addLogRef = useRef3(addLog);
|
|
1004
|
+
addLogRef.current = addLog;
|
|
1005
|
+
useInput5((input2) => {
|
|
235
1006
|
if (showHelp) {
|
|
236
1007
|
if (input2 === "?" || input2 === "h") {
|
|
237
1008
|
setShowHelp(false);
|
|
238
1009
|
}
|
|
239
1010
|
return;
|
|
240
1011
|
}
|
|
241
|
-
if (
|
|
1012
|
+
if (showBranchWizard || showOpenEditorWizard) {
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
if (input2 === "q") {
|
|
242
1016
|
void onQuit();
|
|
243
1017
|
} else if (input2 === "?" || input2 === "h") {
|
|
244
1018
|
setShowHelp(true);
|
|
1019
|
+
} else if (input2 === "c" && status === "idle") {
|
|
1020
|
+
setShowBranchWizard(true);
|
|
1021
|
+
} else if (input2 === "o" && status === "idle") {
|
|
1022
|
+
setShowOpenEditorWizard(true);
|
|
245
1023
|
} else if (input2 === "s" && status !== "syncing") {
|
|
246
1024
|
setStatus("syncing");
|
|
247
1025
|
void (async () => {
|
|
@@ -264,21 +1042,71 @@ var App = ({ repositoryCount, cronSchedule, onManualSync, onReload, onQuit }) =>
|
|
|
264
1042
|
})();
|
|
265
1043
|
}
|
|
266
1044
|
});
|
|
267
|
-
const updateLastSyncTime =
|
|
1045
|
+
const updateLastSyncTime = useCallback3(() => {
|
|
268
1046
|
setLastSyncTime(/* @__PURE__ */ new Date());
|
|
269
1047
|
setStatus("idle");
|
|
270
1048
|
}, []);
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
updateLastSyncTime,
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
1049
|
+
useEffect5(() => {
|
|
1050
|
+
const unsubscribers = [
|
|
1051
|
+
appEvents.on("updateLastSyncTime", () => {
|
|
1052
|
+
setLastSyncTime(/* @__PURE__ */ new Date());
|
|
1053
|
+
setStatus("idle");
|
|
1054
|
+
}),
|
|
1055
|
+
appEvents.on("setStatus", (newStatus) => {
|
|
1056
|
+
setStatus(newStatus);
|
|
1057
|
+
}),
|
|
1058
|
+
appEvents.on("setDiskSpace", (diskSpace) => {
|
|
1059
|
+
setDiskSpaceUsed(diskSpace);
|
|
1060
|
+
}),
|
|
1061
|
+
appEvents.on("addLog", ({ message, level }) => {
|
|
1062
|
+
addLogRef.current(message, level);
|
|
1063
|
+
})
|
|
1064
|
+
];
|
|
1065
|
+
appEvents.emit("uiReady");
|
|
277
1066
|
return () => {
|
|
278
|
-
|
|
1067
|
+
unsubscribers.forEach((unsub) => unsub());
|
|
279
1068
|
};
|
|
280
|
-
}, [
|
|
281
|
-
|
|
1069
|
+
}, []);
|
|
1070
|
+
const statusBarHeight = 5;
|
|
1071
|
+
const terminalRows = stdout.rows ?? 24;
|
|
1072
|
+
const logPanelHeight = Math.max(5, terminalRows - statusBarHeight);
|
|
1073
|
+
const showModal = showHelp || showBranchWizard || showOpenEditorWizard;
|
|
1074
|
+
return /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column", minHeight: terminalRows }, !showModal && /* @__PURE__ */ React6.createElement(LogPanel_default, { logs, height: logPanelHeight, isActive: !showModal }), showHelp && /* @__PURE__ */ React6.createElement(HelpModal_default, { onClose: () => setShowHelp(false) }), showBranchWizard && /* @__PURE__ */ React6.createElement(
|
|
1075
|
+
BranchCreationWizard_default,
|
|
1076
|
+
{
|
|
1077
|
+
repositories: getRepositoryList(),
|
|
1078
|
+
getBranchesForRepo,
|
|
1079
|
+
getDefaultBranchForRepo,
|
|
1080
|
+
createAndPushBranch,
|
|
1081
|
+
onClose: () => setShowBranchWizard(false),
|
|
1082
|
+
onComplete: (success, context) => {
|
|
1083
|
+
setShowBranchWizard(false);
|
|
1084
|
+
if (success && context) {
|
|
1085
|
+
setStatus("syncing");
|
|
1086
|
+
void (async () => {
|
|
1087
|
+
try {
|
|
1088
|
+
await createWorktreeForBranch(context.repoIndex, context.newBranch);
|
|
1089
|
+
if (copyBranchFiles) {
|
|
1090
|
+
await copyBranchFiles(context.repoIndex, context.baseBranch, context.newBranch);
|
|
1091
|
+
}
|
|
1092
|
+
} catch (error) {
|
|
1093
|
+
console.error("Failed to create worktree:", error);
|
|
1094
|
+
} finally {
|
|
1095
|
+
setStatus("idle");
|
|
1096
|
+
}
|
|
1097
|
+
})();
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
), showOpenEditorWizard && /* @__PURE__ */ React6.createElement(
|
|
1102
|
+
OpenEditorWizard_default,
|
|
1103
|
+
{
|
|
1104
|
+
repositories: getRepositoryList(),
|
|
1105
|
+
getWorktreesForRepo,
|
|
1106
|
+
openEditorInWorktree,
|
|
1107
|
+
onClose: () => setShowOpenEditorWizard(false)
|
|
1108
|
+
}
|
|
1109
|
+
), /* @__PURE__ */ React6.createElement(
|
|
282
1110
|
StatusBar_default,
|
|
283
1111
|
{
|
|
284
1112
|
status,
|
|
@@ -287,13 +1115,14 @@ var App = ({ repositoryCount, cronSchedule, onManualSync, onReload, onQuit }) =>
|
|
|
287
1115
|
cronSchedule,
|
|
288
1116
|
diskSpaceUsed: diskSpaceUsed ?? void 0
|
|
289
1117
|
}
|
|
290
|
-
)
|
|
1118
|
+
));
|
|
291
1119
|
};
|
|
292
1120
|
var App_default = App;
|
|
293
1121
|
|
|
294
1122
|
// src/services/worktree-sync.service.ts
|
|
295
1123
|
import * as fs5 from "fs/promises";
|
|
296
1124
|
import * as path5 from "path";
|
|
1125
|
+
import pLimit from "p-limit";
|
|
297
1126
|
|
|
298
1127
|
// src/utils/date-filter.ts
|
|
299
1128
|
function parseDuration(durationStr) {
|
|
@@ -373,6 +1202,7 @@ var DEFAULT_OPTIONS = {
|
|
|
373
1202
|
maxDelayMs: 6e5,
|
|
374
1203
|
// 10 minutes
|
|
375
1204
|
backoffMultiplier: 2,
|
|
1205
|
+
jitterMs: 0,
|
|
376
1206
|
shouldRetry: (error, context) => {
|
|
377
1207
|
const err = error;
|
|
378
1208
|
if (isLfsErrorFromError(error)) {
|
|
@@ -430,7 +1260,9 @@ async function retry(fn, options = {}) {
|
|
|
430
1260
|
if (lfsContext.isLfsError && opts.lfsRetryHandler) {
|
|
431
1261
|
opts.lfsRetryHandler(lfsContext);
|
|
432
1262
|
}
|
|
433
|
-
const
|
|
1263
|
+
const baseDelay = Math.min(opts.initialDelayMs * Math.pow(opts.backoffMultiplier, attempt - 1), opts.maxDelayMs);
|
|
1264
|
+
const jitter = opts.jitterMs > 0 ? Math.random() * opts.jitterMs : 0;
|
|
1265
|
+
const delay = baseDelay + jitter;
|
|
434
1266
|
opts.onRetry(error, attempt, lfsContext);
|
|
435
1267
|
await new Promise((resolve6) => setTimeout(resolve6, delay));
|
|
436
1268
|
attempt++;
|
|
@@ -438,6 +1270,106 @@ async function retry(fn, options = {}) {
|
|
|
438
1270
|
}
|
|
439
1271
|
}
|
|
440
1272
|
|
|
1273
|
+
// src/utils/timing.ts
|
|
1274
|
+
import Table from "cli-table3";
|
|
1275
|
+
var Timer = class {
|
|
1276
|
+
startTime;
|
|
1277
|
+
endTime;
|
|
1278
|
+
constructor() {
|
|
1279
|
+
this.startTime = Date.now();
|
|
1280
|
+
}
|
|
1281
|
+
stop() {
|
|
1282
|
+
this.endTime = Date.now();
|
|
1283
|
+
return this.getDuration();
|
|
1284
|
+
}
|
|
1285
|
+
getDuration() {
|
|
1286
|
+
const end = this.endTime ?? Date.now();
|
|
1287
|
+
return end - this.startTime;
|
|
1288
|
+
}
|
|
1289
|
+
};
|
|
1290
|
+
var PhaseTimer = class {
|
|
1291
|
+
phases = /* @__PURE__ */ new Map();
|
|
1292
|
+
currentPhase;
|
|
1293
|
+
startPhase(name, parallelism) {
|
|
1294
|
+
if (this.currentPhase) {
|
|
1295
|
+
this.endPhase();
|
|
1296
|
+
}
|
|
1297
|
+
this.currentPhase = name;
|
|
1298
|
+
this.phases.set(name, { timer: new Timer(), parallelism });
|
|
1299
|
+
}
|
|
1300
|
+
endPhase() {
|
|
1301
|
+
if (this.currentPhase) {
|
|
1302
|
+
const phase = this.phases.get(this.currentPhase);
|
|
1303
|
+
if (phase) {
|
|
1304
|
+
phase.timer.stop();
|
|
1305
|
+
}
|
|
1306
|
+
this.currentPhase = void 0;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
setPhaseCount(name, count) {
|
|
1310
|
+
const phase = this.phases.get(name);
|
|
1311
|
+
if (phase) {
|
|
1312
|
+
phase.count = count;
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
getResults() {
|
|
1316
|
+
if (this.currentPhase) {
|
|
1317
|
+
this.endPhase();
|
|
1318
|
+
}
|
|
1319
|
+
const results = [];
|
|
1320
|
+
for (const [name, { timer, count, parallelism }] of this.phases.entries()) {
|
|
1321
|
+
const duration = timer.getDuration();
|
|
1322
|
+
const result = {
|
|
1323
|
+
name,
|
|
1324
|
+
duration,
|
|
1325
|
+
count
|
|
1326
|
+
};
|
|
1327
|
+
if (count && count > 0 && parallelism && parallelism > 1) {
|
|
1328
|
+
const batches = Math.ceil(count / parallelism);
|
|
1329
|
+
const avgTimePerBatch = duration / batches;
|
|
1330
|
+
const theoreticalSequentialTime = count * avgTimePerBatch;
|
|
1331
|
+
result.efficiency = theoreticalSequentialTime > 0 ? Math.round(theoreticalSequentialTime / duration * 100) : 100;
|
|
1332
|
+
}
|
|
1333
|
+
results.push(result);
|
|
1334
|
+
}
|
|
1335
|
+
return results;
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1338
|
+
function formatDuration2(ms) {
|
|
1339
|
+
if (ms < 1e3) {
|
|
1340
|
+
return `${ms}ms`;
|
|
1341
|
+
}
|
|
1342
|
+
if (ms < 6e4) {
|
|
1343
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
1344
|
+
}
|
|
1345
|
+
const minutes = Math.floor(ms / 6e4);
|
|
1346
|
+
const seconds = Math.floor(ms % 6e4 / 1e3);
|
|
1347
|
+
return `${minutes}m ${seconds}s`;
|
|
1348
|
+
}
|
|
1349
|
+
function formatTimingTable(totalDuration, phaseResults, repoName) {
|
|
1350
|
+
const header = repoName ? `Performance Summary - [${repoName}]` : "Performance Summary";
|
|
1351
|
+
const table = new Table({
|
|
1352
|
+
head: ["Operation", "Duration", "Efficiency"],
|
|
1353
|
+
colWidths: [35, 12, 12],
|
|
1354
|
+
style: {
|
|
1355
|
+
head: ["cyan", "bold"],
|
|
1356
|
+
border: ["gray"]
|
|
1357
|
+
}
|
|
1358
|
+
});
|
|
1359
|
+
table.push([{ colSpan: 3, content: header, hAlign: "center" }]);
|
|
1360
|
+
table.push(["Total Sync", formatDuration2(totalDuration), ""]);
|
|
1361
|
+
for (let i = 0; i < phaseResults.length; i++) {
|
|
1362
|
+
const result = phaseResults[i];
|
|
1363
|
+
const isLast = i === phaseResults.length - 1;
|
|
1364
|
+
const countStr = result.count ? ` (${result.count})` : "";
|
|
1365
|
+
const prefix = isLast ? "\u2514\u2500" : "\u251C\u2500";
|
|
1366
|
+
const name = ` ${prefix} ${result.name}${countStr}`;
|
|
1367
|
+
const efficiency = result.efficiency ? `${result.efficiency}%` : "";
|
|
1368
|
+
table.push([name, formatDuration2(result.duration), efficiency]);
|
|
1369
|
+
}
|
|
1370
|
+
return table.toString();
|
|
1371
|
+
}
|
|
1372
|
+
|
|
441
1373
|
// src/services/git.service.ts
|
|
442
1374
|
import * as fs4 from "fs/promises";
|
|
443
1375
|
import * as path4 from "path";
|
|
@@ -469,29 +1401,86 @@ function getDefaultBareRepoDir(repoUrl, baseDir = ".bare") {
|
|
|
469
1401
|
return `${baseDir}/${repoName}`;
|
|
470
1402
|
}
|
|
471
1403
|
|
|
1404
|
+
// src/services/logger.service.ts
|
|
1405
|
+
var Logger = class _Logger {
|
|
1406
|
+
repoName;
|
|
1407
|
+
debugEnabled;
|
|
1408
|
+
outputFn;
|
|
1409
|
+
constructor(options = {}) {
|
|
1410
|
+
this.repoName = options.repoName;
|
|
1411
|
+
this.debugEnabled = options.debug ?? false;
|
|
1412
|
+
this.outputFn = options.outputFn;
|
|
1413
|
+
}
|
|
1414
|
+
prefix() {
|
|
1415
|
+
return this.repoName ? `[${this.repoName}] ` : "";
|
|
1416
|
+
}
|
|
1417
|
+
debug(message, ...args) {
|
|
1418
|
+
if (!this.debugEnabled) return;
|
|
1419
|
+
const formattedMessage = this.prefix() + this.formatMessage(message, args);
|
|
1420
|
+
if (this.outputFn) {
|
|
1421
|
+
this.outputFn(formattedMessage, "debug");
|
|
1422
|
+
} else {
|
|
1423
|
+
console.log(formattedMessage);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
info(message, ...args) {
|
|
1427
|
+
const formattedMessage = this.prefix() + this.formatMessage(message, args);
|
|
1428
|
+
if (this.outputFn) {
|
|
1429
|
+
this.outputFn(formattedMessage, "info");
|
|
1430
|
+
} else {
|
|
1431
|
+
console.log(formattedMessage);
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
warn(message, ...args) {
|
|
1435
|
+
const formattedMessage = this.prefix() + this.formatMessage(message, args);
|
|
1436
|
+
if (this.outputFn) {
|
|
1437
|
+
this.outputFn(formattedMessage, "warn");
|
|
1438
|
+
} else {
|
|
1439
|
+
console.warn(formattedMessage);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
error(message, error) {
|
|
1443
|
+
let formattedMessage = this.prefix() + message;
|
|
1444
|
+
if (error instanceof Error) {
|
|
1445
|
+
formattedMessage += ` ${error.message}`;
|
|
1446
|
+
} else if (error) {
|
|
1447
|
+
formattedMessage += ` ${String(error)}`;
|
|
1448
|
+
}
|
|
1449
|
+
if (this.outputFn) {
|
|
1450
|
+
this.outputFn(formattedMessage, "error");
|
|
1451
|
+
} else {
|
|
1452
|
+
if (error instanceof Error) {
|
|
1453
|
+
console.error(this.prefix() + message, error);
|
|
1454
|
+
} else if (error) {
|
|
1455
|
+
console.error(this.prefix() + message, error);
|
|
1456
|
+
} else {
|
|
1457
|
+
console.error(this.prefix() + message);
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
table(content) {
|
|
1462
|
+
const formattedMessage = "\n" + content + "\n";
|
|
1463
|
+
if (this.outputFn) {
|
|
1464
|
+
this.outputFn(formattedMessage, "info");
|
|
1465
|
+
} else {
|
|
1466
|
+
console.log(formattedMessage);
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
formatMessage(message, args) {
|
|
1470
|
+
if (args.length === 0) {
|
|
1471
|
+
return message;
|
|
1472
|
+
}
|
|
1473
|
+
return args.reduce((msg, arg) => msg.replace("%s", String(arg)), message);
|
|
1474
|
+
}
|
|
1475
|
+
static createDefault(repoName, debug) {
|
|
1476
|
+
return new _Logger({ repoName, debug });
|
|
1477
|
+
}
|
|
1478
|
+
};
|
|
1479
|
+
|
|
472
1480
|
// src/services/worktree-metadata.service.ts
|
|
473
1481
|
import * as fs2 from "fs/promises";
|
|
474
1482
|
import * as path2 from "path";
|
|
475
1483
|
import simpleGit from "simple-git";
|
|
476
|
-
|
|
477
|
-
// src/constants.ts
|
|
478
|
-
var GIT_OPERATIONS = {
|
|
479
|
-
MERGE_HEAD: "MERGE_HEAD",
|
|
480
|
-
CHERRY_PICK_HEAD: "CHERRY_PICK_HEAD",
|
|
481
|
-
REVERT_HEAD: "REVERT_HEAD",
|
|
482
|
-
BISECT_LOG: "BISECT_LOG",
|
|
483
|
-
REBASE_MERGE: "rebase-merge",
|
|
484
|
-
REBASE_APPLY: "rebase-apply"
|
|
485
|
-
};
|
|
486
|
-
var PATH_CONSTANTS = {
|
|
487
|
-
GIT_DIR: ".git",
|
|
488
|
-
README: "README"
|
|
489
|
-
};
|
|
490
|
-
var METADATA_CONSTANTS = {
|
|
491
|
-
MAX_HISTORY_ENTRIES: 10
|
|
492
|
-
};
|
|
493
|
-
|
|
494
|
-
// src/services/worktree-metadata.service.ts
|
|
495
1484
|
var WorktreeMetadataService = class {
|
|
496
1485
|
/**
|
|
497
1486
|
* Gets the internal worktree directory name from a worktree path.
|
|
@@ -502,7 +1491,12 @@ var WorktreeMetadataService = class {
|
|
|
502
1491
|
return path2.basename(worktreePath);
|
|
503
1492
|
}
|
|
504
1493
|
async getMetadataPath(bareRepoPath, worktreeName) {
|
|
505
|
-
return path2.join(
|
|
1494
|
+
return path2.join(
|
|
1495
|
+
bareRepoPath,
|
|
1496
|
+
METADATA_CONSTANTS.WORKTREE_METADATA_PATH,
|
|
1497
|
+
worktreeName,
|
|
1498
|
+
METADATA_CONSTANTS.METADATA_FILENAME
|
|
1499
|
+
);
|
|
506
1500
|
}
|
|
507
1501
|
async getMetadataPathFromWorktreePath(bareRepoPath, worktreePath) {
|
|
508
1502
|
const worktreeDirName = this.getWorktreeDirectoryName(worktreePath);
|
|
@@ -537,7 +1531,12 @@ var WorktreeMetadataService = class {
|
|
|
537
1531
|
const branchName = path2.basename(worktreePath);
|
|
538
1532
|
const parentDir = path2.dirname(worktreePath);
|
|
539
1533
|
const possibleBranchWithSlash = path2.join(path2.basename(parentDir), branchName);
|
|
540
|
-
const oldPath = path2.join(
|
|
1534
|
+
const oldPath = path2.join(
|
|
1535
|
+
bareRepoPath,
|
|
1536
|
+
METADATA_CONSTANTS.WORKTREE_METADATA_PATH,
|
|
1537
|
+
possibleBranchWithSlash,
|
|
1538
|
+
METADATA_CONSTANTS.METADATA_FILENAME
|
|
1539
|
+
);
|
|
541
1540
|
const content = await fs2.readFile(oldPath, "utf-8");
|
|
542
1541
|
const metadata = JSON.parse(content);
|
|
543
1542
|
if (!await this.validateMetadata(metadata)) {
|
|
@@ -620,7 +1619,7 @@ var WorktreeMetadataService = class {
|
|
|
620
1619
|
}
|
|
621
1620
|
} catch {
|
|
622
1621
|
}
|
|
623
|
-
const parentBranch = defaultBranch ||
|
|
1622
|
+
const parentBranch = defaultBranch || GIT_CONSTANTS.DEFAULT_BRANCH;
|
|
624
1623
|
await this.createInitialMetadataFromPath(
|
|
625
1624
|
bareRepoPath,
|
|
626
1625
|
worktreePath,
|
|
@@ -736,9 +1735,9 @@ var WorktreeError = class extends SyncWorktreesError {
|
|
|
736
1735
|
}
|
|
737
1736
|
};
|
|
738
1737
|
var WorktreeNotCleanError = class extends WorktreeError {
|
|
739
|
-
constructor(
|
|
740
|
-
super(`Worktree at '${
|
|
741
|
-
this.path =
|
|
1738
|
+
constructor(path11, reasons) {
|
|
1739
|
+
super(`Worktree at '${path11}' is not clean: ${reasons.join(", ")}`, "NOT_CLEAN");
|
|
1740
|
+
this.path = path11;
|
|
742
1741
|
this.reasons = reasons;
|
|
743
1742
|
}
|
|
744
1743
|
};
|
|
@@ -853,7 +1852,7 @@ var WorktreeStatusService = class {
|
|
|
853
1852
|
const lines = result.split("\n").filter((line) => line.trim());
|
|
854
1853
|
for (const line of lines) {
|
|
855
1854
|
const firstChar = line.charAt(0);
|
|
856
|
-
if (firstChar ===
|
|
1855
|
+
if (firstChar === GIT_CONSTANTS.SUBMODULE_STATUS_ADDED || firstChar === GIT_CONSTANTS.SUBMODULE_STATUS_REMOVED) {
|
|
857
1856
|
return true;
|
|
858
1857
|
}
|
|
859
1858
|
}
|
|
@@ -951,7 +1950,7 @@ var WorktreeStatusService = class {
|
|
|
951
1950
|
const modifiedSubmodules = [];
|
|
952
1951
|
for (const line of lines) {
|
|
953
1952
|
const firstChar = line.charAt(0);
|
|
954
|
-
if (firstChar ===
|
|
1953
|
+
if (firstChar === GIT_CONSTANTS.SUBMODULE_STATUS_ADDED || firstChar === GIT_CONSTANTS.SUBMODULE_STATUS_REMOVED) {
|
|
955
1954
|
const match = line.match(/^[+-]\s*(\S+)/);
|
|
956
1955
|
if (match) {
|
|
957
1956
|
modifiedSubmodules.push(match[1]);
|
|
@@ -1000,7 +1999,7 @@ var WorktreeStatusService = class {
|
|
|
1000
1999
|
return files.filter((f) => !ignoredFiles.has(f));
|
|
1001
2000
|
} catch (error) {
|
|
1002
2001
|
const errorMessage = getErrorMessage(error);
|
|
1003
|
-
if (errorMessage.includes(
|
|
2002
|
+
if (errorMessage.includes(GIT_CONSTANTS.GIT_CHECK_IGNORE_NO_MATCH)) {
|
|
1004
2003
|
return files;
|
|
1005
2004
|
}
|
|
1006
2005
|
console.warn(`Warning: Could not check gitignore status for files in ${worktreePath}: ${errorMessage}`);
|
|
@@ -1021,7 +2020,7 @@ var WorktreeStatusService = class {
|
|
|
1021
2020
|
const stat4 = await fs3.stat(gitPath);
|
|
1022
2021
|
if (stat4.isFile()) {
|
|
1023
2022
|
const content = await fs3.readFile(gitPath, "utf-8");
|
|
1024
|
-
const gitdirMatch = content.match(
|
|
2023
|
+
const gitdirMatch = content.match(new RegExp(`^${GIT_CONSTANTS.GITDIR_PREFIX}\\s*(.+)$`, "m"));
|
|
1025
2024
|
if (gitdirMatch) {
|
|
1026
2025
|
return path3.resolve(worktreePath, gitdirMatch[1].trim());
|
|
1027
2026
|
}
|
|
@@ -1043,31 +2042,37 @@ var WorktreeStatusService = class {
|
|
|
1043
2042
|
|
|
1044
2043
|
// src/services/git.service.ts
|
|
1045
2044
|
var GitService = class {
|
|
1046
|
-
constructor(config) {
|
|
2045
|
+
constructor(config, logger) {
|
|
1047
2046
|
this.config = config;
|
|
2047
|
+
this.logger = logger ?? Logger.createDefault(void 0, config.debug);
|
|
1048
2048
|
this.bareRepoPath = this.config.bareRepoDir || getDefaultBareRepoDir(this.config.repoUrl);
|
|
1049
|
-
this.mainWorktreePath = path4.join(this.config.worktreeDir,
|
|
2049
|
+
this.mainWorktreePath = path4.join(this.config.worktreeDir, GIT_CONSTANTS.DEFAULT_BRANCH);
|
|
1050
2050
|
this.metadataService = new WorktreeMetadataService();
|
|
1051
2051
|
this.statusService = new WorktreeStatusService({ skipLfs: this.config.skipLfs });
|
|
1052
2052
|
}
|
|
1053
2053
|
git = null;
|
|
1054
2054
|
bareRepoPath;
|
|
1055
2055
|
mainWorktreePath;
|
|
1056
|
-
defaultBranch =
|
|
2056
|
+
defaultBranch = GIT_CONSTANTS.DEFAULT_BRANCH;
|
|
1057
2057
|
// Will be updated after detection
|
|
1058
2058
|
metadataService;
|
|
1059
2059
|
statusService;
|
|
2060
|
+
logger;
|
|
2061
|
+
updateLogger(logger) {
|
|
2062
|
+
this.logger = logger;
|
|
2063
|
+
}
|
|
1060
2064
|
async initialize() {
|
|
1061
2065
|
const { repoUrl } = this.config;
|
|
2066
|
+
let needsClone = false;
|
|
1062
2067
|
try {
|
|
1063
2068
|
await fs4.access(path4.join(this.bareRepoPath, "HEAD"));
|
|
1064
|
-
console.log(`Bare repository at "${this.bareRepoPath}" already exists. Using it.`);
|
|
1065
2069
|
} catch {
|
|
1066
|
-
|
|
2070
|
+
needsClone = true;
|
|
2071
|
+
this.logger.info(`Cloning from "${repoUrl}" as bare repository into "${this.bareRepoPath}"...`);
|
|
1067
2072
|
await fs4.mkdir(path4.dirname(this.bareRepoPath), { recursive: true });
|
|
1068
|
-
const cloneGit = this.isLfsSkipEnabled() ? simpleGit3().env({ GIT_LFS_SKIP_SMUDGE: "1" }) : simpleGit3();
|
|
2073
|
+
const cloneGit = this.isLfsSkipEnabled() ? simpleGit3().env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3();
|
|
1069
2074
|
await cloneGit.clone(repoUrl, this.bareRepoPath, ["--bare"]);
|
|
1070
|
-
|
|
2075
|
+
this.logger.info("\u2705 Clone successful.");
|
|
1071
2076
|
}
|
|
1072
2077
|
const bareGit = simpleGit3(this.bareRepoPath);
|
|
1073
2078
|
try {
|
|
@@ -1079,11 +2084,12 @@ var GitService = class {
|
|
|
1079
2084
|
} catch {
|
|
1080
2085
|
await bareGit.addConfig("remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*");
|
|
1081
2086
|
}
|
|
1082
|
-
|
|
1083
|
-
|
|
2087
|
+
if (needsClone) {
|
|
2088
|
+
this.logger.info("Fetching remote branches...");
|
|
2089
|
+
await bareGit.fetch(["--all"]);
|
|
2090
|
+
}
|
|
1084
2091
|
this.defaultBranch = await this.detectDefaultBranch(bareGit);
|
|
1085
2092
|
this.mainWorktreePath = path4.join(this.config.worktreeDir, this.defaultBranch);
|
|
1086
|
-
console.log(`Detected default branch: ${this.defaultBranch}`);
|
|
1087
2093
|
let needsMainWorktree = true;
|
|
1088
2094
|
try {
|
|
1089
2095
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
@@ -1091,7 +2097,7 @@ var GitService = class {
|
|
|
1091
2097
|
} catch {
|
|
1092
2098
|
}
|
|
1093
2099
|
if (needsMainWorktree) {
|
|
1094
|
-
|
|
2100
|
+
this.logger.info(`Creating ${this.defaultBranch} worktree at "${this.mainWorktreePath}"...`);
|
|
1095
2101
|
await fs4.mkdir(this.config.worktreeDir, { recursive: true });
|
|
1096
2102
|
const absoluteWorktreePath = path4.resolve(this.mainWorktreePath);
|
|
1097
2103
|
try {
|
|
@@ -1099,7 +2105,7 @@ var GitService = class {
|
|
|
1099
2105
|
const defaultBranchExists = branches.all.includes(this.defaultBranch);
|
|
1100
2106
|
if (defaultBranchExists) {
|
|
1101
2107
|
await bareGit.raw(["worktree", "add", absoluteWorktreePath, this.defaultBranch]);
|
|
1102
|
-
const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(absoluteWorktreePath).env({ GIT_LFS_SKIP_SMUDGE: "1" }) : simpleGit3(absoluteWorktreePath);
|
|
2108
|
+
const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(absoluteWorktreePath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(absoluteWorktreePath);
|
|
1103
2109
|
await worktreeGit.branch(["--set-upstream-to", `origin/${this.defaultBranch}`, this.defaultBranch]);
|
|
1104
2110
|
} else {
|
|
1105
2111
|
await bareGit.raw([
|
|
@@ -1115,17 +2121,17 @@ var GitService = class {
|
|
|
1115
2121
|
} catch (error) {
|
|
1116
2122
|
const errorMessage = getErrorMessage(error);
|
|
1117
2123
|
if (errorMessage.includes("already exists")) {
|
|
1118
|
-
|
|
2124
|
+
this.logger.info(
|
|
1119
2125
|
`${this.defaultBranch} worktree directory already exists at '${absoluteWorktreePath}', skipping creation.`
|
|
1120
2126
|
);
|
|
1121
2127
|
} else {
|
|
1122
|
-
|
|
2128
|
+
this.logger.warn(`Failed to create ${this.defaultBranch} worktree with tracking, using simple add: ${error}`);
|
|
1123
2129
|
try {
|
|
1124
2130
|
await bareGit.raw(["worktree", "add", absoluteWorktreePath, this.defaultBranch]);
|
|
1125
2131
|
} catch (fallbackError) {
|
|
1126
2132
|
const fallbackErrorMessage = getErrorMessage(fallbackError);
|
|
1127
2133
|
if (fallbackErrorMessage.includes("already exists")) {
|
|
1128
|
-
|
|
2134
|
+
this.logger.info(
|
|
1129
2135
|
`${this.defaultBranch} worktree directory already exists at '${absoluteWorktreePath}', skipping creation.`
|
|
1130
2136
|
);
|
|
1131
2137
|
} else {
|
|
@@ -1139,8 +2145,8 @@ var GitService = class {
|
|
|
1139
2145
|
(w) => path4.resolve(w.path) === path4.resolve(this.mainWorktreePath)
|
|
1140
2146
|
);
|
|
1141
2147
|
if (!mainWorktreeRegistered) {
|
|
1142
|
-
if (process.env.NODE_ENV !==
|
|
1143
|
-
|
|
2148
|
+
if (process.env.NODE_ENV !== ENV_CONSTANTS.NODE_ENV_TEST) {
|
|
2149
|
+
this.logger.warn(`Main worktree was created but not found in worktree list. This may cause issues.`);
|
|
1144
2150
|
}
|
|
1145
2151
|
}
|
|
1146
2152
|
}
|
|
@@ -1153,14 +2159,17 @@ var GitService = class {
|
|
|
1153
2159
|
}
|
|
1154
2160
|
return this.git;
|
|
1155
2161
|
}
|
|
2162
|
+
isInitialized() {
|
|
2163
|
+
return this.git !== null;
|
|
2164
|
+
}
|
|
1156
2165
|
getDefaultBranch() {
|
|
1157
2166
|
return this.defaultBranch;
|
|
1158
2167
|
}
|
|
1159
2168
|
async fetchAll() {
|
|
1160
2169
|
const git = this.getGit();
|
|
1161
|
-
|
|
2170
|
+
this.logger.info("Fetching latest data from remote...");
|
|
1162
2171
|
if (this.isLfsSkipEnabled()) {
|
|
1163
|
-
await git.env({ GIT_LFS_SKIP_SMUDGE: "1" }).fetch(["--all", "--prune"]);
|
|
2172
|
+
await git.env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }).fetch(["--all", "--prune"]);
|
|
1164
2173
|
} else {
|
|
1165
2174
|
await git.fetch(["--all", "--prune"]);
|
|
1166
2175
|
}
|
|
@@ -1168,7 +2177,7 @@ var GitService = class {
|
|
|
1168
2177
|
async fetchBranch(branchName) {
|
|
1169
2178
|
const git = this.getGit();
|
|
1170
2179
|
if (this.isLfsSkipEnabled()) {
|
|
1171
|
-
await git.env({ GIT_LFS_SKIP_SMUDGE: "1" }).fetch(["origin", branchName, "--prune"]);
|
|
2180
|
+
await git.env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }).fetch(["origin", branchName, "--prune"]);
|
|
1172
2181
|
} else {
|
|
1173
2182
|
await git.fetch(["origin", branchName, "--prune"]);
|
|
1174
2183
|
}
|
|
@@ -1211,7 +2220,7 @@ var GitService = class {
|
|
|
1211
2220
|
return;
|
|
1212
2221
|
}
|
|
1213
2222
|
if (this.config.debug) {
|
|
1214
|
-
|
|
2223
|
+
this.logger.info(` - Verifying ${lfsFileList.length} LFS files are downloaded...`);
|
|
1215
2224
|
}
|
|
1216
2225
|
const sampleSize = Math.min(5, lfsFileList.length);
|
|
1217
2226
|
const samplesToCheck = [];
|
|
@@ -1233,7 +2242,7 @@ var GitService = class {
|
|
|
1233
2242
|
const buffer = Buffer.alloc(200);
|
|
1234
2243
|
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
1235
2244
|
const header = buffer.subarray(0, bytesRead).toString("utf8");
|
|
1236
|
-
if (header.startsWith(
|
|
2245
|
+
if (header.startsWith(GIT_CONSTANTS.LFS_HEADER)) {
|
|
1237
2246
|
allDownloaded = false;
|
|
1238
2247
|
notDownloaded.push(file);
|
|
1239
2248
|
}
|
|
@@ -1247,7 +2256,7 @@ var GitService = class {
|
|
|
1247
2256
|
}
|
|
1248
2257
|
if (allDownloaded) {
|
|
1249
2258
|
if (this.config.debug) {
|
|
1250
|
-
|
|
2259
|
+
this.logger.info(` - \u2705 LFS files verified (${samplesToCheck.length} samples checked)`);
|
|
1251
2260
|
}
|
|
1252
2261
|
return;
|
|
1253
2262
|
}
|
|
@@ -1256,16 +2265,16 @@ var GitService = class {
|
|
|
1256
2265
|
await new Promise((resolve6) => setTimeout(resolve6, retryDelay));
|
|
1257
2266
|
}
|
|
1258
2267
|
}
|
|
1259
|
-
|
|
2268
|
+
this.logger.warn(
|
|
1260
2269
|
` - \u26A0\uFE0F Warning: Some LFS files may not be fully downloaded after ${maxRetries} seconds. This might cause issues if tools access the worktree immediately.`
|
|
1261
2270
|
);
|
|
1262
2271
|
} catch (error) {
|
|
1263
|
-
|
|
2272
|
+
this.logger.warn(` - \u26A0\uFE0F Warning: Could not verify LFS files for '${branchName}': ${error}`);
|
|
1264
2273
|
}
|
|
1265
2274
|
}
|
|
1266
2275
|
async createWorktreeMetadata(bareGit, worktreePath, branchName) {
|
|
1267
2276
|
try {
|
|
1268
|
-
const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(worktreePath).env({ GIT_LFS_SKIP_SMUDGE: "1" }) : simpleGit3(worktreePath);
|
|
2277
|
+
const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(worktreePath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(worktreePath);
|
|
1269
2278
|
const currentCommit = await worktreeGit.revparse(["HEAD"]);
|
|
1270
2279
|
const parentCommit = await bareGit.revparse([this.defaultBranch]);
|
|
1271
2280
|
await this.metadataService.createInitialMetadataFromPath(
|
|
@@ -1277,12 +2286,12 @@ var GitService = class {
|
|
|
1277
2286
|
parentCommit.trim()
|
|
1278
2287
|
);
|
|
1279
2288
|
} catch (metadataError) {
|
|
1280
|
-
|
|
2289
|
+
this.logger.error(` - \u274C Failed to create metadata for '${branchName}': ${metadataError}`);
|
|
1281
2290
|
throw new Error(`Metadata creation failed for ${branchName}. This worktree cannot be auto-managed.`);
|
|
1282
2291
|
}
|
|
1283
2292
|
}
|
|
1284
2293
|
async addWorktree(branchName, worktreePath) {
|
|
1285
|
-
const bareGit = this.isLfsSkipEnabled() ? simpleGit3(this.bareRepoPath).env({ GIT_LFS_SKIP_SMUDGE: "1" }) : simpleGit3(this.bareRepoPath);
|
|
2294
|
+
const bareGit = this.isLfsSkipEnabled() ? simpleGit3(this.bareRepoPath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(this.bareRepoPath);
|
|
1286
2295
|
const absoluteWorktreePath = path4.resolve(worktreePath);
|
|
1287
2296
|
await fs4.mkdir(path4.dirname(absoluteWorktreePath), { recursive: true });
|
|
1288
2297
|
try {
|
|
@@ -1290,10 +2299,10 @@ var GitService = class {
|
|
|
1290
2299
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
1291
2300
|
const isValidWorktree = worktrees.some((w) => path4.resolve(w.path) === absoluteWorktreePath);
|
|
1292
2301
|
if (isValidWorktree) {
|
|
1293
|
-
|
|
2302
|
+
this.logger.info(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
|
|
1294
2303
|
return;
|
|
1295
2304
|
} else {
|
|
1296
|
-
|
|
2305
|
+
this.logger.info(` - Cleaning up orphaned directory at '${absoluteWorktreePath}'`);
|
|
1297
2306
|
await fs4.rm(absoluteWorktreePath, { recursive: true, force: true });
|
|
1298
2307
|
}
|
|
1299
2308
|
} catch {
|
|
@@ -1303,7 +2312,7 @@ var GitService = class {
|
|
|
1303
2312
|
const localBranchExists = branches.all.includes(branchName);
|
|
1304
2313
|
if (localBranchExists || branchName.includes("/")) {
|
|
1305
2314
|
await bareGit.raw(["worktree", "add", absoluteWorktreePath, branchName]);
|
|
1306
|
-
const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(absoluteWorktreePath).env({ GIT_LFS_SKIP_SMUDGE: "1" }) : simpleGit3(absoluteWorktreePath);
|
|
2315
|
+
const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(absoluteWorktreePath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(absoluteWorktreePath);
|
|
1307
2316
|
await worktreeGit.branch(["--set-upstream-to", `origin/${branchName}`, branchName]);
|
|
1308
2317
|
} else {
|
|
1309
2318
|
await bareGit.raw([
|
|
@@ -1316,7 +2325,7 @@ var GitService = class {
|
|
|
1316
2325
|
`origin/${branchName}`
|
|
1317
2326
|
]);
|
|
1318
2327
|
}
|
|
1319
|
-
|
|
2328
|
+
this.logger.info(` - Created worktree for '${branchName}' with tracking to origin/${branchName}`);
|
|
1320
2329
|
if (!this.isLfsSkipEnabled()) {
|
|
1321
2330
|
await this.verifyLfsFilesDownloaded(absoluteWorktreePath, branchName);
|
|
1322
2331
|
}
|
|
@@ -1327,7 +2336,13 @@ var GitService = class {
|
|
|
1327
2336
|
throw error;
|
|
1328
2337
|
}
|
|
1329
2338
|
if (errorMessage.includes("already registered worktree")) {
|
|
1330
|
-
|
|
2339
|
+
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
2340
|
+
const existingWorktree = worktrees.find((w) => path4.resolve(w.path) === absoluteWorktreePath);
|
|
2341
|
+
if (existingWorktree && !existingWorktree.isPrunable) {
|
|
2342
|
+
this.logger.info(` - Worktree for '${branchName}' was created by concurrent operation`);
|
|
2343
|
+
return;
|
|
2344
|
+
}
|
|
2345
|
+
this.logger.warn(` - Worktree already registered but missing. Pruning and retrying...`);
|
|
1331
2346
|
await bareGit.raw(["worktree", "prune"]);
|
|
1332
2347
|
try {
|
|
1333
2348
|
await fs4.rm(absoluteWorktreePath, { recursive: true, force: true });
|
|
@@ -1343,53 +2358,66 @@ var GitService = class {
|
|
|
1343
2358
|
absoluteWorktreePath,
|
|
1344
2359
|
`origin/${branchName}`
|
|
1345
2360
|
]);
|
|
1346
|
-
|
|
2361
|
+
this.logger.info(` - Created worktree for '${branchName}' after pruning`);
|
|
1347
2362
|
if (!this.isLfsSkipEnabled()) {
|
|
1348
2363
|
await this.verifyLfsFilesDownloaded(absoluteWorktreePath, branchName);
|
|
1349
2364
|
}
|
|
1350
2365
|
await this.createWorktreeMetadata(bareGit, absoluteWorktreePath, branchName);
|
|
1351
2366
|
return;
|
|
1352
2367
|
} catch (retryError) {
|
|
1353
|
-
|
|
2368
|
+
this.logger.error(` - Failed to create worktree after pruning: ${retryError}`);
|
|
1354
2369
|
throw retryError;
|
|
1355
2370
|
}
|
|
1356
2371
|
}
|
|
1357
|
-
|
|
2372
|
+
this.logger.warn(` - Failed to create worktree with tracking, falling back to simple add: ${error}`);
|
|
1358
2373
|
try {
|
|
1359
2374
|
await fs4.access(absoluteWorktreePath);
|
|
1360
2375
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
1361
2376
|
const isValidWorktree = worktrees.some((w) => path4.resolve(w.path) === absoluteWorktreePath);
|
|
1362
2377
|
if (isValidWorktree) {
|
|
1363
|
-
|
|
2378
|
+
this.logger.info(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
|
|
1364
2379
|
return;
|
|
1365
2380
|
} else {
|
|
1366
|
-
|
|
2381
|
+
this.logger.info(` - Cleaning up orphaned directory at '${absoluteWorktreePath}' before fallback attempt`);
|
|
1367
2382
|
await fs4.rm(absoluteWorktreePath, { recursive: true, force: true });
|
|
1368
2383
|
}
|
|
1369
2384
|
} catch {
|
|
1370
2385
|
}
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
2386
|
+
try {
|
|
2387
|
+
await bareGit.raw(["worktree", "add", absoluteWorktreePath, branchName]);
|
|
2388
|
+
this.logger.info(` - Created worktree for '${branchName}' (without tracking)`);
|
|
2389
|
+
if (!this.isLfsSkipEnabled()) {
|
|
2390
|
+
await this.verifyLfsFilesDownloaded(absoluteWorktreePath, branchName);
|
|
2391
|
+
}
|
|
2392
|
+
await this.createWorktreeMetadata(bareGit, absoluteWorktreePath, branchName);
|
|
2393
|
+
} catch (fallbackError) {
|
|
2394
|
+
const fallbackErrorMessage = getErrorMessage(fallbackError);
|
|
2395
|
+
if (fallbackErrorMessage.includes("already registered worktree")) {
|
|
2396
|
+
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
2397
|
+
const existingWorktree = worktrees.find((w) => path4.resolve(w.path) === absoluteWorktreePath);
|
|
2398
|
+
if (existingWorktree && !existingWorktree.isPrunable) {
|
|
2399
|
+
this.logger.info(` - Worktree for '${branchName}' was created by concurrent operation during fallback`);
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
throw fallbackError;
|
|
1375
2404
|
}
|
|
1376
|
-
await this.createWorktreeMetadata(bareGit, absoluteWorktreePath, branchName);
|
|
1377
2405
|
}
|
|
1378
2406
|
}
|
|
1379
2407
|
async removeWorktree(worktreePath) {
|
|
1380
2408
|
const bareGit = simpleGit3(this.bareRepoPath);
|
|
1381
2409
|
await bareGit.raw(["worktree", "remove", worktreePath, "--force"]);
|
|
1382
|
-
|
|
2410
|
+
this.logger.info(` - \u2705 Safely removed stale worktree at '${worktreePath}'.`);
|
|
1383
2411
|
try {
|
|
1384
2412
|
await this.metadataService.deleteMetadataFromPath(this.bareRepoPath, worktreePath);
|
|
1385
2413
|
} catch (metadataError) {
|
|
1386
|
-
|
|
2414
|
+
this.logger.warn(`Failed to delete metadata for worktree: ${metadataError}`);
|
|
1387
2415
|
}
|
|
1388
2416
|
}
|
|
1389
2417
|
async pruneWorktrees() {
|
|
1390
2418
|
const bareGit = simpleGit3(this.bareRepoPath);
|
|
1391
2419
|
await bareGit.raw(["worktree", "prune"]);
|
|
1392
|
-
|
|
2420
|
+
this.logger.info("Pruned worktree metadata.");
|
|
1393
2421
|
}
|
|
1394
2422
|
async checkWorktreeStatus(worktreePath) {
|
|
1395
2423
|
const worktreeGit = simpleGit3(worktreePath);
|
|
@@ -1428,7 +2456,7 @@ var GitService = class {
|
|
|
1428
2456
|
const unpushedCount = parseInt(result.trim(), 10);
|
|
1429
2457
|
return unpushedCount > 0;
|
|
1430
2458
|
} catch (error) {
|
|
1431
|
-
|
|
2459
|
+
this.logger.error(`Error checking unpushed commits: ${error}`);
|
|
1432
2460
|
return false;
|
|
1433
2461
|
}
|
|
1434
2462
|
}
|
|
@@ -1466,7 +2494,7 @@ var GitService = class {
|
|
|
1466
2494
|
}
|
|
1467
2495
|
return false;
|
|
1468
2496
|
}
|
|
1469
|
-
|
|
2497
|
+
this.logger.error(
|
|
1470
2498
|
`Unexpected error checking upstream status for ${worktreePath}. This might indicate a real issue rather than a missing upstream. Error: ${errorMessage}`
|
|
1471
2499
|
);
|
|
1472
2500
|
return false;
|
|
@@ -1478,7 +2506,7 @@ var GitService = class {
|
|
|
1478
2506
|
const stashList = await worktreeGit.stashList();
|
|
1479
2507
|
return stashList.total > 0;
|
|
1480
2508
|
} catch (error) {
|
|
1481
|
-
|
|
2509
|
+
this.logger.error(`Error checking stash: ${error}`);
|
|
1482
2510
|
return true;
|
|
1483
2511
|
}
|
|
1484
2512
|
}
|
|
@@ -1544,7 +2572,7 @@ var GitService = class {
|
|
|
1544
2572
|
} catch {
|
|
1545
2573
|
try {
|
|
1546
2574
|
const remoteBranches = await bareGit.branch(["-r"]);
|
|
1547
|
-
const commonDefaults =
|
|
2575
|
+
const commonDefaults = GIT_CONSTANTS.COMMON_DEFAULT_BRANCHES;
|
|
1548
2576
|
for (const defaultName of commonDefaults) {
|
|
1549
2577
|
if (remoteBranches.all.some((branch) => branch === `origin/${defaultName}`)) {
|
|
1550
2578
|
return defaultName;
|
|
@@ -1554,10 +2582,10 @@ var GitService = class {
|
|
|
1554
2582
|
}
|
|
1555
2583
|
}
|
|
1556
2584
|
}
|
|
1557
|
-
return
|
|
2585
|
+
return GIT_CONSTANTS.DEFAULT_BRANCH;
|
|
1558
2586
|
}
|
|
1559
2587
|
isLfsSkipEnabled() {
|
|
1560
|
-
return this.config.skipLfs || process.env.GIT_LFS_SKIP_SMUDGE === "1";
|
|
2588
|
+
return this.config.skipLfs || process.env[ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE] === "1";
|
|
1561
2589
|
}
|
|
1562
2590
|
async getWorktrees() {
|
|
1563
2591
|
const bareGit = simpleGit3(this.bareRepoPath);
|
|
@@ -1579,7 +2607,7 @@ var GitService = class {
|
|
|
1579
2607
|
}
|
|
1580
2608
|
}
|
|
1581
2609
|
async updateWorktree(worktreePath) {
|
|
1582
|
-
const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(worktreePath).env({ GIT_LFS_SKIP_SMUDGE: "1" }) : simpleGit3(worktreePath);
|
|
2610
|
+
const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(worktreePath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(worktreePath);
|
|
1583
2611
|
const branchSummary = await worktreeGit.branch();
|
|
1584
2612
|
const currentBranch = branchSummary.current;
|
|
1585
2613
|
await worktreeGit.merge([`origin/${currentBranch}`, "--ff-only"]);
|
|
@@ -1597,14 +2625,14 @@ var GitService = class {
|
|
|
1597
2625
|
this.defaultBranch
|
|
1598
2626
|
);
|
|
1599
2627
|
} catch (metadataError) {
|
|
1600
|
-
|
|
2628
|
+
this.logger.warn(`Failed to update metadata for worktree: ${metadataError}`);
|
|
1601
2629
|
}
|
|
1602
2630
|
}
|
|
1603
2631
|
async hasDivergedHistory(worktreePath, expectedBranch) {
|
|
1604
2632
|
const worktreeGit = simpleGit3(worktreePath);
|
|
1605
2633
|
const branchInfo = await worktreeGit.branch();
|
|
1606
2634
|
if (branchInfo.current !== expectedBranch) {
|
|
1607
|
-
|
|
2635
|
+
this.logger.warn(`Branch mismatch in hasDivergedHistory: expected ${expectedBranch}, got ${branchInfo.current}`);
|
|
1608
2636
|
return false;
|
|
1609
2637
|
}
|
|
1610
2638
|
try {
|
|
@@ -1626,6 +2654,18 @@ var GitService = class {
|
|
|
1626
2654
|
return false;
|
|
1627
2655
|
}
|
|
1628
2656
|
}
|
|
2657
|
+
async isLocalAheadOfRemote(worktreePath, branch) {
|
|
2658
|
+
const worktreeGit = simpleGit3(worktreePath);
|
|
2659
|
+
try {
|
|
2660
|
+
const mergeBase = await worktreeGit.raw(["merge-base", "HEAD", `origin/${branch}`]);
|
|
2661
|
+
const mergeBaseSha = mergeBase.trim();
|
|
2662
|
+
const remoteSha = await worktreeGit.revparse([`origin/${branch}`]);
|
|
2663
|
+
const remoteShaTrimmed = remoteSha.trim();
|
|
2664
|
+
return mergeBaseSha === remoteShaTrimmed;
|
|
2665
|
+
} catch {
|
|
2666
|
+
return false;
|
|
2667
|
+
}
|
|
2668
|
+
}
|
|
1629
2669
|
async compareTreeContent(worktreePath, branch) {
|
|
1630
2670
|
const worktreeGit = simpleGit3(worktreePath);
|
|
1631
2671
|
try {
|
|
@@ -1633,12 +2673,12 @@ var GitService = class {
|
|
|
1633
2673
|
const remoteTree = await worktreeGit.raw(["rev-parse", `origin/${branch}^{tree}`]);
|
|
1634
2674
|
return localTree.trim() === remoteTree.trim();
|
|
1635
2675
|
} catch (error) {
|
|
1636
|
-
|
|
2676
|
+
this.logger.error(`Error comparing tree content: ${error}`);
|
|
1637
2677
|
return false;
|
|
1638
2678
|
}
|
|
1639
2679
|
}
|
|
1640
2680
|
async resetToUpstream(worktreePath, branch) {
|
|
1641
|
-
const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(worktreePath).env({ GIT_LFS_SKIP_SMUDGE: "1" }) : simpleGit3(worktreePath);
|
|
2681
|
+
const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(worktreePath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(worktreePath);
|
|
1642
2682
|
await worktreeGit.reset(["--hard", `origin/${branch}`]);
|
|
1643
2683
|
try {
|
|
1644
2684
|
const currentCommit = await worktreeGit.revparse(["HEAD"]);
|
|
@@ -1650,7 +2690,7 @@ var GitService = class {
|
|
|
1650
2690
|
this.defaultBranch
|
|
1651
2691
|
);
|
|
1652
2692
|
} catch (metadataError) {
|
|
1653
|
-
|
|
2693
|
+
this.logger.warn(`Failed to update metadata after reset: ${metadataError}`);
|
|
1654
2694
|
}
|
|
1655
2695
|
}
|
|
1656
2696
|
async getCurrentCommit(worktreePath) {
|
|
@@ -1663,6 +2703,29 @@ var GitService = class {
|
|
|
1663
2703
|
const commit = await git.revparse([ref]);
|
|
1664
2704
|
return commit.trim();
|
|
1665
2705
|
}
|
|
2706
|
+
async branchExists(branchName) {
|
|
2707
|
+
const bareGit = simpleGit3(this.bareRepoPath);
|
|
2708
|
+
const localBranches = await bareGit.branch();
|
|
2709
|
+
const local = localBranches.all.includes(branchName);
|
|
2710
|
+
const remoteBranches = await bareGit.branch(["-r"]);
|
|
2711
|
+
const remote = remoteBranches.all.includes(`origin/${branchName}`);
|
|
2712
|
+
return { local, remote };
|
|
2713
|
+
}
|
|
2714
|
+
async getLocalBranches() {
|
|
2715
|
+
const bareGit = simpleGit3(this.bareRepoPath);
|
|
2716
|
+
const branches = await bareGit.branch();
|
|
2717
|
+
return branches.all;
|
|
2718
|
+
}
|
|
2719
|
+
async createBranch(branchName, baseBranch) {
|
|
2720
|
+
const bareGit = simpleGit3(this.bareRepoPath);
|
|
2721
|
+
await bareGit.raw(["branch", branchName, `origin/${baseBranch}`]);
|
|
2722
|
+
this.logger.info(`Created branch '${branchName}' from '${baseBranch}'`);
|
|
2723
|
+
}
|
|
2724
|
+
async pushBranch(branchName) {
|
|
2725
|
+
const bareGit = simpleGit3(this.bareRepoPath);
|
|
2726
|
+
await bareGit.push(["origin", `${branchName}:${branchName}`, "-u"]);
|
|
2727
|
+
this.logger.info(`Pushed branch '${branchName}' to remote`);
|
|
2728
|
+
}
|
|
1666
2729
|
async getWorktreeMetadata(worktreePath) {
|
|
1667
2730
|
return this.metadataService.loadMetadataFromPath(this.bareRepoPath, worktreePath);
|
|
1668
2731
|
}
|
|
@@ -1678,17 +2741,27 @@ var GitService = class {
|
|
|
1678
2741
|
currentWorktree.branch = line.substring(7).replace("refs/heads/", "");
|
|
1679
2742
|
} else if (line === "detached") {
|
|
1680
2743
|
currentWorktree.detached = true;
|
|
2744
|
+
} else if (line === "prunable") {
|
|
2745
|
+
currentWorktree.prunable = true;
|
|
1681
2746
|
} else if (line.trim() === "") {
|
|
1682
2747
|
if (currentWorktree.path) {
|
|
1683
2748
|
if (currentWorktree.branch && !currentWorktree.detached) {
|
|
1684
|
-
worktrees.push({
|
|
2749
|
+
worktrees.push({
|
|
2750
|
+
path: currentWorktree.path,
|
|
2751
|
+
branch: currentWorktree.branch,
|
|
2752
|
+
isPrunable: currentWorktree.prunable || false
|
|
2753
|
+
});
|
|
1685
2754
|
}
|
|
1686
2755
|
}
|
|
1687
2756
|
currentWorktree = {};
|
|
1688
2757
|
}
|
|
1689
2758
|
}
|
|
1690
2759
|
if (currentWorktree.path && currentWorktree.branch && !currentWorktree.detached) {
|
|
1691
|
-
worktrees.push({
|
|
2760
|
+
worktrees.push({
|
|
2761
|
+
path: currentWorktree.path,
|
|
2762
|
+
branch: currentWorktree.branch,
|
|
2763
|
+
isPrunable: currentWorktree.prunable || false
|
|
2764
|
+
});
|
|
1692
2765
|
}
|
|
1693
2766
|
return worktrees;
|
|
1694
2767
|
}
|
|
@@ -1698,23 +2771,37 @@ var GitService = class {
|
|
|
1698
2771
|
var WorktreeSyncService = class {
|
|
1699
2772
|
constructor(config) {
|
|
1700
2773
|
this.config = config;
|
|
1701
|
-
this.
|
|
2774
|
+
this.logger = config.logger ?? Logger.createDefault(void 0, config.debug);
|
|
2775
|
+
this.gitService = new GitService(config, this.logger);
|
|
1702
2776
|
}
|
|
1703
2777
|
gitService;
|
|
2778
|
+
logger;
|
|
1704
2779
|
syncInProgress = false;
|
|
1705
2780
|
async initialize() {
|
|
1706
2781
|
await this.gitService.initialize();
|
|
1707
2782
|
}
|
|
2783
|
+
isInitialized() {
|
|
2784
|
+
return this.gitService.isInitialized();
|
|
2785
|
+
}
|
|
1708
2786
|
isSyncInProgress() {
|
|
1709
2787
|
return this.syncInProgress;
|
|
1710
2788
|
}
|
|
2789
|
+
getGitService() {
|
|
2790
|
+
return this.gitService;
|
|
2791
|
+
}
|
|
2792
|
+
updateLogger(logger) {
|
|
2793
|
+
this.logger = logger;
|
|
2794
|
+
this.gitService.updateLogger(logger);
|
|
2795
|
+
}
|
|
1711
2796
|
async sync() {
|
|
1712
2797
|
if (this.syncInProgress) {
|
|
1713
|
-
|
|
2798
|
+
this.logger.warn("\u26A0\uFE0F Sync already in progress, skipping...");
|
|
1714
2799
|
return;
|
|
1715
2800
|
}
|
|
1716
2801
|
this.syncInProgress = true;
|
|
1717
|
-
|
|
2802
|
+
this.logger.info(`[${(/* @__PURE__ */ new Date()).toISOString()}] Starting worktree synchronization...`);
|
|
2803
|
+
const totalTimer = new Timer();
|
|
2804
|
+
const phaseTimer = new PhaseTimer();
|
|
1718
2805
|
let lfsSkipEnabled = false;
|
|
1719
2806
|
const retryOptions = {
|
|
1720
2807
|
maxAttempts: this.config.retry?.maxAttempts ?? 3,
|
|
@@ -1724,18 +2811,18 @@ var WorktreeSyncService = class {
|
|
|
1724
2811
|
backoffMultiplier: this.config.retry?.backoffMultiplier ?? 2,
|
|
1725
2812
|
onRetry: (error, attempt, context) => {
|
|
1726
2813
|
const errorMessage = getErrorMessage(error);
|
|
1727
|
-
|
|
2814
|
+
this.logger.info(`
|
|
1728
2815
|
\u26A0\uFE0F Sync attempt ${attempt} failed: ${errorMessage}`);
|
|
1729
2816
|
if (context?.isLfsError && !this.config.skipLfs) {
|
|
1730
|
-
|
|
2817
|
+
this.logger.info(`\u{1F504} LFS error detected. Will retry with LFS skipped...`);
|
|
1731
2818
|
} else {
|
|
1732
|
-
|
|
2819
|
+
this.logger.info(`\u{1F504} Retrying synchronization...
|
|
1733
2820
|
`);
|
|
1734
2821
|
}
|
|
1735
2822
|
},
|
|
1736
2823
|
lfsRetryHandler: () => {
|
|
1737
2824
|
if (!this.config.skipLfs && !lfsSkipEnabled) {
|
|
1738
|
-
|
|
2825
|
+
this.logger.info("\u26A0\uFE0F Temporarily disabling LFS downloads for this sync...");
|
|
1739
2826
|
process.env.GIT_LFS_SKIP_SMUDGE = "1";
|
|
1740
2827
|
lfsSkipEnabled = true;
|
|
1741
2828
|
}
|
|
@@ -1744,14 +2831,15 @@ var WorktreeSyncService = class {
|
|
|
1744
2831
|
try {
|
|
1745
2832
|
await retry(async () => {
|
|
1746
2833
|
await this.gitService.pruneWorktrees();
|
|
1747
|
-
|
|
2834
|
+
this.logger.info("Step 1: Fetching latest data from remote...");
|
|
2835
|
+
phaseTimer.startPhase("Phase 1: Fetch");
|
|
1748
2836
|
try {
|
|
1749
2837
|
await this.gitService.fetchAll();
|
|
1750
2838
|
} catch (fetchError) {
|
|
1751
2839
|
const errorMessage = getErrorMessage(fetchError);
|
|
1752
2840
|
if (isLfsError(errorMessage) && !lfsSkipEnabled && !this.config.skipLfs) {
|
|
1753
|
-
|
|
1754
|
-
|
|
2841
|
+
this.logger.info("\u26A0\uFE0F Fetch all failed due to LFS error. Attempting branch-by-branch fetch...");
|
|
2842
|
+
this.logger.info("\u26A0\uFE0F Temporarily disabling LFS downloads for branch-by-branch fetch...");
|
|
1755
2843
|
process.env.GIT_LFS_SKIP_SMUDGE = "1";
|
|
1756
2844
|
lfsSkipEnabled = true;
|
|
1757
2845
|
await this.fetchBranchByBranch();
|
|
@@ -1759,143 +2847,221 @@ var WorktreeSyncService = class {
|
|
|
1759
2847
|
throw fetchError;
|
|
1760
2848
|
}
|
|
1761
2849
|
}
|
|
2850
|
+
phaseTimer.endPhase();
|
|
1762
2851
|
let remoteBranches;
|
|
1763
2852
|
if (this.config.branchMaxAge) {
|
|
1764
2853
|
const branchesWithActivity = await this.gitService.getRemoteBranchesWithActivity();
|
|
1765
2854
|
const filteredBranches = filterBranchesByAge(branchesWithActivity, this.config.branchMaxAge);
|
|
1766
2855
|
remoteBranches = filteredBranches.map((b) => b.branch);
|
|
1767
|
-
|
|
1768
|
-
|
|
2856
|
+
this.logger.info(`Found ${branchesWithActivity.length} remote branches.`);
|
|
2857
|
+
this.logger.info(
|
|
1769
2858
|
`After filtering by age (${formatDuration(this.config.branchMaxAge)}): ${remoteBranches.length} branches.`
|
|
1770
2859
|
);
|
|
1771
2860
|
if (branchesWithActivity.length > remoteBranches.length) {
|
|
1772
2861
|
const excludedCount = branchesWithActivity.length - remoteBranches.length;
|
|
1773
|
-
|
|
2862
|
+
this.logger.info(` - Excluded ${excludedCount} stale branches.`);
|
|
1774
2863
|
}
|
|
1775
2864
|
} else {
|
|
1776
2865
|
remoteBranches = await this.gitService.getRemoteBranches();
|
|
1777
|
-
|
|
2866
|
+
this.logger.info(`Found ${remoteBranches.length} remote branches.`);
|
|
1778
2867
|
}
|
|
1779
2868
|
const defaultBranch = this.gitService.getDefaultBranch();
|
|
1780
2869
|
if (!remoteBranches.includes(defaultBranch)) {
|
|
1781
2870
|
remoteBranches.push(defaultBranch);
|
|
1782
|
-
|
|
2871
|
+
this.logger.info(`Ensuring default branch '${defaultBranch}' is retained.`);
|
|
1783
2872
|
}
|
|
1784
2873
|
await fs5.mkdir(this.config.worktreeDir, { recursive: true });
|
|
1785
2874
|
const worktrees = await this.gitService.getWorktrees();
|
|
1786
2875
|
const worktreeBranches = worktrees.map((w) => w.branch);
|
|
1787
|
-
|
|
2876
|
+
this.logger.info(`Found ${worktrees.length} existing Git worktrees.`);
|
|
1788
2877
|
await this.cleanupOrphanedDirectories(worktrees);
|
|
1789
|
-
await this.
|
|
1790
|
-
await this.
|
|
2878
|
+
await this.createNewWorktreesWithTiming(remoteBranches, worktreeBranches, defaultBranch, phaseTimer);
|
|
2879
|
+
await this.pruneOldWorktreesWithTiming(remoteBranches, worktreeBranches, phaseTimer);
|
|
1791
2880
|
if (this.config.updateExistingWorktrees !== false) {
|
|
1792
|
-
await this.
|
|
2881
|
+
await this.updateExistingWorktreesWithTiming(worktrees, remoteBranches, phaseTimer);
|
|
1793
2882
|
}
|
|
2883
|
+
phaseTimer.startPhase("Phase 5: Cleanup");
|
|
1794
2884
|
await this.gitService.pruneWorktrees();
|
|
1795
|
-
|
|
2885
|
+
this.logger.info("Step 5: Pruned worktree metadata.");
|
|
2886
|
+
phaseTimer.endPhase();
|
|
1796
2887
|
}, retryOptions);
|
|
1797
2888
|
} catch (error) {
|
|
1798
|
-
|
|
2889
|
+
this.logger.error("\n\u274C Error during worktree synchronization after all retry attempts:", error);
|
|
1799
2890
|
throw error;
|
|
1800
2891
|
} finally {
|
|
1801
2892
|
if (lfsSkipEnabled && !this.config.skipLfs) {
|
|
1802
2893
|
delete process.env.GIT_LFS_SKIP_SMUDGE;
|
|
1803
2894
|
}
|
|
1804
2895
|
this.syncInProgress = false;
|
|
1805
|
-
|
|
2896
|
+
this.logger.info(`[${(/* @__PURE__ */ new Date()).toISOString()}] Synchronization finished.
|
|
1806
2897
|
`);
|
|
2898
|
+
if (this.config.debug) {
|
|
2899
|
+
const totalDuration = totalTimer.stop();
|
|
2900
|
+
const phaseResults = phaseTimer.getResults();
|
|
2901
|
+
const repoName = this.config.name;
|
|
2902
|
+
this.logger.table(formatTimingTable(totalDuration, phaseResults, repoName));
|
|
2903
|
+
}
|
|
1807
2904
|
}
|
|
1808
2905
|
}
|
|
2906
|
+
async createNewWorktreesWithTiming(remoteBranches, existingWorktreeBranches, defaultBranch, phaseTimer) {
|
|
2907
|
+
const maxConcurrent = this.config.parallelism?.maxWorktreeCreation ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_CREATION;
|
|
2908
|
+
phaseTimer.startPhase("Phase 2: Create", maxConcurrent);
|
|
2909
|
+
await this.createNewWorktrees(remoteBranches, existingWorktreeBranches, defaultBranch);
|
|
2910
|
+
const newBranches = remoteBranches.filter((b) => !existingWorktreeBranches.includes(b)).filter((b) => b !== defaultBranch);
|
|
2911
|
+
phaseTimer.setPhaseCount("Phase 2: Create", newBranches.length);
|
|
2912
|
+
phaseTimer.endPhase();
|
|
2913
|
+
}
|
|
1809
2914
|
async createNewWorktrees(remoteBranches, existingWorktreeBranches, defaultBranch) {
|
|
1810
2915
|
const newBranches = remoteBranches.filter((b) => !existingWorktreeBranches.includes(b)).filter((b) => b !== defaultBranch);
|
|
1811
2916
|
if (newBranches.length > 0) {
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
2917
|
+
this.logger.info(`Step 2: Creating ${newBranches.length} new worktrees...`);
|
|
2918
|
+
const maxConcurrent = this.config.parallelism?.maxWorktreeCreation ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_CREATION;
|
|
2919
|
+
const limit = pLimit(maxConcurrent);
|
|
2920
|
+
const results = await Promise.allSettled(
|
|
2921
|
+
newBranches.map(
|
|
2922
|
+
(branchName) => limit(async () => {
|
|
2923
|
+
const worktreePath = path5.join(this.config.worktreeDir, branchName);
|
|
2924
|
+
try {
|
|
2925
|
+
await this.gitService.addWorktree(branchName, worktreePath);
|
|
2926
|
+
this.logger.info(` \u2705 Created worktree for '${branchName}'`);
|
|
2927
|
+
} catch (error) {
|
|
2928
|
+
this.logger.error(` \u274C Failed to create worktree for '${branchName}':`, getErrorMessage(error));
|
|
2929
|
+
throw error;
|
|
2930
|
+
}
|
|
2931
|
+
})
|
|
2932
|
+
)
|
|
2933
|
+
);
|
|
2934
|
+
const successCount = results.filter((r) => r.status === "fulfilled").length;
|
|
2935
|
+
this.logger.info(` Created ${successCount}/${newBranches.length} worktrees successfully`);
|
|
1817
2936
|
} else {
|
|
1818
|
-
|
|
2937
|
+
this.logger.info("Step 2: No new branches to create worktrees for.");
|
|
1819
2938
|
}
|
|
1820
2939
|
}
|
|
2940
|
+
async pruneOldWorktreesWithTiming(remoteBranches, existingWorktreeBranches, phaseTimer) {
|
|
2941
|
+
const maxConcurrent = this.config.parallelism?.maxStatusChecks ?? DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS;
|
|
2942
|
+
phaseTimer.startPhase("Phase 3: Prune", maxConcurrent);
|
|
2943
|
+
await this.pruneOldWorktrees(remoteBranches, existingWorktreeBranches);
|
|
2944
|
+
const deletedBranches = existingWorktreeBranches.filter((branch) => !remoteBranches.includes(branch));
|
|
2945
|
+
phaseTimer.setPhaseCount("Phase 3: Prune", deletedBranches.length);
|
|
2946
|
+
phaseTimer.endPhase();
|
|
2947
|
+
}
|
|
1821
2948
|
async pruneOldWorktrees(remoteBranches, existingWorktreeBranches) {
|
|
1822
2949
|
const deletedBranches = existingWorktreeBranches.filter((branch) => !remoteBranches.includes(branch));
|
|
1823
2950
|
if (deletedBranches.length > 0) {
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
2951
|
+
this.logger.info(`Step 3: Checking ${deletedBranches.length} stale worktrees to prune...`);
|
|
2952
|
+
const maxConcurrent = this.config.parallelism?.maxStatusChecks ?? DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS;
|
|
2953
|
+
const limit = pLimit(maxConcurrent);
|
|
2954
|
+
const statusResults = await Promise.allSettled(
|
|
2955
|
+
deletedBranches.map(
|
|
2956
|
+
(branchName) => limit(async () => {
|
|
2957
|
+
const worktreePath = path5.join(this.config.worktreeDir, branchName);
|
|
2958
|
+
const status = await this.gitService.getFullWorktreeStatus(worktreePath, this.config.debug);
|
|
2959
|
+
return { branchName, worktreePath, status };
|
|
2960
|
+
})
|
|
2961
|
+
)
|
|
2962
|
+
);
|
|
2963
|
+
const toRemove = [];
|
|
2964
|
+
const toSkip = [];
|
|
2965
|
+
for (const result of statusResults) {
|
|
2966
|
+
if (result.status === "fulfilled") {
|
|
2967
|
+
const { branchName, worktreePath, status } = result.value;
|
|
1829
2968
|
if (status.canRemove) {
|
|
1830
|
-
|
|
2969
|
+
toRemove.push({ branchName, worktreePath });
|
|
1831
2970
|
} else {
|
|
1832
|
-
|
|
1833
|
-
console.warn(` - \u26A0\uFE0F Cannot automatically remove '${branchName}' - upstream branch was deleted.`);
|
|
1834
|
-
console.log(` Please review manually: cd ${worktreePath} && git log`);
|
|
1835
|
-
console.log(
|
|
1836
|
-
` If changes were squash-merged, you can safely remove with: git worktree remove ${worktreePath}`
|
|
1837
|
-
);
|
|
1838
|
-
} else {
|
|
1839
|
-
console.log(` - \u26A0\uFE0F Skipping removal of '${branchName}' due to: ${status.reasons.join(", ")}.`);
|
|
1840
|
-
}
|
|
1841
|
-
if (this.config.debug && status.details) {
|
|
1842
|
-
this.logDebugDetails(branchName, status.details);
|
|
1843
|
-
}
|
|
2971
|
+
toSkip.push({ branchName, status });
|
|
1844
2972
|
}
|
|
1845
|
-
}
|
|
1846
|
-
|
|
2973
|
+
} else {
|
|
2974
|
+
this.logger.error(` - Error checking worktree:`, result.reason);
|
|
2975
|
+
}
|
|
2976
|
+
}
|
|
2977
|
+
if (toRemove.length > 0) {
|
|
2978
|
+
const removeLimit = pLimit(
|
|
2979
|
+
this.config.parallelism?.maxWorktreeRemoval ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_REMOVAL
|
|
2980
|
+
);
|
|
2981
|
+
const removeResults = await Promise.allSettled(
|
|
2982
|
+
toRemove.map(
|
|
2983
|
+
({ branchName, worktreePath }) => removeLimit(async () => {
|
|
2984
|
+
try {
|
|
2985
|
+
await this.gitService.removeWorktree(worktreePath);
|
|
2986
|
+
this.logger.info(` \u2705 Removed worktree for '${branchName}'`);
|
|
2987
|
+
} catch (error) {
|
|
2988
|
+
this.logger.error(` \u274C Failed to remove worktree for '${branchName}':`, getErrorMessage(error));
|
|
2989
|
+
throw error;
|
|
2990
|
+
}
|
|
2991
|
+
})
|
|
2992
|
+
)
|
|
2993
|
+
);
|
|
2994
|
+
const removedCount = removeResults.filter((r) => r.status === "fulfilled").length;
|
|
2995
|
+
this.logger.info(` Removed ${removedCount}/${toRemove.length} worktrees successfully`);
|
|
2996
|
+
}
|
|
2997
|
+
if (toSkip.length > 0) {
|
|
2998
|
+
this.logger.info(` Skipped ${toSkip.length} worktree(s) with local changes or unpushed commits`);
|
|
2999
|
+
}
|
|
3000
|
+
for (const { branchName, status } of toSkip) {
|
|
3001
|
+
if (status.upstreamGone && status.hasUnpushedCommits) {
|
|
3002
|
+
const worktreePath = path5.join(this.config.worktreeDir, branchName);
|
|
3003
|
+
this.logger.warn(` - \u26A0\uFE0F Cannot automatically remove '${branchName}' - upstream branch was deleted.`);
|
|
3004
|
+
this.logger.info(` Please review manually: cd ${worktreePath} && git log`);
|
|
3005
|
+
this.logger.info(
|
|
3006
|
+
` If changes were squash-merged, you can safely remove with: git worktree remove ${worktreePath}`
|
|
3007
|
+
);
|
|
3008
|
+
} else {
|
|
3009
|
+
this.logger.info(` - \u26A0\uFE0F Skipping removal of '${branchName}' due to: ${status.reasons.join(", ")}.`);
|
|
3010
|
+
}
|
|
3011
|
+
if (this.config.debug && status.details) {
|
|
3012
|
+
this.logDebugDetails(branchName, status.details);
|
|
1847
3013
|
}
|
|
1848
3014
|
}
|
|
1849
3015
|
} else {
|
|
1850
|
-
|
|
3016
|
+
this.logger.info("Step 3: No stale worktrees to prune.");
|
|
1851
3017
|
}
|
|
1852
3018
|
}
|
|
1853
3019
|
logDebugDetails(branchName, details) {
|
|
1854
|
-
|
|
3020
|
+
this.logger.info(`
|
|
1855
3021
|
\u{1F50D} Debug details for '${branchName}':`);
|
|
1856
3022
|
if (details.modifiedFiles > 0 && details.modifiedFilesList) {
|
|
1857
|
-
|
|
1858
|
-
details.modifiedFilesList.forEach((file) =>
|
|
3023
|
+
this.logger.info(` - Modified files (${details.modifiedFiles}):`);
|
|
3024
|
+
details.modifiedFilesList.forEach((file) => this.logger.info(` \u2022 ${file}`));
|
|
1859
3025
|
}
|
|
1860
3026
|
if (details.deletedFiles > 0 && details.deletedFilesList) {
|
|
1861
|
-
|
|
1862
|
-
details.deletedFilesList.forEach((file) =>
|
|
3027
|
+
this.logger.info(` - Deleted files (${details.deletedFiles}):`);
|
|
3028
|
+
details.deletedFilesList.forEach((file) => this.logger.info(` \u2022 ${file}`));
|
|
1863
3029
|
}
|
|
1864
3030
|
if (details.renamedFiles > 0 && details.renamedFilesList) {
|
|
1865
|
-
|
|
1866
|
-
details.renamedFilesList.forEach((file) =>
|
|
3031
|
+
this.logger.info(` - Renamed files (${details.renamedFiles}):`);
|
|
3032
|
+
details.renamedFilesList.forEach((file) => this.logger.info(` \u2022 ${file.from} \u2192 ${file.to}`));
|
|
1867
3033
|
}
|
|
1868
3034
|
if (details.createdFiles > 0 && details.createdFilesList) {
|
|
1869
|
-
|
|
1870
|
-
details.createdFilesList.forEach((file) =>
|
|
3035
|
+
this.logger.info(` - Created files (${details.createdFiles}):`);
|
|
3036
|
+
details.createdFilesList.forEach((file) => this.logger.info(` \u2022 ${file}`));
|
|
1871
3037
|
}
|
|
1872
3038
|
if (details.conflictedFiles > 0 && details.conflictedFilesList) {
|
|
1873
|
-
|
|
1874
|
-
details.conflictedFilesList.forEach((file) =>
|
|
3039
|
+
this.logger.info(` - Conflicted files (${details.conflictedFiles}):`);
|
|
3040
|
+
details.conflictedFilesList.forEach((file) => this.logger.info(` \u2022 ${file}`));
|
|
1875
3041
|
}
|
|
1876
3042
|
if (details.untrackedFiles > 0 && details.untrackedFilesList) {
|
|
1877
|
-
|
|
1878
|
-
details.untrackedFilesList.forEach((file) =>
|
|
3043
|
+
this.logger.info(` - Untracked files (not ignored) (${details.untrackedFiles}):`);
|
|
3044
|
+
details.untrackedFilesList.forEach((file) => this.logger.info(` \u2022 ${file}`));
|
|
1879
3045
|
}
|
|
1880
3046
|
if (details.unpushedCommitCount !== void 0 && details.unpushedCommitCount > 0) {
|
|
1881
|
-
|
|
3047
|
+
this.logger.info(` - Unpushed commits: ${details.unpushedCommitCount}`);
|
|
1882
3048
|
}
|
|
1883
3049
|
if (details.stashCount !== void 0 && details.stashCount > 0) {
|
|
1884
|
-
|
|
3050
|
+
this.logger.info(` - Stashed changes: ${details.stashCount}`);
|
|
1885
3051
|
}
|
|
1886
3052
|
if (details.operationType) {
|
|
1887
|
-
|
|
3053
|
+
this.logger.info(` - Operation in progress: ${details.operationType}`);
|
|
1888
3054
|
}
|
|
1889
3055
|
if (details.modifiedSubmodules && details.modifiedSubmodules.length > 0) {
|
|
1890
|
-
|
|
1891
|
-
details.modifiedSubmodules.forEach((submodule) =>
|
|
3056
|
+
this.logger.info(` - Modified submodules (${details.modifiedSubmodules.length}):`);
|
|
3057
|
+
details.modifiedSubmodules.forEach((submodule) => this.logger.info(` \u2022 ${submodule}`));
|
|
1892
3058
|
}
|
|
1893
|
-
|
|
3059
|
+
this.logger.info("");
|
|
1894
3060
|
}
|
|
1895
3061
|
async fetchBranchByBranch() {
|
|
1896
|
-
|
|
3062
|
+
this.logger.info("Fetching branches individually to isolate LFS errors...");
|
|
1897
3063
|
const remoteBranches = await this.gitService.getRemoteBranches();
|
|
1898
|
-
|
|
3064
|
+
this.logger.info(`Found ${remoteBranches.length} remote branches to fetch.`);
|
|
1899
3065
|
const failedBranches = [];
|
|
1900
3066
|
let successCount = 0;
|
|
1901
3067
|
for (const branch of remoteBranches) {
|
|
@@ -1904,79 +3070,109 @@ var WorktreeSyncService = class {
|
|
|
1904
3070
|
successCount++;
|
|
1905
3071
|
} catch (error) {
|
|
1906
3072
|
const errorMessage = getErrorMessage(error);
|
|
1907
|
-
|
|
3073
|
+
this.logger.info(` \u26A0\uFE0F Failed to fetch branch '${branch}': ${errorMessage}`);
|
|
1908
3074
|
failedBranches.push(branch);
|
|
1909
3075
|
}
|
|
1910
3076
|
}
|
|
1911
|
-
|
|
3077
|
+
this.logger.info(`Branch-by-branch fetch completed: ${successCount}/${remoteBranches.length} successful`);
|
|
1912
3078
|
if (failedBranches.length > 0) {
|
|
1913
|
-
|
|
1914
|
-
|
|
3079
|
+
this.logger.info(`\u26A0\uFE0F Failed to fetch ${failedBranches.length} branches due to errors.`);
|
|
3080
|
+
this.logger.info(` These branches will be skipped: ${failedBranches.join(", ")}`);
|
|
1915
3081
|
}
|
|
1916
3082
|
}
|
|
3083
|
+
async updateExistingWorktreesWithTiming(worktrees, remoteBranches, phaseTimer) {
|
|
3084
|
+
const maxConcurrent = this.config.parallelism?.maxWorktreeUpdates ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_UPDATES;
|
|
3085
|
+
phaseTimer.startPhase("Phase 4: Update", maxConcurrent);
|
|
3086
|
+
await this.updateExistingWorktrees(worktrees, remoteBranches);
|
|
3087
|
+
const activeWorktrees = worktrees.filter((w) => remoteBranches.includes(w.branch));
|
|
3088
|
+
phaseTimer.setPhaseCount("Phase 4: Update", activeWorktrees.length);
|
|
3089
|
+
phaseTimer.endPhase();
|
|
3090
|
+
}
|
|
1917
3091
|
async updateExistingWorktrees(worktrees, remoteBranches) {
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
const divergedDir = path5.join(this.config.worktreeDir, ".diverged");
|
|
3092
|
+
this.logger.info("Step 4: Checking for worktrees that need updates...");
|
|
3093
|
+
const divergedDir = path5.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
|
|
1921
3094
|
try {
|
|
1922
3095
|
const diverged = await fs5.readdir(divergedDir);
|
|
1923
3096
|
if (diverged.length > 0) {
|
|
1924
|
-
|
|
3097
|
+
this.logger.info(
|
|
3098
|
+
`\u{1F4E6} Note: ${diverged.length} diverged worktree(s) in ${path5.relative(process.cwd(), divergedDir)}`
|
|
3099
|
+
);
|
|
1925
3100
|
}
|
|
1926
3101
|
} catch {
|
|
1927
3102
|
}
|
|
1928
3103
|
const activeWorktrees = worktrees.filter((w) => remoteBranches.includes(w.branch));
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
3104
|
+
const maxConcurrent = this.config.parallelism?.maxStatusChecks ?? DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS;
|
|
3105
|
+
const limit = pLimit(maxConcurrent);
|
|
3106
|
+
const checkResults = await Promise.allSettled(
|
|
3107
|
+
activeWorktrees.map(
|
|
3108
|
+
(worktree) => limit(async () => {
|
|
3109
|
+
try {
|
|
3110
|
+
await fs5.access(worktree.path);
|
|
3111
|
+
} catch {
|
|
3112
|
+
return null;
|
|
3113
|
+
}
|
|
3114
|
+
const hasOp = await this.gitService.hasOperationInProgress(worktree.path);
|
|
3115
|
+
if (hasOp) return null;
|
|
3116
|
+
const isClean = await this.gitService.checkWorktreeStatus(worktree.path);
|
|
3117
|
+
if (!isClean) return null;
|
|
3118
|
+
const canFastForward = await this.gitService.canFastForward(worktree.path, worktree.branch);
|
|
3119
|
+
if (!canFastForward) {
|
|
3120
|
+
const isAhead = await this.gitService.isLocalAheadOfRemote(worktree.path, worktree.branch);
|
|
3121
|
+
if (isAhead) {
|
|
3122
|
+
this.logger.info(`\u23ED\uFE0F Skipping '${worktree.branch}' - has unpushed commits`);
|
|
3123
|
+
return null;
|
|
3124
|
+
}
|
|
3125
|
+
await this.handleDivergedBranch(worktree);
|
|
3126
|
+
return null;
|
|
3127
|
+
}
|
|
3128
|
+
const isBehind = await this.gitService.isWorktreeBehind(worktree.path);
|
|
3129
|
+
return isBehind ? worktree : null;
|
|
3130
|
+
})
|
|
3131
|
+
)
|
|
3132
|
+
);
|
|
3133
|
+
const worktreesToUpdate = [];
|
|
3134
|
+
for (const result of checkResults) {
|
|
3135
|
+
if (result.status === "fulfilled" && result.value) {
|
|
3136
|
+
worktreesToUpdate.push(result.value);
|
|
3137
|
+
} else if (result.status === "rejected") {
|
|
3138
|
+
this.logger.error(` - Error checking worktree:`, result.reason);
|
|
1955
3139
|
}
|
|
1956
3140
|
}
|
|
1957
3141
|
if (worktreesToUpdate.length > 0) {
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
const errorMessage = getErrorMessage(error);
|
|
1966
|
-
if (errorMessage.includes("Not possible to fast-forward") || errorMessage.includes("fatal: Not possible to fast-forward, aborting") || errorMessage.includes("cannot fast-forward")) {
|
|
1967
|
-
console.log(` \u26A0\uFE0F Branch '${worktree.branch}' cannot be fast-forwarded. Checking for divergence...`);
|
|
3142
|
+
this.logger.info(` - Found ${worktreesToUpdate.length} worktrees behind their upstream branches.`);
|
|
3143
|
+
const updateLimit = pLimit(
|
|
3144
|
+
this.config.parallelism?.maxWorktreeUpdates ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_UPDATES
|
|
3145
|
+
);
|
|
3146
|
+
const updateResults = await Promise.allSettled(
|
|
3147
|
+
worktreesToUpdate.map(
|
|
3148
|
+
(worktree) => updateLimit(async () => {
|
|
1968
3149
|
try {
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
3150
|
+
this.logger.info(` - Updating worktree '${worktree.branch}'...`);
|
|
3151
|
+
await this.gitService.updateWorktree(worktree.path);
|
|
3152
|
+
this.logger.info(` \u2705 Successfully updated '${worktree.branch}'.`);
|
|
3153
|
+
} catch (error) {
|
|
3154
|
+
const errorMessage = getErrorMessage(error);
|
|
3155
|
+
if (ERROR_MESSAGES.FAST_FORWARD_FAILED.some((msg) => errorMessage.includes(msg))) {
|
|
3156
|
+
this.logger.info(
|
|
3157
|
+
` \u26A0\uFE0F Branch '${worktree.branch}' cannot be fast-forwarded. Checking for divergence...`
|
|
3158
|
+
);
|
|
3159
|
+
try {
|
|
3160
|
+
await this.handleDivergedBranch(worktree);
|
|
3161
|
+
} catch (divergedError) {
|
|
3162
|
+
this.logger.error(` \u274C Failed to handle diverged branch '${worktree.branch}':`, divergedError);
|
|
3163
|
+
}
|
|
3164
|
+
} else {
|
|
3165
|
+
this.logger.error(` \u274C Failed to update '${worktree.branch}':`, error);
|
|
3166
|
+
}
|
|
3167
|
+
throw error;
|
|
1972
3168
|
}
|
|
1973
|
-
}
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
}
|
|
3169
|
+
})
|
|
3170
|
+
)
|
|
3171
|
+
);
|
|
3172
|
+
const successCount = updateResults.filter((r) => r.status === "fulfilled").length;
|
|
3173
|
+
this.logger.info(` Updated ${successCount}/${worktreesToUpdate.length} worktrees successfully`);
|
|
1978
3174
|
} else {
|
|
1979
|
-
|
|
3175
|
+
this.logger.info(" - All worktrees are up to date.");
|
|
1980
3176
|
}
|
|
1981
3177
|
}
|
|
1982
3178
|
async cleanupOrphanedDirectories(worktrees) {
|
|
@@ -1994,50 +3190,50 @@ var WorktreeSyncService = class {
|
|
|
1994
3190
|
}
|
|
1995
3191
|
}
|
|
1996
3192
|
if (orphanedDirs.length > 0) {
|
|
1997
|
-
|
|
3193
|
+
this.logger.info(`Found ${orphanedDirs.length} orphaned directories: ${orphanedDirs.join(", ")}`);
|
|
1998
3194
|
for (const dir of orphanedDirs) {
|
|
1999
3195
|
const dirPath = path5.join(this.config.worktreeDir, dir);
|
|
2000
3196
|
try {
|
|
2001
3197
|
const stat4 = await fs5.stat(dirPath);
|
|
2002
3198
|
if (stat4.isDirectory()) {
|
|
2003
3199
|
await fs5.rm(dirPath, { recursive: true, force: true });
|
|
2004
|
-
|
|
3200
|
+
this.logger.info(` - Removed orphaned directory: ${dir}`);
|
|
2005
3201
|
}
|
|
2006
3202
|
} catch (error) {
|
|
2007
|
-
|
|
3203
|
+
this.logger.error(` - Failed to remove orphaned directory ${dir}:`, error);
|
|
2008
3204
|
}
|
|
2009
3205
|
}
|
|
2010
3206
|
}
|
|
2011
3207
|
} catch (error) {
|
|
2012
|
-
|
|
3208
|
+
this.logger.error("Error during orphaned directory cleanup:", error);
|
|
2013
3209
|
}
|
|
2014
3210
|
}
|
|
2015
3211
|
async handleDivergedBranch(worktree) {
|
|
2016
|
-
|
|
3212
|
+
this.logger.info(`\u26A0\uFE0F Branch '${worktree.branch}' has diverged from upstream. Analyzing...`);
|
|
2017
3213
|
const treesIdentical = await this.gitService.compareTreeContent(worktree.path, worktree.branch);
|
|
2018
3214
|
if (treesIdentical) {
|
|
2019
|
-
|
|
3215
|
+
this.logger.info(`\u2705 Branch '${worktree.branch}' was rebased but files are identical. Resetting to upstream...`);
|
|
2020
3216
|
await this.gitService.resetToUpstream(worktree.path, worktree.branch);
|
|
2021
|
-
|
|
3217
|
+
this.logger.info(` Successfully updated '${worktree.branch}' to match upstream.`);
|
|
2022
3218
|
} else {
|
|
2023
3219
|
const hasLocalChanges = await this.hasLocalChangesSinceLastSync(worktree.path);
|
|
2024
3220
|
if (!hasLocalChanges) {
|
|
2025
|
-
|
|
3221
|
+
this.logger.info(
|
|
2026
3222
|
`\u2705 Branch '${worktree.branch}' has diverged but you made no local changes. Resetting to upstream...`
|
|
2027
3223
|
);
|
|
2028
3224
|
await this.gitService.resetToUpstream(worktree.path, worktree.branch);
|
|
2029
|
-
|
|
3225
|
+
this.logger.info(` Successfully updated '${worktree.branch}' to match upstream.`);
|
|
2030
3226
|
} else {
|
|
2031
|
-
|
|
3227
|
+
this.logger.info(`\u{1F512} Branch '${worktree.branch}' has diverged with local changes. Moving to diverged...`);
|
|
2032
3228
|
const divergedPath = await this.divergeWorktree(worktree.path, worktree.branch);
|
|
2033
3229
|
const relativePath = path5.relative(process.cwd(), divergedPath);
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
3230
|
+
this.logger.info(` Moved to: ${relativePath}`);
|
|
3231
|
+
this.logger.info(` Your local changes are preserved. To review:`);
|
|
3232
|
+
this.logger.info(` cd ${relativePath}`);
|
|
3233
|
+
this.logger.info(` git diff origin/${worktree.branch}`);
|
|
2038
3234
|
await this.gitService.removeWorktree(worktree.path);
|
|
2039
3235
|
await this.gitService.addWorktree(worktree.branch, worktree.path);
|
|
2040
|
-
|
|
3236
|
+
this.logger.info(` Created fresh worktree from upstream at: ${worktree.path}`);
|
|
2041
3237
|
}
|
|
2042
3238
|
}
|
|
2043
3239
|
}
|
|
@@ -2054,9 +3250,9 @@ var WorktreeSyncService = class {
|
|
|
2054
3250
|
}
|
|
2055
3251
|
}
|
|
2056
3252
|
async divergeWorktree(worktreePath, branchName) {
|
|
2057
|
-
const divergedBaseDir = path5.join(this.config.worktreeDir,
|
|
3253
|
+
const divergedBaseDir = path5.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
|
|
2058
3254
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
2059
|
-
const uniqueSuffix = Date.now().toString(36) + Math.random().toString(36).
|
|
3255
|
+
const uniqueSuffix = Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
|
|
2060
3256
|
const safeBranchName = branchName.replace(/\//g, "-");
|
|
2061
3257
|
const divergedName = `${timestamp}-${safeBranchName}-${uniqueSuffix}`;
|
|
2062
3258
|
const divergedPath = path5.join(divergedBaseDir, divergedName);
|
|
@@ -2075,7 +3271,7 @@ var WorktreeSyncService = class {
|
|
|
2075
3271
|
const metadata = {
|
|
2076
3272
|
originalBranch: branchName,
|
|
2077
3273
|
divergedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2078
|
-
reason:
|
|
3274
|
+
reason: METADATA_CONSTANTS.DIVERGED_REASON,
|
|
2079
3275
|
originalPath: worktreePath,
|
|
2080
3276
|
localCommit: await this.gitService.getCurrentCommit(divergedPath),
|
|
2081
3277
|
remoteCommit: await this.gitService.getRemoteCommit(`origin/${branchName}`),
|
|
@@ -2086,11 +3282,92 @@ var WorktreeSyncService = class {
|
|
|
2086
3282
|
|
|
2087
3283
|
Original worktree location: ${worktreePath}`
|
|
2088
3284
|
};
|
|
2089
|
-
await fs5.writeFile(
|
|
3285
|
+
await fs5.writeFile(
|
|
3286
|
+
path5.join(divergedPath, METADATA_CONSTANTS.DIVERGED_INFO_FILE),
|
|
3287
|
+
JSON.stringify(metadata, null, 2)
|
|
3288
|
+
);
|
|
2090
3289
|
return divergedPath;
|
|
2091
3290
|
}
|
|
2092
3291
|
};
|
|
2093
3292
|
|
|
3293
|
+
// src/services/file-copy.service.ts
|
|
3294
|
+
import * as fs6 from "fs/promises";
|
|
3295
|
+
import * as path6 from "path";
|
|
3296
|
+
import { glob } from "glob";
|
|
3297
|
+
var DEFAULT_IGNORE_PATTERNS = [
|
|
3298
|
+
"**/node_modules/**",
|
|
3299
|
+
"**/.git/**",
|
|
3300
|
+
"**/dist/**",
|
|
3301
|
+
"**/build/**",
|
|
3302
|
+
"**/.next/**",
|
|
3303
|
+
"**/coverage/**"
|
|
3304
|
+
];
|
|
3305
|
+
var FileCopyService = class {
|
|
3306
|
+
/**
|
|
3307
|
+
* Copy files matching patterns from source to destination directory.
|
|
3308
|
+
* Skips files that already exist at destination.
|
|
3309
|
+
* Preserves directory structure relative to source.
|
|
3310
|
+
*/
|
|
3311
|
+
async copyFiles(sourceDir, destDir, patterns) {
|
|
3312
|
+
const result = {
|
|
3313
|
+
copied: [],
|
|
3314
|
+
skipped: [],
|
|
3315
|
+
errors: []
|
|
3316
|
+
};
|
|
3317
|
+
if (!patterns || patterns.length === 0) {
|
|
3318
|
+
return result;
|
|
3319
|
+
}
|
|
3320
|
+
const filesToCopy = await this.expandPatterns(sourceDir, patterns);
|
|
3321
|
+
for (const relativePath of filesToCopy) {
|
|
3322
|
+
const sourcePath = path6.join(sourceDir, relativePath);
|
|
3323
|
+
const destPath = path6.join(destDir, relativePath);
|
|
3324
|
+
try {
|
|
3325
|
+
const copied = await this.copyFile(sourcePath, destPath);
|
|
3326
|
+
if (copied) {
|
|
3327
|
+
result.copied.push(relativePath);
|
|
3328
|
+
} else {
|
|
3329
|
+
result.skipped.push(relativePath);
|
|
3330
|
+
}
|
|
3331
|
+
} catch (error) {
|
|
3332
|
+
result.errors.push({
|
|
3333
|
+
file: relativePath,
|
|
3334
|
+
error: error instanceof Error ? error.message : String(error)
|
|
3335
|
+
});
|
|
3336
|
+
}
|
|
3337
|
+
}
|
|
3338
|
+
return result;
|
|
3339
|
+
}
|
|
3340
|
+
async expandPatterns(sourceDir, patterns) {
|
|
3341
|
+
const allFiles = /* @__PURE__ */ new Set();
|
|
3342
|
+
for (const pattern of patterns) {
|
|
3343
|
+
try {
|
|
3344
|
+
const matches = await glob(pattern, {
|
|
3345
|
+
cwd: sourceDir,
|
|
3346
|
+
nodir: true,
|
|
3347
|
+
dot: true,
|
|
3348
|
+
ignore: DEFAULT_IGNORE_PATTERNS
|
|
3349
|
+
});
|
|
3350
|
+
for (const match of matches) {
|
|
3351
|
+
allFiles.add(match);
|
|
3352
|
+
}
|
|
3353
|
+
} catch {
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
return Array.from(allFiles);
|
|
3357
|
+
}
|
|
3358
|
+
async copyFile(sourcePath, destPath) {
|
|
3359
|
+
try {
|
|
3360
|
+
await fs6.access(destPath);
|
|
3361
|
+
return false;
|
|
3362
|
+
} catch {
|
|
3363
|
+
}
|
|
3364
|
+
const destDir = path6.dirname(destPath);
|
|
3365
|
+
await fs6.mkdir(destDir, { recursive: true });
|
|
3366
|
+
await fs6.copyFile(sourcePath, destPath);
|
|
3367
|
+
return true;
|
|
3368
|
+
}
|
|
3369
|
+
};
|
|
3370
|
+
|
|
2094
3371
|
// src/utils/disk-space.ts
|
|
2095
3372
|
import fastFolderSize from "fast-folder-size";
|
|
2096
3373
|
async function calculateDirectorySize(dirPath) {
|
|
@@ -2139,9 +3416,8 @@ var InteractiveUIService = class {
|
|
|
2139
3416
|
cronSchedule;
|
|
2140
3417
|
cronJobs = [];
|
|
2141
3418
|
repositoryCount;
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
originalConsoleError;
|
|
3419
|
+
logBuffer = [];
|
|
3420
|
+
uiReady = false;
|
|
2145
3421
|
constructor(syncServices, configPath, cronSchedule) {
|
|
2146
3422
|
if (syncServices.length === 0) {
|
|
2147
3423
|
throw new Error("InteractiveUIService requires at least one WorktreeSyncService");
|
|
@@ -2150,31 +3426,52 @@ var InteractiveUIService = class {
|
|
|
2150
3426
|
this.configPath = configPath;
|
|
2151
3427
|
this.cronSchedule = cronSchedule;
|
|
2152
3428
|
this.repositoryCount = syncServices.length;
|
|
2153
|
-
this.originalConsoleLog = console.log.bind(console);
|
|
2154
|
-
this.originalConsoleWarn = console.warn.bind(console);
|
|
2155
|
-
this.originalConsoleError = console.error.bind(console);
|
|
2156
|
-
this.redirectConsole();
|
|
2157
3429
|
this.setupCronJobs();
|
|
3430
|
+
this.startBufferFlushCheck();
|
|
2158
3431
|
this.renderUI();
|
|
3432
|
+
this.injectLoggersIntoServices();
|
|
3433
|
+
setTimeout(() => {
|
|
3434
|
+
this.addLog("\u{1F680} sync-worktrees UI initialized", "info");
|
|
3435
|
+
}, 100);
|
|
3436
|
+
}
|
|
3437
|
+
startBufferFlushCheck() {
|
|
3438
|
+
const unsubscribe = appEvents.on("uiReady", () => {
|
|
3439
|
+
this.uiReady = true;
|
|
3440
|
+
this.flushLogBuffer();
|
|
3441
|
+
unsubscribe();
|
|
3442
|
+
});
|
|
2159
3443
|
}
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
const
|
|
2163
|
-
this.
|
|
2164
|
-
};
|
|
2165
|
-
console.warn = (...args) => {
|
|
2166
|
-
const message = args.map((arg) => typeof arg === "string" ? arg : JSON.stringify(arg, null, 2)).join(" ");
|
|
2167
|
-
this.originalConsoleWarn(message);
|
|
2168
|
-
};
|
|
2169
|
-
console.error = (...args) => {
|
|
2170
|
-
const message = args.map((arg) => typeof arg === "string" ? arg : JSON.stringify(arg, null, 2)).join(" ");
|
|
2171
|
-
this.originalConsoleError(message);
|
|
3444
|
+
createOutputFn() {
|
|
3445
|
+
return (message, level) => {
|
|
3446
|
+
const uiLevel = level === "debug" ? "info" : level;
|
|
3447
|
+
this.addLog(message, uiLevel);
|
|
2172
3448
|
};
|
|
2173
3449
|
}
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
3450
|
+
injectLoggersIntoServices() {
|
|
3451
|
+
const outputFn = this.createOutputFn();
|
|
3452
|
+
for (const service of this.syncServices) {
|
|
3453
|
+
const config = service.config;
|
|
3454
|
+
service.updateLogger(
|
|
3455
|
+
new Logger({
|
|
3456
|
+
repoName: config.name,
|
|
3457
|
+
debug: config.debug,
|
|
3458
|
+
outputFn
|
|
3459
|
+
})
|
|
3460
|
+
);
|
|
3461
|
+
}
|
|
3462
|
+
}
|
|
3463
|
+
addLog(message, level = "info") {
|
|
3464
|
+
if (this.uiReady) {
|
|
3465
|
+
appEvents.emit("addLog", { message, level });
|
|
3466
|
+
} else {
|
|
3467
|
+
this.logBuffer.push({ message, level });
|
|
3468
|
+
}
|
|
3469
|
+
}
|
|
3470
|
+
flushLogBuffer() {
|
|
3471
|
+
for (const log of this.logBuffer) {
|
|
3472
|
+
appEvents.emit("addLog", { message: log.message, level: log.level });
|
|
3473
|
+
}
|
|
3474
|
+
this.logBuffer = [];
|
|
2178
3475
|
}
|
|
2179
3476
|
setupCronJobs() {
|
|
2180
3477
|
if (!this.cronSchedule) {
|
|
@@ -2188,6 +3485,9 @@ var InteractiveUIService = class {
|
|
|
2188
3485
|
const task = cron.schedule(schedule3, async () => {
|
|
2189
3486
|
this.setStatus("syncing");
|
|
2190
3487
|
try {
|
|
3488
|
+
if (!service.isInitialized()) {
|
|
3489
|
+
await service.initialize();
|
|
3490
|
+
}
|
|
2191
3491
|
await service.sync();
|
|
2192
3492
|
} catch (error) {
|
|
2193
3493
|
console.error(`Error syncing: ${error.message}`);
|
|
@@ -2211,28 +3511,42 @@ var InteractiveUIService = class {
|
|
|
2211
3511
|
this.app.unmount();
|
|
2212
3512
|
}
|
|
2213
3513
|
this.app = render(
|
|
2214
|
-
/* @__PURE__ */
|
|
3514
|
+
/* @__PURE__ */ React7.createElement(
|
|
2215
3515
|
App_default,
|
|
2216
3516
|
{
|
|
2217
3517
|
repositoryCount: this.repositoryCount,
|
|
2218
3518
|
cronSchedule: this.cronSchedule,
|
|
2219
3519
|
onManualSync: () => this.handleManualSync(),
|
|
2220
3520
|
onReload: () => this.handleReload(),
|
|
2221
|
-
onQuit: () => this.handleQuit()
|
|
3521
|
+
onQuit: () => this.handleQuit(),
|
|
3522
|
+
getRepositoryList: () => this.getRepositoryList(),
|
|
3523
|
+
getBranchesForRepo: (index) => this.getBranchesForRepo(index),
|
|
3524
|
+
getDefaultBranchForRepo: (index) => this.getDefaultBranchForRepo(index),
|
|
3525
|
+
createAndPushBranch: (repoIndex, baseBranch, branchName) => this.createAndPushBranch(repoIndex, baseBranch, branchName),
|
|
3526
|
+
getWorktreesForRepo: (index) => this.getWorktreesForRepo(index),
|
|
3527
|
+
openEditorInWorktree: (path11) => this.openEditorInWorktree(path11),
|
|
3528
|
+
copyBranchFiles: (repoIndex, baseBranch, targetBranch) => this.copyBranchFiles(repoIndex, baseBranch, targetBranch),
|
|
3529
|
+
createWorktreeForBranch: (repoIndex, branchName) => this.createWorktreeForBranch(repoIndex, branchName)
|
|
2222
3530
|
}
|
|
2223
3531
|
)
|
|
2224
3532
|
);
|
|
2225
3533
|
}
|
|
2226
3534
|
async handleManualSync() {
|
|
3535
|
+
await this.triggerInitialSync();
|
|
3536
|
+
}
|
|
3537
|
+
async triggerInitialSync() {
|
|
2227
3538
|
this.setStatus("syncing");
|
|
2228
3539
|
try {
|
|
2229
3540
|
for (const service of this.syncServices) {
|
|
3541
|
+
if (!service.isInitialized()) {
|
|
3542
|
+
await service.initialize();
|
|
3543
|
+
}
|
|
2230
3544
|
await service.sync();
|
|
2231
3545
|
}
|
|
2232
3546
|
this.updateLastSyncTime();
|
|
2233
3547
|
await this.calculateAndUpdateDiskSpace();
|
|
2234
3548
|
} catch (error) {
|
|
2235
|
-
console.error("
|
|
3549
|
+
console.error("Sync failed:", error);
|
|
2236
3550
|
} finally {
|
|
2237
3551
|
this.setStatus("idle");
|
|
2238
3552
|
}
|
|
@@ -2316,22 +3630,13 @@ var InteractiveUIService = class {
|
|
|
2316
3630
|
}
|
|
2317
3631
|
}
|
|
2318
3632
|
updateLastSyncTime() {
|
|
2319
|
-
|
|
2320
|
-
if (methods && methods.updateLastSyncTime) {
|
|
2321
|
-
methods.updateLastSyncTime();
|
|
2322
|
-
}
|
|
3633
|
+
appEvents.emit("updateLastSyncTime");
|
|
2323
3634
|
}
|
|
2324
3635
|
setStatus(status) {
|
|
2325
|
-
|
|
2326
|
-
if (methods && methods.setStatus) {
|
|
2327
|
-
methods.setStatus(status);
|
|
2328
|
-
}
|
|
3636
|
+
appEvents.emit("setStatus", status);
|
|
2329
3637
|
}
|
|
2330
3638
|
setDiskSpace(diskSpace) {
|
|
2331
|
-
|
|
2332
|
-
if (methods && methods.setDiskSpace) {
|
|
2333
|
-
methods.setDiskSpace(diskSpace);
|
|
2334
|
-
}
|
|
3639
|
+
appEvents.emit("setDiskSpace", diskSpace);
|
|
2335
3640
|
}
|
|
2336
3641
|
async calculateAndUpdateDiskSpace() {
|
|
2337
3642
|
try {
|
|
@@ -2346,14 +3651,137 @@ var InteractiveUIService = class {
|
|
|
2346
3651
|
this.setDiskSpace("N/A");
|
|
2347
3652
|
}
|
|
2348
3653
|
}
|
|
3654
|
+
getRepositoryList() {
|
|
3655
|
+
return this.syncServices.map((service, index) => ({
|
|
3656
|
+
index,
|
|
3657
|
+
name: service.config.name || `repo-${index}`,
|
|
3658
|
+
repoUrl: service.config.repoUrl
|
|
3659
|
+
}));
|
|
3660
|
+
}
|
|
3661
|
+
async getBranchesForRepo(repoIndex) {
|
|
3662
|
+
if (repoIndex < 0 || repoIndex >= this.syncServices.length) {
|
|
3663
|
+
throw new Error(`Invalid repository index: ${repoIndex}`);
|
|
3664
|
+
}
|
|
3665
|
+
const service = this.syncServices[repoIndex];
|
|
3666
|
+
const gitService = service.getGitService();
|
|
3667
|
+
return gitService.getRemoteBranches();
|
|
3668
|
+
}
|
|
3669
|
+
getDefaultBranchForRepo(repoIndex) {
|
|
3670
|
+
if (repoIndex < 0 || repoIndex >= this.syncServices.length) {
|
|
3671
|
+
throw new Error(`Invalid repository index: ${repoIndex}`);
|
|
3672
|
+
}
|
|
3673
|
+
const service = this.syncServices[repoIndex];
|
|
3674
|
+
const gitService = service.getGitService();
|
|
3675
|
+
return gitService.getDefaultBranch();
|
|
3676
|
+
}
|
|
3677
|
+
async createAndPushBranch(repoIndex, baseBranch, branchName) {
|
|
3678
|
+
if (repoIndex < 0 || repoIndex >= this.syncServices.length) {
|
|
3679
|
+
return { success: false, finalName: branchName, error: `Invalid repository index: ${repoIndex}` };
|
|
3680
|
+
}
|
|
3681
|
+
const service = this.syncServices[repoIndex];
|
|
3682
|
+
const gitService = service.getGitService();
|
|
3683
|
+
try {
|
|
3684
|
+
let finalName = branchName;
|
|
3685
|
+
let suffix = 0;
|
|
3686
|
+
while (true) {
|
|
3687
|
+
const exists = await gitService.branchExists(finalName);
|
|
3688
|
+
if (!exists.local && !exists.remote) {
|
|
3689
|
+
break;
|
|
3690
|
+
}
|
|
3691
|
+
suffix++;
|
|
3692
|
+
finalName = `${branchName}-${suffix}`;
|
|
3693
|
+
}
|
|
3694
|
+
await gitService.createBranch(finalName, baseBranch);
|
|
3695
|
+
await gitService.pushBranch(finalName);
|
|
3696
|
+
return { success: true, finalName };
|
|
3697
|
+
} catch (error) {
|
|
3698
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3699
|
+
return { success: false, finalName: branchName, error: errorMessage };
|
|
3700
|
+
}
|
|
3701
|
+
}
|
|
3702
|
+
async getWorktreesForRepo(repoIndex) {
|
|
3703
|
+
if (repoIndex < 0 || repoIndex >= this.syncServices.length) {
|
|
3704
|
+
throw new Error(`Invalid repository index: ${repoIndex}`);
|
|
3705
|
+
}
|
|
3706
|
+
const service = this.syncServices[repoIndex];
|
|
3707
|
+
const gitService = service.getGitService();
|
|
3708
|
+
return gitService.getWorktrees();
|
|
3709
|
+
}
|
|
3710
|
+
async createWorktreeForBranch(repoIndex, branchName) {
|
|
3711
|
+
if (repoIndex < 0 || repoIndex >= this.syncServices.length) {
|
|
3712
|
+
throw new Error(`Invalid repository index: ${repoIndex}`);
|
|
3713
|
+
}
|
|
3714
|
+
const service = this.syncServices[repoIndex];
|
|
3715
|
+
const gitService = service.getGitService();
|
|
3716
|
+
const worktreeDir = service.config.worktreeDir;
|
|
3717
|
+
const worktreePath = path7.join(worktreeDir, branchName);
|
|
3718
|
+
await gitService.addWorktree(branchName, worktreePath);
|
|
3719
|
+
}
|
|
3720
|
+
openEditorInWorktree(worktreePath) {
|
|
3721
|
+
const editor = process.env.EDITOR || process.env.VISUAL || "code";
|
|
3722
|
+
try {
|
|
3723
|
+
const child = spawn(editor, [worktreePath], {
|
|
3724
|
+
detached: true,
|
|
3725
|
+
stdio: "ignore"
|
|
3726
|
+
});
|
|
3727
|
+
child.on("error", (err) => {
|
|
3728
|
+
this.addLog(`Failed to open editor '${editor}': ${err.message}`, "error");
|
|
3729
|
+
this.addLog("Set EDITOR or VISUAL environment variable to your preferred editor", "warn");
|
|
3730
|
+
});
|
|
3731
|
+
child.unref();
|
|
3732
|
+
return { success: true };
|
|
3733
|
+
} catch (err) {
|
|
3734
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
3735
|
+
this.addLog(`Failed to open editor '${editor}': ${errorMessage}`, "error");
|
|
3736
|
+
return { success: false, error: errorMessage };
|
|
3737
|
+
}
|
|
3738
|
+
}
|
|
3739
|
+
async copyBranchFiles(repoIndex, baseBranch, targetBranch) {
|
|
3740
|
+
if (repoIndex < 0 || repoIndex >= this.syncServices.length) {
|
|
3741
|
+
return;
|
|
3742
|
+
}
|
|
3743
|
+
const service = this.syncServices[repoIndex];
|
|
3744
|
+
const config = service.config;
|
|
3745
|
+
if (!config.filesToCopyOnBranchCreate?.length) {
|
|
3746
|
+
return;
|
|
3747
|
+
}
|
|
3748
|
+
const gitService = service.getGitService();
|
|
3749
|
+
const worktrees = await gitService.getWorktrees();
|
|
3750
|
+
const sourceWorktree = worktrees.find((w) => w.branch === baseBranch);
|
|
3751
|
+
const targetWorktree = worktrees.find((w) => w.branch === targetBranch);
|
|
3752
|
+
if (!sourceWorktree || !targetWorktree) {
|
|
3753
|
+
console.warn(`Could not find worktrees for file copy: source=${baseBranch}, target=${targetBranch}`);
|
|
3754
|
+
return;
|
|
3755
|
+
}
|
|
3756
|
+
const fileCopyService = new FileCopyService();
|
|
3757
|
+
try {
|
|
3758
|
+
const result = await fileCopyService.copyFiles(
|
|
3759
|
+
sourceWorktree.path,
|
|
3760
|
+
targetWorktree.path,
|
|
3761
|
+
config.filesToCopyOnBranchCreate
|
|
3762
|
+
);
|
|
3763
|
+
if (result.copied.length > 0) {
|
|
3764
|
+
console.log(`\u{1F4CB} Copied ${result.copied.length} file(s) to new branch: ${result.copied.join(", ")}`);
|
|
3765
|
+
}
|
|
3766
|
+
if (result.errors.length > 0) {
|
|
3767
|
+
console.warn(`\u26A0\uFE0F Failed to copy ${result.errors.length} file(s):`);
|
|
3768
|
+
for (const err of result.errors) {
|
|
3769
|
+
console.warn(` - ${err.file}: ${err.error}`);
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
} catch (error) {
|
|
3773
|
+
console.error(`Failed to copy files to new branch: ${error}`);
|
|
3774
|
+
}
|
|
3775
|
+
}
|
|
2349
3776
|
destroy() {
|
|
2350
3777
|
this.cancelCronJobs();
|
|
2351
|
-
this.restoreConsole();
|
|
2352
3778
|
if (this.app) {
|
|
2353
3779
|
this.app.unmount();
|
|
2354
3780
|
this.app = null;
|
|
2355
3781
|
}
|
|
2356
|
-
|
|
3782
|
+
appEvents.removeAllListeners();
|
|
3783
|
+
this.uiReady = false;
|
|
3784
|
+
this.logBuffer = [];
|
|
2357
3785
|
}
|
|
2358
3786
|
};
|
|
2359
3787
|
|
|
@@ -2412,6 +3840,10 @@ function parseArguments() {
|
|
|
2412
3840
|
type: "boolean",
|
|
2413
3841
|
description: "Enable debug mode to show detailed reasons why worktrees are not cleaned up.",
|
|
2414
3842
|
default: false
|
|
3843
|
+
}).option("sync-on-start", {
|
|
3844
|
+
type: "boolean",
|
|
3845
|
+
description: "Run sync immediately when starting the interactive UI (config mode only).",
|
|
3846
|
+
default: false
|
|
2415
3847
|
}).help().alias("help", "h").parseSync();
|
|
2416
3848
|
return {
|
|
2417
3849
|
config: argv.config,
|
|
@@ -2425,7 +3857,8 @@ function parseArguments() {
|
|
|
2425
3857
|
branchMaxAge: argv.branchMaxAge,
|
|
2426
3858
|
skipLfs: argv.skipLfs,
|
|
2427
3859
|
noUpdateExisting: argv["no-update-existing"],
|
|
2428
|
-
debug: argv.debug
|
|
3860
|
+
debug: argv.debug,
|
|
3861
|
+
syncOnStart: argv["sync-on-start"]
|
|
2429
3862
|
};
|
|
2430
3863
|
}
|
|
2431
3864
|
function isInteractiveMode(config) {
|
|
@@ -2463,12 +3896,12 @@ function reconstructCliCommand(config) {
|
|
|
2463
3896
|
}
|
|
2464
3897
|
|
|
2465
3898
|
// src/utils/interactive.ts
|
|
2466
|
-
import * as
|
|
3899
|
+
import * as path9 from "path";
|
|
2467
3900
|
import { confirm, input, select } from "@inquirer/prompts";
|
|
2468
3901
|
|
|
2469
3902
|
// src/utils/config-generator.ts
|
|
2470
|
-
import * as
|
|
2471
|
-
import * as
|
|
3903
|
+
import * as fs7 from "fs/promises";
|
|
3904
|
+
import * as path8 from "path";
|
|
2472
3905
|
function serializeToESM(obj, indent = 0) {
|
|
2473
3906
|
const spaces = " ".repeat(indent);
|
|
2474
3907
|
const innerSpaces = " ".repeat(indent + 2);
|
|
@@ -2498,9 +3931,9 @@ ${spaces}}`;
|
|
|
2498
3931
|
return String(obj);
|
|
2499
3932
|
}
|
|
2500
3933
|
async function generateConfigFile(config, configPath) {
|
|
2501
|
-
const configDir =
|
|
2502
|
-
await
|
|
2503
|
-
const worktreeDirRelative =
|
|
3934
|
+
const configDir = path8.dirname(configPath);
|
|
3935
|
+
await fs7.mkdir(configDir, { recursive: true });
|
|
3936
|
+
const worktreeDirRelative = path8.relative(configDir, config.worktreeDir);
|
|
2504
3937
|
const useRelativeWorktree = !worktreeDirRelative.startsWith("../../../");
|
|
2505
3938
|
const repoName = extractRepoNameFromUrl(config.repoUrl);
|
|
2506
3939
|
const repository = {
|
|
@@ -2509,7 +3942,7 @@ async function generateConfigFile(config, configPath) {
|
|
|
2509
3942
|
worktreeDir: useRelativeWorktree ? `./${worktreeDirRelative}` : config.worktreeDir
|
|
2510
3943
|
};
|
|
2511
3944
|
if (config.bareRepoDir) {
|
|
2512
|
-
const bareRepoDirRelative =
|
|
3945
|
+
const bareRepoDirRelative = path8.relative(configDir, config.bareRepoDir);
|
|
2513
3946
|
const useRelativeBare = !bareRepoDirRelative.startsWith("../../../");
|
|
2514
3947
|
repository.bareRepoDir = useRelativeBare ? `./${bareRepoDirRelative}` : config.bareRepoDir;
|
|
2515
3948
|
}
|
|
@@ -2527,10 +3960,10 @@ async function generateConfigFile(config, configPath) {
|
|
|
2527
3960
|
|
|
2528
3961
|
export default ${serializeToESM(configObject)};
|
|
2529
3962
|
`;
|
|
2530
|
-
await
|
|
3963
|
+
await fs7.writeFile(configPath, configContent, "utf-8");
|
|
2531
3964
|
}
|
|
2532
3965
|
function getDefaultConfigPath() {
|
|
2533
|
-
return
|
|
3966
|
+
return path8.join(process.cwd(), "sync-worktrees.config.js");
|
|
2534
3967
|
}
|
|
2535
3968
|
|
|
2536
3969
|
// src/utils/interactive.ts
|
|
@@ -2572,8 +4005,8 @@ async function promptForConfig(partialConfig) {
|
|
|
2572
4005
|
if (!worktreeDir.trim() && defaultWorktreeDir) {
|
|
2573
4006
|
worktreeDir = defaultWorktreeDir;
|
|
2574
4007
|
}
|
|
2575
|
-
if (!
|
|
2576
|
-
worktreeDir =
|
|
4008
|
+
if (!path9.isAbsolute(worktreeDir)) {
|
|
4009
|
+
worktreeDir = path9.resolve(worktreeDir);
|
|
2577
4010
|
}
|
|
2578
4011
|
}
|
|
2579
4012
|
let bareRepoDir = partialConfig.bareRepoDir;
|
|
@@ -2592,8 +4025,8 @@ async function promptForConfig(partialConfig) {
|
|
|
2592
4025
|
return true;
|
|
2593
4026
|
}
|
|
2594
4027
|
});
|
|
2595
|
-
if (!
|
|
2596
|
-
bareRepoDir =
|
|
4028
|
+
if (!path9.isAbsolute(bareRepoDir)) {
|
|
4029
|
+
bareRepoDir = path9.resolve(bareRepoDir);
|
|
2597
4030
|
}
|
|
2598
4031
|
}
|
|
2599
4032
|
let runOnce = partialConfig.runOnce;
|
|
@@ -2664,8 +4097,8 @@ async function promptForConfig(partialConfig) {
|
|
|
2664
4097
|
return true;
|
|
2665
4098
|
}
|
|
2666
4099
|
});
|
|
2667
|
-
if (!
|
|
2668
|
-
configPath =
|
|
4100
|
+
if (!path9.isAbsolute(configPath)) {
|
|
4101
|
+
configPath = path9.resolve(configPath);
|
|
2669
4102
|
}
|
|
2670
4103
|
try {
|
|
2671
4104
|
await generateConfigFile(finalConfig, configPath);
|
|
@@ -2673,7 +4106,7 @@ async function promptForConfig(partialConfig) {
|
|
|
2673
4106
|
\u2705 Configuration saved to: ${configPath}`);
|
|
2674
4107
|
console.log(`
|
|
2675
4108
|
\u{1F4A1} You can now use this config file with:`);
|
|
2676
|
-
console.log(` sync-worktrees --config ${
|
|
4109
|
+
console.log(` sync-worktrees --config ${path9.relative(process.cwd(), configPath)}`);
|
|
2677
4110
|
console.log("");
|
|
2678
4111
|
} catch (error) {
|
|
2679
4112
|
console.error(`
|
|
@@ -2685,14 +4118,18 @@ async function promptForConfig(partialConfig) {
|
|
|
2685
4118
|
|
|
2686
4119
|
// src/index.ts
|
|
2687
4120
|
async function runSingleRepository(config) {
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
4121
|
+
const logger = Logger.createDefault(void 0, config.debug);
|
|
4122
|
+
logger.info("\n\u{1F4CB} CLI Command (for future reference):");
|
|
4123
|
+
logger.info(` ${reconstructCliCommand(config)}`);
|
|
4124
|
+
logger.info("");
|
|
4125
|
+
if (!config.logger) {
|
|
4126
|
+
config.logger = logger;
|
|
4127
|
+
}
|
|
2691
4128
|
const syncService = new WorktreeSyncService(config);
|
|
2692
4129
|
try {
|
|
2693
4130
|
await syncService.initialize();
|
|
2694
4131
|
if (config.runOnce) {
|
|
2695
|
-
|
|
4132
|
+
logger.info("Running the sync process once as requested by --runOnce flag.");
|
|
2696
4133
|
await syncService.sync();
|
|
2697
4134
|
} else {
|
|
2698
4135
|
const uiService = new InteractiveUIService([syncService], void 0, config.cronSchedule);
|
|
@@ -2706,43 +4143,76 @@ async function runSingleRepository(config) {
|
|
|
2706
4143
|
uiService.updateLastSyncTime();
|
|
2707
4144
|
void uiService.calculateAndUpdateDiskSpace();
|
|
2708
4145
|
} catch (error) {
|
|
2709
|
-
|
|
4146
|
+
logger.error(`Error during scheduled sync: ${error.message}`, error);
|
|
2710
4147
|
uiService.setStatus("idle");
|
|
2711
4148
|
}
|
|
2712
4149
|
});
|
|
2713
4150
|
}
|
|
2714
4151
|
} catch (error) {
|
|
2715
|
-
|
|
4152
|
+
logger.error("\u274C Fatal Error during initialization:", error);
|
|
2716
4153
|
process.exit(1);
|
|
2717
4154
|
}
|
|
2718
4155
|
}
|
|
2719
|
-
async function runMultipleRepositories(repositories, runOnce, configPath) {
|
|
4156
|
+
async function runMultipleRepositories(repositories, runOnce, configPath, maxParallel, syncOnStart) {
|
|
2720
4157
|
const services = /* @__PURE__ */ new Map();
|
|
2721
|
-
|
|
4158
|
+
const globalLogger = Logger.createDefault();
|
|
4159
|
+
const limit = pLimit2(maxParallel ?? DEFAULT_CONFIG.PARALLELISM.MAX_REPOSITORIES);
|
|
4160
|
+
if (runOnce) {
|
|
4161
|
+
globalLogger.info(`
|
|
2722
4162
|
\u{1F504} Syncing ${repositories.length} repositories...`);
|
|
2723
|
-
|
|
2724
|
-
|
|
4163
|
+
const initResults = await Promise.allSettled(
|
|
4164
|
+
repositories.map(
|
|
4165
|
+
(repoConfig) => limit(async () => {
|
|
4166
|
+
const repoLogger = Logger.createDefault(repoConfig.name, repoConfig.debug);
|
|
4167
|
+
repoLogger.info(`
|
|
2725
4168
|
\u{1F4E6} Repository: ${repoConfig.name}`);
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
4169
|
+
repoLogger.info(` URL: ${repoConfig.repoUrl}`);
|
|
4170
|
+
repoLogger.info(` Worktrees: ${repoConfig.worktreeDir}`);
|
|
4171
|
+
if (repoConfig.bareRepoDir) {
|
|
4172
|
+
repoLogger.info(` Bare repo: ${repoConfig.bareRepoDir}`);
|
|
4173
|
+
}
|
|
4174
|
+
if (!repoConfig.logger) {
|
|
4175
|
+
repoConfig.logger = repoLogger;
|
|
4176
|
+
}
|
|
4177
|
+
const syncService = new WorktreeSyncService(repoConfig);
|
|
4178
|
+
await syncService.initialize();
|
|
4179
|
+
return { name: repoConfig.name, service: syncService };
|
|
4180
|
+
})
|
|
4181
|
+
)
|
|
4182
|
+
);
|
|
4183
|
+
const servicesToSync = [];
|
|
4184
|
+
for (const result of initResults) {
|
|
4185
|
+
if (result.status === "fulfilled") {
|
|
4186
|
+
services.set(result.value.name, result.value.service);
|
|
4187
|
+
servicesToSync.push(result.value);
|
|
4188
|
+
} else {
|
|
4189
|
+
globalLogger.error(`\u274C Failed to initialize repository:`, result.reason);
|
|
4190
|
+
}
|
|
2730
4191
|
}
|
|
2731
|
-
const
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
4192
|
+
const syncResults = await Promise.allSettled(
|
|
4193
|
+
servicesToSync.map(
|
|
4194
|
+
({ name, service }) => limit(async () => {
|
|
4195
|
+
try {
|
|
4196
|
+
await service.sync();
|
|
4197
|
+
} catch (error) {
|
|
4198
|
+
globalLogger.error(`\u274C Error syncing repository '${name}':`, error);
|
|
4199
|
+
throw error;
|
|
4200
|
+
}
|
|
4201
|
+
})
|
|
4202
|
+
)
|
|
4203
|
+
);
|
|
4204
|
+
const successCount = syncResults.filter((r) => r.status === "fulfilled").length;
|
|
4205
|
+
globalLogger.info(`
|
|
4206
|
+
\u2705 Successfully synced ${successCount}/${servicesToSync.length} repositories`);
|
|
4207
|
+
} else {
|
|
4208
|
+
for (const repoConfig of repositories) {
|
|
4209
|
+
const syncService = new WorktreeSyncService(repoConfig);
|
|
4210
|
+
services.set(repoConfig.name, syncService);
|
|
2738
4211
|
}
|
|
2739
|
-
}
|
|
2740
|
-
if (!runOnce) {
|
|
2741
4212
|
const uniqueSchedules = [...new Set(repositories.map((r) => r.cronSchedule))];
|
|
2742
4213
|
const displaySchedule = uniqueSchedules.length === 1 ? uniqueSchedules[0] : void 0;
|
|
2743
4214
|
const allServices = Array.from(services.values());
|
|
2744
4215
|
const uiService = new InteractiveUIService(allServices, configPath, displaySchedule);
|
|
2745
|
-
uiService.updateLastSyncTime();
|
|
2746
4216
|
void uiService.calculateAndUpdateDiskSpace();
|
|
2747
4217
|
const cronJobs = /* @__PURE__ */ new Map();
|
|
2748
4218
|
for (const repoConfig of repositories) {
|
|
@@ -2753,25 +4223,35 @@ async function runMultipleRepositories(repositories, runOnce, configPath) {
|
|
|
2753
4223
|
cron2.schedule(repoConfig.cronSchedule, async () => {
|
|
2754
4224
|
const reposToSync = repositories.filter((r) => r.cronSchedule === repoConfig.cronSchedule);
|
|
2755
4225
|
uiService.setStatus("syncing");
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
4226
|
+
await Promise.allSettled(
|
|
4227
|
+
reposToSync.map(
|
|
4228
|
+
(repo) => limit(async () => {
|
|
4229
|
+
const service = services.get(repo.name);
|
|
4230
|
+
if (!service) return;
|
|
4231
|
+
uiService.addLog(`Running scheduled sync for: ${repo.name}`);
|
|
4232
|
+
try {
|
|
4233
|
+
if (!service.isInitialized()) {
|
|
4234
|
+
await service.initialize();
|
|
4235
|
+
}
|
|
4236
|
+
await service.sync();
|
|
4237
|
+
} catch (error) {
|
|
4238
|
+
uiService.addLog(`Error syncing '${repo.name}': ${error}`, "error");
|
|
4239
|
+
}
|
|
4240
|
+
})
|
|
4241
|
+
)
|
|
4242
|
+
);
|
|
2766
4243
|
uiService.updateLastSyncTime();
|
|
2767
4244
|
void uiService.calculateAndUpdateDiskSpace();
|
|
2768
4245
|
});
|
|
2769
4246
|
}
|
|
2770
4247
|
}
|
|
2771
|
-
|
|
4248
|
+
uiService.addLog(`\u{1F4CB} ${repositories.length} repositories configured`);
|
|
2772
4249
|
for (const [schedule3] of cronJobs) {
|
|
2773
4250
|
const repoCount = repositories.filter((r) => r.cronSchedule === schedule3).length;
|
|
2774
|
-
|
|
4251
|
+
uiService.addLog(`\u23F0 ${schedule3}: ${repoCount} repository(ies)`);
|
|
4252
|
+
}
|
|
4253
|
+
if (syncOnStart) {
|
|
4254
|
+
await uiService.triggerInitialSync();
|
|
2775
4255
|
}
|
|
2776
4256
|
}
|
|
2777
4257
|
}
|
|
@@ -2779,7 +4259,7 @@ async function listRepositories(configPath, filter) {
|
|
|
2779
4259
|
const configLoader = new ConfigLoaderService();
|
|
2780
4260
|
try {
|
|
2781
4261
|
const configFile = await configLoader.loadConfigFile(configPath);
|
|
2782
|
-
const configDir =
|
|
4262
|
+
const configDir = path10.dirname(path10.resolve(configPath));
|
|
2783
4263
|
let repositories = configFile.repositories.map(
|
|
2784
4264
|
(repo) => configLoader.resolveRepositoryConfig(repo, configFile.defaults, configDir, configFile.retry)
|
|
2785
4265
|
);
|
|
@@ -2820,7 +4300,7 @@ async function main() {
|
|
|
2820
4300
|
}
|
|
2821
4301
|
try {
|
|
2822
4302
|
const configFile = await configLoader.loadConfigFile(options.config);
|
|
2823
|
-
const configDir =
|
|
4303
|
+
const configDir = path10.dirname(path10.resolve(options.config));
|
|
2824
4304
|
let repositories = configFile.repositories.map(
|
|
2825
4305
|
(repo) => configLoader.resolveRepositoryConfig(repo, configFile.defaults, configDir, configFile.retry)
|
|
2826
4306
|
);
|
|
@@ -2844,7 +4324,8 @@ async function main() {
|
|
|
2844
4324
|
debug: true
|
|
2845
4325
|
}));
|
|
2846
4326
|
}
|
|
2847
|
-
|
|
4327
|
+
const maxParallel = configFile.parallelism?.maxRepositories ?? configFile.defaults?.parallelism?.maxRepositories ?? DEFAULT_CONFIG.PARALLELISM.MAX_REPOSITORIES;
|
|
4328
|
+
await runMultipleRepositories(repositories, globalRunOnce, options.config, maxParallel, options.syncOnStart);
|
|
2848
4329
|
} catch (error) {
|
|
2849
4330
|
if (error instanceof Error && error.message.includes("Config file not found")) {
|
|
2850
4331
|
console.error(`
|