-
Notifications
You must be signed in to change notification settings - Fork 0
/
delete-squashed-merged-branches.js
147 lines (134 loc) · 4.74 KB
/
delete-squashed-merged-branches.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
/**
* @author Jacques Favreau (@betaorbust)
* @version 1.0.0
* @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
*
*/
'use strict';
const childProcess = require('child_process');
const assert = require('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++) {
await callback(array[index], 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((element, index) => {
return !!transformedValues[index];
});
}
/**
* Calls `git` with the given arguments from the CWD
* @param {string[]} args A list of arguments
* @returns {Promise<string>} The output from `git`
*/
async function git(args) {
return new Promise((resolve, reject) => {
const child = childProcess.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 (err) {
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;
});
}
}
module.exports = deleteSquashedMergedBranches;