sync-worktrees 2.0.0 → 2.1.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 +733 -265
- package/dist/index.js.map +4 -4
- package/package.json +3 -1
package/dist/index.js
CHANGED
|
@@ -4,6 +4,96 @@
|
|
|
4
4
|
import * as path8 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";
|
|
@@ -114,13 +204,66 @@ var ConfigLoaderService = class {
|
|
|
114
204
|
throw new Error("Invalid 'backoffMultiplier' in retry config");
|
|
115
205
|
}
|
|
116
206
|
}
|
|
207
|
+
if (configObj.parallelism !== void 0) {
|
|
208
|
+
this.validateParallelismConfig(configObj.parallelism, "global");
|
|
209
|
+
}
|
|
210
|
+
if (configObj.defaults && typeof configObj.defaults === "object") {
|
|
211
|
+
const defaults = configObj.defaults;
|
|
212
|
+
if (defaults.parallelism !== void 0) {
|
|
213
|
+
this.validateParallelismConfig(defaults.parallelism, "defaults");
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
validateParallelismConfig(parallelism, context) {
|
|
218
|
+
if (typeof parallelism !== "object" || parallelism === null) {
|
|
219
|
+
throw new Error(`'parallelism' in ${context} must be an object`);
|
|
220
|
+
}
|
|
221
|
+
const config = parallelism;
|
|
222
|
+
if (config.maxRepositories !== void 0) {
|
|
223
|
+
if (typeof config.maxRepositories !== "number" || config.maxRepositories < 1) {
|
|
224
|
+
throw new Error(`Invalid 'maxRepositories' in ${context} parallelism config. Must be a positive number`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (config.maxWorktreeCreation !== void 0) {
|
|
228
|
+
if (typeof config.maxWorktreeCreation !== "number" || config.maxWorktreeCreation < 1) {
|
|
229
|
+
throw new Error(`Invalid 'maxWorktreeCreation' in ${context} parallelism config. Must be a positive number`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (config.maxWorktreeUpdates !== void 0) {
|
|
233
|
+
if (typeof config.maxWorktreeUpdates !== "number" || config.maxWorktreeUpdates < 1) {
|
|
234
|
+
throw new Error(`Invalid 'maxWorktreeUpdates' in ${context} parallelism config. Must be a positive number`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (config.maxWorktreeRemoval !== void 0) {
|
|
238
|
+
if (typeof config.maxWorktreeRemoval !== "number" || config.maxWorktreeRemoval < 1) {
|
|
239
|
+
throw new Error(`Invalid 'maxWorktreeRemoval' in ${context} parallelism config. Must be a positive number`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (config.maxStatusChecks !== void 0) {
|
|
243
|
+
if (typeof config.maxStatusChecks !== "number" || config.maxStatusChecks < 1) {
|
|
244
|
+
throw new Error(`Invalid 'maxStatusChecks' in ${context} parallelism config. Must be a positive number`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
const maxRepos = config.maxRepositories ?? DEFAULT_CONFIG.PARALLELISM.MAX_REPOSITORIES;
|
|
248
|
+
const maxCreation = config.maxWorktreeCreation ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_CREATION;
|
|
249
|
+
const maxUpdates = config.maxWorktreeUpdates ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_UPDATES;
|
|
250
|
+
const maxRemoval = config.maxWorktreeRemoval ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_REMOVAL;
|
|
251
|
+
const maxStatus = config.maxStatusChecks ?? DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS;
|
|
252
|
+
const maxPerRepoOps = maxCreation + maxUpdates + maxRemoval + maxStatus;
|
|
253
|
+
const totalMaxConcurrent = maxRepos * maxPerRepoOps;
|
|
254
|
+
if (totalMaxConcurrent > DEFAULT_CONFIG.PARALLELISM.MAX_SAFE_TOTAL_CONCURRENT_OPS) {
|
|
255
|
+
const safeMaxRepos = Math.floor(DEFAULT_CONFIG.PARALLELISM.MAX_SAFE_TOTAL_CONCURRENT_OPS / maxPerRepoOps);
|
|
256
|
+
throw new Error(
|
|
257
|
+
`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.`
|
|
258
|
+
);
|
|
259
|
+
}
|
|
117
260
|
}
|
|
118
261
|
resolveRepositoryConfig(repo, defaults, configDir, globalRetry) {
|
|
119
262
|
const resolved = {
|
|
120
263
|
name: repo.name,
|
|
121
264
|
repoUrl: repo.repoUrl,
|
|
122
265
|
worktreeDir: this.resolvePath(repo.worktreeDir, configDir),
|
|
123
|
-
cronSchedule: repo.cronSchedule ?? defaults?.cronSchedule ??
|
|
266
|
+
cronSchedule: repo.cronSchedule ?? defaults?.cronSchedule ?? DEFAULT_CONFIG.CRON_SCHEDULE,
|
|
124
267
|
runOnce: repo.runOnce ?? defaults?.runOnce ?? false
|
|
125
268
|
};
|
|
126
269
|
if (repo.bareRepoDir) {
|
|
@@ -139,6 +282,12 @@ var ConfigLoaderService = class {
|
|
|
139
282
|
...repo.retry || {}
|
|
140
283
|
};
|
|
141
284
|
}
|
|
285
|
+
if (repo.parallelism || defaults?.parallelism) {
|
|
286
|
+
resolved.parallelism = {
|
|
287
|
+
...defaults?.parallelism || {},
|
|
288
|
+
...repo.parallelism || {}
|
|
289
|
+
};
|
|
290
|
+
}
|
|
142
291
|
if (repo.updateExistingWorktrees !== void 0 || defaults?.updateExistingWorktrees !== void 0) {
|
|
143
292
|
resolved.updateExistingWorktrees = repo.updateExistingWorktrees ?? defaults?.updateExistingWorktrees ?? true;
|
|
144
293
|
}
|
|
@@ -158,7 +307,7 @@ var ConfigLoaderService = class {
|
|
|
158
307
|
return repositories.filter((repo) => {
|
|
159
308
|
return patterns.some((pattern) => {
|
|
160
309
|
if (pattern.includes("*")) {
|
|
161
|
-
const regex = new RegExp("^" + pattern.replace(/\*/g,
|
|
310
|
+
const regex = new RegExp("^" + pattern.replace(/\*/g, CONFIG_CONSTANTS.WILDCARD_PATTERN) + "$");
|
|
162
311
|
return regex.test(repo.name);
|
|
163
312
|
}
|
|
164
313
|
return repo.name === pattern;
|
|
@@ -294,6 +443,7 @@ var App_default = App;
|
|
|
294
443
|
// src/services/worktree-sync.service.ts
|
|
295
444
|
import * as fs5 from "fs/promises";
|
|
296
445
|
import * as path5 from "path";
|
|
446
|
+
import pLimit from "p-limit";
|
|
297
447
|
|
|
298
448
|
// src/utils/date-filter.ts
|
|
299
449
|
function parseDuration(durationStr) {
|
|
@@ -373,6 +523,7 @@ var DEFAULT_OPTIONS = {
|
|
|
373
523
|
maxDelayMs: 6e5,
|
|
374
524
|
// 10 minutes
|
|
375
525
|
backoffMultiplier: 2,
|
|
526
|
+
jitterMs: 0,
|
|
376
527
|
shouldRetry: (error, context) => {
|
|
377
528
|
const err = error;
|
|
378
529
|
if (isLfsErrorFromError(error)) {
|
|
@@ -430,7 +581,9 @@ async function retry(fn, options = {}) {
|
|
|
430
581
|
if (lfsContext.isLfsError && opts.lfsRetryHandler) {
|
|
431
582
|
opts.lfsRetryHandler(lfsContext);
|
|
432
583
|
}
|
|
433
|
-
const
|
|
584
|
+
const baseDelay = Math.min(opts.initialDelayMs * Math.pow(opts.backoffMultiplier, attempt - 1), opts.maxDelayMs);
|
|
585
|
+
const jitter = opts.jitterMs > 0 ? Math.random() * opts.jitterMs : 0;
|
|
586
|
+
const delay = baseDelay + jitter;
|
|
434
587
|
opts.onRetry(error, attempt, lfsContext);
|
|
435
588
|
await new Promise((resolve6) => setTimeout(resolve6, delay));
|
|
436
589
|
attempt++;
|
|
@@ -438,6 +591,106 @@ async function retry(fn, options = {}) {
|
|
|
438
591
|
}
|
|
439
592
|
}
|
|
440
593
|
|
|
594
|
+
// src/utils/timing.ts
|
|
595
|
+
import Table from "cli-table3";
|
|
596
|
+
var Timer = class {
|
|
597
|
+
startTime;
|
|
598
|
+
endTime;
|
|
599
|
+
constructor() {
|
|
600
|
+
this.startTime = Date.now();
|
|
601
|
+
}
|
|
602
|
+
stop() {
|
|
603
|
+
this.endTime = Date.now();
|
|
604
|
+
return this.getDuration();
|
|
605
|
+
}
|
|
606
|
+
getDuration() {
|
|
607
|
+
const end = this.endTime ?? Date.now();
|
|
608
|
+
return end - this.startTime;
|
|
609
|
+
}
|
|
610
|
+
};
|
|
611
|
+
var PhaseTimer = class {
|
|
612
|
+
phases = /* @__PURE__ */ new Map();
|
|
613
|
+
currentPhase;
|
|
614
|
+
startPhase(name, parallelism) {
|
|
615
|
+
if (this.currentPhase) {
|
|
616
|
+
this.endPhase();
|
|
617
|
+
}
|
|
618
|
+
this.currentPhase = name;
|
|
619
|
+
this.phases.set(name, { timer: new Timer(), parallelism });
|
|
620
|
+
}
|
|
621
|
+
endPhase() {
|
|
622
|
+
if (this.currentPhase) {
|
|
623
|
+
const phase = this.phases.get(this.currentPhase);
|
|
624
|
+
if (phase) {
|
|
625
|
+
phase.timer.stop();
|
|
626
|
+
}
|
|
627
|
+
this.currentPhase = void 0;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
setPhaseCount(name, count) {
|
|
631
|
+
const phase = this.phases.get(name);
|
|
632
|
+
if (phase) {
|
|
633
|
+
phase.count = count;
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
getResults() {
|
|
637
|
+
if (this.currentPhase) {
|
|
638
|
+
this.endPhase();
|
|
639
|
+
}
|
|
640
|
+
const results = [];
|
|
641
|
+
for (const [name, { timer, count, parallelism }] of this.phases.entries()) {
|
|
642
|
+
const duration = timer.getDuration();
|
|
643
|
+
const result = {
|
|
644
|
+
name,
|
|
645
|
+
duration,
|
|
646
|
+
count
|
|
647
|
+
};
|
|
648
|
+
if (count && count > 0 && parallelism && parallelism > 1) {
|
|
649
|
+
const batches = Math.ceil(count / parallelism);
|
|
650
|
+
const avgTimePerBatch = duration / batches;
|
|
651
|
+
const theoreticalSequentialTime = count * avgTimePerBatch;
|
|
652
|
+
result.efficiency = theoreticalSequentialTime > 0 ? Math.round(theoreticalSequentialTime / duration * 100) : 100;
|
|
653
|
+
}
|
|
654
|
+
results.push(result);
|
|
655
|
+
}
|
|
656
|
+
return results;
|
|
657
|
+
}
|
|
658
|
+
};
|
|
659
|
+
function formatDuration2(ms) {
|
|
660
|
+
if (ms < 1e3) {
|
|
661
|
+
return `${ms}ms`;
|
|
662
|
+
}
|
|
663
|
+
if (ms < 6e4) {
|
|
664
|
+
return `${(ms / 1e3).toFixed(1)}s`;
|
|
665
|
+
}
|
|
666
|
+
const minutes = Math.floor(ms / 6e4);
|
|
667
|
+
const seconds = Math.floor(ms % 6e4 / 1e3);
|
|
668
|
+
return `${minutes}m ${seconds}s`;
|
|
669
|
+
}
|
|
670
|
+
function formatTimingTable(totalDuration, phaseResults, repoName) {
|
|
671
|
+
const header = repoName ? `Performance Summary - [${repoName}]` : "Performance Summary";
|
|
672
|
+
const table = new Table({
|
|
673
|
+
head: ["Operation", "Duration", "Efficiency"],
|
|
674
|
+
colWidths: [35, 12, 12],
|
|
675
|
+
style: {
|
|
676
|
+
head: ["cyan", "bold"],
|
|
677
|
+
border: ["gray"]
|
|
678
|
+
}
|
|
679
|
+
});
|
|
680
|
+
table.push([{ colSpan: 3, content: header, hAlign: "center" }]);
|
|
681
|
+
table.push(["Total Sync", formatDuration2(totalDuration), ""]);
|
|
682
|
+
for (let i = 0; i < phaseResults.length; i++) {
|
|
683
|
+
const result = phaseResults[i];
|
|
684
|
+
const isLast = i === phaseResults.length - 1;
|
|
685
|
+
const countStr = result.count ? ` (${result.count})` : "";
|
|
686
|
+
const prefix = isLast ? "\u2514\u2500" : "\u251C\u2500";
|
|
687
|
+
const name = ` ${prefix} ${result.name}${countStr}`;
|
|
688
|
+
const efficiency = result.efficiency ? `${result.efficiency}%` : "";
|
|
689
|
+
table.push([name, formatDuration2(result.duration), efficiency]);
|
|
690
|
+
}
|
|
691
|
+
return table.toString();
|
|
692
|
+
}
|
|
693
|
+
|
|
441
694
|
// src/services/git.service.ts
|
|
442
695
|
import * as fs4 from "fs/promises";
|
|
443
696
|
import * as path4 from "path";
|
|
@@ -469,29 +722,54 @@ function getDefaultBareRepoDir(repoUrl, baseDir = ".bare") {
|
|
|
469
722
|
return `${baseDir}/${repoName}`;
|
|
470
723
|
}
|
|
471
724
|
|
|
725
|
+
// src/services/logger.service.ts
|
|
726
|
+
var Logger = class _Logger {
|
|
727
|
+
repoName;
|
|
728
|
+
debugEnabled;
|
|
729
|
+
constructor(options = {}) {
|
|
730
|
+
this.repoName = options.repoName;
|
|
731
|
+
this.debugEnabled = options.debug ?? false;
|
|
732
|
+
}
|
|
733
|
+
prefix() {
|
|
734
|
+
return this.repoName ? `[${this.repoName}] ` : "";
|
|
735
|
+
}
|
|
736
|
+
debug(message, ...args) {
|
|
737
|
+
if (!this.debugEnabled) return;
|
|
738
|
+
console.log(this.prefix() + this.formatMessage(message, args));
|
|
739
|
+
}
|
|
740
|
+
info(message, ...args) {
|
|
741
|
+
console.log(this.prefix() + this.formatMessage(message, args));
|
|
742
|
+
}
|
|
743
|
+
warn(message, ...args) {
|
|
744
|
+
console.warn(this.prefix() + this.formatMessage(message, args));
|
|
745
|
+
}
|
|
746
|
+
error(message, error) {
|
|
747
|
+
if (error instanceof Error) {
|
|
748
|
+
console.error(this.prefix() + message, error);
|
|
749
|
+
} else if (error) {
|
|
750
|
+
console.error(this.prefix() + message, error);
|
|
751
|
+
} else {
|
|
752
|
+
console.error(this.prefix() + message);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
table(content) {
|
|
756
|
+
console.log("\n" + content + "\n");
|
|
757
|
+
}
|
|
758
|
+
formatMessage(message, args) {
|
|
759
|
+
if (args.length === 0) {
|
|
760
|
+
return message;
|
|
761
|
+
}
|
|
762
|
+
return args.reduce((msg, arg) => msg.replace("%s", String(arg)), message);
|
|
763
|
+
}
|
|
764
|
+
static createDefault(repoName, debug) {
|
|
765
|
+
return new _Logger({ repoName, debug });
|
|
766
|
+
}
|
|
767
|
+
};
|
|
768
|
+
|
|
472
769
|
// src/services/worktree-metadata.service.ts
|
|
473
770
|
import * as fs2 from "fs/promises";
|
|
474
771
|
import * as path2 from "path";
|
|
475
772
|
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
773
|
var WorktreeMetadataService = class {
|
|
496
774
|
/**
|
|
497
775
|
* Gets the internal worktree directory name from a worktree path.
|
|
@@ -502,7 +780,12 @@ var WorktreeMetadataService = class {
|
|
|
502
780
|
return path2.basename(worktreePath);
|
|
503
781
|
}
|
|
504
782
|
async getMetadataPath(bareRepoPath, worktreeName) {
|
|
505
|
-
return path2.join(
|
|
783
|
+
return path2.join(
|
|
784
|
+
bareRepoPath,
|
|
785
|
+
METADATA_CONSTANTS.WORKTREE_METADATA_PATH,
|
|
786
|
+
worktreeName,
|
|
787
|
+
METADATA_CONSTANTS.METADATA_FILENAME
|
|
788
|
+
);
|
|
506
789
|
}
|
|
507
790
|
async getMetadataPathFromWorktreePath(bareRepoPath, worktreePath) {
|
|
508
791
|
const worktreeDirName = this.getWorktreeDirectoryName(worktreePath);
|
|
@@ -537,7 +820,12 @@ var WorktreeMetadataService = class {
|
|
|
537
820
|
const branchName = path2.basename(worktreePath);
|
|
538
821
|
const parentDir = path2.dirname(worktreePath);
|
|
539
822
|
const possibleBranchWithSlash = path2.join(path2.basename(parentDir), branchName);
|
|
540
|
-
const oldPath = path2.join(
|
|
823
|
+
const oldPath = path2.join(
|
|
824
|
+
bareRepoPath,
|
|
825
|
+
METADATA_CONSTANTS.WORKTREE_METADATA_PATH,
|
|
826
|
+
possibleBranchWithSlash,
|
|
827
|
+
METADATA_CONSTANTS.METADATA_FILENAME
|
|
828
|
+
);
|
|
541
829
|
const content = await fs2.readFile(oldPath, "utf-8");
|
|
542
830
|
const metadata = JSON.parse(content);
|
|
543
831
|
if (!await this.validateMetadata(metadata)) {
|
|
@@ -620,7 +908,7 @@ var WorktreeMetadataService = class {
|
|
|
620
908
|
}
|
|
621
909
|
} catch {
|
|
622
910
|
}
|
|
623
|
-
const parentBranch = defaultBranch ||
|
|
911
|
+
const parentBranch = defaultBranch || GIT_CONSTANTS.DEFAULT_BRANCH;
|
|
624
912
|
await this.createInitialMetadataFromPath(
|
|
625
913
|
bareRepoPath,
|
|
626
914
|
worktreePath,
|
|
@@ -853,7 +1141,7 @@ var WorktreeStatusService = class {
|
|
|
853
1141
|
const lines = result.split("\n").filter((line) => line.trim());
|
|
854
1142
|
for (const line of lines) {
|
|
855
1143
|
const firstChar = line.charAt(0);
|
|
856
|
-
if (firstChar ===
|
|
1144
|
+
if (firstChar === GIT_CONSTANTS.SUBMODULE_STATUS_ADDED || firstChar === GIT_CONSTANTS.SUBMODULE_STATUS_REMOVED) {
|
|
857
1145
|
return true;
|
|
858
1146
|
}
|
|
859
1147
|
}
|
|
@@ -951,7 +1239,7 @@ var WorktreeStatusService = class {
|
|
|
951
1239
|
const modifiedSubmodules = [];
|
|
952
1240
|
for (const line of lines) {
|
|
953
1241
|
const firstChar = line.charAt(0);
|
|
954
|
-
if (firstChar ===
|
|
1242
|
+
if (firstChar === GIT_CONSTANTS.SUBMODULE_STATUS_ADDED || firstChar === GIT_CONSTANTS.SUBMODULE_STATUS_REMOVED) {
|
|
955
1243
|
const match = line.match(/^[+-]\s*(\S+)/);
|
|
956
1244
|
if (match) {
|
|
957
1245
|
modifiedSubmodules.push(match[1]);
|
|
@@ -1000,7 +1288,7 @@ var WorktreeStatusService = class {
|
|
|
1000
1288
|
return files.filter((f) => !ignoredFiles.has(f));
|
|
1001
1289
|
} catch (error) {
|
|
1002
1290
|
const errorMessage = getErrorMessage(error);
|
|
1003
|
-
if (errorMessage.includes(
|
|
1291
|
+
if (errorMessage.includes(GIT_CONSTANTS.GIT_CHECK_IGNORE_NO_MATCH)) {
|
|
1004
1292
|
return files;
|
|
1005
1293
|
}
|
|
1006
1294
|
console.warn(`Warning: Could not check gitignore status for files in ${worktreePath}: ${errorMessage}`);
|
|
@@ -1021,7 +1309,7 @@ var WorktreeStatusService = class {
|
|
|
1021
1309
|
const stat4 = await fs3.stat(gitPath);
|
|
1022
1310
|
if (stat4.isFile()) {
|
|
1023
1311
|
const content = await fs3.readFile(gitPath, "utf-8");
|
|
1024
|
-
const gitdirMatch = content.match(
|
|
1312
|
+
const gitdirMatch = content.match(new RegExp(`^${GIT_CONSTANTS.GITDIR_PREFIX}\\s*(.+)$`, "m"));
|
|
1025
1313
|
if (gitdirMatch) {
|
|
1026
1314
|
return path3.resolve(worktreePath, gitdirMatch[1].trim());
|
|
1027
1315
|
}
|
|
@@ -1043,31 +1331,33 @@ var WorktreeStatusService = class {
|
|
|
1043
1331
|
|
|
1044
1332
|
// src/services/git.service.ts
|
|
1045
1333
|
var GitService = class {
|
|
1046
|
-
constructor(config) {
|
|
1334
|
+
constructor(config, logger) {
|
|
1047
1335
|
this.config = config;
|
|
1336
|
+
this.logger = logger ?? Logger.createDefault(void 0, config.debug);
|
|
1048
1337
|
this.bareRepoPath = this.config.bareRepoDir || getDefaultBareRepoDir(this.config.repoUrl);
|
|
1049
|
-
this.mainWorktreePath = path4.join(this.config.worktreeDir,
|
|
1338
|
+
this.mainWorktreePath = path4.join(this.config.worktreeDir, GIT_CONSTANTS.DEFAULT_BRANCH);
|
|
1050
1339
|
this.metadataService = new WorktreeMetadataService();
|
|
1051
1340
|
this.statusService = new WorktreeStatusService({ skipLfs: this.config.skipLfs });
|
|
1052
1341
|
}
|
|
1053
1342
|
git = null;
|
|
1054
1343
|
bareRepoPath;
|
|
1055
1344
|
mainWorktreePath;
|
|
1056
|
-
defaultBranch =
|
|
1345
|
+
defaultBranch = GIT_CONSTANTS.DEFAULT_BRANCH;
|
|
1057
1346
|
// Will be updated after detection
|
|
1058
1347
|
metadataService;
|
|
1059
1348
|
statusService;
|
|
1349
|
+
logger;
|
|
1060
1350
|
async initialize() {
|
|
1061
1351
|
const { repoUrl } = this.config;
|
|
1062
1352
|
try {
|
|
1063
1353
|
await fs4.access(path4.join(this.bareRepoPath, "HEAD"));
|
|
1064
|
-
|
|
1354
|
+
this.logger.info(`Bare repository at "${this.bareRepoPath}" already exists. Using it.`);
|
|
1065
1355
|
} catch {
|
|
1066
|
-
|
|
1356
|
+
this.logger.info(`Cloning from "${repoUrl}" as bare repository into "${this.bareRepoPath}"...`);
|
|
1067
1357
|
await fs4.mkdir(path4.dirname(this.bareRepoPath), { recursive: true });
|
|
1068
|
-
const cloneGit = this.isLfsSkipEnabled() ? simpleGit3().env({ GIT_LFS_SKIP_SMUDGE: "1" }) : simpleGit3();
|
|
1358
|
+
const cloneGit = this.isLfsSkipEnabled() ? simpleGit3().env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3();
|
|
1069
1359
|
await cloneGit.clone(repoUrl, this.bareRepoPath, ["--bare"]);
|
|
1070
|
-
|
|
1360
|
+
this.logger.info("\u2705 Clone successful.");
|
|
1071
1361
|
}
|
|
1072
1362
|
const bareGit = simpleGit3(this.bareRepoPath);
|
|
1073
1363
|
try {
|
|
@@ -1079,11 +1369,11 @@ var GitService = class {
|
|
|
1079
1369
|
} catch {
|
|
1080
1370
|
await bareGit.addConfig("remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*");
|
|
1081
1371
|
}
|
|
1082
|
-
|
|
1372
|
+
this.logger.info("Fetching remote branches...");
|
|
1083
1373
|
await bareGit.fetch(["--all"]);
|
|
1084
1374
|
this.defaultBranch = await this.detectDefaultBranch(bareGit);
|
|
1085
1375
|
this.mainWorktreePath = path4.join(this.config.worktreeDir, this.defaultBranch);
|
|
1086
|
-
|
|
1376
|
+
this.logger.info(`Detected default branch: ${this.defaultBranch}`);
|
|
1087
1377
|
let needsMainWorktree = true;
|
|
1088
1378
|
try {
|
|
1089
1379
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
@@ -1091,7 +1381,7 @@ var GitService = class {
|
|
|
1091
1381
|
} catch {
|
|
1092
1382
|
}
|
|
1093
1383
|
if (needsMainWorktree) {
|
|
1094
|
-
|
|
1384
|
+
this.logger.info(`Creating ${this.defaultBranch} worktree at "${this.mainWorktreePath}"...`);
|
|
1095
1385
|
await fs4.mkdir(this.config.worktreeDir, { recursive: true });
|
|
1096
1386
|
const absoluteWorktreePath = path4.resolve(this.mainWorktreePath);
|
|
1097
1387
|
try {
|
|
@@ -1099,7 +1389,7 @@ var GitService = class {
|
|
|
1099
1389
|
const defaultBranchExists = branches.all.includes(this.defaultBranch);
|
|
1100
1390
|
if (defaultBranchExists) {
|
|
1101
1391
|
await bareGit.raw(["worktree", "add", absoluteWorktreePath, this.defaultBranch]);
|
|
1102
|
-
const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(absoluteWorktreePath).env({ GIT_LFS_SKIP_SMUDGE: "1" }) : simpleGit3(absoluteWorktreePath);
|
|
1392
|
+
const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(absoluteWorktreePath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(absoluteWorktreePath);
|
|
1103
1393
|
await worktreeGit.branch(["--set-upstream-to", `origin/${this.defaultBranch}`, this.defaultBranch]);
|
|
1104
1394
|
} else {
|
|
1105
1395
|
await bareGit.raw([
|
|
@@ -1115,17 +1405,17 @@ var GitService = class {
|
|
|
1115
1405
|
} catch (error) {
|
|
1116
1406
|
const errorMessage = getErrorMessage(error);
|
|
1117
1407
|
if (errorMessage.includes("already exists")) {
|
|
1118
|
-
|
|
1408
|
+
this.logger.info(
|
|
1119
1409
|
`${this.defaultBranch} worktree directory already exists at '${absoluteWorktreePath}', skipping creation.`
|
|
1120
1410
|
);
|
|
1121
1411
|
} else {
|
|
1122
|
-
|
|
1412
|
+
this.logger.warn(`Failed to create ${this.defaultBranch} worktree with tracking, using simple add: ${error}`);
|
|
1123
1413
|
try {
|
|
1124
1414
|
await bareGit.raw(["worktree", "add", absoluteWorktreePath, this.defaultBranch]);
|
|
1125
1415
|
} catch (fallbackError) {
|
|
1126
1416
|
const fallbackErrorMessage = getErrorMessage(fallbackError);
|
|
1127
1417
|
if (fallbackErrorMessage.includes("already exists")) {
|
|
1128
|
-
|
|
1418
|
+
this.logger.info(
|
|
1129
1419
|
`${this.defaultBranch} worktree directory already exists at '${absoluteWorktreePath}', skipping creation.`
|
|
1130
1420
|
);
|
|
1131
1421
|
} else {
|
|
@@ -1139,8 +1429,8 @@ var GitService = class {
|
|
|
1139
1429
|
(w) => path4.resolve(w.path) === path4.resolve(this.mainWorktreePath)
|
|
1140
1430
|
);
|
|
1141
1431
|
if (!mainWorktreeRegistered) {
|
|
1142
|
-
if (process.env.NODE_ENV !==
|
|
1143
|
-
|
|
1432
|
+
if (process.env.NODE_ENV !== ENV_CONSTANTS.NODE_ENV_TEST) {
|
|
1433
|
+
this.logger.warn(`Main worktree was created but not found in worktree list. This may cause issues.`);
|
|
1144
1434
|
}
|
|
1145
1435
|
}
|
|
1146
1436
|
}
|
|
@@ -1158,9 +1448,9 @@ var GitService = class {
|
|
|
1158
1448
|
}
|
|
1159
1449
|
async fetchAll() {
|
|
1160
1450
|
const git = this.getGit();
|
|
1161
|
-
|
|
1451
|
+
this.logger.info("Fetching latest data from remote...");
|
|
1162
1452
|
if (this.isLfsSkipEnabled()) {
|
|
1163
|
-
await git.env({ GIT_LFS_SKIP_SMUDGE: "1" }).fetch(["--all", "--prune"]);
|
|
1453
|
+
await git.env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }).fetch(["--all", "--prune"]);
|
|
1164
1454
|
} else {
|
|
1165
1455
|
await git.fetch(["--all", "--prune"]);
|
|
1166
1456
|
}
|
|
@@ -1168,7 +1458,7 @@ var GitService = class {
|
|
|
1168
1458
|
async fetchBranch(branchName) {
|
|
1169
1459
|
const git = this.getGit();
|
|
1170
1460
|
if (this.isLfsSkipEnabled()) {
|
|
1171
|
-
await git.env({ GIT_LFS_SKIP_SMUDGE: "1" }).fetch(["origin", branchName, "--prune"]);
|
|
1461
|
+
await git.env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }).fetch(["origin", branchName, "--prune"]);
|
|
1172
1462
|
} else {
|
|
1173
1463
|
await git.fetch(["origin", branchName, "--prune"]);
|
|
1174
1464
|
}
|
|
@@ -1211,7 +1501,7 @@ var GitService = class {
|
|
|
1211
1501
|
return;
|
|
1212
1502
|
}
|
|
1213
1503
|
if (this.config.debug) {
|
|
1214
|
-
|
|
1504
|
+
this.logger.info(` - Verifying ${lfsFileList.length} LFS files are downloaded...`);
|
|
1215
1505
|
}
|
|
1216
1506
|
const sampleSize = Math.min(5, lfsFileList.length);
|
|
1217
1507
|
const samplesToCheck = [];
|
|
@@ -1233,7 +1523,7 @@ var GitService = class {
|
|
|
1233
1523
|
const buffer = Buffer.alloc(200);
|
|
1234
1524
|
const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0);
|
|
1235
1525
|
const header = buffer.subarray(0, bytesRead).toString("utf8");
|
|
1236
|
-
if (header.startsWith(
|
|
1526
|
+
if (header.startsWith(GIT_CONSTANTS.LFS_HEADER)) {
|
|
1237
1527
|
allDownloaded = false;
|
|
1238
1528
|
notDownloaded.push(file);
|
|
1239
1529
|
}
|
|
@@ -1247,7 +1537,7 @@ var GitService = class {
|
|
|
1247
1537
|
}
|
|
1248
1538
|
if (allDownloaded) {
|
|
1249
1539
|
if (this.config.debug) {
|
|
1250
|
-
|
|
1540
|
+
this.logger.info(` - \u2705 LFS files verified (${samplesToCheck.length} samples checked)`);
|
|
1251
1541
|
}
|
|
1252
1542
|
return;
|
|
1253
1543
|
}
|
|
@@ -1256,16 +1546,16 @@ var GitService = class {
|
|
|
1256
1546
|
await new Promise((resolve6) => setTimeout(resolve6, retryDelay));
|
|
1257
1547
|
}
|
|
1258
1548
|
}
|
|
1259
|
-
|
|
1549
|
+
this.logger.warn(
|
|
1260
1550
|
` - \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
1551
|
);
|
|
1262
1552
|
} catch (error) {
|
|
1263
|
-
|
|
1553
|
+
this.logger.warn(` - \u26A0\uFE0F Warning: Could not verify LFS files for '${branchName}': ${error}`);
|
|
1264
1554
|
}
|
|
1265
1555
|
}
|
|
1266
1556
|
async createWorktreeMetadata(bareGit, worktreePath, branchName) {
|
|
1267
1557
|
try {
|
|
1268
|
-
const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(worktreePath).env({ GIT_LFS_SKIP_SMUDGE: "1" }) : simpleGit3(worktreePath);
|
|
1558
|
+
const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(worktreePath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(worktreePath);
|
|
1269
1559
|
const currentCommit = await worktreeGit.revparse(["HEAD"]);
|
|
1270
1560
|
const parentCommit = await bareGit.revparse([this.defaultBranch]);
|
|
1271
1561
|
await this.metadataService.createInitialMetadataFromPath(
|
|
@@ -1277,12 +1567,12 @@ var GitService = class {
|
|
|
1277
1567
|
parentCommit.trim()
|
|
1278
1568
|
);
|
|
1279
1569
|
} catch (metadataError) {
|
|
1280
|
-
|
|
1570
|
+
this.logger.error(` - \u274C Failed to create metadata for '${branchName}': ${metadataError}`);
|
|
1281
1571
|
throw new Error(`Metadata creation failed for ${branchName}. This worktree cannot be auto-managed.`);
|
|
1282
1572
|
}
|
|
1283
1573
|
}
|
|
1284
1574
|
async addWorktree(branchName, worktreePath) {
|
|
1285
|
-
const bareGit = this.isLfsSkipEnabled() ? simpleGit3(this.bareRepoPath).env({ GIT_LFS_SKIP_SMUDGE: "1" }) : simpleGit3(this.bareRepoPath);
|
|
1575
|
+
const bareGit = this.isLfsSkipEnabled() ? simpleGit3(this.bareRepoPath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(this.bareRepoPath);
|
|
1286
1576
|
const absoluteWorktreePath = path4.resolve(worktreePath);
|
|
1287
1577
|
await fs4.mkdir(path4.dirname(absoluteWorktreePath), { recursive: true });
|
|
1288
1578
|
try {
|
|
@@ -1290,10 +1580,10 @@ var GitService = class {
|
|
|
1290
1580
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
1291
1581
|
const isValidWorktree = worktrees.some((w) => path4.resolve(w.path) === absoluteWorktreePath);
|
|
1292
1582
|
if (isValidWorktree) {
|
|
1293
|
-
|
|
1583
|
+
this.logger.info(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
|
|
1294
1584
|
return;
|
|
1295
1585
|
} else {
|
|
1296
|
-
|
|
1586
|
+
this.logger.info(` - Cleaning up orphaned directory at '${absoluteWorktreePath}'`);
|
|
1297
1587
|
await fs4.rm(absoluteWorktreePath, { recursive: true, force: true });
|
|
1298
1588
|
}
|
|
1299
1589
|
} catch {
|
|
@@ -1303,7 +1593,7 @@ var GitService = class {
|
|
|
1303
1593
|
const localBranchExists = branches.all.includes(branchName);
|
|
1304
1594
|
if (localBranchExists || branchName.includes("/")) {
|
|
1305
1595
|
await bareGit.raw(["worktree", "add", absoluteWorktreePath, branchName]);
|
|
1306
|
-
const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(absoluteWorktreePath).env({ GIT_LFS_SKIP_SMUDGE: "1" }) : simpleGit3(absoluteWorktreePath);
|
|
1596
|
+
const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(absoluteWorktreePath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(absoluteWorktreePath);
|
|
1307
1597
|
await worktreeGit.branch(["--set-upstream-to", `origin/${branchName}`, branchName]);
|
|
1308
1598
|
} else {
|
|
1309
1599
|
await bareGit.raw([
|
|
@@ -1316,7 +1606,7 @@ var GitService = class {
|
|
|
1316
1606
|
`origin/${branchName}`
|
|
1317
1607
|
]);
|
|
1318
1608
|
}
|
|
1319
|
-
|
|
1609
|
+
this.logger.info(` - Created worktree for '${branchName}' with tracking to origin/${branchName}`);
|
|
1320
1610
|
if (!this.isLfsSkipEnabled()) {
|
|
1321
1611
|
await this.verifyLfsFilesDownloaded(absoluteWorktreePath, branchName);
|
|
1322
1612
|
}
|
|
@@ -1327,7 +1617,13 @@ var GitService = class {
|
|
|
1327
1617
|
throw error;
|
|
1328
1618
|
}
|
|
1329
1619
|
if (errorMessage.includes("already registered worktree")) {
|
|
1330
|
-
|
|
1620
|
+
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
1621
|
+
const existingWorktree = worktrees.find((w) => path4.resolve(w.path) === absoluteWorktreePath);
|
|
1622
|
+
if (existingWorktree && !existingWorktree.isPrunable) {
|
|
1623
|
+
this.logger.info(` - Worktree for '${branchName}' was created by concurrent operation`);
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
this.logger.warn(` - Worktree already registered but missing. Pruning and retrying...`);
|
|
1331
1627
|
await bareGit.raw(["worktree", "prune"]);
|
|
1332
1628
|
try {
|
|
1333
1629
|
await fs4.rm(absoluteWorktreePath, { recursive: true, force: true });
|
|
@@ -1343,53 +1639,66 @@ var GitService = class {
|
|
|
1343
1639
|
absoluteWorktreePath,
|
|
1344
1640
|
`origin/${branchName}`
|
|
1345
1641
|
]);
|
|
1346
|
-
|
|
1642
|
+
this.logger.info(` - Created worktree for '${branchName}' after pruning`);
|
|
1347
1643
|
if (!this.isLfsSkipEnabled()) {
|
|
1348
1644
|
await this.verifyLfsFilesDownloaded(absoluteWorktreePath, branchName);
|
|
1349
1645
|
}
|
|
1350
1646
|
await this.createWorktreeMetadata(bareGit, absoluteWorktreePath, branchName);
|
|
1351
1647
|
return;
|
|
1352
1648
|
} catch (retryError) {
|
|
1353
|
-
|
|
1649
|
+
this.logger.error(` - Failed to create worktree after pruning: ${retryError}`);
|
|
1354
1650
|
throw retryError;
|
|
1355
1651
|
}
|
|
1356
1652
|
}
|
|
1357
|
-
|
|
1653
|
+
this.logger.warn(` - Failed to create worktree with tracking, falling back to simple add: ${error}`);
|
|
1358
1654
|
try {
|
|
1359
1655
|
await fs4.access(absoluteWorktreePath);
|
|
1360
1656
|
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
1361
1657
|
const isValidWorktree = worktrees.some((w) => path4.resolve(w.path) === absoluteWorktreePath);
|
|
1362
1658
|
if (isValidWorktree) {
|
|
1363
|
-
|
|
1659
|
+
this.logger.info(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
|
|
1364
1660
|
return;
|
|
1365
1661
|
} else {
|
|
1366
|
-
|
|
1662
|
+
this.logger.info(` - Cleaning up orphaned directory at '${absoluteWorktreePath}' before fallback attempt`);
|
|
1367
1663
|
await fs4.rm(absoluteWorktreePath, { recursive: true, force: true });
|
|
1368
1664
|
}
|
|
1369
1665
|
} catch {
|
|
1370
1666
|
}
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1667
|
+
try {
|
|
1668
|
+
await bareGit.raw(["worktree", "add", absoluteWorktreePath, branchName]);
|
|
1669
|
+
this.logger.info(` - Created worktree for '${branchName}' (without tracking)`);
|
|
1670
|
+
if (!this.isLfsSkipEnabled()) {
|
|
1671
|
+
await this.verifyLfsFilesDownloaded(absoluteWorktreePath, branchName);
|
|
1672
|
+
}
|
|
1673
|
+
await this.createWorktreeMetadata(bareGit, absoluteWorktreePath, branchName);
|
|
1674
|
+
} catch (fallbackError) {
|
|
1675
|
+
const fallbackErrorMessage = getErrorMessage(fallbackError);
|
|
1676
|
+
if (fallbackErrorMessage.includes("already registered worktree")) {
|
|
1677
|
+
const worktrees = await this.getWorktreesFromBare(bareGit);
|
|
1678
|
+
const existingWorktree = worktrees.find((w) => path4.resolve(w.path) === absoluteWorktreePath);
|
|
1679
|
+
if (existingWorktree && !existingWorktree.isPrunable) {
|
|
1680
|
+
this.logger.info(` - Worktree for '${branchName}' was created by concurrent operation during fallback`);
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
throw fallbackError;
|
|
1375
1685
|
}
|
|
1376
|
-
await this.createWorktreeMetadata(bareGit, absoluteWorktreePath, branchName);
|
|
1377
1686
|
}
|
|
1378
1687
|
}
|
|
1379
1688
|
async removeWorktree(worktreePath) {
|
|
1380
1689
|
const bareGit = simpleGit3(this.bareRepoPath);
|
|
1381
1690
|
await bareGit.raw(["worktree", "remove", worktreePath, "--force"]);
|
|
1382
|
-
|
|
1691
|
+
this.logger.info(` - \u2705 Safely removed stale worktree at '${worktreePath}'.`);
|
|
1383
1692
|
try {
|
|
1384
1693
|
await this.metadataService.deleteMetadataFromPath(this.bareRepoPath, worktreePath);
|
|
1385
1694
|
} catch (metadataError) {
|
|
1386
|
-
|
|
1695
|
+
this.logger.warn(`Failed to delete metadata for worktree: ${metadataError}`);
|
|
1387
1696
|
}
|
|
1388
1697
|
}
|
|
1389
1698
|
async pruneWorktrees() {
|
|
1390
1699
|
const bareGit = simpleGit3(this.bareRepoPath);
|
|
1391
1700
|
await bareGit.raw(["worktree", "prune"]);
|
|
1392
|
-
|
|
1701
|
+
this.logger.info("Pruned worktree metadata.");
|
|
1393
1702
|
}
|
|
1394
1703
|
async checkWorktreeStatus(worktreePath) {
|
|
1395
1704
|
const worktreeGit = simpleGit3(worktreePath);
|
|
@@ -1428,7 +1737,7 @@ var GitService = class {
|
|
|
1428
1737
|
const unpushedCount = parseInt(result.trim(), 10);
|
|
1429
1738
|
return unpushedCount > 0;
|
|
1430
1739
|
} catch (error) {
|
|
1431
|
-
|
|
1740
|
+
this.logger.error(`Error checking unpushed commits: ${error}`);
|
|
1432
1741
|
return false;
|
|
1433
1742
|
}
|
|
1434
1743
|
}
|
|
@@ -1466,7 +1775,7 @@ var GitService = class {
|
|
|
1466
1775
|
}
|
|
1467
1776
|
return false;
|
|
1468
1777
|
}
|
|
1469
|
-
|
|
1778
|
+
this.logger.error(
|
|
1470
1779
|
`Unexpected error checking upstream status for ${worktreePath}. This might indicate a real issue rather than a missing upstream. Error: ${errorMessage}`
|
|
1471
1780
|
);
|
|
1472
1781
|
return false;
|
|
@@ -1478,7 +1787,7 @@ var GitService = class {
|
|
|
1478
1787
|
const stashList = await worktreeGit.stashList();
|
|
1479
1788
|
return stashList.total > 0;
|
|
1480
1789
|
} catch (error) {
|
|
1481
|
-
|
|
1790
|
+
this.logger.error(`Error checking stash: ${error}`);
|
|
1482
1791
|
return true;
|
|
1483
1792
|
}
|
|
1484
1793
|
}
|
|
@@ -1544,7 +1853,7 @@ var GitService = class {
|
|
|
1544
1853
|
} catch {
|
|
1545
1854
|
try {
|
|
1546
1855
|
const remoteBranches = await bareGit.branch(["-r"]);
|
|
1547
|
-
const commonDefaults =
|
|
1856
|
+
const commonDefaults = GIT_CONSTANTS.COMMON_DEFAULT_BRANCHES;
|
|
1548
1857
|
for (const defaultName of commonDefaults) {
|
|
1549
1858
|
if (remoteBranches.all.some((branch) => branch === `origin/${defaultName}`)) {
|
|
1550
1859
|
return defaultName;
|
|
@@ -1554,10 +1863,10 @@ var GitService = class {
|
|
|
1554
1863
|
}
|
|
1555
1864
|
}
|
|
1556
1865
|
}
|
|
1557
|
-
return
|
|
1866
|
+
return GIT_CONSTANTS.DEFAULT_BRANCH;
|
|
1558
1867
|
}
|
|
1559
1868
|
isLfsSkipEnabled() {
|
|
1560
|
-
return this.config.skipLfs || process.env.GIT_LFS_SKIP_SMUDGE === "1";
|
|
1869
|
+
return this.config.skipLfs || process.env[ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE] === "1";
|
|
1561
1870
|
}
|
|
1562
1871
|
async getWorktrees() {
|
|
1563
1872
|
const bareGit = simpleGit3(this.bareRepoPath);
|
|
@@ -1579,7 +1888,7 @@ var GitService = class {
|
|
|
1579
1888
|
}
|
|
1580
1889
|
}
|
|
1581
1890
|
async updateWorktree(worktreePath) {
|
|
1582
|
-
const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(worktreePath).env({ GIT_LFS_SKIP_SMUDGE: "1" }) : simpleGit3(worktreePath);
|
|
1891
|
+
const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(worktreePath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(worktreePath);
|
|
1583
1892
|
const branchSummary = await worktreeGit.branch();
|
|
1584
1893
|
const currentBranch = branchSummary.current;
|
|
1585
1894
|
await worktreeGit.merge([`origin/${currentBranch}`, "--ff-only"]);
|
|
@@ -1597,14 +1906,14 @@ var GitService = class {
|
|
|
1597
1906
|
this.defaultBranch
|
|
1598
1907
|
);
|
|
1599
1908
|
} catch (metadataError) {
|
|
1600
|
-
|
|
1909
|
+
this.logger.warn(`Failed to update metadata for worktree: ${metadataError}`);
|
|
1601
1910
|
}
|
|
1602
1911
|
}
|
|
1603
1912
|
async hasDivergedHistory(worktreePath, expectedBranch) {
|
|
1604
1913
|
const worktreeGit = simpleGit3(worktreePath);
|
|
1605
1914
|
const branchInfo = await worktreeGit.branch();
|
|
1606
1915
|
if (branchInfo.current !== expectedBranch) {
|
|
1607
|
-
|
|
1916
|
+
this.logger.warn(`Branch mismatch in hasDivergedHistory: expected ${expectedBranch}, got ${branchInfo.current}`);
|
|
1608
1917
|
return false;
|
|
1609
1918
|
}
|
|
1610
1919
|
try {
|
|
@@ -1633,12 +1942,12 @@ var GitService = class {
|
|
|
1633
1942
|
const remoteTree = await worktreeGit.raw(["rev-parse", `origin/${branch}^{tree}`]);
|
|
1634
1943
|
return localTree.trim() === remoteTree.trim();
|
|
1635
1944
|
} catch (error) {
|
|
1636
|
-
|
|
1945
|
+
this.logger.error(`Error comparing tree content: ${error}`);
|
|
1637
1946
|
return false;
|
|
1638
1947
|
}
|
|
1639
1948
|
}
|
|
1640
1949
|
async resetToUpstream(worktreePath, branch) {
|
|
1641
|
-
const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(worktreePath).env({ GIT_LFS_SKIP_SMUDGE: "1" }) : simpleGit3(worktreePath);
|
|
1950
|
+
const worktreeGit = this.isLfsSkipEnabled() ? simpleGit3(worktreePath).env({ [ENV_CONSTANTS.GIT_LFS_SKIP_SMUDGE]: "1" }) : simpleGit3(worktreePath);
|
|
1642
1951
|
await worktreeGit.reset(["--hard", `origin/${branch}`]);
|
|
1643
1952
|
try {
|
|
1644
1953
|
const currentCommit = await worktreeGit.revparse(["HEAD"]);
|
|
@@ -1650,7 +1959,7 @@ var GitService = class {
|
|
|
1650
1959
|
this.defaultBranch
|
|
1651
1960
|
);
|
|
1652
1961
|
} catch (metadataError) {
|
|
1653
|
-
|
|
1962
|
+
this.logger.warn(`Failed to update metadata after reset: ${metadataError}`);
|
|
1654
1963
|
}
|
|
1655
1964
|
}
|
|
1656
1965
|
async getCurrentCommit(worktreePath) {
|
|
@@ -1678,17 +1987,27 @@ var GitService = class {
|
|
|
1678
1987
|
currentWorktree.branch = line.substring(7).replace("refs/heads/", "");
|
|
1679
1988
|
} else if (line === "detached") {
|
|
1680
1989
|
currentWorktree.detached = true;
|
|
1990
|
+
} else if (line === "prunable") {
|
|
1991
|
+
currentWorktree.prunable = true;
|
|
1681
1992
|
} else if (line.trim() === "") {
|
|
1682
1993
|
if (currentWorktree.path) {
|
|
1683
1994
|
if (currentWorktree.branch && !currentWorktree.detached) {
|
|
1684
|
-
worktrees.push({
|
|
1995
|
+
worktrees.push({
|
|
1996
|
+
path: currentWorktree.path,
|
|
1997
|
+
branch: currentWorktree.branch,
|
|
1998
|
+
isPrunable: currentWorktree.prunable || false
|
|
1999
|
+
});
|
|
1685
2000
|
}
|
|
1686
2001
|
}
|
|
1687
2002
|
currentWorktree = {};
|
|
1688
2003
|
}
|
|
1689
2004
|
}
|
|
1690
2005
|
if (currentWorktree.path && currentWorktree.branch && !currentWorktree.detached) {
|
|
1691
|
-
worktrees.push({
|
|
2006
|
+
worktrees.push({
|
|
2007
|
+
path: currentWorktree.path,
|
|
2008
|
+
branch: currentWorktree.branch,
|
|
2009
|
+
isPrunable: currentWorktree.prunable || false
|
|
2010
|
+
});
|
|
1692
2011
|
}
|
|
1693
2012
|
return worktrees;
|
|
1694
2013
|
}
|
|
@@ -1698,9 +2017,11 @@ var GitService = class {
|
|
|
1698
2017
|
var WorktreeSyncService = class {
|
|
1699
2018
|
constructor(config) {
|
|
1700
2019
|
this.config = config;
|
|
1701
|
-
this.
|
|
2020
|
+
this.logger = config.logger ?? Logger.createDefault(void 0, config.debug);
|
|
2021
|
+
this.gitService = new GitService(config, this.logger);
|
|
1702
2022
|
}
|
|
1703
2023
|
gitService;
|
|
2024
|
+
logger;
|
|
1704
2025
|
syncInProgress = false;
|
|
1705
2026
|
async initialize() {
|
|
1706
2027
|
await this.gitService.initialize();
|
|
@@ -1710,11 +2031,13 @@ var WorktreeSyncService = class {
|
|
|
1710
2031
|
}
|
|
1711
2032
|
async sync() {
|
|
1712
2033
|
if (this.syncInProgress) {
|
|
1713
|
-
|
|
2034
|
+
this.logger.warn("\u26A0\uFE0F Sync already in progress, skipping...");
|
|
1714
2035
|
return;
|
|
1715
2036
|
}
|
|
1716
2037
|
this.syncInProgress = true;
|
|
1717
|
-
|
|
2038
|
+
this.logger.info(`[${(/* @__PURE__ */ new Date()).toISOString()}] Starting worktree synchronization...`);
|
|
2039
|
+
const totalTimer = new Timer();
|
|
2040
|
+
const phaseTimer = new PhaseTimer();
|
|
1718
2041
|
let lfsSkipEnabled = false;
|
|
1719
2042
|
const retryOptions = {
|
|
1720
2043
|
maxAttempts: this.config.retry?.maxAttempts ?? 3,
|
|
@@ -1724,18 +2047,18 @@ var WorktreeSyncService = class {
|
|
|
1724
2047
|
backoffMultiplier: this.config.retry?.backoffMultiplier ?? 2,
|
|
1725
2048
|
onRetry: (error, attempt, context) => {
|
|
1726
2049
|
const errorMessage = getErrorMessage(error);
|
|
1727
|
-
|
|
2050
|
+
this.logger.info(`
|
|
1728
2051
|
\u26A0\uFE0F Sync attempt ${attempt} failed: ${errorMessage}`);
|
|
1729
2052
|
if (context?.isLfsError && !this.config.skipLfs) {
|
|
1730
|
-
|
|
2053
|
+
this.logger.info(`\u{1F504} LFS error detected. Will retry with LFS skipped...`);
|
|
1731
2054
|
} else {
|
|
1732
|
-
|
|
2055
|
+
this.logger.info(`\u{1F504} Retrying synchronization...
|
|
1733
2056
|
`);
|
|
1734
2057
|
}
|
|
1735
2058
|
},
|
|
1736
2059
|
lfsRetryHandler: () => {
|
|
1737
2060
|
if (!this.config.skipLfs && !lfsSkipEnabled) {
|
|
1738
|
-
|
|
2061
|
+
this.logger.info("\u26A0\uFE0F Temporarily disabling LFS downloads for this sync...");
|
|
1739
2062
|
process.env.GIT_LFS_SKIP_SMUDGE = "1";
|
|
1740
2063
|
lfsSkipEnabled = true;
|
|
1741
2064
|
}
|
|
@@ -1744,14 +2067,15 @@ var WorktreeSyncService = class {
|
|
|
1744
2067
|
try {
|
|
1745
2068
|
await retry(async () => {
|
|
1746
2069
|
await this.gitService.pruneWorktrees();
|
|
1747
|
-
|
|
2070
|
+
this.logger.info("Step 1: Fetching latest data from remote...");
|
|
2071
|
+
phaseTimer.startPhase("Phase 1: Fetch");
|
|
1748
2072
|
try {
|
|
1749
2073
|
await this.gitService.fetchAll();
|
|
1750
2074
|
} catch (fetchError) {
|
|
1751
2075
|
const errorMessage = getErrorMessage(fetchError);
|
|
1752
2076
|
if (isLfsError(errorMessage) && !lfsSkipEnabled && !this.config.skipLfs) {
|
|
1753
|
-
|
|
1754
|
-
|
|
2077
|
+
this.logger.info("\u26A0\uFE0F Fetch all failed due to LFS error. Attempting branch-by-branch fetch...");
|
|
2078
|
+
this.logger.info("\u26A0\uFE0F Temporarily disabling LFS downloads for branch-by-branch fetch...");
|
|
1755
2079
|
process.env.GIT_LFS_SKIP_SMUDGE = "1";
|
|
1756
2080
|
lfsSkipEnabled = true;
|
|
1757
2081
|
await this.fetchBranchByBranch();
|
|
@@ -1759,143 +2083,221 @@ var WorktreeSyncService = class {
|
|
|
1759
2083
|
throw fetchError;
|
|
1760
2084
|
}
|
|
1761
2085
|
}
|
|
2086
|
+
phaseTimer.endPhase();
|
|
1762
2087
|
let remoteBranches;
|
|
1763
2088
|
if (this.config.branchMaxAge) {
|
|
1764
2089
|
const branchesWithActivity = await this.gitService.getRemoteBranchesWithActivity();
|
|
1765
2090
|
const filteredBranches = filterBranchesByAge(branchesWithActivity, this.config.branchMaxAge);
|
|
1766
2091
|
remoteBranches = filteredBranches.map((b) => b.branch);
|
|
1767
|
-
|
|
1768
|
-
|
|
2092
|
+
this.logger.info(`Found ${branchesWithActivity.length} remote branches.`);
|
|
2093
|
+
this.logger.info(
|
|
1769
2094
|
`After filtering by age (${formatDuration(this.config.branchMaxAge)}): ${remoteBranches.length} branches.`
|
|
1770
2095
|
);
|
|
1771
2096
|
if (branchesWithActivity.length > remoteBranches.length) {
|
|
1772
2097
|
const excludedCount = branchesWithActivity.length - remoteBranches.length;
|
|
1773
|
-
|
|
2098
|
+
this.logger.info(` - Excluded ${excludedCount} stale branches.`);
|
|
1774
2099
|
}
|
|
1775
2100
|
} else {
|
|
1776
2101
|
remoteBranches = await this.gitService.getRemoteBranches();
|
|
1777
|
-
|
|
2102
|
+
this.logger.info(`Found ${remoteBranches.length} remote branches.`);
|
|
1778
2103
|
}
|
|
1779
2104
|
const defaultBranch = this.gitService.getDefaultBranch();
|
|
1780
2105
|
if (!remoteBranches.includes(defaultBranch)) {
|
|
1781
2106
|
remoteBranches.push(defaultBranch);
|
|
1782
|
-
|
|
2107
|
+
this.logger.info(`Ensuring default branch '${defaultBranch}' is retained.`);
|
|
1783
2108
|
}
|
|
1784
2109
|
await fs5.mkdir(this.config.worktreeDir, { recursive: true });
|
|
1785
2110
|
const worktrees = await this.gitService.getWorktrees();
|
|
1786
2111
|
const worktreeBranches = worktrees.map((w) => w.branch);
|
|
1787
|
-
|
|
2112
|
+
this.logger.info(`Found ${worktrees.length} existing Git worktrees.`);
|
|
1788
2113
|
await this.cleanupOrphanedDirectories(worktrees);
|
|
1789
|
-
await this.
|
|
1790
|
-
await this.
|
|
2114
|
+
await this.createNewWorktreesWithTiming(remoteBranches, worktreeBranches, defaultBranch, phaseTimer);
|
|
2115
|
+
await this.pruneOldWorktreesWithTiming(remoteBranches, worktreeBranches, phaseTimer);
|
|
1791
2116
|
if (this.config.updateExistingWorktrees !== false) {
|
|
1792
|
-
await this.
|
|
2117
|
+
await this.updateExistingWorktreesWithTiming(worktrees, remoteBranches, phaseTimer);
|
|
1793
2118
|
}
|
|
2119
|
+
phaseTimer.startPhase("Phase 5: Cleanup");
|
|
1794
2120
|
await this.gitService.pruneWorktrees();
|
|
1795
|
-
|
|
2121
|
+
this.logger.info("Step 5: Pruned worktree metadata.");
|
|
2122
|
+
phaseTimer.endPhase();
|
|
1796
2123
|
}, retryOptions);
|
|
1797
2124
|
} catch (error) {
|
|
1798
|
-
|
|
2125
|
+
this.logger.error("\n\u274C Error during worktree synchronization after all retry attempts:", error);
|
|
1799
2126
|
throw error;
|
|
1800
2127
|
} finally {
|
|
1801
2128
|
if (lfsSkipEnabled && !this.config.skipLfs) {
|
|
1802
2129
|
delete process.env.GIT_LFS_SKIP_SMUDGE;
|
|
1803
2130
|
}
|
|
1804
2131
|
this.syncInProgress = false;
|
|
1805
|
-
|
|
2132
|
+
this.logger.info(`[${(/* @__PURE__ */ new Date()).toISOString()}] Synchronization finished.
|
|
1806
2133
|
`);
|
|
2134
|
+
if (this.config.debug) {
|
|
2135
|
+
const totalDuration = totalTimer.stop();
|
|
2136
|
+
const phaseResults = phaseTimer.getResults();
|
|
2137
|
+
const repoName = this.config.name;
|
|
2138
|
+
this.logger.table(formatTimingTable(totalDuration, phaseResults, repoName));
|
|
2139
|
+
}
|
|
1807
2140
|
}
|
|
1808
2141
|
}
|
|
2142
|
+
async createNewWorktreesWithTiming(remoteBranches, existingWorktreeBranches, defaultBranch, phaseTimer) {
|
|
2143
|
+
const maxConcurrent = this.config.parallelism?.maxWorktreeCreation ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_CREATION;
|
|
2144
|
+
phaseTimer.startPhase("Phase 2: Create", maxConcurrent);
|
|
2145
|
+
await this.createNewWorktrees(remoteBranches, existingWorktreeBranches, defaultBranch);
|
|
2146
|
+
const newBranches = remoteBranches.filter((b) => !existingWorktreeBranches.includes(b)).filter((b) => b !== defaultBranch);
|
|
2147
|
+
phaseTimer.setPhaseCount("Phase 2: Create", newBranches.length);
|
|
2148
|
+
phaseTimer.endPhase();
|
|
2149
|
+
}
|
|
1809
2150
|
async createNewWorktrees(remoteBranches, existingWorktreeBranches, defaultBranch) {
|
|
1810
2151
|
const newBranches = remoteBranches.filter((b) => !existingWorktreeBranches.includes(b)).filter((b) => b !== defaultBranch);
|
|
1811
2152
|
if (newBranches.length > 0) {
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
2153
|
+
this.logger.info(`Step 2: Creating ${newBranches.length} new worktrees...`);
|
|
2154
|
+
const maxConcurrent = this.config.parallelism?.maxWorktreeCreation ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_CREATION;
|
|
2155
|
+
const limit = pLimit(maxConcurrent);
|
|
2156
|
+
const results = await Promise.allSettled(
|
|
2157
|
+
newBranches.map(
|
|
2158
|
+
(branchName) => limit(async () => {
|
|
2159
|
+
const worktreePath = path5.join(this.config.worktreeDir, branchName);
|
|
2160
|
+
try {
|
|
2161
|
+
await this.gitService.addWorktree(branchName, worktreePath);
|
|
2162
|
+
this.logger.info(` \u2705 Created worktree for '${branchName}'`);
|
|
2163
|
+
} catch (error) {
|
|
2164
|
+
this.logger.error(` \u274C Failed to create worktree for '${branchName}':`, getErrorMessage(error));
|
|
2165
|
+
throw error;
|
|
2166
|
+
}
|
|
2167
|
+
})
|
|
2168
|
+
)
|
|
2169
|
+
);
|
|
2170
|
+
const successCount = results.filter((r) => r.status === "fulfilled").length;
|
|
2171
|
+
this.logger.info(` Created ${successCount}/${newBranches.length} worktrees successfully`);
|
|
1817
2172
|
} else {
|
|
1818
|
-
|
|
2173
|
+
this.logger.info("Step 2: No new branches to create worktrees for.");
|
|
1819
2174
|
}
|
|
1820
2175
|
}
|
|
2176
|
+
async pruneOldWorktreesWithTiming(remoteBranches, existingWorktreeBranches, phaseTimer) {
|
|
2177
|
+
const maxConcurrent = this.config.parallelism?.maxStatusChecks ?? DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS;
|
|
2178
|
+
phaseTimer.startPhase("Phase 3: Prune", maxConcurrent);
|
|
2179
|
+
await this.pruneOldWorktrees(remoteBranches, existingWorktreeBranches);
|
|
2180
|
+
const deletedBranches = existingWorktreeBranches.filter((branch) => !remoteBranches.includes(branch));
|
|
2181
|
+
phaseTimer.setPhaseCount("Phase 3: Prune", deletedBranches.length);
|
|
2182
|
+
phaseTimer.endPhase();
|
|
2183
|
+
}
|
|
1821
2184
|
async pruneOldWorktrees(remoteBranches, existingWorktreeBranches) {
|
|
1822
2185
|
const deletedBranches = existingWorktreeBranches.filter((branch) => !remoteBranches.includes(branch));
|
|
1823
2186
|
if (deletedBranches.length > 0) {
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
2187
|
+
this.logger.info(`Step 3: Checking ${deletedBranches.length} stale worktrees to prune...`);
|
|
2188
|
+
const maxConcurrent = this.config.parallelism?.maxStatusChecks ?? DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS;
|
|
2189
|
+
const limit = pLimit(maxConcurrent);
|
|
2190
|
+
const statusResults = await Promise.allSettled(
|
|
2191
|
+
deletedBranches.map(
|
|
2192
|
+
(branchName) => limit(async () => {
|
|
2193
|
+
const worktreePath = path5.join(this.config.worktreeDir, branchName);
|
|
2194
|
+
const status = await this.gitService.getFullWorktreeStatus(worktreePath, this.config.debug);
|
|
2195
|
+
return { branchName, worktreePath, status };
|
|
2196
|
+
})
|
|
2197
|
+
)
|
|
2198
|
+
);
|
|
2199
|
+
const toRemove = [];
|
|
2200
|
+
const toSkip = [];
|
|
2201
|
+
for (const result of statusResults) {
|
|
2202
|
+
if (result.status === "fulfilled") {
|
|
2203
|
+
const { branchName, worktreePath, status } = result.value;
|
|
1829
2204
|
if (status.canRemove) {
|
|
1830
|
-
|
|
2205
|
+
toRemove.push({ branchName, worktreePath });
|
|
1831
2206
|
} 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
|
-
}
|
|
2207
|
+
toSkip.push({ branchName, status });
|
|
1844
2208
|
}
|
|
1845
|
-
}
|
|
1846
|
-
|
|
2209
|
+
} else {
|
|
2210
|
+
this.logger.error(` - Error checking worktree:`, result.reason);
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2213
|
+
if (toRemove.length > 0) {
|
|
2214
|
+
const removeLimit = pLimit(
|
|
2215
|
+
this.config.parallelism?.maxWorktreeRemoval ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_REMOVAL
|
|
2216
|
+
);
|
|
2217
|
+
const removeResults = await Promise.allSettled(
|
|
2218
|
+
toRemove.map(
|
|
2219
|
+
({ branchName, worktreePath }) => removeLimit(async () => {
|
|
2220
|
+
try {
|
|
2221
|
+
await this.gitService.removeWorktree(worktreePath);
|
|
2222
|
+
this.logger.info(` \u2705 Removed worktree for '${branchName}'`);
|
|
2223
|
+
} catch (error) {
|
|
2224
|
+
this.logger.error(` \u274C Failed to remove worktree for '${branchName}':`, getErrorMessage(error));
|
|
2225
|
+
throw error;
|
|
2226
|
+
}
|
|
2227
|
+
})
|
|
2228
|
+
)
|
|
2229
|
+
);
|
|
2230
|
+
const removedCount = removeResults.filter((r) => r.status === "fulfilled").length;
|
|
2231
|
+
this.logger.info(` Removed ${removedCount}/${toRemove.length} worktrees successfully`);
|
|
2232
|
+
}
|
|
2233
|
+
if (toSkip.length > 0) {
|
|
2234
|
+
this.logger.info(` Skipped ${toSkip.length} worktree(s) with local changes or unpushed commits`);
|
|
2235
|
+
}
|
|
2236
|
+
for (const { branchName, status } of toSkip) {
|
|
2237
|
+
if (status.upstreamGone && status.hasUnpushedCommits) {
|
|
2238
|
+
const worktreePath = path5.join(this.config.worktreeDir, branchName);
|
|
2239
|
+
this.logger.warn(` - \u26A0\uFE0F Cannot automatically remove '${branchName}' - upstream branch was deleted.`);
|
|
2240
|
+
this.logger.info(` Please review manually: cd ${worktreePath} && git log`);
|
|
2241
|
+
this.logger.info(
|
|
2242
|
+
` If changes were squash-merged, you can safely remove with: git worktree remove ${worktreePath}`
|
|
2243
|
+
);
|
|
2244
|
+
} else {
|
|
2245
|
+
this.logger.info(` - \u26A0\uFE0F Skipping removal of '${branchName}' due to: ${status.reasons.join(", ")}.`);
|
|
2246
|
+
}
|
|
2247
|
+
if (this.config.debug && status.details) {
|
|
2248
|
+
this.logDebugDetails(branchName, status.details);
|
|
1847
2249
|
}
|
|
1848
2250
|
}
|
|
1849
2251
|
} else {
|
|
1850
|
-
|
|
2252
|
+
this.logger.info("Step 3: No stale worktrees to prune.");
|
|
1851
2253
|
}
|
|
1852
2254
|
}
|
|
1853
2255
|
logDebugDetails(branchName, details) {
|
|
1854
|
-
|
|
2256
|
+
this.logger.info(`
|
|
1855
2257
|
\u{1F50D} Debug details for '${branchName}':`);
|
|
1856
2258
|
if (details.modifiedFiles > 0 && details.modifiedFilesList) {
|
|
1857
|
-
|
|
1858
|
-
details.modifiedFilesList.forEach((file) =>
|
|
2259
|
+
this.logger.info(` - Modified files (${details.modifiedFiles}):`);
|
|
2260
|
+
details.modifiedFilesList.forEach((file) => this.logger.info(` \u2022 ${file}`));
|
|
1859
2261
|
}
|
|
1860
2262
|
if (details.deletedFiles > 0 && details.deletedFilesList) {
|
|
1861
|
-
|
|
1862
|
-
details.deletedFilesList.forEach((file) =>
|
|
2263
|
+
this.logger.info(` - Deleted files (${details.deletedFiles}):`);
|
|
2264
|
+
details.deletedFilesList.forEach((file) => this.logger.info(` \u2022 ${file}`));
|
|
1863
2265
|
}
|
|
1864
2266
|
if (details.renamedFiles > 0 && details.renamedFilesList) {
|
|
1865
|
-
|
|
1866
|
-
details.renamedFilesList.forEach((file) =>
|
|
2267
|
+
this.logger.info(` - Renamed files (${details.renamedFiles}):`);
|
|
2268
|
+
details.renamedFilesList.forEach((file) => this.logger.info(` \u2022 ${file.from} \u2192 ${file.to}`));
|
|
1867
2269
|
}
|
|
1868
2270
|
if (details.createdFiles > 0 && details.createdFilesList) {
|
|
1869
|
-
|
|
1870
|
-
details.createdFilesList.forEach((file) =>
|
|
2271
|
+
this.logger.info(` - Created files (${details.createdFiles}):`);
|
|
2272
|
+
details.createdFilesList.forEach((file) => this.logger.info(` \u2022 ${file}`));
|
|
1871
2273
|
}
|
|
1872
2274
|
if (details.conflictedFiles > 0 && details.conflictedFilesList) {
|
|
1873
|
-
|
|
1874
|
-
details.conflictedFilesList.forEach((file) =>
|
|
2275
|
+
this.logger.info(` - Conflicted files (${details.conflictedFiles}):`);
|
|
2276
|
+
details.conflictedFilesList.forEach((file) => this.logger.info(` \u2022 ${file}`));
|
|
1875
2277
|
}
|
|
1876
2278
|
if (details.untrackedFiles > 0 && details.untrackedFilesList) {
|
|
1877
|
-
|
|
1878
|
-
details.untrackedFilesList.forEach((file) =>
|
|
2279
|
+
this.logger.info(` - Untracked files (not ignored) (${details.untrackedFiles}):`);
|
|
2280
|
+
details.untrackedFilesList.forEach((file) => this.logger.info(` \u2022 ${file}`));
|
|
1879
2281
|
}
|
|
1880
2282
|
if (details.unpushedCommitCount !== void 0 && details.unpushedCommitCount > 0) {
|
|
1881
|
-
|
|
2283
|
+
this.logger.info(` - Unpushed commits: ${details.unpushedCommitCount}`);
|
|
1882
2284
|
}
|
|
1883
2285
|
if (details.stashCount !== void 0 && details.stashCount > 0) {
|
|
1884
|
-
|
|
2286
|
+
this.logger.info(` - Stashed changes: ${details.stashCount}`);
|
|
1885
2287
|
}
|
|
1886
2288
|
if (details.operationType) {
|
|
1887
|
-
|
|
2289
|
+
this.logger.info(` - Operation in progress: ${details.operationType}`);
|
|
1888
2290
|
}
|
|
1889
2291
|
if (details.modifiedSubmodules && details.modifiedSubmodules.length > 0) {
|
|
1890
|
-
|
|
1891
|
-
details.modifiedSubmodules.forEach((submodule) =>
|
|
2292
|
+
this.logger.info(` - Modified submodules (${details.modifiedSubmodules.length}):`);
|
|
2293
|
+
details.modifiedSubmodules.forEach((submodule) => this.logger.info(` \u2022 ${submodule}`));
|
|
1892
2294
|
}
|
|
1893
|
-
|
|
2295
|
+
this.logger.info("");
|
|
1894
2296
|
}
|
|
1895
2297
|
async fetchBranchByBranch() {
|
|
1896
|
-
|
|
2298
|
+
this.logger.info("Fetching branches individually to isolate LFS errors...");
|
|
1897
2299
|
const remoteBranches = await this.gitService.getRemoteBranches();
|
|
1898
|
-
|
|
2300
|
+
this.logger.info(`Found ${remoteBranches.length} remote branches to fetch.`);
|
|
1899
2301
|
const failedBranches = [];
|
|
1900
2302
|
let successCount = 0;
|
|
1901
2303
|
for (const branch of remoteBranches) {
|
|
@@ -1904,79 +2306,104 @@ var WorktreeSyncService = class {
|
|
|
1904
2306
|
successCount++;
|
|
1905
2307
|
} catch (error) {
|
|
1906
2308
|
const errorMessage = getErrorMessage(error);
|
|
1907
|
-
|
|
2309
|
+
this.logger.info(` \u26A0\uFE0F Failed to fetch branch '${branch}': ${errorMessage}`);
|
|
1908
2310
|
failedBranches.push(branch);
|
|
1909
2311
|
}
|
|
1910
2312
|
}
|
|
1911
|
-
|
|
2313
|
+
this.logger.info(`Branch-by-branch fetch completed: ${successCount}/${remoteBranches.length} successful`);
|
|
1912
2314
|
if (failedBranches.length > 0) {
|
|
1913
|
-
|
|
1914
|
-
|
|
2315
|
+
this.logger.info(`\u26A0\uFE0F Failed to fetch ${failedBranches.length} branches due to errors.`);
|
|
2316
|
+
this.logger.info(` These branches will be skipped: ${failedBranches.join(", ")}`);
|
|
1915
2317
|
}
|
|
1916
2318
|
}
|
|
2319
|
+
async updateExistingWorktreesWithTiming(worktrees, remoteBranches, phaseTimer) {
|
|
2320
|
+
const maxConcurrent = this.config.parallelism?.maxWorktreeUpdates ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_UPDATES;
|
|
2321
|
+
phaseTimer.startPhase("Phase 4: Update", maxConcurrent);
|
|
2322
|
+
await this.updateExistingWorktrees(worktrees, remoteBranches);
|
|
2323
|
+
const activeWorktrees = worktrees.filter((w) => remoteBranches.includes(w.branch));
|
|
2324
|
+
phaseTimer.setPhaseCount("Phase 4: Update", activeWorktrees.length);
|
|
2325
|
+
phaseTimer.endPhase();
|
|
2326
|
+
}
|
|
1917
2327
|
async updateExistingWorktrees(worktrees, remoteBranches) {
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
const divergedDir = path5.join(this.config.worktreeDir, ".diverged");
|
|
2328
|
+
this.logger.info("Step 4: Checking for worktrees that need updates...");
|
|
2329
|
+
const divergedDir = path5.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
|
|
1921
2330
|
try {
|
|
1922
2331
|
const diverged = await fs5.readdir(divergedDir);
|
|
1923
2332
|
if (diverged.length > 0) {
|
|
1924
|
-
|
|
2333
|
+
this.logger.info(
|
|
2334
|
+
`\u{1F4E6} Note: ${diverged.length} diverged worktree(s) in ${path5.relative(process.cwd(), divergedDir)}`
|
|
2335
|
+
);
|
|
1925
2336
|
}
|
|
1926
2337
|
} catch {
|
|
1927
2338
|
}
|
|
1928
2339
|
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
|
-
|
|
2340
|
+
const maxConcurrent = this.config.parallelism?.maxStatusChecks ?? DEFAULT_CONFIG.PARALLELISM.MAX_STATUS_CHECKS;
|
|
2341
|
+
const limit = pLimit(maxConcurrent);
|
|
2342
|
+
const checkResults = await Promise.allSettled(
|
|
2343
|
+
activeWorktrees.map(
|
|
2344
|
+
(worktree) => limit(async () => {
|
|
2345
|
+
try {
|
|
2346
|
+
await fs5.access(worktree.path);
|
|
2347
|
+
} catch {
|
|
2348
|
+
return null;
|
|
2349
|
+
}
|
|
2350
|
+
const hasOp = await this.gitService.hasOperationInProgress(worktree.path);
|
|
2351
|
+
if (hasOp) return null;
|
|
2352
|
+
const isClean = await this.gitService.checkWorktreeStatus(worktree.path);
|
|
2353
|
+
if (!isClean) return null;
|
|
2354
|
+
const canFastForward = await this.gitService.canFastForward(worktree.path, worktree.branch);
|
|
2355
|
+
if (!canFastForward) {
|
|
2356
|
+
await this.handleDivergedBranch(worktree);
|
|
2357
|
+
return null;
|
|
2358
|
+
}
|
|
2359
|
+
const isBehind = await this.gitService.isWorktreeBehind(worktree.path);
|
|
2360
|
+
return isBehind ? worktree : null;
|
|
2361
|
+
})
|
|
2362
|
+
)
|
|
2363
|
+
);
|
|
2364
|
+
const worktreesToUpdate = [];
|
|
2365
|
+
for (const result of checkResults) {
|
|
2366
|
+
if (result.status === "fulfilled" && result.value) {
|
|
2367
|
+
worktreesToUpdate.push(result.value);
|
|
2368
|
+
} else if (result.status === "rejected") {
|
|
2369
|
+
this.logger.error(` - Error checking worktree:`, result.reason);
|
|
1955
2370
|
}
|
|
1956
2371
|
}
|
|
1957
2372
|
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...`);
|
|
2373
|
+
this.logger.info(` - Found ${worktreesToUpdate.length} worktrees behind their upstream branches.`);
|
|
2374
|
+
const updateLimit = pLimit(
|
|
2375
|
+
this.config.parallelism?.maxWorktreeUpdates ?? DEFAULT_CONFIG.PARALLELISM.MAX_WORKTREE_UPDATES
|
|
2376
|
+
);
|
|
2377
|
+
const updateResults = await Promise.allSettled(
|
|
2378
|
+
worktreesToUpdate.map(
|
|
2379
|
+
(worktree) => updateLimit(async () => {
|
|
1968
2380
|
try {
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
2381
|
+
this.logger.info(` - Updating worktree '${worktree.branch}'...`);
|
|
2382
|
+
await this.gitService.updateWorktree(worktree.path);
|
|
2383
|
+
this.logger.info(` \u2705 Successfully updated '${worktree.branch}'.`);
|
|
2384
|
+
} catch (error) {
|
|
2385
|
+
const errorMessage = getErrorMessage(error);
|
|
2386
|
+
if (ERROR_MESSAGES.FAST_FORWARD_FAILED.some((msg) => errorMessage.includes(msg))) {
|
|
2387
|
+
this.logger.info(
|
|
2388
|
+
` \u26A0\uFE0F Branch '${worktree.branch}' cannot be fast-forwarded. Checking for divergence...`
|
|
2389
|
+
);
|
|
2390
|
+
try {
|
|
2391
|
+
await this.handleDivergedBranch(worktree);
|
|
2392
|
+
} catch (divergedError) {
|
|
2393
|
+
this.logger.error(` \u274C Failed to handle diverged branch '${worktree.branch}':`, divergedError);
|
|
2394
|
+
}
|
|
2395
|
+
} else {
|
|
2396
|
+
this.logger.error(` \u274C Failed to update '${worktree.branch}':`, error);
|
|
2397
|
+
}
|
|
2398
|
+
throw error;
|
|
1972
2399
|
}
|
|
1973
|
-
}
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
}
|
|
2400
|
+
})
|
|
2401
|
+
)
|
|
2402
|
+
);
|
|
2403
|
+
const successCount = updateResults.filter((r) => r.status === "fulfilled").length;
|
|
2404
|
+
this.logger.info(` Updated ${successCount}/${worktreesToUpdate.length} worktrees successfully`);
|
|
1978
2405
|
} else {
|
|
1979
|
-
|
|
2406
|
+
this.logger.info(" - All worktrees are up to date.");
|
|
1980
2407
|
}
|
|
1981
2408
|
}
|
|
1982
2409
|
async cleanupOrphanedDirectories(worktrees) {
|
|
@@ -1994,50 +2421,50 @@ var WorktreeSyncService = class {
|
|
|
1994
2421
|
}
|
|
1995
2422
|
}
|
|
1996
2423
|
if (orphanedDirs.length > 0) {
|
|
1997
|
-
|
|
2424
|
+
this.logger.info(`Found ${orphanedDirs.length} orphaned directories: ${orphanedDirs.join(", ")}`);
|
|
1998
2425
|
for (const dir of orphanedDirs) {
|
|
1999
2426
|
const dirPath = path5.join(this.config.worktreeDir, dir);
|
|
2000
2427
|
try {
|
|
2001
2428
|
const stat4 = await fs5.stat(dirPath);
|
|
2002
2429
|
if (stat4.isDirectory()) {
|
|
2003
2430
|
await fs5.rm(dirPath, { recursive: true, force: true });
|
|
2004
|
-
|
|
2431
|
+
this.logger.info(` - Removed orphaned directory: ${dir}`);
|
|
2005
2432
|
}
|
|
2006
2433
|
} catch (error) {
|
|
2007
|
-
|
|
2434
|
+
this.logger.error(` - Failed to remove orphaned directory ${dir}:`, error);
|
|
2008
2435
|
}
|
|
2009
2436
|
}
|
|
2010
2437
|
}
|
|
2011
2438
|
} catch (error) {
|
|
2012
|
-
|
|
2439
|
+
this.logger.error("Error during orphaned directory cleanup:", error);
|
|
2013
2440
|
}
|
|
2014
2441
|
}
|
|
2015
2442
|
async handleDivergedBranch(worktree) {
|
|
2016
|
-
|
|
2443
|
+
this.logger.info(`\u26A0\uFE0F Branch '${worktree.branch}' has diverged from upstream. Analyzing...`);
|
|
2017
2444
|
const treesIdentical = await this.gitService.compareTreeContent(worktree.path, worktree.branch);
|
|
2018
2445
|
if (treesIdentical) {
|
|
2019
|
-
|
|
2446
|
+
this.logger.info(`\u2705 Branch '${worktree.branch}' was rebased but files are identical. Resetting to upstream...`);
|
|
2020
2447
|
await this.gitService.resetToUpstream(worktree.path, worktree.branch);
|
|
2021
|
-
|
|
2448
|
+
this.logger.info(` Successfully updated '${worktree.branch}' to match upstream.`);
|
|
2022
2449
|
} else {
|
|
2023
2450
|
const hasLocalChanges = await this.hasLocalChangesSinceLastSync(worktree.path);
|
|
2024
2451
|
if (!hasLocalChanges) {
|
|
2025
|
-
|
|
2452
|
+
this.logger.info(
|
|
2026
2453
|
`\u2705 Branch '${worktree.branch}' has diverged but you made no local changes. Resetting to upstream...`
|
|
2027
2454
|
);
|
|
2028
2455
|
await this.gitService.resetToUpstream(worktree.path, worktree.branch);
|
|
2029
|
-
|
|
2456
|
+
this.logger.info(` Successfully updated '${worktree.branch}' to match upstream.`);
|
|
2030
2457
|
} else {
|
|
2031
|
-
|
|
2458
|
+
this.logger.info(`\u{1F512} Branch '${worktree.branch}' has diverged with local changes. Moving to diverged...`);
|
|
2032
2459
|
const divergedPath = await this.divergeWorktree(worktree.path, worktree.branch);
|
|
2033
2460
|
const relativePath = path5.relative(process.cwd(), divergedPath);
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2461
|
+
this.logger.info(` Moved to: ${relativePath}`);
|
|
2462
|
+
this.logger.info(` Your local changes are preserved. To review:`);
|
|
2463
|
+
this.logger.info(` cd ${relativePath}`);
|
|
2464
|
+
this.logger.info(` git diff origin/${worktree.branch}`);
|
|
2038
2465
|
await this.gitService.removeWorktree(worktree.path);
|
|
2039
2466
|
await this.gitService.addWorktree(worktree.branch, worktree.path);
|
|
2040
|
-
|
|
2467
|
+
this.logger.info(` Created fresh worktree from upstream at: ${worktree.path}`);
|
|
2041
2468
|
}
|
|
2042
2469
|
}
|
|
2043
2470
|
}
|
|
@@ -2054,9 +2481,9 @@ var WorktreeSyncService = class {
|
|
|
2054
2481
|
}
|
|
2055
2482
|
}
|
|
2056
2483
|
async divergeWorktree(worktreePath, branchName) {
|
|
2057
|
-
const divergedBaseDir = path5.join(this.config.worktreeDir,
|
|
2484
|
+
const divergedBaseDir = path5.join(this.config.worktreeDir, GIT_CONSTANTS.DIVERGED_DIR_NAME);
|
|
2058
2485
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
2059
|
-
const uniqueSuffix = Date.now().toString(36) + Math.random().toString(36).
|
|
2486
|
+
const uniqueSuffix = Date.now().toString(36) + Math.random().toString(36).substring(2, 7);
|
|
2060
2487
|
const safeBranchName = branchName.replace(/\//g, "-");
|
|
2061
2488
|
const divergedName = `${timestamp}-${safeBranchName}-${uniqueSuffix}`;
|
|
2062
2489
|
const divergedPath = path5.join(divergedBaseDir, divergedName);
|
|
@@ -2075,7 +2502,7 @@ var WorktreeSyncService = class {
|
|
|
2075
2502
|
const metadata = {
|
|
2076
2503
|
originalBranch: branchName,
|
|
2077
2504
|
divergedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2078
|
-
reason:
|
|
2505
|
+
reason: METADATA_CONSTANTS.DIVERGED_REASON,
|
|
2079
2506
|
originalPath: worktreePath,
|
|
2080
2507
|
localCommit: await this.gitService.getCurrentCommit(divergedPath),
|
|
2081
2508
|
remoteCommit: await this.gitService.getRemoteCommit(`origin/${branchName}`),
|
|
@@ -2086,7 +2513,10 @@ var WorktreeSyncService = class {
|
|
|
2086
2513
|
|
|
2087
2514
|
Original worktree location: ${worktreePath}`
|
|
2088
2515
|
};
|
|
2089
|
-
await fs5.writeFile(
|
|
2516
|
+
await fs5.writeFile(
|
|
2517
|
+
path5.join(divergedPath, METADATA_CONSTANTS.DIVERGED_INFO_FILE),
|
|
2518
|
+
JSON.stringify(metadata, null, 2)
|
|
2519
|
+
);
|
|
2090
2520
|
return divergedPath;
|
|
2091
2521
|
}
|
|
2092
2522
|
};
|
|
@@ -2685,14 +3115,18 @@ async function promptForConfig(partialConfig) {
|
|
|
2685
3115
|
|
|
2686
3116
|
// src/index.ts
|
|
2687
3117
|
async function runSingleRepository(config) {
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
3118
|
+
const logger = Logger.createDefault(void 0, config.debug);
|
|
3119
|
+
logger.info("\n\u{1F4CB} CLI Command (for future reference):");
|
|
3120
|
+
logger.info(` ${reconstructCliCommand(config)}`);
|
|
3121
|
+
logger.info("");
|
|
3122
|
+
if (!config.logger) {
|
|
3123
|
+
config.logger = logger;
|
|
3124
|
+
}
|
|
2691
3125
|
const syncService = new WorktreeSyncService(config);
|
|
2692
3126
|
try {
|
|
2693
3127
|
await syncService.initialize();
|
|
2694
3128
|
if (config.runOnce) {
|
|
2695
|
-
|
|
3129
|
+
logger.info("Running the sync process once as requested by --runOnce flag.");
|
|
2696
3130
|
await syncService.sync();
|
|
2697
3131
|
} else {
|
|
2698
3132
|
const uiService = new InteractiveUIService([syncService], void 0, config.cronSchedule);
|
|
@@ -2706,37 +3140,66 @@ async function runSingleRepository(config) {
|
|
|
2706
3140
|
uiService.updateLastSyncTime();
|
|
2707
3141
|
void uiService.calculateAndUpdateDiskSpace();
|
|
2708
3142
|
} catch (error) {
|
|
2709
|
-
|
|
3143
|
+
logger.error(`Error during scheduled sync: ${error.message}`, error);
|
|
2710
3144
|
uiService.setStatus("idle");
|
|
2711
3145
|
}
|
|
2712
3146
|
});
|
|
2713
3147
|
}
|
|
2714
3148
|
} catch (error) {
|
|
2715
|
-
|
|
3149
|
+
logger.error("\u274C Fatal Error during initialization:", error);
|
|
2716
3150
|
process.exit(1);
|
|
2717
3151
|
}
|
|
2718
3152
|
}
|
|
2719
|
-
async function runMultipleRepositories(repositories, runOnce, configPath) {
|
|
3153
|
+
async function runMultipleRepositories(repositories, runOnce, configPath, maxParallel) {
|
|
2720
3154
|
const services = /* @__PURE__ */ new Map();
|
|
2721
|
-
|
|
3155
|
+
const globalLogger = Logger.createDefault();
|
|
3156
|
+
globalLogger.info(`
|
|
2722
3157
|
\u{1F504} Syncing ${repositories.length} repositories...`);
|
|
2723
|
-
|
|
2724
|
-
|
|
3158
|
+
const limit = pLimit2(maxParallel ?? DEFAULT_CONFIG.PARALLELISM.MAX_REPOSITORIES);
|
|
3159
|
+
const initResults = await Promise.allSettled(
|
|
3160
|
+
repositories.map(
|
|
3161
|
+
(repoConfig) => limit(async () => {
|
|
3162
|
+
const repoLogger = Logger.createDefault(repoConfig.name, repoConfig.debug);
|
|
3163
|
+
repoLogger.info(`
|
|
2725
3164
|
\u{1F4E6} Repository: ${repoConfig.name}`);
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
3165
|
+
repoLogger.info(` URL: ${repoConfig.repoUrl}`);
|
|
3166
|
+
repoLogger.info(` Worktrees: ${repoConfig.worktreeDir}`);
|
|
3167
|
+
if (repoConfig.bareRepoDir) {
|
|
3168
|
+
repoLogger.info(` Bare repo: ${repoConfig.bareRepoDir}`);
|
|
3169
|
+
}
|
|
3170
|
+
if (!repoConfig.logger) {
|
|
3171
|
+
repoConfig.logger = repoLogger;
|
|
3172
|
+
}
|
|
3173
|
+
const syncService = new WorktreeSyncService(repoConfig);
|
|
3174
|
+
await syncService.initialize();
|
|
3175
|
+
return { name: repoConfig.name, service: syncService };
|
|
3176
|
+
})
|
|
3177
|
+
)
|
|
3178
|
+
);
|
|
3179
|
+
const servicesToSync = [];
|
|
3180
|
+
for (const result of initResults) {
|
|
3181
|
+
if (result.status === "fulfilled") {
|
|
3182
|
+
services.set(result.value.name, result.value.service);
|
|
3183
|
+
servicesToSync.push(result.value);
|
|
3184
|
+
} else {
|
|
3185
|
+
globalLogger.error(`\u274C Failed to initialize repository:`, result.reason);
|
|
2738
3186
|
}
|
|
2739
3187
|
}
|
|
3188
|
+
const syncResults = await Promise.allSettled(
|
|
3189
|
+
servicesToSync.map(
|
|
3190
|
+
({ name, service }) => limit(async () => {
|
|
3191
|
+
try {
|
|
3192
|
+
await service.sync();
|
|
3193
|
+
} catch (error) {
|
|
3194
|
+
globalLogger.error(`\u274C Error syncing repository '${name}':`, error);
|
|
3195
|
+
throw error;
|
|
3196
|
+
}
|
|
3197
|
+
})
|
|
3198
|
+
)
|
|
3199
|
+
);
|
|
3200
|
+
const successCount = syncResults.filter((r) => r.status === "fulfilled").length;
|
|
3201
|
+
globalLogger.info(`
|
|
3202
|
+
\u2705 Successfully synced ${successCount}/${servicesToSync.length} repositories`);
|
|
2740
3203
|
if (!runOnce) {
|
|
2741
3204
|
const uniqueSchedules = [...new Set(repositories.map((r) => r.cronSchedule))];
|
|
2742
3205
|
const displaySchedule = uniqueSchedules.length === 1 ? uniqueSchedules[0] : void 0;
|
|
@@ -2753,25 +3216,29 @@ async function runMultipleRepositories(repositories, runOnce, configPath) {
|
|
|
2753
3216
|
cron2.schedule(repoConfig.cronSchedule, async () => {
|
|
2754
3217
|
const reposToSync = repositories.filter((r) => r.cronSchedule === repoConfig.cronSchedule);
|
|
2755
3218
|
uiService.setStatus("syncing");
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
3219
|
+
await Promise.allSettled(
|
|
3220
|
+
reposToSync.map(
|
|
3221
|
+
(repo) => limit(async () => {
|
|
3222
|
+
const service = services.get(repo.name);
|
|
3223
|
+
if (!service) return;
|
|
3224
|
+
globalLogger.info(`Running scheduled sync for: ${repo.name}`);
|
|
3225
|
+
try {
|
|
3226
|
+
await service.sync();
|
|
3227
|
+
} catch (error) {
|
|
3228
|
+
globalLogger.error(`Error syncing '${repo.name}':`, error);
|
|
3229
|
+
}
|
|
3230
|
+
})
|
|
3231
|
+
)
|
|
3232
|
+
);
|
|
2766
3233
|
uiService.updateLastSyncTime();
|
|
2767
3234
|
void uiService.calculateAndUpdateDiskSpace();
|
|
2768
3235
|
});
|
|
2769
3236
|
}
|
|
2770
3237
|
}
|
|
2771
|
-
|
|
3238
|
+
globalLogger.info(`All ${repositories.length} repositories scheduled`);
|
|
2772
3239
|
for (const [schedule3] of cronJobs) {
|
|
2773
3240
|
const repoCount = repositories.filter((r) => r.cronSchedule === schedule3).length;
|
|
2774
|
-
|
|
3241
|
+
globalLogger.info(`${schedule3}: ${repoCount} repository(ies)`);
|
|
2775
3242
|
}
|
|
2776
3243
|
}
|
|
2777
3244
|
}
|
|
@@ -2844,7 +3311,8 @@ async function main() {
|
|
|
2844
3311
|
debug: true
|
|
2845
3312
|
}));
|
|
2846
3313
|
}
|
|
2847
|
-
|
|
3314
|
+
const maxParallel = configFile.parallelism?.maxRepositories ?? configFile.defaults?.parallelism?.maxRepositories ?? DEFAULT_CONFIG.PARALLELISM.MAX_REPOSITORIES;
|
|
3315
|
+
await runMultipleRepositories(repositories, globalRunOnce, options.config, maxParallel);
|
|
2848
3316
|
} catch (error) {
|
|
2849
3317
|
if (error instanceof Error && error.message.includes("Config file not found")) {
|
|
2850
3318
|
console.error(`
|