Skip to content

Commit

Permalink
Pathspec / file lists supported in all TaskOptions (#924)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
steveukx committed May 15, 2023
1 parent a52466d commit f702b61
Show file tree
Hide file tree
Showing 13 changed files with 194 additions and 6 deletions.
4 changes: 3 additions & 1 deletion .changeset/config.json
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
@@ -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
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
@@ -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
@@ -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
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
@@ -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
@@ -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
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
@@ -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
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
@@ -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
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

0 comments on commit f702b61

Please sign in to comment.