-
-
Notifications
You must be signed in to change notification settings - Fork 319
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat git-grep Add support for `git.grep(searchTerm)` to list files matching a search term / terms.
- Loading branch information
Showing
11 changed files
with
430 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
## Git Grep | ||
|
||
The official documentation for [git grep](https://git-scm.com/docs/git-grep) gives the full set of options that can be passed to the `simple-git` `git.grep` method as [options](../readme.md#how-to-specify-options) (note that `-h` to hide the file name is disallowed). | ||
|
||
The simplest version is to search with a single search token: | ||
|
||
```typescript | ||
import simpleGit from 'simple-git'; | ||
|
||
console.log(await simpleGit().grep('search-term')); | ||
``` | ||
|
||
To search with multiple terms, use the `grepQueryBuilder` helper to construct the remaining arguments: | ||
|
||
```typescript | ||
import simpleGit, { grepQueryBuilder } from 'simple-git'; | ||
|
||
// logs all files that contain `aaa` AND either `bbb` or `ccc` | ||
console.log( | ||
await simpleGit().grep(grepQueryBuilder('aaa').and('bbb', 'ccc')) | ||
); | ||
``` | ||
|
||
The builder interface is purely there to simplify the many `-e` flags needed to instruct `git` to treat an argument as a search term - the code above translates to: | ||
|
||
```typescript | ||
console.log(Array.from(grepQueryBuilder('aaa').and('bbb', 'ccc'))) | ||
// [ '-e', 'aaa', '--and', '(', '-e', 'bbb', '-e', 'ccc', ')' ] | ||
``` | ||
|
||
To build your own query instead of using the `grepQueryBuilder`, use the array form of [options](../readme.md#how-to-specify-options): | ||
|
||
```typescript | ||
import simpleGit from 'simple-git'; | ||
|
||
console.log(await simpleGit().grep('search-term', ['-e', 'another search term'])); | ||
``` | ||
|
||
`git.grep` will include previews around the matched term in the resulting data, to disable this use options such as `-l` to only show the file name or `-c` to show the number of instances of a match in the file rather than the text that was matched. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import { GrepResult, SimpleGit } from '../../../typings'; | ||
import { SimpleGitApi } from '../simple-git-api'; | ||
import { | ||
asNumber, | ||
forEachLineWithContent, | ||
getTrailingOptions, | ||
NULL, | ||
prefixedArray, | ||
trailingFunctionArgument | ||
} from '../utils'; | ||
|
||
import { configurationErrorTask } from './task'; | ||
|
||
const disallowedOptions = ['-h']; | ||
|
||
const Query = Symbol('grepQuery'); | ||
|
||
export interface GitGrepQuery extends Iterable<string> { | ||
/** Adds one or more terms to be grouped as an "and" to any other terms */ | ||
and(...and: string[]): this; | ||
|
||
/** Adds one or more search terms - git.grep will "or" this to other terms */ | ||
param(...param: string[]): this; | ||
} | ||
|
||
class GrepQuery implements GitGrepQuery { | ||
private [Query]: string[] = []; | ||
|
||
* [Symbol.iterator]() { | ||
for (const query of this[Query]) { | ||
yield query; | ||
} | ||
} | ||
|
||
and(...and: string[]) { | ||
and.length && this[Query].push('--and', '(', ...prefixedArray(and, '-e'), ')'); | ||
return this; | ||
} | ||
|
||
param(...param: string[]) { | ||
this[Query].push(...prefixedArray(param, '-e')); | ||
return this; | ||
} | ||
} | ||
|
||
/** | ||
* Creates a new builder for a `git.grep` query with optional params | ||
*/ | ||
export function grepQueryBuilder(...params: string[]): GitGrepQuery { | ||
return new GrepQuery().param(...params); | ||
} | ||
|
||
function parseGrep(grep: string): GrepResult { | ||
const paths: GrepResult['paths'] = new Set<string>(); | ||
const results: GrepResult['results'] = {}; | ||
|
||
forEachLineWithContent(grep, (input) => { | ||
const [path, line, preview] = input.split(NULL); | ||
paths.add(path); | ||
(results[path] = results[path] || []).push({ | ||
line: asNumber(line), | ||
path, | ||
preview, | ||
}); | ||
}); | ||
|
||
return { | ||
paths, | ||
results, | ||
}; | ||
} | ||
|
||
export default function (): Pick<SimpleGit, 'grep'> { | ||
return { | ||
grep(this: SimpleGitApi, searchTerm: string | GitGrepQuery) { | ||
const then = trailingFunctionArgument(arguments); | ||
const options = getTrailingOptions(arguments); | ||
|
||
for (const option of disallowedOptions) { | ||
if (options.includes(option)) { | ||
return this._runTask( | ||
configurationErrorTask(`git.grep: use of "${option}" is not supported.`), | ||
then, | ||
); | ||
} | ||
} | ||
|
||
if (typeof searchTerm === 'string') { | ||
searchTerm = grepQueryBuilder().param(searchTerm); | ||
} | ||
|
||
const commands = ['grep', '--null', '-n', '--full-name', ...options, ...searchTerm]; | ||
|
||
return this._runTask({ | ||
commands, | ||
format: 'utf-8', | ||
parser(stdOut) { | ||
return parseGrep(stdOut); | ||
}, | ||
}, then); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
import { createTestContext, newSimpleGit, SimpleGitTestContext } from '../__fixtures__'; | ||
import { grepQueryBuilder } from '../..'; | ||
|
||
describe('grep', () => { | ||
|
||
let context: SimpleGitTestContext; | ||
|
||
beforeEach(async () => { | ||
context = await createTestContext(); | ||
await setUpFiles(context); | ||
}); | ||
|
||
it('finds tracked files matching a string', async () => { | ||
const result = await newSimpleGit(context.root).grep('foo'); | ||
|
||
expect(result).toEqual({ | ||
paths: new Set(['foo/bar.txt']), | ||
results: { | ||
'foo/bar.txt': [ | ||
{line: 4, path: 'foo/bar.txt', preview: ' foo/bar'}, | ||
], | ||
}, | ||
}); | ||
}); | ||
|
||
it('finds tracked files matching multiple strings', async () => { | ||
// finds all instances of `line` when there is also either `one` or `two` on the line | ||
// ie: doesn't find `another line` | ||
const result = await newSimpleGit(context.root).grep( | ||
grepQueryBuilder('line').and('one', 'two') | ||
); | ||
|
||
expect(result).toEqual({ | ||
paths: new Set(['a/aaa.txt', 'foo/bar.txt']), | ||
results: { | ||
'a/aaa.txt': [ | ||
{line: 1, path: 'a/aaa.txt', preview: 'something on line one'}, | ||
{line: 2, path: 'a/aaa.txt', preview: 'this is line two'}, | ||
], | ||
'foo/bar.txt': [ | ||
{line: 1, path: 'foo/bar.txt', preview: 'something on line one'}, | ||
{line: 2, path: 'foo/bar.txt', preview: 'this is line two'}, | ||
], | ||
}, | ||
}); | ||
}); | ||
|
||
it('finds multiple tracked files matching a string', async () => { | ||
const result = await newSimpleGit(context.root).grep('something'); | ||
|
||
expect(result).toEqual({ | ||
paths: new Set(['a/aaa.txt', 'foo/bar.txt']), | ||
results: { | ||
'foo/bar.txt': [ | ||
{line: 1, path: 'foo/bar.txt', preview: 'something on line one'}, | ||
], | ||
'a/aaa.txt': [ | ||
{line: 1, path: 'a/aaa.txt', preview: 'something on line one'}, | ||
], | ||
}, | ||
}); | ||
}); | ||
|
||
it('finds multiple tracked files matching any string', async () => { | ||
const result = await newSimpleGit(context.root).grep( | ||
grepQueryBuilder('something', 'foo') | ||
); | ||
|
||
expect(result).toEqual({ | ||
paths: new Set(['a/aaa.txt', 'foo/bar.txt']), | ||
results: { | ||
'foo/bar.txt': [ | ||
{line: 1, path: 'foo/bar.txt', preview: 'something on line one'}, | ||
{line: 4, path: 'foo/bar.txt', preview: ' foo/bar'}, | ||
], | ||
'a/aaa.txt': [ | ||
{line: 1, path: 'a/aaa.txt', preview: 'something on line one'}, | ||
], | ||
}, | ||
}); | ||
}); | ||
|
||
it('can be used to find the matching lines count per file without line detail', async () => { | ||
const result = await newSimpleGit(context.root).grep('line', {'-c': null}); | ||
|
||
expect(result).toEqual({ | ||
paths: new Set(['a/aaa.txt', 'foo/bar.txt']), | ||
results: { | ||
'foo/bar.txt': [ | ||
{line: 3, path: 'foo/bar.txt'}, | ||
], | ||
'a/aaa.txt': [ | ||
{line: 3, path: 'a/aaa.txt'}, | ||
], | ||
}, | ||
}); | ||
}); | ||
|
||
it('also finds untracked files on request', async () => { | ||
const result = await newSimpleGit(context.root).grep('foo', {'--untracked': null}); | ||
|
||
expect(result).toEqual({ | ||
paths: new Set(['foo/bar.txt', 'foo/baz.txt']), | ||
results: { | ||
'foo/bar.txt': [ | ||
{line: 4, path: 'foo/bar.txt', preview: ' foo/bar'}, | ||
], | ||
'foo/baz.txt': [ | ||
{line: 4, path: 'foo/baz.txt', preview: ' foo/baz'}, | ||
], | ||
}, | ||
}); | ||
}); | ||
|
||
}); | ||
|
||
async function setUpFiles(context: SimpleGitTestContext) { | ||
const content = `something on line one\nthis is line two\n another line `; | ||
|
||
await context.git.init(); | ||
|
||
// tracked files | ||
await context.file(['foo', 'bar.txt'], `${content}\n foo/bar `); | ||
await context.file(['a', 'aaa.txt'], `${content}\n a/aaa `); | ||
await context.git.add('*'); | ||
|
||
// untracked files | ||
await context.file(['foo', 'baz.txt'], `${content}\n foo/baz `); | ||
await context.file(['a', 'bbb.txt'], `${content}\n a/bbb `); | ||
} |
Oops, something went wrong.