UNPKG

@harryisfish/gitt

Version:

A command-line tool to help you manage Git repositories and remote repositories, such as keeping in sync, pushing, pulling, etc.

165 lines (164 loc) 7.77 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.cleanDeletedBranches = cleanDeletedBranches; const simple_git_1 = require("simple-git"); const listr2_1 = require("listr2"); const prompts_1 = require("@inquirer/prompts"); const minimatch_1 = require("minimatch"); const errors_1 = require("../errors"); const git_1 = require("../utils/git"); const config_1 = require("../utils/config"); const git = (0, simple_git_1.simpleGit)(); /** * Clean up local branches that have been deleted on the remote * @throws {GitError} When cleaning operation fails */ async function cleanDeletedBranches(options = {}) { try { const state = { mainBranch: '', currentBranch: '', deletedBranches: [], isStaleMode: options.stale || false }; // Phase 1: Discovery const discoveryTasks = new listr2_1.Listr([ { title: 'Fetch and switch to main branch', task: async (ctx) => { const mainBranch = await (0, git_1.getMainBranch)(); ctx.mainBranch = mainBranch; const branchInfo = await git.branchLocal(); ctx.currentBranch = branchInfo.current; // Fetch main branch first to avoid conflicts await git.fetch(['origin', mainBranch]); // Switch to main branch if not already on it if (ctx.currentBranch !== mainBranch) { await git.checkout(mainBranch); await git.pull(); } } }, { title: 'Analyze branches', task: async (ctx) => { await git.fetch(['--prune']); const branchSummary = await git.branch(['-vv']); const config = await (0, config_1.readConfigFile)(); const ignorePatterns = config.ignoreBranches || []; const worktreeBranches = await (0, git_1.getWorktrees)(); let candidates = []; if (options.stale) { // Stale mode: check all local branches const allBranches = branchSummary.all; for (const branch of allBranches) { if (branch === ctx.mainBranch) continue; const days = await (0, git_1.getBranchLastCommitTime)(branch); if (days > (options.staleDays || 90)) { candidates.push({ name: branch, reason: `Stale (${days} days)` }); } } } else { // Default mode: check "gone" branches // Note: label may contain newlines/whitespace when branch names are long const goneBranches = branchSummary.all.filter(branch => { const branchInfo = branchSummary.branches[branch]; if (!branchInfo.label) return false; // Normalize whitespace and check for "gone" status const normalizedLabel = branchInfo.label.replace(/\s+/g, ' '); return normalizedLabel.includes(': gone]'); }); candidates = goneBranches.map(b => ({ name: b, reason: 'Remote deleted' })); } // Filter out ignored branches if (ignorePatterns.length > 0) { candidates = candidates.filter(c => { const isIgnored = ignorePatterns.some(pattern => (0, minimatch_1.minimatch)(c.name, pattern)); return !isIgnored; }); } // Filter out worktree branches candidates = candidates.filter(c => !worktreeBranches.includes(c.name)); // For stale mode, check merge status; for gone mode, skip (PR completed = safe to delete) if (options.stale) { const branchesWithStatus = await Promise.all(candidates.map(async (c) => { const isMerged = await (0, git_1.isBranchMerged)(c.name, ctx.mainBranch); return { ...c, isMerged }; })); // In stale mode, only auto-delete merged branches ctx.deletedBranches = branchesWithStatus.filter(b => b.isMerged); } else { // Gone mode: remote deleted = PR completed, safe to delete all ctx.deletedBranches = candidates; } } } ]); await discoveryTasks.run(state); // Phase 2: Interaction / Filtering if (state.deletedBranches.length === 0) { (0, errors_1.printSuccess)('No branches need to be cleaned up'); return; } if (options.interactive) { try { const choices = state.deletedBranches.map(b => ({ name: `${b.name} (${b.reason})`, value: b, checked: true // All candidates are safe to delete })); const selected = await (0, prompts_1.checkbox)({ message: 'Select branches to delete:', choices: choices, }); state.deletedBranches = selected; } catch (e) { // User cancelled throw new errors_1.UserCancelError('Operation cancelled'); } } // Non-interactive mode: delete all candidates (already filtered in discovery phase) if (state.deletedBranches.length === 0) { (0, errors_1.printSuccess)('No branches selected for deletion'); return; } if (options.dryRun) { console.log('\nDry Run: The following branches would be deleted:'); state.deletedBranches.forEach(b => console.log(` - ${b.name} (${b.reason})`)); return; } // Phase 3: Execution const deleteTasks = new listr2_1.Listr([ { title: 'Delete branches', task: (_ctx) => { return new listr2_1.Listr(state.deletedBranches.map(branch => ({ title: `Delete ${branch.name}`, task: async () => { // Always use -D to force delete if we are here (user confirmed or it's merged) await git.branch(['-D', branch.name]); } })), { concurrent: false }); } } ]); await deleteTasks.run(state); (0, errors_1.printSuccess)('Branch cleanup completed'); } catch (error) { if (error instanceof errors_1.GitError || error instanceof errors_1.UserCancelError) { // Re-throw our custom errors as-is throw error; } // Wrap other errors and preserve the original error throw new errors_1.GitError(error instanceof Error ? error.message : 'Unknown error occurred while cleaning branches', error instanceof Error ? error : undefined); } }