avil13-co 2.2.4 → 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.
@@ -1,31 +1,38 @@
1
- import inquirer from 'inquirer';
1
+ import { prompt } from '../prompt';
2
2
  import { exec } from './exec';
3
- import { clc } from '../color';
3
+ import { print } from '../terminal';
4
4
 
5
- export async function deleteBranch(branchName: string) {
6
- if (!branchName) {
7
- throw new Error('No branch...');
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
- const res = await inquirer.prompt({
11
- type: 'confirm',
12
- name: 'answer',
13
- message: clc.yellow('Are you sure you want to delete the branch "') + clc.red(branchName) + clc.yellow('"?'),
14
- default: false,
15
- });
16
-
17
- if (res.answer === true) {
18
- const command = `git branch -D ${branchName}`;
19
-
20
- console.log(' ' + clc.redBg(`git branch -D ${branchName}`, '') + '\n');
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
- return;
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
- console.log(clc.yellow('The branch was not deleted'));
36
+ export async function deleteBranch(branchName: string) {
37
+ return deleteBranches([branchName]);
31
38
  }
@@ -1,9 +1,13 @@
1
- import { exec as exe } from 'child_process';
1
+ import { execFile } from 'child_process';
2
2
 
3
- export function exec(command): Promise<string> {
4
- return new Promise((resolve, reject) =>
5
- exe(command, (err, stdout) =>
6
- err ? reject(err) : resolve(stdout)
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
- const commandsForGetBranches = {
11
- [BranchesType.All]: 'git branch --all',
12
- [BranchesType.Merged]: 'git branch --all --merged',
13
- [BranchesType.NotMerged]: 'git branch --all --no-merged',
14
- } as const;
15
-
16
- /**
17
- * get branch list
18
- */
19
- export function getBranches(type: BranchesType): Promise<string[]> {
20
- const command = commandsForGetBranches[type];
21
-
22
- return exec(command)
23
- .then(parseBranches)
24
- .then(branches => {
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
- const currentBranchName = `git rev-parse --abbrev-ref HEAD`;
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 { clc } from '../color';
3
- import inquirer from 'inquirer';
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
- throw new Error('No branch...');
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 res = await inquirer.prompt({
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
- if (res.answer === true) {
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
- console.log(clc.yellow('The branch was not merged'));
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, getWishOfUser } from './cli-commands';
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 { deleteBranch } from './commands/delete-barnch';
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 userWish = getWishOfUser();
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
- let branchType: BranchesType = BranchesType.All;
16
-
17
- if (Commands.ShowMerged === userWish) {
18
- branchType = BranchesType.Merged;
19
- } else if (Commands.ShowUnMerged === userWish) {
20
- branchType = BranchesType.NotMerged;
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
- if (Commands.MergeBranch === userWish) {
33
- mergeBranch(branch);
34
- return;
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
- run();
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
+ }
@@ -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
+ }