@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
JavaScript
;
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);
}
}