sync-worktrees 1.7.5 → 2.0.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.
Files changed (74) hide show
  1. package/README.md +10 -5
  2. package/dist/index.js +2862 -203
  3. package/dist/index.js.map +7 -1
  4. package/package.json +19 -11
  5. package/dist/constants.d.ts +0 -54
  6. package/dist/constants.d.ts.map +0 -1
  7. package/dist/constants.js +0 -66
  8. package/dist/constants.js.map +0 -1
  9. package/dist/errors/index.d.ts +0 -51
  10. package/dist/errors/index.d.ts.map +0 -1
  11. package/dist/errors/index.js +0 -119
  12. package/dist/errors/index.js.map +0 -1
  13. package/dist/index.d.ts +0 -3
  14. package/dist/index.d.ts.map +0 -1
  15. package/dist/services/config-loader.service.d.ts +0 -9
  16. package/dist/services/config-loader.service.d.ts.map +0 -1
  17. package/dist/services/config-loader.service.js +0 -193
  18. package/dist/services/config-loader.service.js.map +0 -1
  19. package/dist/services/git.service.d.ts +0 -49
  20. package/dist/services/git.service.d.ts.map +0 -1
  21. package/dist/services/git.service.js +0 -746
  22. package/dist/services/git.service.js.map +0 -1
  23. package/dist/services/path-resolution.service.d.ts +0 -7
  24. package/dist/services/path-resolution.service.d.ts.map +0 -1
  25. package/dist/services/path-resolution.service.js +0 -58
  26. package/dist/services/path-resolution.service.js.map +0 -1
  27. package/dist/services/worktree-metadata.service.d.ts +0 -22
  28. package/dist/services/worktree-metadata.service.d.ts.map +0 -1
  29. package/dist/services/worktree-metadata.service.js +0 -276
  30. package/dist/services/worktree-metadata.service.js.map +0 -1
  31. package/dist/services/worktree-status.service.d.ts +0 -28
  32. package/dist/services/worktree-status.service.d.ts.map +0 -1
  33. package/dist/services/worktree-status.service.js +0 -229
  34. package/dist/services/worktree-status.service.js.map +0 -1
  35. package/dist/services/worktree-sync.service.d.ts +0 -16
  36. package/dist/services/worktree-sync.service.d.ts.map +0 -1
  37. package/dist/services/worktree-sync.service.js +0 -434
  38. package/dist/services/worktree-sync.service.js.map +0 -1
  39. package/dist/types/index.d.ts +0 -32
  40. package/dist/types/index.d.ts.map +0 -1
  41. package/dist/types/index.js +0 -3
  42. package/dist/types/index.js.map +0 -1
  43. package/dist/types/sync-metadata.d.ts +0 -16
  44. package/dist/types/sync-metadata.d.ts.map +0 -1
  45. package/dist/types/sync-metadata.js +0 -3
  46. package/dist/types/sync-metadata.js.map +0 -1
  47. package/dist/utils/cli.d.ts +0 -14
  48. package/dist/utils/cli.d.ts.map +0 -1
  49. package/dist/utils/cli.js +0 -117
  50. package/dist/utils/cli.js.map +0 -1
  51. package/dist/utils/config-generator.d.ts +0 -4
  52. package/dist/utils/config-generator.d.ts.map +0 -1
  53. package/dist/utils/config-generator.js +0 -112
  54. package/dist/utils/config-generator.js.map +0 -1
  55. package/dist/utils/date-filter.d.ts +0 -10
  56. package/dist/utils/date-filter.d.ts.map +0 -1
  57. package/dist/utils/date-filter.js +0 -47
  58. package/dist/utils/date-filter.js.map +0 -1
  59. package/dist/utils/git-url.d.ts +0 -15
  60. package/dist/utils/git-url.d.ts.map +0 -1
  61. package/dist/utils/git-url.js +0 -46
  62. package/dist/utils/git-url.js.map +0 -1
  63. package/dist/utils/interactive.d.ts +0 -3
  64. package/dist/utils/interactive.d.ts.map +0 -1
  65. package/dist/utils/interactive.js +0 -195
  66. package/dist/utils/interactive.js.map +0 -1
  67. package/dist/utils/lfs-error.d.ts +0 -23
  68. package/dist/utils/lfs-error.d.ts.map +0 -1
  69. package/dist/utils/lfs-error.js +0 -45
  70. package/dist/utils/lfs-error.js.map +0 -1
  71. package/dist/utils/retry.d.ts +0 -15
  72. package/dist/utils/retry.d.ts.map +0 -1
  73. package/dist/utils/retry.js +0 -78
  74. package/dist/utils/retry.js.map +0 -1
@@ -1,746 +0,0 @@
1
- "use strict";
2
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
- if (k2 === undefined) k2 = k;
4
- var desc = Object.getOwnPropertyDescriptor(m, k);
5
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
- desc = { enumerable: true, get: function() { return m[k]; } };
7
- }
8
- Object.defineProperty(o, k2, desc);
9
- }) : (function(o, m, k, k2) {
10
- if (k2 === undefined) k2 = k;
11
- o[k2] = m[k];
12
- }));
13
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
- Object.defineProperty(o, "default", { enumerable: true, value: v });
15
- }) : function(o, v) {
16
- o["default"] = v;
17
- });
18
- var __importStar = (this && this.__importStar) || (function () {
19
- var ownKeys = function(o) {
20
- ownKeys = Object.getOwnPropertyNames || function (o) {
21
- var ar = [];
22
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
- return ar;
24
- };
25
- return ownKeys(o);
26
- };
27
- return function (mod) {
28
- if (mod && mod.__esModule) return mod;
29
- var result = {};
30
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
- __setModuleDefault(result, mod);
32
- return result;
33
- };
34
- })();
35
- var __importDefault = (this && this.__importDefault) || function (mod) {
36
- return (mod && mod.__esModule) ? mod : { "default": mod };
37
- };
38
- Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.GitService = void 0;
40
- const fs = __importStar(require("fs/promises"));
41
- const path = __importStar(require("path"));
42
- const simple_git_1 = __importDefault(require("simple-git"));
43
- const git_url_1 = require("../utils/git-url");
44
- const lfs_error_1 = require("../utils/lfs-error");
45
- const worktree_metadata_service_1 = require("./worktree-metadata.service");
46
- class GitService {
47
- config;
48
- git = null;
49
- bareRepoPath;
50
- mainWorktreePath;
51
- defaultBranch = "main"; // Will be updated after detection
52
- metadataService;
53
- constructor(config) {
54
- this.config = config;
55
- this.bareRepoPath = this.config.bareRepoDir || (0, git_url_1.getDefaultBareRepoDir)(this.config.repoUrl);
56
- this.mainWorktreePath = path.join(this.config.worktreeDir, "main"); // Temporary, will be updated
57
- this.metadataService = new worktree_metadata_service_1.WorktreeMetadataService();
58
- }
59
- async initialize() {
60
- const { repoUrl } = this.config;
61
- try {
62
- // Check if bare repo already exists
63
- await fs.access(path.join(this.bareRepoPath, "HEAD"));
64
- console.log(`Bare repository at "${this.bareRepoPath}" already exists. Using it.`);
65
- }
66
- catch {
67
- // Clone as bare repository
68
- console.log(`Cloning from "${repoUrl}" as bare repository into "${this.bareRepoPath}"...`);
69
- await fs.mkdir(path.dirname(this.bareRepoPath), { recursive: true });
70
- const cloneGit = this.isLfsSkipEnabled() ? (0, simple_git_1.default)().env({ GIT_LFS_SKIP_SMUDGE: "1" }) : (0, simple_git_1.default)();
71
- await cloneGit.clone(repoUrl, this.bareRepoPath, ["--bare"]);
72
- console.log("✅ Clone successful.");
73
- }
74
- // Configure bare repository for worktrees
75
- const bareGit = (0, simple_git_1.default)(this.bareRepoPath);
76
- // Check if fetch config already exists
77
- try {
78
- const existingConfig = await bareGit.raw(["config", "--get-all", "remote.origin.fetch"]);
79
- const targetConfig = "+refs/heads/*:refs/remotes/origin/*";
80
- if (!existingConfig.includes(targetConfig)) {
81
- await bareGit.addConfig("remote.origin.fetch", targetConfig);
82
- }
83
- }
84
- catch {
85
- // Config doesn't exist, add it
86
- await bareGit.addConfig("remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*");
87
- }
88
- // Fetch all remote branches to ensure they exist locally
89
- console.log("Fetching remote branches...");
90
- await bareGit.fetch(["--all"]);
91
- // Detect the default branch
92
- this.defaultBranch = await this.detectDefaultBranch(bareGit);
93
- this.mainWorktreePath = path.join(this.config.worktreeDir, this.defaultBranch);
94
- console.log(`Detected default branch: ${this.defaultBranch}`);
95
- // Check if main worktree exists
96
- let needsMainWorktree = true;
97
- try {
98
- const worktrees = await this.getWorktreesFromBare(bareGit);
99
- needsMainWorktree = !worktrees.some((w) => path.resolve(w.path) === path.resolve(this.mainWorktreePath));
100
- }
101
- catch {
102
- // If worktree list fails, assume we need main worktree
103
- }
104
- if (needsMainWorktree) {
105
- // Create main worktree if it doesn't exist
106
- console.log(`Creating ${this.defaultBranch} worktree at "${this.mainWorktreePath}"...`);
107
- await fs.mkdir(this.config.worktreeDir, { recursive: true });
108
- // Use absolute path for worktree add to avoid relative path issues
109
- const absoluteWorktreePath = path.resolve(this.mainWorktreePath);
110
- try {
111
- // Check if local branch exists
112
- const branches = await bareGit.branch();
113
- const defaultBranchExists = branches.all.includes(this.defaultBranch);
114
- if (defaultBranchExists) {
115
- await bareGit.raw(["worktree", "add", absoluteWorktreePath, this.defaultBranch]);
116
- // Set upstream tracking after creating worktree
117
- const worktreeGit = this.isLfsSkipEnabled()
118
- ? (0, simple_git_1.default)(absoluteWorktreePath).env({ GIT_LFS_SKIP_SMUDGE: "1" })
119
- : (0, simple_git_1.default)(absoluteWorktreePath);
120
- await worktreeGit.branch(["--set-upstream-to", `origin/${this.defaultBranch}`, this.defaultBranch]);
121
- }
122
- else {
123
- // Create new branch tracking the remote branch
124
- await bareGit.raw([
125
- "worktree",
126
- "add",
127
- "--track",
128
- "-b",
129
- this.defaultBranch,
130
- absoluteWorktreePath,
131
- `origin/${this.defaultBranch}`,
132
- ]);
133
- }
134
- }
135
- catch (error) {
136
- const errorMessage = (0, lfs_error_1.getErrorMessage)(error);
137
- // Check if error is because directory already exists
138
- if (errorMessage.includes("already exists")) {
139
- console.log(`${this.defaultBranch} worktree directory already exists at '${absoluteWorktreePath}', skipping creation.`);
140
- }
141
- else {
142
- // Fallback to simple add if tracking setup fails
143
- console.warn(`Failed to create ${this.defaultBranch} worktree with tracking, using simple add: ${error}`);
144
- try {
145
- await bareGit.raw(["worktree", "add", absoluteWorktreePath, this.defaultBranch]);
146
- }
147
- catch (fallbackError) {
148
- const fallbackErrorMessage = (0, lfs_error_1.getErrorMessage)(fallbackError);
149
- if (fallbackErrorMessage.includes("already exists")) {
150
- console.log(`${this.defaultBranch} worktree directory already exists at '${absoluteWorktreePath}', skipping creation.`);
151
- }
152
- else {
153
- throw fallbackError;
154
- }
155
- }
156
- }
157
- }
158
- // Ensure the worktree is registered by checking it exists in the list
159
- const updatedWorktrees = await this.getWorktreesFromBare(bareGit);
160
- const mainWorktreeRegistered = updatedWorktrees.some((w) => path.resolve(w.path) === path.resolve(this.mainWorktreePath));
161
- if (!mainWorktreeRegistered) {
162
- // Only warn in non-test environments as this is common in tests due to Git state
163
- if (process.env.NODE_ENV !== "test") {
164
- console.warn(`Main worktree was created but not found in worktree list. This may cause issues.`);
165
- }
166
- }
167
- }
168
- // Use the main worktree as our primary git instance
169
- this.git = (0, simple_git_1.default)(this.mainWorktreePath);
170
- return this.git;
171
- }
172
- getGit() {
173
- if (!this.git) {
174
- throw new Error("Git service not initialized. Call initialize() first.");
175
- }
176
- return this.git;
177
- }
178
- getDefaultBranch() {
179
- return this.defaultBranch;
180
- }
181
- async fetchAll() {
182
- const git = this.getGit();
183
- console.log("Fetching latest data from remote...");
184
- if (this.isLfsSkipEnabled()) {
185
- await git.env({ GIT_LFS_SKIP_SMUDGE: "1" }).fetch(["--all", "--prune"]);
186
- }
187
- else {
188
- await git.fetch(["--all", "--prune"]);
189
- }
190
- }
191
- async fetchBranch(branchName) {
192
- const git = this.getGit();
193
- // Update only the remote ref for the branch to keep refs/remotes/origin/* fresh
194
- if (this.isLfsSkipEnabled()) {
195
- await git.env({ GIT_LFS_SKIP_SMUDGE: "1" }).fetch(["origin", branchName, "--prune"]);
196
- }
197
- else {
198
- await git.fetch(["origin", branchName, "--prune"]);
199
- }
200
- }
201
- async getRemoteBranches() {
202
- const git = this.getGit();
203
- const branches = await git.branch(["-r"]);
204
- return branches.all
205
- .filter((b) => b.startsWith("origin/") && !b.endsWith("/HEAD"))
206
- .map((b) => b.replace("origin/", ""))
207
- .filter((b) => b !== "origin" && b.length > 0);
208
- }
209
- async getRemoteBranchesWithActivity() {
210
- const git = this.getGit();
211
- // Use for-each-ref to get branch names with their last commit dates
212
- const result = await git.raw([
213
- "for-each-ref",
214
- "--format=%(refname:short)|%(committerdate:iso8601)",
215
- "refs/remotes/origin",
216
- ]);
217
- const branches = [];
218
- const lines = result
219
- .trim()
220
- .split("\n")
221
- .filter((line) => line);
222
- for (const line of lines) {
223
- const [ref, dateStr] = line.split("|", 2);
224
- if (ref && dateStr && !ref.endsWith("/HEAD")) {
225
- const branch = ref.replace("origin/", "");
226
- // Skip invalid branch names
227
- if (branch === "origin" || branch.length === 0) {
228
- continue;
229
- }
230
- const lastActivity = new Date(dateStr);
231
- // Skip if the date is invalid
232
- if (!isNaN(lastActivity.getTime())) {
233
- branches.push({ branch, lastActivity });
234
- }
235
- }
236
- }
237
- return branches;
238
- }
239
- async createWorktreeMetadata(bareGit, worktreePath, branchName) {
240
- try {
241
- const worktreeGit = this.isLfsSkipEnabled()
242
- ? (0, simple_git_1.default)(worktreePath).env({ GIT_LFS_SKIP_SMUDGE: "1" })
243
- : (0, simple_git_1.default)(worktreePath);
244
- const currentCommit = await worktreeGit.revparse(["HEAD"]);
245
- const parentCommit = await bareGit.revparse([this.defaultBranch]);
246
- await this.metadataService.createInitialMetadataFromPath(this.bareRepoPath, worktreePath, currentCommit.trim(), `origin/${branchName}`, this.defaultBranch, parentCommit.trim());
247
- }
248
- catch (metadataError) {
249
- console.error(` - ❌ Failed to create metadata for '${branchName}': ${metadataError}`);
250
- throw new Error(`Metadata creation failed for ${branchName}. This worktree cannot be auto-managed.`);
251
- }
252
- }
253
- async addWorktree(branchName, worktreePath) {
254
- const bareGit = this.isLfsSkipEnabled()
255
- ? (0, simple_git_1.default)(this.bareRepoPath).env({ GIT_LFS_SKIP_SMUDGE: "1" })
256
- : (0, simple_git_1.default)(this.bareRepoPath);
257
- // Use absolute path for worktree add to avoid relative path issues
258
- const absoluteWorktreePath = path.resolve(worktreePath);
259
- // Ensure parent directory exists for nested branch paths
260
- await fs.mkdir(path.dirname(absoluteWorktreePath), { recursive: true });
261
- // Check if directory already exists (could be from a failed previous attempt)
262
- try {
263
- await fs.access(absoluteWorktreePath);
264
- // Directory exists - check if it's already a valid worktree
265
- const worktrees = await this.getWorktreesFromBare(bareGit);
266
- const isValidWorktree = worktrees.some((w) => path.resolve(w.path) === absoluteWorktreePath);
267
- if (isValidWorktree) {
268
- console.log(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
269
- return;
270
- }
271
- else {
272
- // Directory exists but is not a valid worktree - clean it up
273
- console.log(` - Cleaning up orphaned directory at '${absoluteWorktreePath}'`);
274
- await fs.rm(absoluteWorktreePath, { recursive: true, force: true });
275
- }
276
- }
277
- catch {
278
- // Directory doesn't exist, which is expected - continue with creation
279
- }
280
- try {
281
- // Check if local branch already exists
282
- const branches = await bareGit.branch();
283
- const localBranchExists = branches.all.includes(branchName);
284
- if (localBranchExists || branchName.includes("/")) {
285
- await bareGit.raw(["worktree", "add", absoluteWorktreePath, branchName]);
286
- const worktreeGit = this.isLfsSkipEnabled()
287
- ? (0, simple_git_1.default)(absoluteWorktreePath).env({ GIT_LFS_SKIP_SMUDGE: "1" })
288
- : (0, simple_git_1.default)(absoluteWorktreePath);
289
- await worktreeGit.branch(["--set-upstream-to", `origin/${branchName}`, branchName]);
290
- }
291
- else {
292
- // Create new branch tracking the remote branch
293
- await bareGit.raw([
294
- "worktree",
295
- "add",
296
- "--track",
297
- "-b",
298
- branchName,
299
- absoluteWorktreePath,
300
- `origin/${branchName}`,
301
- ]);
302
- }
303
- console.log(` - Created worktree for '${branchName}' with tracking to origin/${branchName}`);
304
- // Create metadata for the new worktree
305
- await this.createWorktreeMetadata(bareGit, absoluteWorktreePath, branchName);
306
- }
307
- catch (error) {
308
- const errorMessage = (0, lfs_error_1.getErrorMessage)(error);
309
- // Re-throw metadata creation errors - these are fatal and should not fall back
310
- if (errorMessage.includes("Metadata creation failed")) {
311
- throw error;
312
- }
313
- // Check if this is an "already registered" error
314
- if (errorMessage.includes("already registered worktree")) {
315
- console.warn(` - Worktree already registered but missing. Pruning and retrying...`);
316
- await bareGit.raw(["worktree", "prune"]);
317
- // Clean up directory if it exists
318
- try {
319
- await fs.rm(absoluteWorktreePath, { recursive: true, force: true });
320
- }
321
- catch {
322
- // Directory might not exist, ignore
323
- }
324
- // Retry once after pruning
325
- try {
326
- await bareGit.raw([
327
- "worktree",
328
- "add",
329
- "--track",
330
- "-b",
331
- branchName,
332
- absoluteWorktreePath,
333
- `origin/${branchName}`,
334
- ]);
335
- console.log(` - Created worktree for '${branchName}' after pruning`);
336
- await this.createWorktreeMetadata(bareGit, absoluteWorktreePath, branchName);
337
- return;
338
- }
339
- catch (retryError) {
340
- console.error(` - Failed to create worktree after pruning: ${retryError}`);
341
- throw retryError;
342
- }
343
- }
344
- // If the worktree add fails with tracking, fall back to non-tracking version
345
- // This handles edge cases where the remote branch might not exist yet
346
- console.warn(` - Failed to create worktree with tracking, falling back to simple add: ${error}`);
347
- // Check again if directory exists before fallback attempt
348
- try {
349
- await fs.access(absoluteWorktreePath);
350
- // Directory exists - check if it's already a valid worktree
351
- const worktrees = await this.getWorktreesFromBare(bareGit);
352
- const isValidWorktree = worktrees.some((w) => path.resolve(w.path) === absoluteWorktreePath);
353
- if (isValidWorktree) {
354
- console.log(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`);
355
- return;
356
- }
357
- else {
358
- // Directory exists but is not a valid worktree - clean it up
359
- console.log(` - Cleaning up orphaned directory at '${absoluteWorktreePath}' before fallback attempt`);
360
- await fs.rm(absoluteWorktreePath, { recursive: true, force: true });
361
- }
362
- }
363
- catch {
364
- // Directory doesn't exist, which is expected - continue with fallback
365
- }
366
- await bareGit.raw(["worktree", "add", absoluteWorktreePath, branchName]);
367
- console.log(` - Created worktree for '${branchName}' (without tracking)`);
368
- // Try to create metadata even without tracking
369
- await this.createWorktreeMetadata(bareGit, absoluteWorktreePath, branchName);
370
- }
371
- }
372
- async removeWorktree(worktreePath) {
373
- const bareGit = (0, simple_git_1.default)(this.bareRepoPath);
374
- await bareGit.raw(["worktree", "remove", worktreePath, "--force"]);
375
- console.log(` - ✅ Safely removed stale worktree at '${worktreePath}'.`);
376
- // Clean up metadata using the worktree path
377
- try {
378
- await this.metadataService.deleteMetadataFromPath(this.bareRepoPath, worktreePath);
379
- }
380
- catch (metadataError) {
381
- console.warn(`Failed to delete metadata for worktree: ${metadataError}`);
382
- }
383
- }
384
- async pruneWorktrees() {
385
- const bareGit = (0, simple_git_1.default)(this.bareRepoPath);
386
- await bareGit.raw(["worktree", "prune"]);
387
- console.log("Pruned worktree metadata.");
388
- }
389
- async checkWorktreeStatus(worktreePath) {
390
- const worktreeGit = (0, simple_git_1.default)(worktreePath);
391
- const status = await worktreeGit.status();
392
- return status.isClean();
393
- }
394
- async isDetachedHead(worktreeGit) {
395
- try {
396
- const branchSummary = await worktreeGit.branch();
397
- return !branchSummary.current || branchSummary.detached;
398
- }
399
- catch {
400
- return true;
401
- }
402
- }
403
- async hasUnpushedCommits(worktreePath) {
404
- const worktreeGit = (0, simple_git_1.default)(worktreePath);
405
- try {
406
- // Check if in detached HEAD state
407
- if (await this.isDetachedHead(worktreeGit)) {
408
- return false;
409
- }
410
- // Get the current branch name
411
- const branchSummary = await worktreeGit.branch();
412
- const currentBranch = branchSummary.current;
413
- // Check if upstream is gone
414
- const upstreamGone = await this.hasUpstreamGone(worktreePath);
415
- if (upstreamGone) {
416
- // Load metadata to check for commits after last sync (use path-based method)
417
- const metadata = await this.metadataService.loadMetadataFromPath(this.bareRepoPath, worktreePath);
418
- if (metadata?.lastSyncCommit) {
419
- try {
420
- // Check for commits after last sync
421
- const newCommitsResult = await worktreeGit.raw(["rev-list", "--count", `${metadata.lastSyncCommit}..HEAD`]);
422
- const newCommitsCount = parseInt(newCommitsResult.trim(), 10);
423
- return newCommitsCount > 0;
424
- }
425
- catch {
426
- // If lastSyncCommit doesn't exist, fall through to regular check
427
- }
428
- }
429
- }
430
- // Count commits that exist in the current branch but not in any remote
431
- const result = await worktreeGit.raw(["rev-list", "--count", currentBranch, "--not", "--remotes"]);
432
- const unpushedCount = parseInt(result.trim(), 10);
433
- return unpushedCount > 0;
434
- }
435
- catch (error) {
436
- // If the command fails (e.g., branch doesn't exist), assume it's safe
437
- console.error(`Error checking unpushed commits: ${error}`);
438
- return false;
439
- }
440
- }
441
- async hasUpstreamGone(worktreePath) {
442
- const worktreeGit = (0, simple_git_1.default)(worktreePath);
443
- try {
444
- // Check if in detached HEAD state
445
- if (await this.isDetachedHead(worktreeGit)) {
446
- return false;
447
- }
448
- const branchSummary = await worktreeGit.branch();
449
- const currentBranch = branchSummary.current;
450
- // Try to get upstream branch
451
- const upstream = await worktreeGit.raw(["rev-parse", "--abbrev-ref", `${currentBranch}@{upstream}`]);
452
- // Check if upstream exists in remotes
453
- const remoteBranches = await worktreeGit.branch(["-r"]);
454
- return !remoteBranches.all.includes(upstream.trim());
455
- }
456
- catch (error) {
457
- const errorMessage = (0, lfs_error_1.getErrorMessage)(error);
458
- if (errorMessage.includes("fatal: no upstream configured") ||
459
- errorMessage.includes("no upstream configured for branch")) {
460
- return false;
461
- }
462
- if (errorMessage.includes("fatal: ambiguous argument") || errorMessage.includes("unknown revision or path")) {
463
- try {
464
- const branchSummary = await worktreeGit.branch();
465
- const currentBranch = branchSummary.current;
466
- const remoteResult = await worktreeGit
467
- .raw(["config", "--get", `branch.${currentBranch}.remote`])
468
- .catch(() => "");
469
- const mergeResult = await worktreeGit
470
- .raw(["config", "--get", `branch.${currentBranch}.merge`])
471
- .catch(() => "");
472
- const remote = remoteResult.trim();
473
- const merge = mergeResult.trim();
474
- if (remote && merge) {
475
- const remoteBranchName = merge.replace("refs/heads/", "");
476
- const expectedUpstream = `${remote}/${remoteBranchName}`;
477
- const remoteBranches = await worktreeGit.branch(["-r"]);
478
- return !remoteBranches.all.includes(expectedUpstream);
479
- }
480
- }
481
- catch {
482
- // Can't determine config, be conservative
483
- }
484
- return false;
485
- }
486
- console.error(`Unexpected error checking upstream status for ${worktreePath}. ` +
487
- `This might indicate a real issue rather than a missing upstream. ` +
488
- `Error: ${errorMessage}`);
489
- return false;
490
- }
491
- }
492
- async hasStashedChanges(worktreePath) {
493
- const worktreeGit = (0, simple_git_1.default)(worktreePath);
494
- try {
495
- const stashList = await worktreeGit.stashList();
496
- return stashList.total > 0;
497
- }
498
- catch (error) {
499
- // If stash check fails, assume it's unsafe to delete
500
- console.error(`Error checking stash: ${error}`);
501
- return true;
502
- }
503
- }
504
- async hasModifiedSubmodules(worktreePath) {
505
- const worktreeGit = (0, simple_git_1.default)(worktreePath);
506
- try {
507
- const result = await worktreeGit.raw(["submodule", "status"]);
508
- // Check for '+' or '-' prefix indicating modifications
509
- return /^[+-]/m.test(result);
510
- }
511
- catch {
512
- return false; // No submodules or submodule command failed
513
- }
514
- }
515
- async hasOperationInProgress(worktreePath) {
516
- // Resolve the actual git directory; in worktrees .git is a file pointing to the real gitdir
517
- let resolvedGitDir = path.join(worktreePath, ".git");
518
- try {
519
- const stat = await fs.stat(resolvedGitDir);
520
- if (stat.isFile()) {
521
- const content = await fs.readFile(resolvedGitDir, "utf-8");
522
- const match = content.match(/gitdir:\s*(.*)/i);
523
- if (match && match[1]) {
524
- resolvedGitDir = match[1].trim();
525
- if (!path.isAbsolute(resolvedGitDir)) {
526
- resolvedGitDir = path.resolve(worktreePath, resolvedGitDir);
527
- }
528
- }
529
- }
530
- }
531
- catch {
532
- // Fall back to default .git directory
533
- }
534
- const checkFiles = ["MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD", "BISECT_LOG", "rebase-merge", "rebase-apply"];
535
- for (const file of checkFiles) {
536
- try {
537
- await fs.access(path.join(resolvedGitDir, file));
538
- return true; // Operation in progress
539
- }
540
- catch {
541
- // File doesn't exist, continue checking
542
- }
543
- }
544
- return false;
545
- }
546
- async getCurrentBranch() {
547
- const git = this.getGit();
548
- const branchSummary = await git.branch();
549
- return branchSummary.current;
550
- }
551
- async detectDefaultBranch(bareGit) {
552
- try {
553
- // Try to get the symbolic ref for origin/HEAD
554
- const headRef = await bareGit.raw(["symbolic-ref", "refs/remotes/origin/HEAD"]);
555
- // Extract branch name from refs/remotes/origin/main or refs/remotes/origin/master
556
- const branch = headRef.trim().split("/").pop();
557
- if (branch) {
558
- return branch;
559
- }
560
- }
561
- catch {
562
- // If that fails, try to set HEAD automatically
563
- try {
564
- await bareGit.raw(["remote", "set-head", "origin", "-a"]);
565
- const headRef = await bareGit.raw(["symbolic-ref", "refs/remotes/origin/HEAD"]);
566
- const branch = headRef.trim().split("/").pop();
567
- if (branch) {
568
- return branch;
569
- }
570
- }
571
- catch {
572
- // If all else fails, try to detect from remote branches
573
- try {
574
- const remoteBranches = await bareGit.branch(["-r"]);
575
- // Common default branch names in order of preference
576
- const commonDefaults = ["main", "master", "develop", "trunk"];
577
- for (const defaultName of commonDefaults) {
578
- if (remoteBranches.all.some((branch) => branch === `origin/${defaultName}`)) {
579
- return defaultName;
580
- }
581
- }
582
- }
583
- catch {
584
- // Ignore and fall through to default
585
- }
586
- }
587
- }
588
- // Final fallback
589
- return "main";
590
- }
591
- isLfsSkipEnabled() {
592
- return this.config.skipLfs || process.env.GIT_LFS_SKIP_SMUDGE === "1";
593
- }
594
- async getWorktrees() {
595
- const bareGit = (0, simple_git_1.default)(this.bareRepoPath);
596
- return this.getWorktreesFromBare(bareGit);
597
- }
598
- async isWorktreeBehind(worktreePath) {
599
- const worktreeGit = (0, simple_git_1.default)(worktreePath);
600
- try {
601
- // Get the current branch
602
- const branchSummary = await worktreeGit.branch();
603
- const currentBranch = branchSummary.current;
604
- // Check if the branch has an upstream
605
- const upstreamInfo = await worktreeGit.raw(["rev-parse", "--abbrev-ref", `${currentBranch}@{upstream}`]);
606
- if (!upstreamInfo.trim()) {
607
- return false; // No upstream, can't be behind
608
- }
609
- // Count commits behind upstream
610
- const behindCount = await worktreeGit.raw(["rev-list", "--count", `HEAD..${upstreamInfo.trim()}`]);
611
- return parseInt(behindCount.trim(), 10) > 0;
612
- }
613
- catch {
614
- // If any command fails, assume not behind
615
- return false;
616
- }
617
- }
618
- async updateWorktree(worktreePath) {
619
- const worktreeGit = this.isLfsSkipEnabled()
620
- ? (0, simple_git_1.default)(worktreePath).env({ GIT_LFS_SKIP_SMUDGE: "1" })
621
- : (0, simple_git_1.default)(worktreePath);
622
- // Perform a fast-forward merge
623
- const branchSummary = await worktreeGit.branch();
624
- const currentBranch = branchSummary.current;
625
- await worktreeGit.merge([`origin/${currentBranch}`, "--ff-only"]);
626
- // Skip metadata update for main worktree
627
- const isMainWorktree = path.resolve(worktreePath) === path.resolve(this.mainWorktreePath);
628
- if (isMainWorktree) {
629
- return;
630
- }
631
- // Update metadata after successful update (use path-based method)
632
- try {
633
- const currentCommit = await worktreeGit.revparse(["HEAD"]);
634
- await this.metadataService.updateLastSyncFromPath(this.bareRepoPath, worktreePath, currentCommit.trim(), "updated", this.defaultBranch);
635
- }
636
- catch (metadataError) {
637
- console.warn(`Failed to update metadata for worktree: ${metadataError}`);
638
- }
639
- }
640
- async hasDivergedHistory(worktreePath, expectedBranch) {
641
- const worktreeGit = (0, simple_git_1.default)(worktreePath);
642
- // Validate branch matches
643
- const branchInfo = await worktreeGit.branch();
644
- if (branchInfo.current !== expectedBranch) {
645
- console.warn(`Branch mismatch in hasDivergedHistory: expected ${expectedBranch}, got ${branchInfo.current}`);
646
- return false; // Conservative: assume can fast-forward
647
- }
648
- try {
649
- // Check if HEAD is an ancestor of the remote branch (can fast-forward)
650
- await worktreeGit.raw(["merge-base", "--is-ancestor", "HEAD", `origin/${expectedBranch}`]);
651
- return false; // Can fast-forward
652
- }
653
- catch {
654
- return true; // Histories have diverged
655
- }
656
- }
657
- async canFastForward(worktreePath, branch) {
658
- const worktreeGit = (0, simple_git_1.default)(worktreePath);
659
- try {
660
- // Get the merge base between HEAD and the remote branch
661
- const mergeBase = await worktreeGit.raw(["merge-base", "HEAD", `origin/${branch}`]);
662
- const mergeBaseSha = mergeBase.trim();
663
- // Get current HEAD SHA
664
- const headSha = await worktreeGit.revparse(["HEAD"]);
665
- const headShaTrimmed = headSha.trim();
666
- // If merge base equals HEAD, then HEAD is an ancestor of remote and can fast-forward
667
- return mergeBaseSha === headShaTrimmed;
668
- }
669
- catch {
670
- // If merge-base fails, branches have diverged
671
- return false;
672
- }
673
- }
674
- async compareTreeContent(worktreePath, branch) {
675
- const worktreeGit = (0, simple_git_1.default)(worktreePath);
676
- try {
677
- // Get the tree SHA for the current HEAD
678
- const localTree = await worktreeGit.raw(["rev-parse", "HEAD^{tree}"]);
679
- // Get the tree SHA for the remote branch
680
- const remoteTree = await worktreeGit.raw(["rev-parse", `origin/${branch}^{tree}`]);
681
- return localTree.trim() === remoteTree.trim();
682
- }
683
- catch (error) {
684
- console.error(`Error comparing tree content: ${error}`);
685
- return false; // Assume trees are different if we can't compare
686
- }
687
- }
688
- async resetToUpstream(worktreePath, branch) {
689
- const worktreeGit = this.isLfsSkipEnabled()
690
- ? (0, simple_git_1.default)(worktreePath).env({ GIT_LFS_SKIP_SMUDGE: "1" })
691
- : (0, simple_git_1.default)(worktreePath);
692
- await worktreeGit.reset(["--hard", `origin/${branch}`]);
693
- // Update metadata after reset (use path-based method)
694
- try {
695
- const currentCommit = await worktreeGit.revparse(["HEAD"]);
696
- await this.metadataService.updateLastSyncFromPath(this.bareRepoPath, worktreePath, currentCommit.trim(), "updated", this.defaultBranch);
697
- }
698
- catch (metadataError) {
699
- console.warn(`Failed to update metadata after reset: ${metadataError}`);
700
- }
701
- }
702
- async getCurrentCommit(worktreePath) {
703
- const worktreeGit = (0, simple_git_1.default)(worktreePath);
704
- const commit = await worktreeGit.revparse(["HEAD"]);
705
- return commit.trim();
706
- }
707
- async getRemoteCommit(ref) {
708
- // Use the bare repository to read remote commit to avoid dependency on main worktree path
709
- const git = (0, simple_git_1.default)(this.bareRepoPath);
710
- const commit = await git.revparse([ref]);
711
- return commit.trim();
712
- }
713
- async getWorktreesFromBare(bareGit) {
714
- const result = await bareGit.raw(["worktree", "list", "--porcelain"]);
715
- const worktrees = [];
716
- const lines = result.trim().split("\n");
717
- let currentWorktree = {};
718
- for (const line of lines) {
719
- if (line.startsWith("worktree ")) {
720
- currentWorktree.path = line.substring(9);
721
- }
722
- else if (line.startsWith("branch ")) {
723
- currentWorktree.branch = line.substring(7).replace("refs/heads/", "");
724
- }
725
- else if (line === "detached") {
726
- currentWorktree.detached = true;
727
- }
728
- else if (line.trim() === "") {
729
- if (currentWorktree.path) {
730
- // Only include worktrees that have a branch (not detached)
731
- if (currentWorktree.branch && !currentWorktree.detached) {
732
- worktrees.push({ path: currentWorktree.path, branch: currentWorktree.branch });
733
- }
734
- }
735
- currentWorktree = {};
736
- }
737
- }
738
- // Handle the last worktree if there's no trailing empty line
739
- if (currentWorktree.path && currentWorktree.branch && !currentWorktree.detached) {
740
- worktrees.push({ path: currentWorktree.path, branch: currentWorktree.branch });
741
- }
742
- return worktrees;
743
- }
744
- }
745
- exports.GitService = GitService;
746
- //# sourceMappingURL=git.service.js.map