avil13-co 2.2.5 → 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -21
- package/dist/ask.d.ts +4 -3
- package/dist/ask.js +54 -32
- package/dist/cli-commands/index.d.ts +5 -1
- package/dist/cli-commands/index.js +37 -37
- package/dist/commands/checkout-branch.js +5 -9
- package/dist/commands/delete-barnch.d.ts +1 -0
- package/dist/commands/delete-barnch.js +34 -23
- package/dist/commands/exec.d.ts +1 -1
- package/dist/commands/exec.js +11 -2
- package/dist/commands/get-branches.d.ts +2 -4
- package/dist/commands/get-branches.js +18 -35
- package/dist/commands/get-current-branch-name.d.ts +1 -1
- package/dist/commands/get-current-branch-name.js +1 -3
- package/dist/commands/merge-branch.js +27 -31
- package/dist/index.d.ts +1 -1
- package/dist/index.js +35 -19
- package/dist/prompt.d.ts +2 -0
- package/dist/prompt.js +24 -0
- package/dist/terminal.d.ts +5 -0
- package/dist/terminal.js +40 -0
- package/package.json +1 -1
- package/src/__tests__/ask.spec.ts +17 -2
- package/src/__tests__/cli-commands.spec.ts +25 -0
- package/src/__tests__/commands.spec.ts +110 -0
- package/src/__tests__/terminal.spec.ts +56 -0
- package/src/ask.ts +51 -47
- package/src/cli-commands/index.ts +30 -46
- package/src/commands/checkout-branch.ts +4 -12
- package/src/commands/delete-barnch.ts +30 -23
- package/src/commands/exec.ts +11 -7
- package/src/commands/get-branches.ts +15 -41
- package/src/commands/get-current-branch-name.ts +2 -6
- package/src/commands/merge-branch.ts +25 -34
- package/src/index.ts +35 -30
- package/src/prompt.ts +18 -0
- package/src/terminal.ts +32 -0
|
@@ -1,31 +1,38 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { prompt } from '../prompt';
|
|
2
2
|
import { exec } from './exec';
|
|
3
|
-
import {
|
|
3
|
+
import { print } from '../terminal';
|
|
4
4
|
|
|
5
|
-
export async function
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
export async function deleteBranches(branchNames: string[], confirm = true, force = true) {
|
|
6
|
+
const branches = [...new Set(branchNames)];
|
|
7
|
+
if (!branches.length || branches.some(branch => !branch)) {
|
|
8
|
+
throw new Error('No branches selected.');
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const checkoutCommandResult = await exec(command);
|
|
23
|
-
|
|
24
|
-
console.log(checkoutCommandResult);
|
|
25
|
-
console.log(clc.yellow('Branch'), clc.red(branchName), clc.yellow('has been deleted'));
|
|
11
|
+
if (confirm) {
|
|
12
|
+
const { answer } = await prompt({
|
|
13
|
+
type: 'confirm',
|
|
14
|
+
name: 'answer',
|
|
15
|
+
message: 'Delete these branches: ' + branches.join(', ') + '?',
|
|
16
|
+
default: false,
|
|
17
|
+
});
|
|
18
|
+
if (!answer) {
|
|
19
|
+
print('Branches were not deleted.');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
26
23
|
|
|
27
|
-
|
|
24
|
+
// Attempt every selected branch and report partial failures as well.
|
|
25
|
+
const errors: string[] = [];
|
|
26
|
+
for (const branch of branches) {
|
|
27
|
+
try {
|
|
28
|
+
print(await exec(['branch', force ? '-D' : '-d', '--', branch]));
|
|
29
|
+
} catch (error) {
|
|
30
|
+
errors.push(branch + ': ' + (error instanceof Error ? error.message : String(error)));
|
|
31
|
+
}
|
|
28
32
|
}
|
|
33
|
+
if (errors.length) throw new Error(errors.join('\n'));
|
|
34
|
+
}
|
|
29
35
|
|
|
30
|
-
|
|
36
|
+
export async function deleteBranch(branchName: string) {
|
|
37
|
+
return deleteBranches([branchName]);
|
|
31
38
|
}
|
package/src/commands/exec.ts
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from 'child_process';
|
|
2
2
|
|
|
3
|
-
export function exec(
|
|
4
|
-
return new Promise((resolve, reject) =>
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
export function exec(args: string[]): Promise<string> {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
execFile('git', args, (error, stdout, stderr) => {
|
|
6
|
+
if (error) {
|
|
7
|
+
reject(new Error([stderr.trim(), stdout.trim()].filter(Boolean).join('\n') || error.message));
|
|
8
|
+
} else {
|
|
9
|
+
resolve([stdout.trimEnd(), stderr.trimEnd()].filter(Boolean).join('\n'));
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
});
|
|
9
13
|
}
|
|
@@ -1,50 +1,24 @@
|
|
|
1
1
|
import { exec } from './exec';
|
|
2
2
|
|
|
3
|
-
|
|
4
3
|
export enum BranchesType {
|
|
5
4
|
All,
|
|
6
5
|
Merged,
|
|
7
6
|
NotMerged,
|
|
8
7
|
}
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
[
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
if (branches.length) {
|
|
26
|
-
return branches;
|
|
27
|
-
}
|
|
28
|
-
throw new Error('No branches found...');
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function parseBranches(stdout) {
|
|
33
|
-
return stdout
|
|
34
|
-
.split('\n')
|
|
35
|
-
.filter(s => !/\s->\s/.test(s))
|
|
36
|
-
.map(cleanBranchName)
|
|
37
|
-
.filter(Boolean)
|
|
38
|
-
.filter(onlyUnique);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function cleanBranchName(name) {
|
|
42
|
-
return name
|
|
43
|
-
.replace(/^\*/, '')
|
|
44
|
-
.replace(/remotes\/origin\//, '')
|
|
45
|
-
.trim();
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function onlyUnique(value, index, self) {
|
|
49
|
-
return self.indexOf(value) === index;
|
|
9
|
+
/** Remote origin refs are also offered for checkout/merge; deletion is local only. */
|
|
10
|
+
export async function getBranches(type: BranchesType, localOnly = false): Promise<string[]> {
|
|
11
|
+
const args = ['branch', '--format=%(refname)'];
|
|
12
|
+
if (!localOnly) args.push('--all');
|
|
13
|
+
if (type === BranchesType.Merged) args.push('--merged');
|
|
14
|
+
if (type === BranchesType.NotMerged) args.push('--no-merged');
|
|
15
|
+
|
|
16
|
+
const stdout = await exec(args);
|
|
17
|
+
const branches = [...new Set(stdout.split('\n')
|
|
18
|
+
.map(name => name.trim())
|
|
19
|
+
.filter(name => name && !/^refs\/remotes\/.+\/HEAD$/.test(name))
|
|
20
|
+
.map(name => name.replace(/^refs\/heads\//, '')
|
|
21
|
+
.replace(/^refs\/remotes\/(?:origin\/)?/, '')))];
|
|
22
|
+
if (!branches.length) throw new Error('No branches found.');
|
|
23
|
+
return branches;
|
|
50
24
|
}
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { exec } from './exec';
|
|
2
2
|
|
|
3
|
-
export async function getCurrentBranchName(){
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
const stdout = await exec(currentBranchName);
|
|
7
|
-
console.log(stdout);
|
|
3
|
+
export async function getCurrentBranchName(): Promise<string> {
|
|
4
|
+
return (await exec(['rev-parse', '--abbrev-ref', 'HEAD'])).trim();
|
|
8
5
|
}
|
|
9
|
-
|
|
@@ -1,46 +1,37 @@
|
|
|
1
1
|
import { exec } from './exec';
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
2
|
+
import { getCurrentBranchName } from './get-current-branch-name';
|
|
3
|
+
import { deleteBranches } from './delete-barnch';
|
|
4
|
+
import { print } from '../terminal';
|
|
5
|
+
import { prompt } from '../prompt';
|
|
4
6
|
|
|
5
|
-
//
|
|
6
7
|
export async function mergeBranch(branchName: string) {
|
|
7
|
-
if (!branchName)
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const currentBranch = await exec('git rev-parse --abbrev-ref HEAD');
|
|
12
|
-
|
|
13
|
-
const message = [
|
|
14
|
-
clc._blue,
|
|
15
|
-
'Do you want to merge branch ',
|
|
16
|
-
clc._green,
|
|
17
|
-
branchName, // branch
|
|
18
|
-
clc._blue,
|
|
19
|
-
' into ',
|
|
20
|
-
clc._green,
|
|
21
|
-
currentBranch, // branch
|
|
22
|
-
clc._blue,
|
|
23
|
-
'?',
|
|
24
|
-
clc._off,
|
|
25
|
-
].join('');
|
|
8
|
+
if (!branchName) throw new Error('No branch selected.');
|
|
9
|
+
const currentBranch = await getCurrentBranchName();
|
|
10
|
+
if (branchName === currentBranch) throw new Error('Cannot merge the current branch into itself.');
|
|
26
11
|
|
|
27
|
-
const
|
|
12
|
+
const { answer } = await prompt({
|
|
28
13
|
type: 'confirm',
|
|
29
14
|
name: 'answer',
|
|
30
|
-
message,
|
|
15
|
+
message: 'Merge branch ' + branchName + ' into ' + currentBranch + '?',
|
|
31
16
|
default: false,
|
|
32
17
|
});
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const checkoutCommand = `git merge ${branchName}`;
|
|
36
|
-
|
|
37
|
-
console.log(' ' + clc.green('git merge', branchName) + '\n');
|
|
38
|
-
|
|
39
|
-
const checkoutCommandResult = await exec(checkoutCommand);
|
|
40
|
-
console.log(checkoutCommandResult);
|
|
41
|
-
|
|
18
|
+
if (!answer) {
|
|
19
|
+
print('Branch was not merged.');
|
|
42
20
|
return;
|
|
43
21
|
}
|
|
44
22
|
|
|
45
|
-
|
|
23
|
+
print('git merge ' + branchName);
|
|
24
|
+
print(await exec(['merge', '--', branchName]));
|
|
25
|
+
|
|
26
|
+
// Only local branches can be deleted; never remove remote refs.
|
|
27
|
+
const locals = (await exec(['for-each-ref', '--format=%(refname)', 'refs/heads/'])).split('\n');
|
|
28
|
+
if (!locals.includes('refs/heads/' + branchName)) return;
|
|
29
|
+
|
|
30
|
+
const { remove } = await prompt({
|
|
31
|
+
type: 'confirm',
|
|
32
|
+
name: 'remove',
|
|
33
|
+
message: 'Delete merged branch ' + branchName + '?',
|
|
34
|
+
default: true,
|
|
35
|
+
});
|
|
36
|
+
if (remove) await deleteBranches([branchName], false, false);
|
|
46
37
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,42 +1,47 @@
|
|
|
1
|
-
import { ask } from './ask';
|
|
2
|
-
import { Commands, getHelp,
|
|
1
|
+
import { ask, askBranches } from './ask';
|
|
2
|
+
import { Commands, getHelp, parseArgs } from './cli-commands';
|
|
3
3
|
import { BranchesType, checkoutBranch, getBranches, getCurrentBranchName } from './commands';
|
|
4
|
-
import {
|
|
4
|
+
import { deleteBranches } from './commands/delete-barnch';
|
|
5
5
|
import { mergeBranch } from './commands/merge-branch';
|
|
6
|
+
import { finishTerminal, installInterruptHandler, print, startTerminal } from './terminal';
|
|
6
7
|
|
|
7
|
-
const run = async () => {
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
if (Commands.Help === userWish) {
|
|
8
|
+
export const run = async () => {
|
|
9
|
+
const { command, filter } = parseArgs();
|
|
10
|
+
if (command === Commands.Help) {
|
|
11
11
|
console.log(getHelp());
|
|
12
12
|
return;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const branches = await getBranches(branchType);
|
|
24
|
-
|
|
25
|
-
const { branch } = await ask('Select branch', branches);
|
|
26
|
-
|
|
27
|
-
if (Commands.DeleteBranch === userWish) {
|
|
28
|
-
deleteBranch(branch);
|
|
29
|
-
return;
|
|
15
|
+
const branchType = command === Commands.ShowMerged ? BranchesType.Merged
|
|
16
|
+
: command === Commands.ShowUnMerged ? BranchesType.NotMerged : BranchesType.All;
|
|
17
|
+
const deleting = command === Commands.DeleteBranch;
|
|
18
|
+
let branches = await getBranches(branchType, deleting);
|
|
19
|
+
if (deleting) {
|
|
20
|
+
const currentBranch = await getCurrentBranchName();
|
|
21
|
+
branches = branches.filter(branch => branch !== currentBranch);
|
|
22
|
+
if (!branches.length) throw new Error('No local branches available for deletion.');
|
|
30
23
|
}
|
|
31
24
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
25
|
+
startTerminal();
|
|
26
|
+
if (deleting) {
|
|
27
|
+
const selected = await askBranches('Select branches to delete (Space to select)', branches, filter);
|
|
28
|
+
await deleteBranches(selected);
|
|
29
|
+
} else {
|
|
30
|
+
const { branch } = await ask('Select branch', branches, filter);
|
|
31
|
+
if (command === Commands.MergeBranch) await mergeBranch(branch);
|
|
32
|
+
else {
|
|
33
|
+
await checkoutBranch(branch);
|
|
34
|
+
print(await getCurrentBranchName());
|
|
35
|
+
}
|
|
35
36
|
}
|
|
36
|
-
|
|
37
|
-
await checkoutBranch(branch);
|
|
38
|
-
|
|
39
|
-
await getCurrentBranchName()
|
|
40
37
|
};
|
|
41
38
|
|
|
42
|
-
|
|
39
|
+
const removeInterruptHandler = installInterruptHandler();
|
|
40
|
+
run().catch(error => {
|
|
41
|
+
finishTerminal();
|
|
42
|
+
console.error('Error: ' + (error instanceof Error ? error.message : String(error)));
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
}).finally(() => {
|
|
45
|
+
finishTerminal();
|
|
46
|
+
removeInterruptHandler();
|
|
47
|
+
});
|
package/src/prompt.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import inquirer, { Answers, QuestionCollection } from 'inquirer';
|
|
2
|
+
import { finishTerminal } from './terminal';
|
|
3
|
+
|
|
4
|
+
export function prompt<T extends Answers = Answers>(questions: QuestionCollection<T>): Promise<T> {
|
|
5
|
+
const result = inquirer.prompt<T>(questions);
|
|
6
|
+
// Inquirer 8 re-sends SIGINT asynchronously and may exit before Node handles
|
|
7
|
+
// it. Handle readline's raw-mode Ctrl+C synchronously instead.
|
|
8
|
+
const ui = result.ui as any;
|
|
9
|
+
if (ui) {
|
|
10
|
+
ui.rl.removeListener('SIGINT', ui.onForceClose);
|
|
11
|
+
ui.rl.once('SIGINT', () => {
|
|
12
|
+
ui.close();
|
|
13
|
+
finishTerminal(true);
|
|
14
|
+
process.exit(130);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
}
|
package/src/terminal.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Keep interactive output isolated from the user's shell history.
|
|
2
|
+
let active = false;
|
|
3
|
+
let messages: string[] = [];
|
|
4
|
+
|
|
5
|
+
export function startTerminal() {
|
|
6
|
+
active = Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
7
|
+
messages = [];
|
|
8
|
+
if (active) process.stdout.write('\x1b[?1049h');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function print(message: string) {
|
|
12
|
+
if (!message.trim()) return;
|
|
13
|
+
if (active) messages.push(message.trimEnd());
|
|
14
|
+
console.log(message.trimEnd());
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function finishTerminal(cancelled = false) {
|
|
18
|
+
if (!active) return;
|
|
19
|
+
active = false;
|
|
20
|
+
process.stdout.write('\x1b[?1049l\x1b[?25h');
|
|
21
|
+
if (!cancelled && messages.length) console.log(messages.join('\n'));
|
|
22
|
+
messages = [];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function installInterruptHandler() {
|
|
26
|
+
const onInterrupt = () => {
|
|
27
|
+
finishTerminal(true);
|
|
28
|
+
process.exit(130);
|
|
29
|
+
};
|
|
30
|
+
process.on('SIGINT', onInterrupt);
|
|
31
|
+
return () => process.removeListener('SIGINT', onInterrupt);
|
|
32
|
+
}
|