UNPKG

git-groom

Version:

A CLI tool for cleaning up Git branches, tags, and remotes

235 lines (220 loc) 8 kB
const picocolors = require("picocolors"); const { execa } = require("execa"); const inquirer = require("inquirer"); const prompt = inquirer.createPromptModule(); // Function to check if the repository is connected to a remote async function getRemotes() { try { const { stdout } = await execa("git", ["remote", "-v"]); return stdout.split("\n").filter(Boolean); } catch (error) { return []; // Repo is not connected } } async function runCleanup(branch) { console.log( picocolors.green( `Cleaning up Git for branch: ${branch || "current branch"}` ) ); try { // List local branches that have been merged to the specified branch const { stdout: localBranches } = await execa("git", [ "branch", "--merged", ]); const branchesToDelete = localBranches .split("\n") .filter((branchLine) => branchLine && !branchLine.startsWith("*")) .filter((branchLine) => branchLine !== branch) .map((branchLine) => branchLine.trim()); if (branchesToDelete.length > 0) { console.log(picocolors.red("Merged local branches to delete:")); branchesToDelete.forEach((branchName) => { console.log(`- ${picocolors.yellow(branchName)}`); }); const confirmDelete = await prompt({ type: "confirm", name: "confirm", message: "Are you sure you want to delete these branches?", }); if (confirmDelete.confirm) { console.log(picocolors.green("Deleting branches...")); await Promise.all( branchesToDelete.map((branchName) => execa("git", ["branch", "-d", branchName]) ) ).then((results) => { results.forEach((result, index) => { if (result.status === "rejected") { console.log( picocolors.red(`Error deleting ${branchesToDelete[index]}`) ); } }); const allGood = results.every( (result) => result.status === "fulfilled" ); if (allGood) { console.log(picocolors.green("All branches deleted successfully.")); } else { console.log( picocolors.red("Some branches were not deleted successfully.") ); } console.log(picocolors.green("Finished deleting branches.")); }); } else { console.log(picocolors.yellow("Skipping branch deletion.")); } } else { console.log(picocolors.yellow("No merged local branches to delete.")); } // List and delete merged tags const { stdout: tags } = await execa("git", ["tag", "--merged", branch]); const tagsToDelete = tags.split("\n").filter((tag) => tag); if (tagsToDelete.length > 0) { console.log(picocolors.red("Merged tags to delete:")); tagsToDelete.forEach((tag) => { console.log(`- ${picocolors.yellow(tag)}`); }); const confirmDelete = await prompt({ type: "confirm", name: "confirm", message: "Are you sure you want to delete these tags?", }); if (confirmDelete.confirm) { await Promise.all( tagsToDelete.map((tag) => execa("git", ["tag", "-d", tag])) ).then((results) => { results.forEach((result, index) => { if (result.status === "rejected") { console.log( picocolors.red(`Error deleting ${tagsToDelete[index]}`) ); } }); const allGood = results.every( (result) => result.status === "fulfilled" ); if (allGood) { console.log(picocolors.green("All tags deleted successfully.")); } else { console.log( picocolors.red("Some tags were not deleted successfully.") ); } console.log(picocolors.green("Finished deleting tags.")); }); } else { console.log(picocolors.yellow("Skipping tag deletion.")); } } else { console.log(picocolors.yellow("No merged tags to delete.")); } const remotes = await getRemotes(); const remoteNames = remotes.map((remote) => remote.split("\t")[0]); const noRemotes = remotes.length === 0; if (!noRemotes && remoteNames.some((name) => name === "origin")) { // Prune remote-tracking branches that no longer exist on the remote console.log( picocolors.green("Pruning stale remote-tracking branches...") ); await execa("git", ["remote", "prune", "origin"]); const viewRemotes = await prompt({ type: "confirm", name: "confirm", message: "Do you want to look at deleting merged remote branches?", }); if (viewRemotes.confirm) { // List and delete merged remote branches const { stdout: remoteBranches } = await execa("git", [ "branch", "-r", "--merged", branch, ]); const remoteBranchesToDelete = remoteBranches .split("\n") .filter( (branchLine) => branchLine && !branchLine.includes("origin/HEAD") && // Skip HEAD ref !branchLine.includes(`origin/${branch}`) // Skip the main branch ) .map((branchLine) => branchLine.trim()); if (remoteBranchesToDelete.length > 0) { console.log(picocolors.red("Merged remote branches to delete:")); remoteBranchesToDelete.forEach((remoteBranch) => { console.log(`- ${picocolors.yellow(remoteBranch)}`); }); const confirmDelete = await prompt({ type: "confirm", name: "confirm", message: "Are you sure you want to delete these remote branches?", }); if (confirmDelete.confirm) { await Promise.all( remoteBranchesToDelete .map((remoteBranch) => { return remoteBranch.replace("origin/", ""); }) .map((remoteBranch) => { return execa("git", [ "push", "origin", "--delete", remoteBranch, ]); }) ).then((results) => { results.forEach((result, index) => { if (result.status === "rejected") { console.log( picocolors.red( `Error deleting ${remoteBranchesToDelete[index]}` ) ); } }); const allGood = results.every( (result) => result.status === "fulfilled" ); if (allGood) { console.log( picocolors.green("All remote branches deleted successfully.") ); } else { const theBad = results.filter( (result) => result.status === "rejected" ); console.log( picocolors.red( "Some remote branches were not deleted successfully." ) ); theBad.forEach((result) => { console.log(picocolors.red(result.stderr)); }); } console.log( picocolors.green("Finished deleting remote branches.") ); }); } else { console.log(picocolors.yellow("Skipping remote branch deletion.")); } } else { console.log( picocolors.yellow("No merged remote branches to delete.") ); } } else { console.log(picocolors.yellow("Skipping remote branch deletion.")); } } } catch (error) { console.error(picocolors.red("Error occurred during cleanup:"), error); } } module.exports = { runCleanup };