Skip to content

Commit f702b61

Browse files
authored
Pathspec / file lists supported in all TaskOptions (#924)
Add the ability to append pathspec / file paths to the parameters passed through to git, automatically adding the `--` argument to separate file paths from the rest of the git command. Closes #914
1 parent a52466d commit f702b61

13 files changed

+194
-6
lines changed

.changeset/config.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,7 @@
66
"access": "public",
77
"baseBranch": "main",
88
"updateInternalDependencies": "patch",
9-
"ignore": []
9+
"ignore": [
10+
"@simple-git/test-utils"
11+
]
1012
}

.changeset/smooth-roses-laugh.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'simple-git': minor
3+
---
4+
5+
Create a utility to append pathspec / file lists to tasks through the TaskOptions array/object

simple-git/readme.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -688,10 +688,12 @@ If the `simple-git` api doesn't explicitly limit the scope of the task being run
688688
be added, but `git.status()` will run against the entire repo), add a `pathspec` to the command using trailing options:
689689

690690
```typescript
691+
import { simpleGit, pathspec } from "simple-git";
692+
691693
const git = simpleGit();
692694
const wholeRepoStatus = await git.status();
693-
const subDirStatusUsingOptArray = await git.status(['--', 'sub-dir']);
694-
const subDirStatusUsingOptObject = await git.status({ '--': null, 'sub-dir': null });
695+
const subDirStatusUsingOptArray = await git.status([pathspec('sub-dir')]);
696+
const subDirStatusUsingOptObject = await git.status({ 'sub-dir': pathspec('sub-dir') });
695697
```
696698

697699
### async await

simple-git/src/lib/api.ts

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { pathspec } from './args/pathspec';
12
import { GitConstructError } from './errors/git-construct-error';
23
import { GitError } from './errors/git-error';
34
import { GitPluginError } from './errors/git-plugin-error';
@@ -20,4 +21,5 @@ export {
2021
ResetMode,
2122
TaskConfigurationError,
2223
grepQueryBuilder,
24+
pathspec,
2325
};

simple-git/src/lib/args/pathspec.ts

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const cache = new WeakMap<String, string[]>();
2+
3+
export function pathspec(...paths: string[]) {
4+
const key = new String(paths);
5+
cache.set(key, paths);
6+
7+
return key as string;
8+
}
9+
10+
export function isPathSpec(path: string | unknown): path is string {
11+
return path instanceof String && cache.has(path);
12+
}
13+
14+
export function toPaths(pathSpec: string): string[] {
15+
return cache.get(pathSpec) || [];
16+
}

simple-git/src/lib/git-factory.ts

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
spawnOptionsPlugin,
1414
timeoutPlugin,
1515
} from './plugins';
16+
import { suffixPathsPlugin } from './plugins/suffix-paths.plugin';
1617
import { createInstanceConfig, folderExists } from './utils';
1718
import { SimpleGitOptions } from './types';
1819

@@ -57,6 +58,7 @@ export function gitInstanceFactory(
5758
}
5859

5960
plugins.add(blockUnsafeOperationsPlugin(config.unsafe));
61+
plugins.add(suffixPathsPlugin());
6062
plugins.add(completionDetectionPlugin(config.completion));
6163
config.abort && plugins.add(abortPlugin(config.abort));
6264
config.progress && plugins.add(progressMonitorPlugin(config.progress));
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { SimpleGitPlugin } from './simple-git-plugin';
2+
import { isPathSpec, toPaths } from '../args/pathspec';
3+
4+
export function suffixPathsPlugin(): SimpleGitPlugin<'spawn.args'> {
5+
return {
6+
type: 'spawn.args',
7+
action(data) {
8+
const prefix: string[] = [];
9+
const suffix: string[] = [];
10+
11+
for (let i = 0; i < data.length; i++) {
12+
const param = data[i];
13+
14+
if (isPathSpec(param)) {
15+
suffix.push(...toPaths(param));
16+
continue;
17+
}
18+
19+
if (param === '--') {
20+
suffix.push(
21+
...data
22+
.slice(i + 1)
23+
.flatMap((item) => (isPathSpec(item) && toPaths(item)) || item)
24+
);
25+
break;
26+
}
27+
28+
prefix.push(param);
29+
}
30+
31+
return !suffix.length ? prefix : [...prefix, '--', ...suffix.map(String)];
32+
},
33+
};
34+
}

simple-git/src/lib/utils/argument-filters.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Maybe, Options, Primitives } from '../types';
22
import { objectToString } from './util';
3+
import { isPathSpec } from '../args/pathspec';
34

45
export interface ArgumentFilterPredicate<T> {
56
(input: any): input is T;
@@ -25,9 +26,11 @@ export function filterPrimitives(
2526
input: unknown,
2627
omit?: Array<'boolean' | 'string' | 'number'>
2728
): input is Primitives {
29+
const type = isPathSpec(input) ? 'string' : typeof input;
30+
2831
return (
29-
/number|string|boolean/.test(typeof input) &&
30-
(!omit || !omit.includes(typeof input as 'boolean' | 'string' | 'number'))
32+
/number|string|boolean/.test(type) &&
33+
(!omit || !omit.includes(type as 'boolean' | 'string' | 'number'))
3134
);
3235
}
3336

simple-git/src/lib/utils/task-options.ts

+4-1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from './argument-filters';
88
import { asFunction, isUserFunction, last } from './util';
99
import { Maybe, Options, OptionsValues } from '../types';
10+
import { isPathSpec } from '../args/pathspec';
1011

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

22-
if (filterPrimitives(value, ['boolean'])) {
23+
if (isPathSpec(value)) {
24+
commands.push(value);
25+
} else if (filterPrimitives(value, ['boolean'])) {
2326
commands.push(key + '=' + value);
2427
} else {
2528
commands.push(key);

simple-git/test/integration/grep.spec.ts

+15
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { createTestContext, newSimpleGit, SimpleGitTestContext } from '@simple-git/test-utils';
22
import { grepQueryBuilder } from '../..';
3+
import { pathspec } from '../../src/lib/args/pathspec';
34

45
describe('grep', () => {
56
let context: SimpleGitTestContext;
@@ -92,6 +93,20 @@ describe('grep', () => {
9293
},
9394
});
9495
});
96+
97+
it('limits within a set of paths', async () => {
98+
const result = await newSimpleGit(context.root).grep('foo', {
99+
'--untracked': null,
100+
'paths': pathspec('foo/bar.txt'),
101+
});
102+
103+
expect(result).toEqual({
104+
paths: new Set(['foo/bar.txt']),
105+
results: {
106+
'foo/bar.txt': [{ line: 4, path: 'foo/bar.txt', preview: ' foo/bar' }],
107+
},
108+
});
109+
});
95110
});
96111

97112
async function setUpFiles(context: SimpleGitTestContext) {

simple-git/test/unit/grep.spec.ts

+47
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99

1010
import { grepQueryBuilder, TaskConfigurationError } from '../..';
1111
import { NULL } from '../../src/lib/utils';
12+
import { pathspec } from '../../src/lib/args/pathspec';
1213

1314
describe('grep', () => {
1415
describe('grepQueryBuilder', () => {
@@ -130,5 +131,51 @@ another/file.txt${NULL}4${NULL}food content
130131
assertExecutedCommands('grep', '--null', '-n', '--full-name', '--c', '-e', 'a', '-e', 'b');
131132
expect(await queue).toHaveProperty('paths', new Set(['file.txt']));
132133
});
134+
135+
it('appends paths provided as a pathspec in array TaskOptions', async () => {
136+
const queue = newSimpleGit().grep(grepQueryBuilder('a', 'b'), [
137+
pathspec('path/to'),
138+
'--c',
139+
]);
140+
await closeWithSuccess(`file.txt${NULL}2${NULL}some foo content`);
141+
142+
assertExecutedCommands(
143+
'grep',
144+
'--null',
145+
'-n',
146+
'--full-name',
147+
'--c',
148+
'-e',
149+
'a',
150+
'-e',
151+
'b',
152+
'--',
153+
'path/to'
154+
);
155+
expect(await queue).toHaveProperty('paths', new Set(['file.txt']));
156+
});
157+
158+
it('appends paths provided as a pathspec in object TaskOptions', async () => {
159+
const queue = newSimpleGit().grep(grepQueryBuilder('a', 'b'), {
160+
'--c': null,
161+
'paths': pathspec('path/to'),
162+
});
163+
await closeWithSuccess(`file.txt${NULL}2${NULL}some foo content`);
164+
165+
assertExecutedCommands(
166+
'grep',
167+
'--null',
168+
'-n',
169+
'--full-name',
170+
'--c',
171+
'-e',
172+
'a',
173+
'-e',
174+
'b',
175+
'--',
176+
'path/to'
177+
);
178+
expect(await queue).toHaveProperty('paths', new Set(['file.txt']));
179+
});
133180
});
134181
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { SimpleGit } from '../../typings';
2+
import { assertExecutedCommands, closeWithSuccess, newSimpleGit } from './__fixtures__';
3+
import { pathspec } from '../../src/lib/args/pathspec';
4+
5+
describe('suffixPathsPlugin', function () {
6+
let git: SimpleGit;
7+
8+
beforeEach(() => (git = newSimpleGit()));
9+
10+
it('moves pathspec to end', async () => {
11+
git.raw(['a', pathspec('b'), 'c']);
12+
await closeWithSuccess();
13+
14+
assertExecutedCommands('a', 'c', '--', 'b');
15+
});
16+
17+
it('moves multiple pathspecs to end', async () => {
18+
git.raw(['a', pathspec('b'), 'c', pathspec('d'), 'e']);
19+
await closeWithSuccess();
20+
21+
assertExecutedCommands('a', 'c', 'e', '--', 'b', 'd');
22+
});
23+
24+
it('ignores processing after a pathspec split', async () => {
25+
git.raw('a', pathspec('b'), '--', 'c', pathspec('d'), 'e');
26+
await closeWithSuccess();
27+
28+
assertExecutedCommands('a', '--', 'b', 'c', 'd', 'e');
29+
});
30+
31+
it('flattens pathspecs after an explicit splitter', async () => {
32+
git.raw('a', '--', 'b', pathspec('c', 'd'), 'e');
33+
await closeWithSuccess();
34+
35+
assertExecutedCommands('a', '--', 'b', 'c', 'd', 'e');
36+
});
37+
38+
it('accepts multiple paths in one pathspec argument', async () => {
39+
git.raw('a', pathspec('b', 'c'), 'd');
40+
await closeWithSuccess();
41+
42+
assertExecutedCommands('a', 'd', '--', 'b', 'c');
43+
});
44+
45+
it('accepted as value of an option', async () => {
46+
git.pull({
47+
foo: null,
48+
blah1: pathspec('a', 'b'),
49+
blah2: pathspec('c', 'd'),
50+
bar: null,
51+
});
52+
53+
await closeWithSuccess();
54+
assertExecutedCommands('pull', 'foo', 'bar', '--', 'a', 'b', 'c', 'd');
55+
});
56+
});

simple-git/typings/types.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export type {
1010
SimpleGitTaskCallback,
1111
} from '../src/lib/types';
1212

13+
export { pathspec } from '../src/lib/args/pathspec';
1314
export type { ApplyOptions } from '../src/lib/tasks/apply-patch';
1415
export { CheckRepoActions } from '../src/lib/tasks/check-is-repo';
1516
export { CleanOptions, CleanMode } from '../src/lib/tasks/clean';

0 commit comments

Comments
 (0)