Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pathspec / file lists supported in all TaskOptions #924

Merged
merged 4 commits into from
May 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
"ignore": [
"@simple-git/test-utils"
]
}
5 changes: 5 additions & 0 deletions .changeset/smooth-roses-laugh.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'simple-git': minor
---

Create a utility to append pathspec / file lists to tasks through the TaskOptions array/object
6 changes: 4 additions & 2 deletions simple-git/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -688,10 +688,12 @@ If the `simple-git` api doesn't explicitly limit the scope of the task being run
be added, but `git.status()` will run against the entire repo), add a `pathspec` to the command using trailing options:

```typescript
import { simpleGit, pathspec } from "simple-git";

const git = simpleGit();
const wholeRepoStatus = await git.status();
const subDirStatusUsingOptArray = await git.status(['--', 'sub-dir']);
const subDirStatusUsingOptObject = await git.status({ '--': null, 'sub-dir': null });
const subDirStatusUsingOptArray = await git.status([pathspec('sub-dir')]);
const subDirStatusUsingOptObject = await git.status({ 'sub-dir': pathspec('sub-dir') });
```

### async await
Expand Down
2 changes: 2 additions & 0 deletions simple-git/src/lib/api.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { pathspec } from './args/pathspec';
import { GitConstructError } from './errors/git-construct-error';
import { GitError } from './errors/git-error';
import { GitPluginError } from './errors/git-plugin-error';
Expand All @@ -20,4 +21,5 @@ export {
ResetMode,
TaskConfigurationError,
grepQueryBuilder,
pathspec,
};
16 changes: 16 additions & 0 deletions simple-git/src/lib/args/pathspec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const cache = new WeakMap<String, string[]>();

export function pathspec(...paths: string[]) {
const key = new String(paths);
cache.set(key, paths);

return key as string;
}

export function isPathSpec(path: string | unknown): path is string {
return path instanceof String && cache.has(path);
}

export function toPaths(pathSpec: string): string[] {
return cache.get(pathSpec) || [];
}
2 changes: 2 additions & 0 deletions simple-git/src/lib/git-factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
spawnOptionsPlugin,
timeoutPlugin,
} from './plugins';
import { suffixPathsPlugin } from './plugins/suffix-paths.plugin';
import { createInstanceConfig, folderExists } from './utils';
import { SimpleGitOptions } from './types';

Expand Down Expand Up @@ -57,6 +58,7 @@ export function gitInstanceFactory(
}

plugins.add(blockUnsafeOperationsPlugin(config.unsafe));
plugins.add(suffixPathsPlugin());
plugins.add(completionDetectionPlugin(config.completion));
config.abort && plugins.add(abortPlugin(config.abort));
config.progress && plugins.add(progressMonitorPlugin(config.progress));
Expand Down
34 changes: 34 additions & 0 deletions simple-git/src/lib/plugins/suffix-paths.plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { SimpleGitPlugin } from './simple-git-plugin';
import { isPathSpec, toPaths } from '../args/pathspec';

export function suffixPathsPlugin(): SimpleGitPlugin<'spawn.args'> {
return {
type: 'spawn.args',
action(data) {
const prefix: string[] = [];
const suffix: string[] = [];

for (let i = 0; i < data.length; i++) {
const param = data[i];

if (isPathSpec(param)) {
suffix.push(...toPaths(param));
continue;
}

if (param === '--') {
suffix.push(
...data
.slice(i + 1)
.flatMap((item) => (isPathSpec(item) && toPaths(item)) || item)
);
break;
}

prefix.push(param);
}

return !suffix.length ? prefix : [...prefix, '--', ...suffix.map(String)];
},
};
}
7 changes: 5 additions & 2 deletions simple-git/src/lib/utils/argument-filters.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Maybe, Options, Primitives } from '../types';
import { objectToString } from './util';
import { isPathSpec } from '../args/pathspec';

export interface ArgumentFilterPredicate<T> {
(input: any): input is T;
Expand All @@ -25,9 +26,11 @@ export function filterPrimitives(
input: unknown,
omit?: Array<'boolean' | 'string' | 'number'>
): input is Primitives {
const type = isPathSpec(input) ? 'string' : typeof input;

return (
/number|string|boolean/.test(typeof input) &&
(!omit || !omit.includes(typeof input as 'boolean' | 'string' | 'number'))
/number|string|boolean/.test(type) &&
(!omit || !omit.includes(type as 'boolean' | 'string' | 'number'))
);
}

Expand Down
5 changes: 4 additions & 1 deletion simple-git/src/lib/utils/task-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from './argument-filters';
import { asFunction, isUserFunction, last } from './util';
import { Maybe, Options, OptionsValues } from '../types';
import { isPathSpec } from '../args/pathspec';

export function appendTaskOptions<T extends Options = Options>(
options: Maybe<T>,
Expand All @@ -19,7 +20,9 @@ export function appendTaskOptions<T extends Options = Options>(
return Object.keys(options).reduce((commands: string[], key: string) => {
const value: OptionsValues = options[key];

if (filterPrimitives(value, ['boolean'])) {
if (isPathSpec(value)) {
commands.push(value);
} else if (filterPrimitives(value, ['boolean'])) {
commands.push(key + '=' + value);
} else {
commands.push(key);
Expand Down
15 changes: 15 additions & 0 deletions simple-git/test/integration/grep.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createTestContext, newSimpleGit, SimpleGitTestContext } from '@simple-git/test-utils';
import { grepQueryBuilder } from '../..';
import { pathspec } from '../../src/lib/args/pathspec';

describe('grep', () => {
let context: SimpleGitTestContext;
Expand Down Expand Up @@ -92,6 +93,20 @@ describe('grep', () => {
},
});
});

it('limits within a set of paths', async () => {
const result = await newSimpleGit(context.root).grep('foo', {
'--untracked': null,
'paths': pathspec('foo/bar.txt'),
});

expect(result).toEqual({
paths: new Set(['foo/bar.txt']),
results: {
'foo/bar.txt': [{ line: 4, path: 'foo/bar.txt', preview: ' foo/bar' }],
},
});
});
});

async function setUpFiles(context: SimpleGitTestContext) {
Expand Down
47 changes: 47 additions & 0 deletions simple-git/test/unit/grep.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {

import { grepQueryBuilder, TaskConfigurationError } from '../..';
import { NULL } from '../../src/lib/utils';
import { pathspec } from '../../src/lib/args/pathspec';

describe('grep', () => {
describe('grepQueryBuilder', () => {
Expand Down Expand Up @@ -130,5 +131,51 @@ another/file.txt${NULL}4${NULL}food content
assertExecutedCommands('grep', '--null', '-n', '--full-name', '--c', '-e', 'a', '-e', 'b');
expect(await queue).toHaveProperty('paths', new Set(['file.txt']));
});

it('appends paths provided as a pathspec in array TaskOptions', async () => {
const queue = newSimpleGit().grep(grepQueryBuilder('a', 'b'), [
pathspec('path/to'),
'--c',
]);
await closeWithSuccess(`file.txt${NULL}2${NULL}some foo content`);

assertExecutedCommands(
'grep',
'--null',
'-n',
'--full-name',
'--c',
'-e',
'a',
'-e',
'b',
'--',
'path/to'
);
expect(await queue).toHaveProperty('paths', new Set(['file.txt']));
});

it('appends paths provided as a pathspec in object TaskOptions', async () => {
const queue = newSimpleGit().grep(grepQueryBuilder('a', 'b'), {
'--c': null,
'paths': pathspec('path/to'),
});
await closeWithSuccess(`file.txt${NULL}2${NULL}some foo content`);

assertExecutedCommands(
'grep',
'--null',
'-n',
'--full-name',
'--c',
'-e',
'a',
'-e',
'b',
'--',
'path/to'
);
expect(await queue).toHaveProperty('paths', new Set(['file.txt']));
});
});
});
56 changes: 56 additions & 0 deletions simple-git/test/unit/plugin.pathspec.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { SimpleGit } from '../../typings';
import { assertExecutedCommands, closeWithSuccess, newSimpleGit } from './__fixtures__';
import { pathspec } from '../../src/lib/args/pathspec';

describe('suffixPathsPlugin', function () {
let git: SimpleGit;

beforeEach(() => (git = newSimpleGit()));

it('moves pathspec to end', async () => {
git.raw(['a', pathspec('b'), 'c']);
await closeWithSuccess();

assertExecutedCommands('a', 'c', '--', 'b');
});

it('moves multiple pathspecs to end', async () => {
git.raw(['a', pathspec('b'), 'c', pathspec('d'), 'e']);
await closeWithSuccess();

assertExecutedCommands('a', 'c', 'e', '--', 'b', 'd');
});

it('ignores processing after a pathspec split', async () => {
git.raw('a', pathspec('b'), '--', 'c', pathspec('d'), 'e');
await closeWithSuccess();

assertExecutedCommands('a', '--', 'b', 'c', 'd', 'e');
});

it('flattens pathspecs after an explicit splitter', async () => {
git.raw('a', '--', 'b', pathspec('c', 'd'), 'e');
await closeWithSuccess();

assertExecutedCommands('a', '--', 'b', 'c', 'd', 'e');
});

it('accepts multiple paths in one pathspec argument', async () => {
git.raw('a', pathspec('b', 'c'), 'd');
await closeWithSuccess();

assertExecutedCommands('a', 'd', '--', 'b', 'c');
});

it('accepted as value of an option', async () => {
git.pull({
foo: null,
blah1: pathspec('a', 'b'),
blah2: pathspec('c', 'd'),
bar: null,
});

await closeWithSuccess();
assertExecutedCommands('pull', 'foo', 'bar', '--', 'a', 'b', 'c', 'd');
});
});
1 change: 1 addition & 0 deletions simple-git/typings/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export type {
SimpleGitTaskCallback,
} from '../src/lib/types';

export { pathspec } from '../src/lib/args/pathspec';
export type { ApplyOptions } from '../src/lib/tasks/apply-patch';
export { CheckRepoActions } from '../src/lib/tasks/check-is-repo';
export { CleanOptions, CleanMode } from '../src/lib/tasks/clean';
Expand Down