@betaorbust/github-loppers
Version:
List or delete local squash-merged git branches. Cleans up that Github mess!
121 lines (120 loc) • 4.58 kB
JavaScript
/**
* @author Jacques Favreau (@betaorbust)
* @overview A no-dependency node utility to delete branches that have already
* been merged into a mainline via the squash-merge strategy. (Frequently [used
* on Github](https://blog.github.com/2016-04-01-squash-your-commits/).)
*
* Usage:
* See https://gist.github.com/betaorbust/6bef07dfd35fb240c8d19fb1bf7f5e04/
*
* Acknowledgments:
* Git logic from @not-an-aardvark's awesome bluebird-based implementation.
* https://github.com/not-an-aardvark/git-delete-squashed
*
*/
;
import { spawn } from 'child_process';
import assert from 'assert';
const log = console.log;
// A set of async helper functions for working with arrays
async function asyncForEach(array, callback) {
for (let index = 0; index < array.length; index++) {
const item = array[index];
if (item !== undefined) {
await callback(item, index, array);
}
}
}
async function asyncMap(array, callback) {
return Promise.all(array.map(callback));
}
async function asyncFilter(array, callback) {
const transformedValues = await asyncMap(array, callback);
return array.filter((_, index) => {
return !!transformedValues[index];
});
}
/**
* Calls `git` with the given arguments from the CWD
* @param A list of arguments
* @returns The output from `git`
*/
async function git(args) {
return new Promise((resolve, reject) => {
const child = spawn('git', args);
let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => (stdout += data));
child.stderr.on('data', (data) => (stderr += data));
child.on('close', (exitCode) => exitCode ? reject(stderr) : resolve(stdout));
}).then((stdout) => stdout.replace(/\n$/, ''));
}
/**
* Async function for deleting squash-merged branches.
* @param {string} baseBranchName The branch to use to see if other branches have been merged in. Usually "master"
* @param {boolean} [actuallyDoIt] Optional bool to actually delete branches. Default is to just list them.
* @returns {promise} Promise that resolves when everything is done.
*/
async function deleteSquashedMergedBranches(baseBranchName, actuallyDoIt = false) {
assert(baseBranchName &&
typeof baseBranchName === 'string' &&
baseBranchName.trim() !== '', 'First param is required and a string of the base branch (usually "master")');
assert(actuallyDoIt === undefined || typeof actuallyDoIt === 'boolean', 'Second, optional parameter is a boolean indicating to actually delete the branches.');
const branchListOutput = await git([
'for-each-ref',
'refs/heads/',
'--format=%(refname:short)'
]);
const branchNames = branchListOutput.split('\n');
if (branchNames.indexOf(baseBranchName) === -1) {
throw new Error(`fatal: no branch named '${baseBranchName}' found in this repo`);
}
const branchesToDelete = await asyncFilter(branchNames, async (branchName) => {
try {
const [ancestorHash, treeId] = await Promise.all([
git(['merge-base', baseBranchName, branchName]),
git(['rev-parse', `${branchName}^{tree}`])
]);
const danglingCommitId = await git([
'commit-tree',
treeId,
'-p',
ancestorHash,
'-m',
`Temp commit for ${branchName}`
]);
const output = await git([
'cherry',
baseBranchName,
danglingCommitId
]);
return output.startsWith('-');
}
catch {
log(`ERROR RUNNING ANALYSIS ON BRANCH "${branchName}". SKIPPING.`);
return Promise.resolve(false);
}
});
if (!actuallyDoIt) {
log('Listing branches to delete:');
if (branchesToDelete.length === 0) {
log('No local branches can be safely removed.');
}
else {
branchesToDelete.forEach((branch) => {
log(branch);
});
log('\n\n To delete these, you can run the following:');
log(`git branch -D ${branchesToDelete.join(' ')}`);
}
}
else {
await git(['checkout', baseBranchName]);
asyncForEach(branchesToDelete, async (branchName) => {
const deleted = await git(['branch', '-D', branchName]);
log(deleted);
return deleted;
});
}
}
export default deleteSquashedMergedBranches;