UNPKG

sync-worktrees

Version:

Automatically synchronize Git worktrees with remote branches - perfect for multi-branch development workflows

715 lines 33.4 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.GitService = void 0; const fs = __importStar(require("fs/promises")); const path = __importStar(require("path")); const simple_git_1 = __importDefault(require("simple-git")); const git_url_1 = require("../utils/git-url"); const worktree_metadata_service_1 = require("./worktree-metadata.service"); class GitService { config; git = null; bareRepoPath; mainWorktreePath; defaultBranch = "main"; // Will be updated after detection metadataService; constructor(config) { this.config = config; this.bareRepoPath = this.config.bareRepoDir || (0, git_url_1.getDefaultBareRepoDir)(this.config.repoUrl); this.mainWorktreePath = path.join(this.config.worktreeDir, "main"); // Temporary, will be updated this.metadataService = new worktree_metadata_service_1.WorktreeMetadataService(); } async initialize() { const { repoUrl } = this.config; try { // Check if bare repo already exists await fs.access(path.join(this.bareRepoPath, "HEAD")); console.log(`Bare repository at "${this.bareRepoPath}" already exists. Using it.`); } catch { // Clone as bare repository console.log(`Cloning from "${repoUrl}" as bare repository into "${this.bareRepoPath}"...`); await fs.mkdir(path.dirname(this.bareRepoPath), { recursive: true }); const cloneGit = this.isLfsSkipEnabled() ? (0, simple_git_1.default)().env({ GIT_LFS_SKIP_SMUDGE: "1" }) : (0, simple_git_1.default)(); await cloneGit.clone(repoUrl, this.bareRepoPath, ["--bare"]); console.log("✅ Clone successful."); } // Configure bare repository for worktrees const bareGit = (0, simple_git_1.default)(this.bareRepoPath); // Check if fetch config already exists try { const existingConfig = await bareGit.raw(["config", "--get-all", "remote.origin.fetch"]); const targetConfig = "+refs/heads/*:refs/remotes/origin/*"; if (!existingConfig.includes(targetConfig)) { await bareGit.addConfig("remote.origin.fetch", targetConfig); } } catch { // Config doesn't exist, add it await bareGit.addConfig("remote.origin.fetch", "+refs/heads/*:refs/remotes/origin/*"); } // Fetch all remote branches to ensure they exist locally console.log("Fetching remote branches..."); if (this.isLfsSkipEnabled()) { await bareGit.env({ GIT_LFS_SKIP_SMUDGE: "1" }).fetch(["--all"]); } else { await bareGit.fetch(["--all"]); } // Detect the default branch this.defaultBranch = await this.detectDefaultBranch(bareGit); this.mainWorktreePath = path.join(this.config.worktreeDir, this.defaultBranch); console.log(`Detected default branch: ${this.defaultBranch}`); // Check if main worktree exists let needsMainWorktree = true; try { const worktrees = await this.getWorktreesFromBare(bareGit); needsMainWorktree = !worktrees.some((w) => path.resolve(w.path) === path.resolve(this.mainWorktreePath)); } catch { // If worktree list fails, assume we need main worktree } if (needsMainWorktree) { // Create main worktree if it doesn't exist console.log(`Creating ${this.defaultBranch} worktree at "${this.mainWorktreePath}"...`); await fs.mkdir(this.config.worktreeDir, { recursive: true }); // Use absolute path for worktree add to avoid relative path issues const absoluteWorktreePath = path.resolve(this.mainWorktreePath); try { // Check if local branch exists const branches = await bareGit.branch(); const defaultBranchExists = branches.all.includes(this.defaultBranch); if (defaultBranchExists) { if (this.isLfsSkipEnabled()) { await bareGit .env({ GIT_LFS_SKIP_SMUDGE: "1" }) .raw(["worktree", "add", absoluteWorktreePath, this.defaultBranch]); } else { await bareGit.raw(["worktree", "add", absoluteWorktreePath, this.defaultBranch]); } // Set upstream tracking after creating worktree const worktreeGit = this.isLfsSkipEnabled() ? (0, simple_git_1.default)(absoluteWorktreePath).env({ GIT_LFS_SKIP_SMUDGE: "1" }) : (0, simple_git_1.default)(absoluteWorktreePath); await worktreeGit.branch(["--set-upstream-to", `origin/${this.defaultBranch}`, this.defaultBranch]); } else { // Create new branch tracking the remote branch if (this.isLfsSkipEnabled()) { await bareGit .env({ GIT_LFS_SKIP_SMUDGE: "1" }) .raw([ "worktree", "add", "--track", "-b", this.defaultBranch, absoluteWorktreePath, `origin/${this.defaultBranch}`, ]); } else { await bareGit.raw([ "worktree", "add", "--track", "-b", this.defaultBranch, absoluteWorktreePath, `origin/${this.defaultBranch}`, ]); } } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); // Check if error is because directory already exists if (errorMessage.includes("already exists")) { console.log(`${this.defaultBranch} worktree directory already exists at '${absoluteWorktreePath}', skipping creation.`); } else { // Fallback to simple add if tracking setup fails console.warn(`Failed to create ${this.defaultBranch} worktree with tracking, using simple add: ${error}`); try { if (this.isLfsSkipEnabled()) { await bareGit .env({ GIT_LFS_SKIP_SMUDGE: "1" }) .raw(["worktree", "add", absoluteWorktreePath, this.defaultBranch]); } else { await bareGit.raw(["worktree", "add", absoluteWorktreePath, this.defaultBranch]); } } catch (fallbackError) { const fallbackErrorMessage = fallbackError instanceof Error ? fallbackError.message : String(fallbackError); if (fallbackErrorMessage.includes("already exists")) { console.log(`${this.defaultBranch} worktree directory already exists at '${absoluteWorktreePath}', skipping creation.`); } else { throw fallbackError; } } } } // Ensure the worktree is registered by checking it exists in the list const updatedWorktrees = await this.getWorktreesFromBare(bareGit); const mainWorktreeRegistered = updatedWorktrees.some((w) => path.resolve(w.path) === path.resolve(this.mainWorktreePath)); if (!mainWorktreeRegistered) { // Only warn in non-test environments as this is common in tests due to Git state if (process.env.NODE_ENV !== "test") { console.warn(`Main worktree was created but not found in worktree list. This may cause issues.`); } } } // Use the main worktree as our primary git instance this.git = (0, simple_git_1.default)(this.mainWorktreePath); return this.git; } getGit() { if (!this.git) { throw new Error("Git service not initialized. Call initialize() first."); } return this.git; } getDefaultBranch() { return this.defaultBranch; } async fetchAll() { const git = this.getGit(); console.log("Fetching latest data from remote..."); if (this.isLfsSkipEnabled()) { await git.env({ GIT_LFS_SKIP_SMUDGE: "1" }).fetch(["--all", "--prune"]); } else { await git.fetch(["--all", "--prune"]); } } async fetchBranch(branchName) { const git = this.getGit(); if (this.isLfsSkipEnabled()) { await git.env({ GIT_LFS_SKIP_SMUDGE: "1" }).fetch(["origin", `${branchName}:${branchName}`, "--prune"]); } else { await git.fetch(["origin", `${branchName}:${branchName}`, "--prune"]); } } async getRemoteBranches() { const git = this.getGit(); const branches = await git.branch(["-r"]); return branches.all .filter((b) => b.startsWith("origin/") && !b.endsWith("/HEAD")) .map((b) => b.replace("origin/", "")); } async getRemoteBranchesWithActivity() { const git = this.getGit(); // Use for-each-ref to get branch names with their last commit dates const result = await git.raw([ "for-each-ref", "--format=%(refname:short)|%(committerdate:iso8601)", "refs/remotes/origin", ]); const branches = []; const lines = result .trim() .split("\n") .filter((line) => line); for (const line of lines) { const [ref, dateStr] = line.split("|", 2); if (ref && dateStr && !ref.endsWith("/HEAD")) { const branch = ref.replace("origin/", ""); const lastActivity = new Date(dateStr); // Skip if the date is invalid if (!isNaN(lastActivity.getTime())) { branches.push({ branch, lastActivity }); } } } return branches; } async createWorktreeMetadata(bareGit, worktreePath, branchName) { try { const worktreeGit = this.isLfsSkipEnabled() ? (0, simple_git_1.default)(worktreePath).env({ GIT_LFS_SKIP_SMUDGE: "1" }) : (0, simple_git_1.default)(worktreePath); const currentCommit = await worktreeGit.revparse(["HEAD"]); const parentCommit = await bareGit.revparse([this.defaultBranch]); await this.metadataService.createInitialMetadata(this.bareRepoPath, branchName, currentCommit.trim(), `origin/${branchName}`, this.defaultBranch, parentCommit.trim()); } catch (metadataError) { console.warn(` - Failed to create metadata for worktree: ${metadataError}`); } } async addWorktree(branchName, worktreePath) { const bareGit = this.isLfsSkipEnabled() ? (0, simple_git_1.default)(this.bareRepoPath).env({ GIT_LFS_SKIP_SMUDGE: "1" }) : (0, simple_git_1.default)(this.bareRepoPath); // Use absolute path for worktree add to avoid relative path issues const absoluteWorktreePath = path.resolve(worktreePath); // Check if directory already exists (could be from a failed previous attempt) try { await fs.access(absoluteWorktreePath); // Directory exists - check if it's already a valid worktree const worktrees = await this.getWorktreesFromBare(bareGit); const isValidWorktree = worktrees.some((w) => path.resolve(w.path) === absoluteWorktreePath); if (isValidWorktree) { console.log(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`); return; } else { // Directory exists but is not a valid worktree - clean it up console.log(` - Cleaning up orphaned directory at '${absoluteWorktreePath}'`); await fs.rm(absoluteWorktreePath, { recursive: true, force: true }); } } catch { // Directory doesn't exist, which is expected - continue with creation } try { // Check if local branch already exists const branches = await bareGit.branch(); const localBranchExists = branches.all.includes(branchName); if (localBranchExists) { // If local branch exists, just add worktree with existing branch await bareGit.raw(["worktree", "add", absoluteWorktreePath, branchName]); // Set upstream tracking after creating worktree const worktreeGit = this.isLfsSkipEnabled() ? (0, simple_git_1.default)(absoluteWorktreePath).env({ GIT_LFS_SKIP_SMUDGE: "1" }) : (0, simple_git_1.default)(absoluteWorktreePath); await worktreeGit.branch(["--set-upstream-to", `origin/${branchName}`, branchName]); } else { // Create new branch tracking the remote branch await bareGit.raw([ "worktree", "add", "--track", "-b", branchName, absoluteWorktreePath, `origin/${branchName}`, ]); } console.log(` - Created worktree for '${branchName}' with tracking to origin/${branchName}`); // Create metadata for the new worktree await this.createWorktreeMetadata(bareGit, absoluteWorktreePath, branchName); } catch (error) { // If the worktree add fails with tracking, fall back to non-tracking version // This handles edge cases where the remote branch might not exist yet console.warn(` - Failed to create worktree with tracking, falling back to simple add: ${error}`); // Check again if directory exists before fallback attempt try { await fs.access(absoluteWorktreePath); // Directory exists - check if it's already a valid worktree const worktrees = await this.getWorktreesFromBare(bareGit); const isValidWorktree = worktrees.some((w) => path.resolve(w.path) === absoluteWorktreePath); if (isValidWorktree) { console.log(` - Worktree for '${branchName}' already exists at '${absoluteWorktreePath}'`); return; } else { // Directory exists but is not a valid worktree - clean it up console.log(` - Cleaning up orphaned directory at '${absoluteWorktreePath}' before fallback attempt`); await fs.rm(absoluteWorktreePath, { recursive: true, force: true }); } } catch { // Directory doesn't exist, which is expected - continue with fallback } await bareGit.raw(["worktree", "add", absoluteWorktreePath, branchName]); console.log(` - Created worktree for '${branchName}' (without tracking)`); // Try to create metadata even without tracking await this.createWorktreeMetadata(bareGit, absoluteWorktreePath, branchName); } } async removeWorktree(worktreePath) { const bareGit = (0, simple_git_1.default)(this.bareRepoPath); // Try to get branch name before removing worktree let branchName = null; try { const worktrees = await this.getWorktreesFromBare(bareGit); const worktree = worktrees.find((w) => path.resolve(w.path) === path.resolve(worktreePath)); branchName = worktree?.branch || null; } catch { // If we can't get the branch name, extract from path as fallback branchName = path.basename(worktreePath); } await bareGit.raw(["worktree", "remove", worktreePath, "--force"]); console.log(` - ✅ Safely removed stale worktree at '${worktreePath}'.`); // Clean up metadata if (branchName) { try { await this.metadataService.deleteMetadata(this.bareRepoPath, branchName); } catch (metadataError) { console.warn(`Failed to delete metadata for worktree: ${metadataError}`); } } } async pruneWorktrees() { const bareGit = (0, simple_git_1.default)(this.bareRepoPath); await bareGit.raw(["worktree", "prune"]); console.log("Pruned worktree metadata."); } async checkWorktreeStatus(worktreePath) { const worktreeGit = (0, simple_git_1.default)(worktreePath); const status = await worktreeGit.status(); return status.isClean(); } async isDetachedHead(worktreeGit) { try { const branchSummary = await worktreeGit.branch(); return !branchSummary.current || branchSummary.detached; } catch { return true; } } async hasUnpushedCommits(worktreePath) { const worktreeGit = (0, simple_git_1.default)(worktreePath); try { // Check if in detached HEAD state if (await this.isDetachedHead(worktreeGit)) { return false; } // Get the current branch name const branchSummary = await worktreeGit.branch(); const currentBranch = branchSummary.current; // Check if upstream is gone const upstreamGone = await this.hasUpstreamGone(worktreePath); if (upstreamGone) { // Load metadata to check for commits after last sync const metadata = await this.metadataService.loadMetadata(this.bareRepoPath, currentBranch); if (metadata?.lastSyncCommit) { try { // Check for commits after last sync const newCommitsResult = await worktreeGit.raw(["rev-list", "--count", `${metadata.lastSyncCommit}..HEAD`]); const newCommitsCount = parseInt(newCommitsResult.trim(), 10); return newCommitsCount > 0; } catch { // If lastSyncCommit doesn't exist, fall through to regular check } } } // Count commits that exist in the current branch but not in any remote const result = await worktreeGit.raw(["rev-list", "--count", currentBranch, "--not", "--remotes"]); const unpushedCount = parseInt(result.trim(), 10); return unpushedCount > 0; } catch (error) { // If the command fails (e.g., branch doesn't exist), assume it's safe console.error(`Error checking unpushed commits: ${error}`); return false; } } async hasUpstreamGone(worktreePath) { const worktreeGit = (0, simple_git_1.default)(worktreePath); try { // Check if in detached HEAD state if (await this.isDetachedHead(worktreeGit)) { return false; } const branchSummary = await worktreeGit.branch(); const currentBranch = branchSummary.current; // Try to get upstream branch const upstream = await worktreeGit.raw(["rev-parse", "--abbrev-ref", `${currentBranch}@{upstream}`]); // Check if upstream exists in remotes const remoteBranches = await worktreeGit.branch(["-r"]); return !remoteBranches.all.includes(upstream.trim()); } catch (error) { // Check if the error is because of no upstream configured const errorMessage = error instanceof Error ? error.message : String(error); // Match specific Git error messages for missing upstream if (errorMessage.includes("fatal: no upstream configured") || errorMessage.includes("no upstream configured for branch") || errorMessage.includes("fatal: ambiguous argument") || errorMessage.includes("unknown revision or path")) { // This is expected when there's no upstream - not an error condition return false; } // Log unexpected errors that don't match known patterns console.error(`Unexpected error checking upstream status for ${worktreePath}. ` + `This might indicate a real issue rather than a missing upstream. ` + `Error: ${errorMessage}`); // Return false to be safe - we don't want to accidentally delete worktrees // due to transient errors return false; } } async hasStashedChanges(worktreePath) { const worktreeGit = (0, simple_git_1.default)(worktreePath); try { const stashList = await worktreeGit.stashList(); return stashList.total > 0; } catch (error) { // If stash check fails, assume it's unsafe to delete console.error(`Error checking stash: ${error}`); return true; } } async hasModifiedSubmodules(worktreePath) { const worktreeGit = (0, simple_git_1.default)(worktreePath); try { const result = await worktreeGit.raw(["submodule", "status"]); // Check for '+' or '-' prefix indicating modifications return /^[+-]/m.test(result); } catch { return false; // No submodules or submodule command failed } } async hasOperationInProgress(worktreePath) { const gitDir = path.join(worktreePath, ".git"); const checkFiles = ["MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD", "BISECT_LOG", "rebase-merge", "rebase-apply"]; for (const file of checkFiles) { try { await fs.access(path.join(gitDir, file)); return true; // Operation in progress } catch { // File doesn't exist, continue checking } } return false; } async getCurrentBranch() { const git = this.getGit(); const branchSummary = await git.branch(); return branchSummary.current; } async detectDefaultBranch(bareGit) { try { // Try to get the symbolic ref for origin/HEAD const headRef = await bareGit.raw(["symbolic-ref", "refs/remotes/origin/HEAD"]); // Extract branch name from refs/remotes/origin/main or refs/remotes/origin/master const branch = headRef.trim().split("/").pop(); if (branch) { return branch; } } catch { // If that fails, try to set HEAD automatically try { await bareGit.raw(["remote", "set-head", "origin", "-a"]); const headRef = await bareGit.raw(["symbolic-ref", "refs/remotes/origin/HEAD"]); const branch = headRef.trim().split("/").pop(); if (branch) { return branch; } } catch { // If all else fails, try to detect from remote branches try { const remoteBranches = await bareGit.branch(["-r"]); // Common default branch names in order of preference const commonDefaults = ["main", "master", "develop", "trunk"]; for (const defaultName of commonDefaults) { if (remoteBranches.all.some((branch) => branch === `origin/${defaultName}`)) { return defaultName; } } } catch { // Ignore and fall through to default } } } // Final fallback return "main"; } isLfsSkipEnabled() { return this.config.skipLfs || process.env.GIT_LFS_SKIP_SMUDGE === "1"; } async getWorktrees() { const bareGit = (0, simple_git_1.default)(this.bareRepoPath); return this.getWorktreesFromBare(bareGit); } async isWorktreeBehind(worktreePath) { const worktreeGit = (0, simple_git_1.default)(worktreePath); try { // Get the current branch const branchSummary = await worktreeGit.branch(); const currentBranch = branchSummary.current; // Check if the branch has an upstream const upstreamInfo = await worktreeGit.raw(["rev-parse", "--abbrev-ref", `${currentBranch}@{upstream}`]); if (!upstreamInfo.trim()) { return false; // No upstream, can't be behind } // Count commits behind upstream const behindCount = await worktreeGit.raw(["rev-list", "--count", `HEAD..${upstreamInfo.trim()}`]); return parseInt(behindCount.trim(), 10) > 0; } catch { // If any command fails, assume not behind return false; } } async updateWorktree(worktreePath) { const worktreeGit = this.isLfsSkipEnabled() ? (0, simple_git_1.default)(worktreePath).env({ GIT_LFS_SKIP_SMUDGE: "1" }) : (0, simple_git_1.default)(worktreePath); // Perform a fast-forward merge const branchSummary = await worktreeGit.branch(); const currentBranch = branchSummary.current; await worktreeGit.merge([`origin/${currentBranch}`, "--ff-only"]); // Skip metadata update for main worktree const isMainWorktree = path.resolve(worktreePath) === path.resolve(this.mainWorktreePath); if (isMainWorktree) { return; } // Update metadata after successful update try { const currentCommit = await worktreeGit.revparse(["HEAD"]); await this.metadataService.updateLastSync(this.bareRepoPath, currentBranch, currentCommit.trim(), "updated"); } catch (metadataError) { console.warn(`Failed to update metadata for worktree: ${metadataError}`); } } async hasDivergedHistory(worktreePath, expectedBranch) { const worktreeGit = (0, simple_git_1.default)(worktreePath); // Validate branch matches const branchInfo = await worktreeGit.branch(); if (branchInfo.current !== expectedBranch) { console.warn(`Branch mismatch in hasDivergedHistory: expected ${expectedBranch}, got ${branchInfo.current}`); return false; // Conservative: assume can fast-forward } try { // Check if HEAD is an ancestor of the remote branch (can fast-forward) await worktreeGit.raw(["merge-base", "--is-ancestor", "HEAD", `origin/${expectedBranch}`]); return false; // Can fast-forward } catch { return true; // Histories have diverged } } async canFastForward(worktreePath, branch) { const worktreeGit = (0, simple_git_1.default)(worktreePath); try { // Get the merge base between HEAD and the remote branch const mergeBase = await worktreeGit.raw(["merge-base", "HEAD", `origin/${branch}`]); const mergeBaseSha = mergeBase.trim(); // Get current HEAD SHA const headSha = await worktreeGit.revparse(["HEAD"]); const headShaTrimmed = headSha.trim(); // If merge base equals HEAD, then HEAD is an ancestor of remote and can fast-forward return mergeBaseSha === headShaTrimmed; } catch { // If merge-base fails, branches have diverged return false; } } async compareTreeContent(worktreePath, branch) { const worktreeGit = (0, simple_git_1.default)(worktreePath); try { // Get the tree SHA for the current HEAD const localTree = await worktreeGit.raw(["rev-parse", "HEAD^{tree}"]); // Get the tree SHA for the remote branch const remoteTree = await worktreeGit.raw(["rev-parse", `origin/${branch}^{tree}`]); return localTree.trim() === remoteTree.trim(); } catch (error) { console.error(`Error comparing tree content: ${error}`); return false; // Assume trees are different if we can't compare } } async resetToUpstream(worktreePath, branch) { const worktreeGit = this.isLfsSkipEnabled() ? (0, simple_git_1.default)(worktreePath).env({ GIT_LFS_SKIP_SMUDGE: "1" }) : (0, simple_git_1.default)(worktreePath); await worktreeGit.reset(["--hard", `origin/${branch}`]); // Update metadata after reset try { const currentCommit = await worktreeGit.revparse(["HEAD"]); await this.metadataService.updateLastSync(this.bareRepoPath, branch, currentCommit.trim(), "updated"); } catch (metadataError) { console.warn(`Failed to update metadata after reset: ${metadataError}`); } } async getCurrentCommit(worktreePath) { const worktreeGit = (0, simple_git_1.default)(worktreePath); const commit = await worktreeGit.revparse(["HEAD"]); return commit.trim(); } async getRemoteCommit(ref) { const git = this.getGit(); const commit = await git.revparse([ref]); return commit.trim(); } async getWorktreesFromBare(bareGit) { const result = await bareGit.raw(["worktree", "list", "--porcelain"]); const worktrees = []; const lines = result.trim().split("\n"); let currentWorktree = {}; for (const line of lines) { if (line.startsWith("worktree ")) { currentWorktree.path = line.substring(9); } else if (line.startsWith("branch ")) { currentWorktree.branch = line.substring(7).replace("refs/heads/", ""); } else if (line === "detached") { currentWorktree.detached = true; } else if (line.trim() === "") { if (currentWorktree.path) { // Only include worktrees that have a branch (not detached) if (currentWorktree.branch && !currentWorktree.detached) { worktrees.push({ path: currentWorktree.path, branch: currentWorktree.branch }); } } currentWorktree = {}; } } // Handle the last worktree if there's no trailing empty line if (currentWorktree.path && currentWorktree.branch && !currentWorktree.detached) { worktrees.push({ path: currentWorktree.path, branch: currentWorktree.branch }); } return worktrees; } } exports.GitService = GitService; //# sourceMappingURL=git.service.js.map