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
package/dist/index.js
CHANGED
|
@@ -1,34 +1,50 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.run = void 0;
|
|
3
4
|
const ask_1 = require("./ask");
|
|
4
5
|
const cli_commands_1 = require("./cli-commands");
|
|
5
6
|
const commands_1 = require("./commands");
|
|
6
7
|
const delete_barnch_1 = require("./commands/delete-barnch");
|
|
7
8
|
const merge_branch_1 = require("./commands/merge-branch");
|
|
9
|
+
const terminal_1 = require("./terminal");
|
|
8
10
|
const run = async () => {
|
|
9
|
-
const
|
|
10
|
-
if (cli_commands_1.Commands.Help
|
|
11
|
+
const { command, filter } = (0, cli_commands_1.parseArgs)();
|
|
12
|
+
if (command === cli_commands_1.Commands.Help) {
|
|
11
13
|
console.log((0, cli_commands_1.getHelp)());
|
|
12
14
|
return;
|
|
13
15
|
}
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
const branchType = command === cli_commands_1.Commands.ShowMerged ? commands_1.BranchesType.Merged
|
|
17
|
+
: command === cli_commands_1.Commands.ShowUnMerged ? commands_1.BranchesType.NotMerged : commands_1.BranchesType.All;
|
|
18
|
+
const deleting = command === cli_commands_1.Commands.DeleteBranch;
|
|
19
|
+
let branches = await (0, commands_1.getBranches)(branchType, deleting);
|
|
20
|
+
if (deleting) {
|
|
21
|
+
const currentBranch = await (0, commands_1.getCurrentBranchName)();
|
|
22
|
+
branches = branches.filter(branch => branch !== currentBranch);
|
|
23
|
+
if (!branches.length)
|
|
24
|
+
throw new Error('No local branches available for deletion.');
|
|
17
25
|
}
|
|
18
|
-
|
|
19
|
-
|
|
26
|
+
(0, terminal_1.startTerminal)();
|
|
27
|
+
if (deleting) {
|
|
28
|
+
const selected = await (0, ask_1.askBranches)('Select branches to delete (Space to select)', branches, filter);
|
|
29
|
+
await (0, delete_barnch_1.deleteBranches)(selected);
|
|
20
30
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return;
|
|
31
|
+
else {
|
|
32
|
+
const { branch } = await (0, ask_1.ask)('Select branch', branches, filter);
|
|
33
|
+
if (command === cli_commands_1.Commands.MergeBranch)
|
|
34
|
+
await (0, merge_branch_1.mergeBranch)(branch);
|
|
35
|
+
else {
|
|
36
|
+
await (0, commands_1.checkoutBranch)(branch);
|
|
37
|
+
(0, terminal_1.print)(await (0, commands_1.getCurrentBranchName)());
|
|
38
|
+
}
|
|
30
39
|
}
|
|
31
|
-
await (0, commands_1.checkoutBranch)(branch);
|
|
32
|
-
await (0, commands_1.getCurrentBranchName)();
|
|
33
40
|
};
|
|
34
|
-
run
|
|
41
|
+
exports.run = run;
|
|
42
|
+
const removeInterruptHandler = (0, terminal_1.installInterruptHandler)();
|
|
43
|
+
(0, exports.run)().catch(error => {
|
|
44
|
+
(0, terminal_1.finishTerminal)();
|
|
45
|
+
console.error('Error: ' + (error instanceof Error ? error.message : String(error)));
|
|
46
|
+
process.exitCode = 1;
|
|
47
|
+
}).finally(() => {
|
|
48
|
+
(0, terminal_1.finishTerminal)();
|
|
49
|
+
removeInterruptHandler();
|
|
50
|
+
});
|
package/dist/prompt.d.ts
ADDED
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.prompt = void 0;
|
|
7
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
8
|
+
const terminal_1 = require("./terminal");
|
|
9
|
+
function prompt(questions) {
|
|
10
|
+
const result = inquirer_1.default.prompt(questions);
|
|
11
|
+
// Inquirer 8 re-sends SIGINT asynchronously and may exit before Node handles
|
|
12
|
+
// it. Handle readline's raw-mode Ctrl+C synchronously instead.
|
|
13
|
+
const ui = result.ui;
|
|
14
|
+
if (ui) {
|
|
15
|
+
ui.rl.removeListener('SIGINT', ui.onForceClose);
|
|
16
|
+
ui.rl.once('SIGINT', () => {
|
|
17
|
+
ui.close();
|
|
18
|
+
(0, terminal_1.finishTerminal)(true);
|
|
19
|
+
process.exit(130);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
exports.prompt = prompt;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
export declare function startTerminal(): void;
|
|
3
|
+
export declare function print(message: string): void;
|
|
4
|
+
export declare function finishTerminal(cancelled?: boolean): void;
|
|
5
|
+
export declare function installInterruptHandler(): () => NodeJS.Process;
|
package/dist/terminal.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.installInterruptHandler = exports.finishTerminal = exports.print = exports.startTerminal = void 0;
|
|
4
|
+
// Keep interactive output isolated from the user's shell history.
|
|
5
|
+
let active = false;
|
|
6
|
+
let messages = [];
|
|
7
|
+
function startTerminal() {
|
|
8
|
+
active = Boolean(process.stdout.isTTY && process.stdin.isTTY);
|
|
9
|
+
messages = [];
|
|
10
|
+
if (active)
|
|
11
|
+
process.stdout.write('\x1b[?1049h');
|
|
12
|
+
}
|
|
13
|
+
exports.startTerminal = startTerminal;
|
|
14
|
+
function print(message) {
|
|
15
|
+
if (!message.trim())
|
|
16
|
+
return;
|
|
17
|
+
if (active)
|
|
18
|
+
messages.push(message.trimEnd());
|
|
19
|
+
console.log(message.trimEnd());
|
|
20
|
+
}
|
|
21
|
+
exports.print = print;
|
|
22
|
+
function finishTerminal(cancelled = false) {
|
|
23
|
+
if (!active)
|
|
24
|
+
return;
|
|
25
|
+
active = false;
|
|
26
|
+
process.stdout.write('\x1b[?1049l\x1b[?25h');
|
|
27
|
+
if (!cancelled && messages.length)
|
|
28
|
+
console.log(messages.join('\n'));
|
|
29
|
+
messages = [];
|
|
30
|
+
}
|
|
31
|
+
exports.finishTerminal = finishTerminal;
|
|
32
|
+
function installInterruptHandler() {
|
|
33
|
+
const onInterrupt = () => {
|
|
34
|
+
finishTerminal(true);
|
|
35
|
+
process.exit(130);
|
|
36
|
+
};
|
|
37
|
+
process.on('SIGINT', onInterrupt);
|
|
38
|
+
return () => process.removeListener('SIGINT', onInterrupt);
|
|
39
|
+
}
|
|
40
|
+
exports.installInterruptHandler = installInterruptHandler;
|
package/package.json
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import { expect, it } from 'vitest';
|
|
2
|
-
import { sortOrder } from '../ask';
|
|
2
|
+
import { checkItem, sortOrder } from '../ask';
|
|
3
3
|
|
|
4
|
-
it('
|
|
4
|
+
it.each(['[', '(', '*', '+', '?', '.', '\\', '$', '^'])('treats %s literally', symbol => {
|
|
5
|
+
expect(checkItem('task/' + symbol + '1234', symbol)).toBe(true);
|
|
6
|
+
expect(checkItem('task/1234', symbol)).toBe(false);
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it('matches case-insensitive subsequences and ignores spaces', () => {
|
|
10
|
+
expect(checkItem('feature/TASK-1234', 't 14')).toBe(true);
|
|
11
|
+
expect(checkItem('feature/TASK-1234', '4321')).toBe(false);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('returns zero for equivalent ranks', () => {
|
|
15
|
+
expect(sortOrder('task', 'first', 'second')).toBe(0);
|
|
16
|
+
expect(sortOrder('', 'first', 'second')).toBe(0);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('ranks contiguous matches by position and keeps other choices stable', () => {
|
|
5
20
|
const list = [
|
|
6
21
|
'chore/EDODEV-10449-act-master',
|
|
7
22
|
'chore/release-to-master',
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { Commands, parseArgs } from '../cli-commands';
|
|
3
|
+
|
|
4
|
+
describe('arguments', () => {
|
|
5
|
+
it('defaults to checkout', () => {
|
|
6
|
+
expect(parseArgs([])).toEqual({ command: Commands.Unknown, filter: '' });
|
|
7
|
+
});
|
|
8
|
+
it('preloads a checkout filter', () => {
|
|
9
|
+
expect(parseArgs(['-', '1234'])).toEqual({ command: Commands.Unknown, filter: '1234' });
|
|
10
|
+
});
|
|
11
|
+
it('accepts filters with commands', () => {
|
|
12
|
+
expect(parseArgs(['-D', '-', 'task', '1234'])).toEqual({
|
|
13
|
+
command: Commands.DeleteBranch, filter: 'task 1234',
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
it.each(['m', '-sm', '--show-merged'])('accepts merged alias %s', alias => {
|
|
17
|
+
expect(parseArgs([alias]).command).toBe(Commands.ShowMerged);
|
|
18
|
+
});
|
|
19
|
+
it.each(['un', '-un', '--no-merged'])('accepts unmerged alias %s', alias => {
|
|
20
|
+
expect(parseArgs([alias]).command).toBe(Commands.ShowUnMerged);
|
|
21
|
+
});
|
|
22
|
+
it('reports unknown options', () => {
|
|
23
|
+
expect(() => parseArgs(['--typo'])).toThrow('Unknown option');
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { execFileSync } from 'child_process';
|
|
3
|
+
import { mkdtempSync, rmSync } from 'fs';
|
|
4
|
+
import { tmpdir } from 'os';
|
|
5
|
+
import { join } from 'path';
|
|
6
|
+
import inquirer from 'inquirer';
|
|
7
|
+
import { exec } from '../commands/exec';
|
|
8
|
+
import { BranchesType, getBranches } from '../commands/get-branches';
|
|
9
|
+
import { checkoutBranch } from '../commands/checkout-branch';
|
|
10
|
+
import { deleteBranches } from '../commands/delete-barnch';
|
|
11
|
+
import { mergeBranch } from '../commands/merge-branch';
|
|
12
|
+
|
|
13
|
+
const context = vi.hoisted(() => ({ cwd: '' }));
|
|
14
|
+
vi.mock('child_process', async importOriginal => {
|
|
15
|
+
const actual = await importOriginal<typeof import('child_process')>();
|
|
16
|
+
return {
|
|
17
|
+
...actual,
|
|
18
|
+
execFile: (file, args, callback) => actual.execFile(file, args, { cwd: context.cwd }, callback),
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
function git(...args: string[]) {
|
|
23
|
+
return execFileSync('git', args, { cwd: context.cwd, encoding: 'utf8' }).trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
context.cwd = mkdtempSync(join(tmpdir(), 'co-test-'));
|
|
28
|
+
git('init', '-q', '-b', 'main');
|
|
29
|
+
git('config', 'user.name', 'Test');
|
|
30
|
+
git('config', 'user.email', 'test@example.test');
|
|
31
|
+
git('commit', '-q', '--allow-empty', '-m', 'initial');
|
|
32
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
vi.restoreAllMocks();
|
|
37
|
+
rmSync(context.cwd, { recursive: true, force: true });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
describe('Git commands', () => {
|
|
41
|
+
it('reports Git diagnostics on failure', async () => {
|
|
42
|
+
await expect(exec(['checkout', 'missing', '--'])).rejects.toThrow('missing');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('checks out branch names containing shell metacharacters literally', async () => {
|
|
46
|
+
const branch = 'task/1234;echo-pwned';
|
|
47
|
+
git('branch', branch);
|
|
48
|
+
await checkoutBranch(branch);
|
|
49
|
+
expect(git('branch', '--show-current')).toBe(branch);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('preserves local origin-prefixed branches and deduplicates origin remote refs', async () => {
|
|
53
|
+
git('branch', 'origin/local');
|
|
54
|
+
git('update-ref', 'refs/remotes/origin/main', 'HEAD');
|
|
55
|
+
git('symbolic-ref', 'refs/remotes/origin/HEAD', 'refs/remotes/origin/main');
|
|
56
|
+
expect(await getBranches(BranchesType.All)).toEqual(['main', 'origin/local']);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('excludes remote refs from deletion candidates', async () => {
|
|
60
|
+
git('update-ref', 'refs/remotes/origin/remote-only', 'HEAD');
|
|
61
|
+
expect(await getBranches(BranchesType.All, true)).toEqual(['main']);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('deletes multiple selected branches after one confirmation', async () => {
|
|
65
|
+
git('branch', 'one');
|
|
66
|
+
git('branch', 'two');
|
|
67
|
+
const prompt = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ answer: true });
|
|
68
|
+
await deleteBranches(['one', 'two']);
|
|
69
|
+
expect(git('branch', '--format=%(refname:short)')).toBe('main');
|
|
70
|
+
expect(prompt).toHaveBeenCalledTimes(1);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('does not delete when confirmation is declined', async () => {
|
|
74
|
+
git('branch', 'one');
|
|
75
|
+
vi.spyOn(inquirer, 'prompt').mockResolvedValue({ answer: false });
|
|
76
|
+
await deleteBranches(['one']);
|
|
77
|
+
expect(git('branch', '--list', 'one')).toContain('one');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('continues deleting and reports partial failures', async () => {
|
|
81
|
+
git('branch', 'one');
|
|
82
|
+
await expect(deleteBranches(['missing', 'one'], false)).rejects.toThrow('missing');
|
|
83
|
+
expect(git('branch', '--list', 'one')).toBe('');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('offers safe deletion after successful merge, defaulting to yes', async () => {
|
|
87
|
+
git('branch', 'feature');
|
|
88
|
+
const prompt = vi.spyOn(inquirer, 'prompt')
|
|
89
|
+
.mockResolvedValueOnce({ answer: true })
|
|
90
|
+
.mockResolvedValueOnce({ remove: true });
|
|
91
|
+
await mergeBranch('feature');
|
|
92
|
+
expect(prompt.mock.calls[1][0]).toMatchObject({ default: true });
|
|
93
|
+
expect(git('branch', '--list', 'feature')).toBe('');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('keeps the merged branch if deletion is declined', async () => {
|
|
97
|
+
git('branch', 'feature');
|
|
98
|
+
vi.spyOn(inquirer, 'prompt')
|
|
99
|
+
.mockResolvedValueOnce({ answer: true })
|
|
100
|
+
.mockResolvedValueOnce({ remove: false });
|
|
101
|
+
await mergeBranch('feature');
|
|
102
|
+
expect(git('branch', '--list', 'feature')).toContain('feature');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('never offers deletion after a failed merge', async () => {
|
|
106
|
+
const prompt = vi.spyOn(inquirer, 'prompt').mockResolvedValue({ answer: true });
|
|
107
|
+
await expect(mergeBranch('missing')).rejects.toThrow('missing');
|
|
108
|
+
expect(prompt).toHaveBeenCalledTimes(1);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { afterEach, beforeEach, expect, it, vi } from 'vitest';
|
|
2
|
+
import { finishTerminal, print, startTerminal } from '../terminal';
|
|
3
|
+
|
|
4
|
+
const descriptors = [process.stdin, process.stdout].map(stream =>
|
|
5
|
+
Object.getOwnPropertyDescriptor(stream, 'isTTY'));
|
|
6
|
+
|
|
7
|
+
function setTTY(value: boolean) {
|
|
8
|
+
for (const stream of [process.stdin, process.stdout]) {
|
|
9
|
+
Object.defineProperty(stream, 'isTTY', { value, configurable: true });
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
vi.spyOn(process.stdout, 'write').mockReturnValue(true);
|
|
15
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
finishTerminal(true);
|
|
20
|
+
[process.stdin, process.stdout].forEach((stream, index) => {
|
|
21
|
+
const descriptor = descriptors[index];
|
|
22
|
+
if (descriptor) Object.defineProperty(stream, 'isTTY', descriptor);
|
|
23
|
+
else delete (stream as any).isTTY;
|
|
24
|
+
});
|
|
25
|
+
vi.restoreAllMocks();
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('does not emit screen controls outside a TTY', () => {
|
|
29
|
+
setTTY(false);
|
|
30
|
+
startTerminal();
|
|
31
|
+
print('result');
|
|
32
|
+
finishTerminal();
|
|
33
|
+
expect(process.stdout.write).not.toHaveBeenCalled();
|
|
34
|
+
expect(console.log).toHaveBeenCalledExactlyOnceWith('result');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('restores the screen and replays results on success only once', () => {
|
|
38
|
+
setTTY(true);
|
|
39
|
+
startTerminal();
|
|
40
|
+
print('result');
|
|
41
|
+
finishTerminal();
|
|
42
|
+
finishTerminal();
|
|
43
|
+
expect(process.stdout.write).toHaveBeenNthCalledWith(1, '\x1b[?1049h');
|
|
44
|
+
expect(process.stdout.write).toHaveBeenNthCalledWith(2, '\x1b[?1049l\x1b[?25h');
|
|
45
|
+
expect(process.stdout.write).toHaveBeenCalledTimes(2);
|
|
46
|
+
expect(console.log).toHaveBeenCalledTimes(2);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('restores the screen without replaying output on cancellation', () => {
|
|
50
|
+
setTTY(true);
|
|
51
|
+
startTerminal();
|
|
52
|
+
print('discard this');
|
|
53
|
+
finishTerminal(true);
|
|
54
|
+
expect(process.stdout.write).toHaveBeenLastCalledWith('\x1b[?1049l\x1b[?25h');
|
|
55
|
+
expect(console.log).toHaveBeenCalledTimes(1);
|
|
56
|
+
});
|
package/src/ask.ts
CHANGED
|
@@ -1,62 +1,66 @@
|
|
|
1
1
|
import inquirer from 'inquirer';
|
|
2
|
+
import { prompt } from './prompt';
|
|
2
3
|
|
|
3
|
-
inquirer
|
|
4
|
-
'autocomplete',
|
|
5
|
-
require('inquirer-autocomplete-prompt')
|
|
6
|
-
);
|
|
4
|
+
const AutocompletePrompt = require('inquirer-autocomplete-prompt');
|
|
7
5
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
6
|
+
class FilteredAutocompletePrompt extends AutocompletePrompt {
|
|
7
|
+
_run(callback) {
|
|
8
|
+
// `default` in the plugin selects a choice, it does not fill the input.
|
|
9
|
+
const filter = this.opt.initialFilter || '';
|
|
10
|
+
this.rl.line = filter;
|
|
11
|
+
this.rl.cursor = filter.length;
|
|
12
|
+
return super._run(callback);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
13
15
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
inquirer.registerPrompt('autocomplete', FilteredAutocompletePrompt as any);
|
|
17
|
+
|
|
18
|
+
export const ask = async (message: string, choices: string[], initialFilter = '') => {
|
|
19
|
+
return prompt<{ branch: string }>({
|
|
17
20
|
type: 'autocomplete',
|
|
18
21
|
name: 'branch',
|
|
19
22
|
message,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
return Promise.resolve(list);
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
return prompt;
|
|
23
|
+
initialFilter,
|
|
24
|
+
source(_answers, input = initialFilter) {
|
|
25
|
+
return Promise.resolve(choices
|
|
26
|
+
.filter(choice => checkItem(choice, input))
|
|
27
|
+
.sort((a, b) => sortOrder(input, a, b)));
|
|
28
|
+
},
|
|
29
|
+
} as any);
|
|
32
30
|
};
|
|
33
31
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
export async function askBranches(message: string, choices: string[], filter = ''): Promise<string[]> {
|
|
33
|
+
const filtered = choices.filter(choice => checkItem(choice, filter));
|
|
34
|
+
if (!filtered.length) throw new Error('No branches match the filter.');
|
|
35
|
+
const { branches } = await prompt<{ branches: string[] }>({
|
|
36
|
+
type: 'checkbox',
|
|
37
|
+
name: 'branches',
|
|
38
|
+
message,
|
|
39
|
+
choices: filtered,
|
|
40
|
+
validate: (selected: string[]) => selected.length > 0 || 'Select at least one branch.',
|
|
41
|
+
});
|
|
42
|
+
return branches;
|
|
39
43
|
}
|
|
40
44
|
|
|
41
|
-
export function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return 1;
|
|
45
|
+
export function checkItem(item: string, filterString: string): boolean {
|
|
46
|
+
// Literal, case-insensitive subsequence matching; no user-supplied regex.
|
|
47
|
+
const needle = filterString.replace(/\s/g, '').toLowerCase();
|
|
48
|
+
const haystack = item.toLowerCase();
|
|
49
|
+
let position = 0;
|
|
50
|
+
for (const character of needle) {
|
|
51
|
+
position = haystack.indexOf(character, position);
|
|
52
|
+
if (position === -1) return false;
|
|
53
|
+
position++;
|
|
51
54
|
}
|
|
52
|
-
|
|
53
|
-
return -1;
|
|
55
|
+
return true;
|
|
54
56
|
}
|
|
55
57
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
);
|
|
58
|
+
export function sortOrder(searchString: string, name1: string, name2: string): number {
|
|
59
|
+
const search = searchString.toLowerCase();
|
|
60
|
+
const first = name1.toLowerCase().indexOf(search);
|
|
61
|
+
const second = name2.toLowerCase().indexOf(search);
|
|
62
|
+
if (first === second) return 0;
|
|
63
|
+
if (first === -1) return 1;
|
|
64
|
+
if (second === -1) return -1;
|
|
65
|
+
return first - second;
|
|
62
66
|
}
|
|
@@ -1,7 +1,3 @@
|
|
|
1
|
-
import { clc } from '../color';
|
|
2
|
-
|
|
3
|
-
const args = process.argv.slice(2);
|
|
4
|
-
|
|
5
1
|
export enum Commands {
|
|
6
2
|
Unknown = 0,
|
|
7
3
|
Help = 1,
|
|
@@ -9,52 +5,40 @@ export enum Commands {
|
|
|
9
5
|
MergeBranch = 4,
|
|
10
6
|
ShowMerged = 8,
|
|
11
7
|
ShowUnMerged = 16,
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const getWishOfUser = (firstArg?: string): Commands => {
|
|
15
|
-
const arg: string = firstArg || args[0];
|
|
16
|
-
|
|
17
|
-
switch (true) {
|
|
18
|
-
case ['help', '-h', '--h', '--help'].includes(arg):
|
|
19
|
-
return Commands.Help;
|
|
20
|
-
|
|
21
|
-
case ['-M', 'merge'].includes(arg):
|
|
22
|
-
return Commands.MergeBranch;
|
|
23
|
-
|
|
24
|
-
case ['-D', 'rm'].includes(arg):
|
|
25
|
-
return Commands.DeleteBranch;
|
|
26
|
-
|
|
27
|
-
case ['-sm', '--show-merged'].includes(arg):
|
|
28
|
-
return Commands.ShowMerged;
|
|
8
|
+
}
|
|
29
9
|
|
|
30
|
-
|
|
31
|
-
|
|
10
|
+
export const getWishOfUser = (firstArg = process.argv[2]): Commands => {
|
|
11
|
+
if (['help', '-h', '--h', '--help'].includes(firstArg)) return Commands.Help;
|
|
12
|
+
if (['-M', 'merge'].includes(firstArg)) return Commands.MergeBranch;
|
|
13
|
+
if (['-D', 'rm'].includes(firstArg)) return Commands.DeleteBranch;
|
|
14
|
+
if (['m', '-sm', '--show-merged'].includes(firstArg)) return Commands.ShowMerged;
|
|
15
|
+
if (['un', '-un', '--no-merged'].includes(firstArg)) return Commands.ShowUnMerged;
|
|
16
|
+
return Commands.Unknown;
|
|
17
|
+
};
|
|
32
18
|
|
|
33
|
-
|
|
34
|
-
|
|
19
|
+
export function parseArgs(args = process.argv.slice(2)) {
|
|
20
|
+
const command = getWishOfUser(args[0] || '');
|
|
21
|
+
if (!args.length) return { command, filter: '' };
|
|
22
|
+
if (command === Commands.Unknown && args[0] !== '-') {
|
|
23
|
+
throw new Error('Unknown option: ' + args[0] + '. Use co --help.');
|
|
35
24
|
}
|
|
25
|
+
const rest = args.slice(1);
|
|
26
|
+
if (rest[0] === '-') rest.shift();
|
|
27
|
+
return { command, filter: rest.join(' ') };
|
|
36
28
|
}
|
|
37
29
|
|
|
38
|
-
|
|
39
|
-
export const getHelp = (): string => {
|
|
40
|
-
return `
|
|
30
|
+
export const getHelp = (): string => `
|
|
41
31
|
DESCRIPTION:
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
-un, --no-merged
|
|
55
|
-
Showing not merged branches
|
|
56
|
-
|
|
57
|
-
-D, rm
|
|
58
|
-
Delete the selected branch
|
|
32
|
+
Quickly switch, merge and delete Git branches.
|
|
33
|
+
|
|
34
|
+
USAGE:
|
|
35
|
+
co Select a branch to check out
|
|
36
|
+
co - 1234 Start with an editable search filter
|
|
37
|
+
co -M [- filter] Merge, then offer to delete the source branch [Y/n]
|
|
38
|
+
co -D [- filter] Delete multiple local branches (Space to select)
|
|
39
|
+
co m, -sm, --show-merged Show merged branches
|
|
40
|
+
co un, -un, --no-merged Show unmerged branches
|
|
41
|
+
co help, -h, --h, --help Show this message
|
|
42
|
+
|
|
43
|
+
Ctrl+C cancels and restores the previous terminal screen.
|
|
59
44
|
`;
|
|
60
|
-
}
|
|
@@ -1,16 +1,8 @@
|
|
|
1
1
|
import { exec } from './exec';
|
|
2
|
-
import {
|
|
2
|
+
import { print } from '../terminal';
|
|
3
3
|
|
|
4
|
-
//
|
|
5
4
|
export async function checkoutBranch(branchName: string) {
|
|
6
|
-
if (!branchName)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const checkoutCommand = `git checkout ${branchName}`;
|
|
11
|
-
|
|
12
|
-
console.log(' ' + clc.green('git checkout', branchName) + '\n');
|
|
13
|
-
|
|
14
|
-
const checkoutCommandResult = await exec(checkoutCommand);
|
|
15
|
-
console.log(checkoutCommandResult);
|
|
5
|
+
if (!branchName) throw new Error('No branch selected.');
|
|
6
|
+
print('git checkout ' + branchName);
|
|
7
|
+
print(await exec(['checkout', branchName, '--']));
|
|
16
8
|
}
|