Skip to content

Commit 1702524

Browse files
authoredJan 13, 2023
feat: Be able to return a list of globs from GitIgnore.ts (#4009)
1 parent d8461c0 commit 1702524

File tree

6 files changed

+311
-45
lines changed

6 files changed

+311
-45
lines changed
 

‎cspell.code-workspace

+11
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,17 @@
5555
"console": "integratedTerminal",
5656
"internalConsoleOptions": "neverOpen",
5757
"disableOptimisticBPs": true
58+
},
59+
{
60+
"type": "node",
61+
"request": "launch",
62+
"name": "ViTest: Current Test File",
63+
"autoAttachChildProcesses": true,
64+
"skipFiles": ["<node_internals>/**", "**/node_modules/**"],
65+
"program": "${workspaceRoot:cspell-monorepo}/node_modules/vitest/vitest.mjs",
66+
"args": ["run", "${relativeFile}"],
67+
"smartStep": true,
68+
"console": "integratedTerminal"
5869
}
5970
],
6071
"compounds": []

‎packages/cspell-gitignore/src/GitIgnore.ts

+6
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export class GitIgnore {
5353
this.resolvedGitIgnoreHierarchies.set(directory, found);
5454
return find;
5555
}
56+
5657
filterOutIgnored(files: string[]): Promise<string[]>;
5758
filterOutIgnored(files: Iterable<string>): Promise<string[]>;
5859
filterOutIgnored(files: AsyncIterable<string>): AsyncIterable<string>;
@@ -89,6 +90,11 @@ export class GitIgnore {
8990
return this.knownGitIgnoreHierarchies.get(directory);
9091
}
9192

93+
async getGlobs(directory: string): Promise<string[]> {
94+
const hierarchy = await this.findGitIgnoreHierarchy(directory);
95+
return hierarchy.getGlobs(directory);
96+
}
97+
9298
private cleanCachedEntries() {
9399
this.knownGitIgnoreHierarchies.clear();
94100
this.resolvedGitIgnoreHierarchies.clear();

‎packages/cspell-gitignore/src/GitIgnoreFile.test.ts

+50-19
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,30 @@ describe('GitIgnoreFile', () => {
3030
const gif = await loadGitIgnore(path.join(__dirname, '../../..'));
3131
expect(gif?.isIgnored(require.resolve('vitest'))).toBe(true);
3232
});
33+
34+
test('getGlobs', () => {
35+
const gif = sampleGitIgnoreFile();
36+
expect(gif.getGlobs(__dirname).sort()).toEqual([
37+
'**/*.test.*',
38+
'**/*.test.*/**',
39+
'**/node_modules',
40+
'**/node_modules/**',
41+
'coverage/**',
42+
'temp',
43+
'temp/**',
44+
]);
45+
});
3346
});
3447

3548
describe('GitIgnoreHierarchy', () => {
3649
test.each`
37-
file | expected
38-
${__filename} | ${true}
39-
${p('GitIgnoreFiles.ts')} | ${false}
40-
${require.resolve('vitest')} | ${true}
41-
${p('package-lock.json')} | ${false}
50+
file | expected
51+
${rel(__filename)} | ${true}
52+
${rel(p('GitIgnoreFiles.ts'))} | ${false}
53+
${rel(require.resolve('vitest'))} | ${true}
54+
${rel(p('package-lock.json'))} | ${false}
4255
`('GitIgnoreHierarchy $file', async ({ file, expected }) => {
56+
file = p(file);
4357
// cspell:ignore gifs
4458
const gifs = [];
4559
const gi = await loadGitIgnore(path.join(__dirname, '../../..'));
@@ -59,12 +73,13 @@ describe('GitIgnoreHierarchy', () => {
5973
});
6074

6175
test.each`
62-
file | expected
63-
${__filename} | ${{ matched: true, gitIgnoreFile: p('./.gitignore'), line: undefined, glob: '*.test.*', root: __dirname }}
64-
${p('GitIgnoreFiles.ts')} | ${undefined}
65-
${require.resolve('vitest')} | ${{ matched: true, gitIgnoreFile, glob: 'node_modules/', line: 59, root: pathRepo }}
66-
${p('package-lock.json')} | ${undefined}
76+
file | expected
77+
${rel(__filename)} | ${{ matched: true, gitIgnoreFile: p('./.gitignore'), line: 5, glob: '*.test.*', root: __dirname }}
78+
${rp('GitIgnoreFiles.ts')} | ${undefined}
79+
${rel(require.resolve('vitest'))} | ${{ matched: true, gitIgnoreFile, glob: 'node_modules/', line: 59, root: pathRepo }}
80+
${rp('package-lock.json')} | ${undefined}
6781
`('ignoreEx $file', async ({ file, expected }) => {
82+
file = p(file);
6883
// cspell:ignore gifs
6984
const gifs = [];
7085
const gi = await loadGitIgnore(path.join(__dirname, '../../..'));
@@ -74,8 +89,27 @@ describe('GitIgnoreHierarchy', () => {
7489
expect(gih.isIgnoredEx(file)).toEqual(expected);
7590
});
7691

77-
function p(...files: string[]): string {
78-
return path.resolve(__dirname, ...files);
92+
test('getGlobs', async () => {
93+
const gifs = [];
94+
const gi = await loadGitIgnore(path.join(__dirname, '../../..'));
95+
if (gi) gifs.push(gi);
96+
gifs.push(sampleGitIgnoreFile());
97+
const gih = new GitIgnoreHierarchy(gifs);
98+
expect(gih.getGlobs(__dirname).sort()).toEqual(
99+
expect.arrayContaining(['**/*.cpuprofile', '**/*.cpuprofile/**', '**/*.test.*', '**/*.test.*/**'])
100+
);
101+
});
102+
103+
function rel(filename: string): string {
104+
return path.relative(__dirname, filename);
105+
}
106+
107+
function rp(...filename: string[]): string {
108+
return rel(p(...filename));
109+
}
110+
111+
function p(...filename: string[]): string {
112+
return path.resolve(__dirname, ...filename);
79113
}
80114
});
81115

@@ -85,16 +119,13 @@ const sampleGitIgnore = `
85119
node_modules
86120
*.test.*
87121
88-
`;
122+
/temp
123+
/coverage/**
89124
90-
function sampleGlobMatcher(): GlobMatcher {
91-
return new GlobMatcher(sampleGitIgnore, __dirname);
92-
}
125+
`;
93126

94127
function sampleGitIgnoreFile(): GitIgnoreFile {
95-
const m = sampleGlobMatcher();
96-
const file = path.join(m.root, '.gitignore');
97-
return new GitIgnoreFile(m, file);
128+
return GitIgnoreFile.parseGitignore(sampleGitIgnore, path.join(__dirname, '.gitignore'));
98129
}
99130

100131
// function oc<T>(v: Partial<T>): T {

‎packages/cspell-gitignore/src/GitIgnoreFile.ts

+46-9
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import type { GlobMatchRule, GlobPatternNormalized } from 'cspell-glob';
1+
import type { GlobMatchRule, GlobPatternNormalized, GlobPatternWithRoot } from 'cspell-glob';
22
import { GlobMatcher } from 'cspell-glob';
33
import { promises as fs } from 'fs';
44
import * as path from 'path';
55

6+
import { isDefined, isParentOf, makeRelativeTo } from './helpers';
7+
68
export interface IsIgnoredExResult {
79
glob: string | undefined;
810
root: string | undefined;
@@ -36,17 +38,34 @@ export class GitIgnoreFile {
3638
return { glob, matched, gitIgnoreFile: this.gitignore, root, line };
3739
}
3840

41+
getGlobPatters(): GlobPatternWithRoot[] {
42+
return this.matcher.patterns;
43+
}
44+
45+
getGlobs(relativeTo: string): string[] {
46+
return this.getGlobPatters()
47+
.map((pat) => globToString(pat, relativeTo))
48+
.filter(isDefined);
49+
}
50+
51+
static parseGitignore(content: string, gitignoreFilename: string): GitIgnoreFile {
52+
const options = { root: path.dirname(gitignoreFilename) };
53+
const globs = content
54+
.split('\n')
55+
.map((glob, index) => ({
56+
glob: glob.replace(/#.*/, '').trim(),
57+
source: gitignoreFilename,
58+
line: index + 1,
59+
}))
60+
.filter((g) => !!g.glob);
61+
const globMatcher = new GlobMatcher(globs, options);
62+
return new GitIgnoreFile(globMatcher, gitignoreFilename);
63+
}
64+
3965
static async loadGitignore(gitignore: string): Promise<GitIgnoreFile> {
4066
gitignore = path.resolve(gitignore);
4167
const content = await fs.readFile(gitignore, 'utf8');
42-
const options = { root: path.dirname(gitignore) };
43-
const globs = content.split('\n').map((glob, index) => ({
44-
glob,
45-
source: gitignore,
46-
line: index + 1,
47-
}));
48-
const globMatcher = new GlobMatcher(globs, options);
49-
return new GitIgnoreFile(globMatcher, gitignore);
68+
return this.parseGitignore(content, gitignore);
5069
}
5170
}
5271

@@ -79,6 +98,14 @@ export class GitIgnoreHierarchy {
7998

8099
return undefined;
81100
}
101+
102+
getGlobPatters(): GlobPatternWithRoot[] {
103+
return this.gitIgnoreChain.flatMap((gf) => gf.getGlobPatters());
104+
}
105+
106+
getGlobs(relativeTo: string): string[] {
107+
return this.gitIgnoreChain.flatMap((gf) => gf.getGlobs(relativeTo));
108+
}
82109
}
83110

84111
export async function loadGitIgnore(dir: string): Promise<GitIgnoreFile | undefined> {
@@ -100,6 +127,16 @@ function mustBeHierarchical(chain: GitIgnoreFile[]): void {
100127
}
101128
}
102129

130+
function globToString(glob: GlobPatternWithRoot, relativeTo: string): string | undefined {
131+
if (glob.isGlobalPattern) return glob.glob;
132+
133+
if (isParentOf(glob.root, relativeTo) && glob.glob.startsWith('**/')) return glob.glob;
134+
135+
const base = makeRelativeTo(glob.root, relativeTo);
136+
if (base === undefined) return undefined;
137+
return (base ? base + '/' : '') + glob.glob;
138+
}
139+
103140
export const __testing__ = {
104141
mustBeHierarchical,
105142
};

‎packages/cspell-gitignore/src/helpers.test.ts

+42-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import * as path from 'path';
2+
import { win32 } from 'path';
23
import { describe, expect, test } from 'vitest';
34

4-
import { contains, directoryRoot, findRepoRoot, isParentOf } from './helpers';
5+
import { contains, directoryRoot, factoryPathHelper, findRepoRoot, isParentOf, makeRelativeTo } from './helpers';
56

67
const pkg = path.resolve(__dirname, '..');
78
const gitRoot = path.resolve(pkg, '../..');
@@ -45,7 +46,47 @@ describe('helpers', () => {
4546
expect(contains(parent, child)).toBe(expected);
4647
});
4748

49+
test.each`
50+
parent | child | expected
51+
${pkg} | ${pkg} | ${''}
52+
${gitRoot} | ${samples} | ${'packages/cspell-gitignore/samples'}
53+
${samples} | ${gitRoot} | ${undefined}
54+
${pkg} | ${p(samples, 'ignore')} | ${'samples/ignore'}
55+
`('is $parent $child', ({ child, parent, expected }) => {
56+
expect(makeRelativeTo(child, parent)).toBe(expected);
57+
});
58+
4859
function p(dir: string, ...dirs: string[]) {
4960
return path.join(dir, ...dirs);
5061
}
5162
});
63+
64+
describe('factoryPathHelper win32', () => {
65+
const path = win32;
66+
const { directoryRoot, contains, isParentOf } = factoryPathHelper(path);
67+
68+
test.each`
69+
dir | expected
70+
${'C:\\user\\project\\code\\myFile.txt'} | ${'C:\\'}
71+
`('directoryRoot', ({ dir, expected }) => {
72+
expect(directoryRoot(dir)).toBe(expected);
73+
});
74+
75+
test.each`
76+
parent | child | expected
77+
${'D:\\user'} | ${'C:\\user\\home'} | ${false}
78+
${'C:\\user'} | ${'C:\\user\\home'} | ${true}
79+
${'C:\\user'} | ${'C:\\user\\'} | ${false}
80+
`('isParentOf $parent $child', ({ child, parent, expected }) => {
81+
expect(isParentOf(parent, child)).toBe(expected);
82+
});
83+
84+
test.each`
85+
parent | child | expected
86+
${'D:\\user'} | ${'C:\\user\\home'} | ${false}
87+
${'C:\\user'} | ${'C:\\user\\home'} | ${true}
88+
${'C:\\user'} | ${'C:\\user\\'} | ${true}
89+
`('contains $parent $child', ({ child, parent, expected }) => {
90+
expect(contains(parent, child)).toBe(expected);
91+
});
92+
});
+156-16
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,185 @@
11
import findUp from 'find-up';
22
import * as path from 'path';
33

4+
interface ParsedPath {
5+
/**
6+
* The root of the path such as '/' or 'c:\'
7+
*/
8+
root: string;
9+
/**
10+
* The full directory path such as '/home/user/dir' or 'c:\path\dir'
11+
*/
12+
dir: string;
13+
/**
14+
* The file name including extension (if any) such as 'index.html'
15+
*/
16+
base: string;
17+
/**
18+
* The file extension (if any) such as '.html'
19+
*/
20+
ext: string;
21+
/**
22+
* The file name without extension (if any) such as 'index'
23+
*/
24+
name: string;
25+
}
26+
27+
export interface PathInterface {
28+
dirname(path: string): string;
29+
isAbsolute(p: string): boolean;
30+
join(...paths: string[]): string;
31+
normalize(p: string): string;
32+
parse(path: string): ParsedPath;
33+
relative(from: string, to: string): string;
34+
resolve(...paths: string[]): string;
35+
sep: string;
36+
}
37+
38+
interface PathHelper {
39+
/**
40+
* Parse a directory and return its root
41+
* @param directory - directory to parse.
42+
* @returns root directory
43+
*/
44+
directoryRoot(directory: string): string;
45+
46+
/**
47+
* Find the git repository root directory.
48+
* @param directory - directory to search up from.
49+
* @returns resolves to `.git` root or undefined
50+
*/
51+
findRepoRoot(directory: string): Promise<string | undefined>;
52+
53+
/**
54+
* Checks to see if the child directory is nested under the parent directory.
55+
* @param parent - parent directory
56+
* @param child - possible child directory
57+
* @returns true iff child is a child of parent.
58+
*/
59+
isParentOf(parent: string, child: string): boolean;
60+
61+
/**
62+
* Check to see if a parent directory contains a child directory.
63+
* @param parent - parent directory
64+
* @param child - child directory
65+
* @returns true iff child is the same as the parent or nested in the parent.
66+
*/
67+
contains(parent: string, child: string): boolean;
68+
69+
/**
70+
* Make a path relative to another if the other is a parent.
71+
* @param path - the path to make relative
72+
* @param rootPath - a root of path
73+
* @returns the normalized relative path or undefined if rootPath is not a parent.
74+
*/
75+
makeRelativeTo(path: string, rootPath: string): string | undefined;
76+
77+
/**
78+
* Normalize a path to have only forward slashes.
79+
* @param path - path to normalize
80+
* @returns a normalized string.
81+
*/
82+
normalizePath(path: string): string;
83+
}
84+
85+
export function factoryPathHelper(path: PathInterface): PathHelper {
86+
function directoryRoot(directory: string): string {
87+
const p = path.parse(directory);
88+
return p.root;
89+
}
90+
91+
async function findRepoRoot(directory: string): Promise<string | undefined> {
92+
const found = await findUp('.git', { cwd: directory, type: 'directory' });
93+
if (!found) return undefined;
94+
return path.dirname(found);
95+
}
96+
97+
function isParentOf(parent: string, child: string): boolean {
98+
const rel = path.relative(parent, child);
99+
return !!rel && !path.isAbsolute(rel) && rel[0] !== '.';
100+
}
101+
102+
function contains(parent: string, child: string): boolean {
103+
const rel = path.relative(parent, child);
104+
return !rel || (!path.isAbsolute(rel) && rel[0] !== '.');
105+
}
106+
107+
function makeRelativeTo(child: string, parent: string): string | undefined {
108+
const rel = path.relative(parent, child);
109+
if (path.isAbsolute(rel) || rel[0] === '.') return undefined;
110+
return normalizePath(rel);
111+
}
112+
113+
function normalizePath(path: string): string {
114+
return path.replace(/\\/g, '/');
115+
}
116+
117+
return {
118+
directoryRoot,
119+
findRepoRoot,
120+
isParentOf,
121+
contains,
122+
normalizePath,
123+
makeRelativeTo,
124+
};
125+
}
126+
127+
const defaultHelper = factoryPathHelper(path);
128+
4129
/**
5130
* Parse a directory and return its root
6131
* @param directory - directory to parse.
7132
* @returns root directory
8133
*/
9-
export function directoryRoot(directory: string): string {
10-
const p = path.parse(directory);
11-
return p.root;
12-
}
134+
export const directoryRoot = defaultHelper.directoryRoot;
13135

14136
/**
15137
* Find the git repository root directory.
16138
* @param directory - directory to search up from.
17139
* @returns resolves to `.git` root or undefined
18140
*/
19-
export async function findRepoRoot(directory: string): Promise<string | undefined> {
20-
const found = await findUp('.git', { cwd: directory, type: 'directory' });
21-
if (!found) return undefined;
22-
return path.dirname(found);
23-
}
141+
export const findRepoRoot = defaultHelper.findRepoRoot;
24142

25143
/**
26144
* Checks to see if the child directory is nested under the parent directory.
27145
* @param parent - parent directory
28146
* @param child - possible child directory
29147
* @returns true iff child is a child of parent.
30148
*/
31-
export function isParentOf(parent: string, child: string): boolean {
32-
const rel = path.relative(parent, child);
33-
return !!rel && !path.isAbsolute(rel) && rel[0] !== '.';
34-
}
149+
export const isParentOf = defaultHelper.isParentOf;
35150

36151
/**
37152
* Check to see if a parent directory contains a child directory.
38153
* @param parent - parent directory
39154
* @param child - child directory
40155
* @returns true iff child is the same as the parent or nested in the parent.
41156
*/
42-
export function contains(parent: string, child: string): boolean {
43-
const rel = path.relative(parent, child);
44-
return !rel || (!path.isAbsolute(rel) && rel[0] !== '.');
157+
export const contains = defaultHelper.contains;
158+
159+
/**
160+
* Make a path relative to another if the other is a parent.
161+
* @param path - the path to make relative
162+
* @param rootPath - a root of path
163+
* @returns the normalized relative path or undefined if rootPath is not a parent.
164+
*/
165+
export const makeRelativeTo = defaultHelper.makeRelativeTo;
166+
167+
/**
168+
* Normalize a path to have only forward slashes.
169+
* @param path - path to normalize
170+
* @returns a normalized string.
171+
*/
172+
export const normalizePath = defaultHelper.normalizePath;
173+
174+
export const DefaultPathHelper: PathHelper = {
175+
directoryRoot,
176+
findRepoRoot,
177+
isParentOf,
178+
contains,
179+
makeRelativeTo,
180+
normalizePath,
181+
};
182+
183+
export function isDefined<T>(v: T | undefined | null): v is T {
184+
return v !== undefined && v !== null;
45185
}

0 commit comments

Comments
 (0)
Please sign in to comment.