Skip to content

Commit

Permalink
feat(git): add support for filtering commits in __p-pick-commit alias
Browse files Browse the repository at this point in the history
Add support for filtering the list of commits to choose from in the
`__g-pick-commits` alias by passing a `--filter-commits` argument. The
value of the argument will be used as a `grep` Basic Regular Expression
pattern to filter the commits (based on their headers; i.e. first line).
  • Loading branch information
gkalpak committed Jul 10, 2024
1 parent f86a917 commit bcfaeea
Show file tree
Hide file tree
Showing 2 changed files with 137 additions and 44 deletions.
33 changes: 25 additions & 8 deletions lib/alias-scripts/g-pick-commit.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ export {
* @function gPickCommit
*
* @description
* Prompt the user to pick one of the available git commits via an interactive list.
* Prompt the user to pick one of the available git commits via an interactive list. Optionally, filter the commits
* based on the commit message. Supported runtime arguments:
* - `--filter-commits` [string]: A basic pattern to filter commit messages (case-sensitive).
*
* @param {string[]} [runtimeArgs=[]] - The runtime arguments.
* @param {IRunConfig} [config={}] - A configuration object. See {@link commandUtils#IRunConfig} for more details.
*
* @return {Promise<void|string>} - A promise that resolves to either `undefined` or the selected commit (depending on
Expand All @@ -31,15 +34,29 @@ export {
};

// Helpers
async function _gPickCommit(config) {
async function _gPickCommit(runtimeArgs = [], config = {}) {
const maxCommits = 50;
const glConfig = Object.assign({}, config, {returnOutput: true});
let glCmd = 'git log --oneline';

const filterCommitsPattern =
(runtimeArgs.find((_arg, idx, arr) => (idx > 0) && (arr[idx - 1] === '--filter-commits')) ??
runtimeArgs.find(arg => arg.startsWith('--filter-commits='))?.slice('--filter-commits='.length))?.
replace(/^(['"])(.*)\1$/, '$2');
if (filterCommitsPattern !== undefined) {
glCmd += ` | grep "${filterCommitsPattern}"`;
}

glCmd += ` --max-count=${maxCommits}`; // `--max-count` works both for `git log` and `grep`.

if (config.dryrun) {
console.log('Pick one from a list of commits.');
console.log(
`Pick one from the first ${maxCommits} commits` +
`${(filterCommitsPattern === undefined) ? '' : ` whose header matches '${filterCommitsPattern}'`}.`);
return;
}

const commitOutput = await commandUtils.run('git log --oneline -50', [], glConfig);
const commitOutput = await commandUtils.run(glCmd, [], glConfig);
const commit = await pickCommit(commitOutput);

if (config.returnOutput) {
Expand All @@ -49,12 +66,12 @@ async function _gPickCommit(config) {
console.log(commit);
}

function gPickCommit(config) {
return internal._gPickCommit(config);
function gPickCommit(runtimeArgs, config) {
return internal._gPickCommit(runtimeArgs, config);
}

function main(_runtimeArgs, config) {
return gPickCommit(config);
function main(runtimeArgs, config) {
return gPickCommit(runtimeArgs, config);
}

async function pickCommit(commitsStr) {
Expand Down
148 changes: 112 additions & 36 deletions test/unit/alias-scripts/g-pick-commit.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,55 +23,123 @@ describe('g-pick-commit', () => {
});

it('should delegate to its internal counterpart', async () => {
const mockConfig = {foo: 'bar'};
const internalSpy = spyOn(_testing, '_gPickCommit').and.resolveTo('foo');
const mockArgs = ['--foo', 'bar'];
const mockConfig = {baz: 'qux'};
const internalSpy = spyOn(_testing, '_gPickCommit').and.resolveTo('quux');

expect(await gPickCommit(mockConfig)).toBe('foo');
expect(internalSpy).toHaveBeenCalledWith(mockConfig);
expect(await gPickCommit(mockArgs, mockConfig)).toBe('quux');
expect(internalSpy).toHaveBeenCalledWith(mockArgs, mockConfig);
});

describe('(dryrun)', () => {
it('should return a resolved promise', async () => {
const promise = gPickCommit({dryrun: true});
const promise = gPickCommit([], {dryrun: true});
expect(promise).toEqual(jasmine.any(Promise));

await promise;
});

it('should log a short description', async () => {
const cmdDesc = 'Pick one from a list of commits.';
await gPickCommit({dryrun: true});
it('should log a short description (when not filtering commits)', async () => {
const cmdDesc = 'Pick one from the first 50 commits.';
await gPickCommit([], {dryrun: true});

expect(consoleLogSpy).toHaveBeenCalledWith(cmdDesc);
});

it('should log a short description (when filtering commits)', async () => {
const cmdDesc = 'Pick one from the first 50 commits whose header matches \'foo or bar\'.';
const runtimeArgsList = [
['--filter-commits', 'foo or bar'],
['--filter-commits', '"foo or bar"'],
['--filter-commits', '\'foo or bar\''],
['--filter-commits=foo or bar'],
['--filter-commits="foo or bar"'],
['--filter-commits=\'foo or bar\''],
];

for (const testArgs of runtimeArgsList) {
consoleLogSpy.calls.reset();
await gPickCommit(testArgs, {dryrun: true});

expect(consoleLogSpy).withContext(`With args: ${testArgs.join(', ')}`).toHaveBeenCalledWith(cmdDesc);
}
});
});

describe('(no dryrun)', () => {
it('should return a promise', async () => {
const promise = gPickCommit({});
expect(promise).toEqual(jasmine.any(Promise));
const promise1 = gPickCommit();
expect(promise1).toEqual(jasmine.any(Promise));

await promise;
const promise2 = gPickCommit([]);
expect(promise2).toEqual(jasmine.any(Promise));

const promise3 = gPickCommit([], {});
expect(promise3).toEqual(jasmine.any(Promise));

await Promise.all([promise1, promise2, promise3]);
});

it('should run `git log ...` (and return the output)', async () => {
await gPickCommit({});
expect(cmdUtilsRunSpy).toHaveBeenCalledWith('git log --oneline -50', [], {returnOutput: true});
await gPickCommit();
expect(cmdUtilsRunSpy).toHaveBeenCalledWith('git log --oneline --max-count=50', [], {returnOutput: true});
});

it('should return `git log ...` output even if `config.returnOutput` is false (but not affect `config`)',
async () => {
const config = {returnOutput: false};
await gPickCommit(config);
await gPickCommit([], config);

expect(cmdUtilsRunSpy).toHaveBeenCalledWith('git log --oneline -50', [], {returnOutput: true});
expect(cmdUtilsRunSpy).toHaveBeenCalledWith('git log --oneline --max-count=50', [], {returnOutput: true});
expect(config.returnOutput).toBe(false);
}
);

it('should propagate errors', async () => {
cmdUtilsRunSpy.and.rejectWith('test');
await expectAsync(gPickCommit({})).toBeRejectedWith('test');
await expectAsync(gPickCommit()).toBeRejectedWith('test');
});

describe('filtering commits', () => {
let runtimeArgsList;

beforeEach(() => {
runtimeArgsList = [
['--filter-commits', 'foo or bar'],
['--filter-commits', '"foo or bar"'],
['--filter-commits', '\'foo or bar\''],
['--filter-commits=foo or bar'],
['--filter-commits="foo or bar"'],
['--filter-commits=\'foo or bar\''],
];
});

it('should run `git log ... | grep ...` (and return the output)', async () => {
for (const testArgs of runtimeArgsList) {
cmdUtilsRunSpy.calls.reset();
await gPickCommit(testArgs);

expect(cmdUtilsRunSpy).withContext(`With args: ${testArgs.join(', ')}`).toHaveBeenCalledWith(
'git log --oneline | grep "foo or bar" --max-count=50', [], {returnOutput: true});
}
});

it(
'should return `git log ... | grep ...` output even if `config.returnOutput` is false (but not affect ' +
'`config`)',
async () => {
for (const testArgs of runtimeArgsList) {
cmdUtilsRunSpy.calls.reset();

const config = {returnOutput: false};
await gPickCommit(testArgs, config);

expect(cmdUtilsRunSpy).withContext(`With args: ${testArgs.join(', ')}`).toHaveBeenCalledWith(
'git log --oneline | grep "foo or bar" --max-count=50', [], {returnOutput: true});
expect(config.returnOutput).toBe(false);
}
}
);
});

describe('picking a commit', () => {
Expand All @@ -91,7 +159,7 @@ describe('g-pick-commit', () => {
});

it('should prompt the user to pick a commit', async () => {
await gPickCommit({});
await gPickCommit();

verifyPromptedWith('type', 'list');
verifyPromptedWith('message', 'Pick a commit:');
Expand All @@ -104,7 +172,7 @@ describe('g-pick-commit', () => {
'3456789 The baz commit',
'456789 The qux commit',
];
await gPickCommit({});
await gPickCommit();

verifyPromptedWith('choices', [
'123456 The foo commit',
Expand All @@ -121,7 +189,7 @@ describe('g-pick-commit', () => {
'\t\t3456789 The baz commit\t\t',
' \n 456789 The qux commit \t ',
];
await gPickCommit({});
await gPickCommit();

verifyPromptedWith('choices', [
'123456 The foo commit',
Expand All @@ -140,7 +208,7 @@ describe('g-pick-commit', () => {
' \t\r\n ',
'456789 The qux commit',
];
await gPickCommit({});
await gPickCommit();

verifyPromptedWith('choices', [
'123456 The foo commit',
Expand All @@ -157,7 +225,7 @@ describe('g-pick-commit', () => {
'3456789 The baz commit',
'456789 The qux commit',
];
await gPickCommit({});
await gPickCommit();

verifyPromptedWith('default', 0);
});
Expand All @@ -171,7 +239,7 @@ describe('g-pick-commit', () => {
return Promise.resolve({commit: ''});
});

await gPickCommit({});
await gPickCommit();

const processExitSpy = spyOn(process, 'exit');

Expand All @@ -195,7 +263,7 @@ describe('g-pick-commit', () => {
return Promise.resolve({commit: ''});
});

await gPickCommit({});
await gPickCommit();

expect(promptSpy).toHaveBeenCalledTimes(1);
expect(unlistenSpy).toHaveBeenCalledWith();
Expand All @@ -211,7 +279,7 @@ describe('g-pick-commit', () => {
return Promise.reject('');
});

await expectAsync(gPickCommit({})).toBeRejected();
await expectAsync(gPickCommit()).toBeRejected();

expect(promptSpy).toHaveBeenCalledTimes(1);
expect(unlistenSpy).toHaveBeenCalledWith();
Expand All @@ -221,21 +289,29 @@ describe('g-pick-commit', () => {
describe('output', () => {
it('should log the selected commit SHA (removing other info)', async () => {
promptSpy.and.returnValues(
Promise.resolve({commit: 'f00ba2 (foo, origin/baz) This is the foo commit message'}),
Promise.resolve({commit: 'b4r9ux (bar, origin/qux) This is the bar commit message'}));
Promise.resolve({commit: 'f00f00 (foo, origin/foo) This is the foo commit message'}),
Promise.resolve({commit: 'b4rb4r (bar, origin/bar) This is the bar commit message'}),
Promise.resolve({commit: 'b4zb4z (baz, origin/baz) This is the baz commit message'}),
Promise.resolve({commit: '9ux9ux (qux, origin/qux) This is the qux commit message'}));

expect(await gPickCommit()).toBeUndefined();
expect(consoleLogSpy).toHaveBeenCalledWith('f00f00');

expect(await gPickCommit([])).toBeUndefined();
expect(consoleLogSpy).toHaveBeenCalledWith('b4rb4r');

expect(await gPickCommit({})).toBeUndefined();
expect(consoleLogSpy).toHaveBeenCalledWith('f00ba2');
expect(await gPickCommit([], {})).toBeUndefined();
expect(consoleLogSpy).toHaveBeenCalledWith('b4zb4z');

expect(await gPickCommit({returnOutput: false})).toBeUndefined();
expect(consoleLogSpy).toHaveBeenCalledWith('b4r9ux');
expect(await gPickCommit([], {returnOutput: false})).toBeUndefined();
expect(consoleLogSpy).toHaveBeenCalledWith('9ux9ux');
});

it('should return the selected commit SHA (removing other info) if `returnOutput` is `true`', async () => {
const commit = 'f00ba2 (foo, origin/baz) This is the commit message';
const commit = 'f00b4r (baz, origin/qux) This is the commit message';
promptSpy.and.resolveTo({commit});

expect(await gPickCommit({returnOutput: true})).toBe('f00ba2');
expect(await gPickCommit([], {returnOutput: true})).toBe('f00b4r');
expect(consoleLogSpy).not.toHaveBeenCalled();
});
});
Expand All @@ -251,11 +327,11 @@ describe('g-pick-commit', () => {
expect(main).toEqual(jasmine.any(Function));
});

it('should delegate to `gPickCommit()` (with appropriate arguments)', () => {
gPickCommitSpy.and.returnValue('foo');
const result = main('runtimeArgs', 'config');
it('should delegate to `gPickCommit()` (with appropriate arguments)', async () => {
gPickCommitSpy.and.resolveTo('foo');
const result = await main('runtimeArgs', 'config');

expect(gPickCommitSpy).toHaveBeenCalledWith('config');
expect(gPickCommitSpy).toHaveBeenCalledWith('runtimeArgs', 'config');
expect(result).toBe('foo');
});
});
Expand Down

0 comments on commit bcfaeea

Please sign in to comment.