Skip to content

Commit

Permalink
fix: Use the repository root by default when no root is specified. (#…
Browse files Browse the repository at this point in the history
…1851)

The integration repositories are clean and should not contain any extra files. If the `--gitignore` option ignores any of those files, then something is wrong. This check would have caught #1846.

Changes:
- fix: Use repository root by default.
- fix: Add a simple cli to `cspell-gitignore`.
- fix: Update README to reflect CLI
- fix: Update PHP snapshot to reflect the ignored files.
    The ignored files are ignored by `.gitignore` but they are checked into the repo.
- test: make sure the integration files checked do not change with `.gitignore`
  • Loading branch information
Jason3S committed Oct 8, 2021
1 parent bb9a9a7 commit 81d005e
Show file tree
Hide file tree
Showing 19 changed files with 457 additions and 33 deletions.
4 changes: 4 additions & 0 deletions integration-tests/config/cspell.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"version": "0.2",
"import": ["../repositories/cspell.yaml"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,5 @@
"languageId": "c,cpp,h"
}
],
"import": ["../../../../repositories/cspell-reporter.json"]
"import": ["../../../cspell.json"]
}
2 changes: 1 addition & 1 deletion integration-tests/snapshots/php/php-src/report.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Repository: php/php-src
Url: https://github.com/php/php-src.git
Args: ["--config=${repoConfig}/cspell.json","**/*.{md,c,h,php}"]
Summary:
files: 1827
files: 1823
filesWithIssues: 1106
issues: 19948
errors: 0
Expand Down
2 changes: 1 addition & 1 deletion integration-tests/snapshots/php/php-src/snapshot.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Repository: php/php-src
Url: "https://github.com/php/php-src.git"
Args: ["--config=../../../../config/repositories/php/php-src/cspell.json","**/*.{md,c,h,php}"]
Lines:
CSpell: Files checked: 1827, Issues found: 19948 in 1106 files
CSpell: Files checked: 1823, Issues found: 19948 in 1106 files
exit code: 1
./CODING_STANDARDS.md:105:8 - Unknown word (setclientencoding) -- pg_setclientencoding
./CODING_STANDARDS.md:126:5 - Unknown word (fooselect) -- fooselect_bar
Expand Down
2 changes: 1 addition & 1 deletion integration-tests/src/check.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { PrefixLogger } from './PrefixLogger';
import { Logger } from './types';

const config = readConfig();
const cspellArgs = '-u --no-progress --relative --show-context';
const cspellArgs = '-u --no-progress --relative --show-context --gitignore --gitignore-root=. ';
const jsCspell = JSON.stringify(Path.resolve(__dirname, '..', '..', 'bin.js'));

const cspellCommand = `node ${jsCspell} ${cspellArgs}`;
Expand Down
38 changes: 36 additions & 2 deletions packages/cspell-gitignore/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ npm install -S cspell-gitignore
## Usage

```ts
import { GitIgnore } from 'cspell-gitignore';
import { GitIgnore, findRepoRoot } from 'cspell-gitignore';

// ...

const gitIgnore = new GitIgnore();
const cwd = process.cwd();
const root = (await findRepoRoot(cwd)) || cwd;
const gitIgnore = new GitIgnore([root]);

const allFiles = glob('**');

Expand All @@ -34,3 +36,35 @@ To prevent searching higher in the directory hierarchy, specify roots:
```ts
const gitIgnore = new GitIgnore([process.cwd()]);
```

# `cspell-gitignore` CLI

`cspell-gitignore` provides a simple cli for debugging .gitignore issues.

In most cases it should provide the same output as `git check-ignore`.

## Usage

```text
Usage cspell-gitignore [options] <files>
Check files against .gitignore
Compare against git check-ignore -v -n <files>
Options:
-r, --root Add a root to prevent searching for .gitignore files above the root if the file is under the root.
This option can be used multiple times to add multiple roots. The default root is the current
repository root determined by the `.git` directory.
Example:
cspell-gitignore README.md
cspell-gitignore -r . node_modules
```

## Example:

```sh
$ cspell-gitignore -r . node_modules
.gitignore:58:node_modules/ node_modules
```
6 changes: 6 additions & 0 deletions packages/cspell-gitignore/bin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env node
'use strict';

const app = require('./dist/app.js');

app.run(process.argv);
4 changes: 4 additions & 0 deletions packages/cspell-gitignore/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@
"homepage": "https://github.com/streetsidesoftware/cspell/tree/master/packages/cspell-gitignore#readme",
"license": "MIT",
"main": "dist/index.js",
"bin": {
"cspell-gitignore": "bin.js"
},
"directories": {
"dist": "dist"
},
"typings": "dist/index.d.ts",
"files": [
"bin.js",
"dist",
"!**/__mocks__",
"!**/*.test.*",
Expand Down
71 changes: 64 additions & 7 deletions packages/cspell-gitignore/src/GitIgnore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ const packages = path.resolve(pkg, '..');
const gitRoot = path.resolve(packages, '..');
const samples = path.resolve(pkg, 'samples');
const pkgCSpellLib = path.join(packages, 'cspell-lib');
const gitIgnoreFile = path.resolve(gitRoot, '.gitignore');
// const pathSamples = path.resolve(pkg, 'samples');
// const gitIgnoreSamples = path.resolve(pathSamples, '.gitignore');

describe('GitIgnoreServer', () => {
test('GitIgnoreServer', () => {
Expand All @@ -29,18 +32,40 @@ describe('GitIgnoreServer', () => {

// cspell:ignore keepme
test.each`
file | expected
${__filename} | ${false}
${p(samples, 'ignored/keepme.md')} | ${false}
${p(samples, 'ignored/file.txt')} | ${true}
${p(pkg, 'node_modules/bin')} | ${true}
`('isIgnored $file', async ({ file, expected }) => {
file | roots | expected
${__filename} | ${undefined} | ${false}
${p(samples, 'ignored/keepme.md')} | ${undefined} | ${false}
${p(samples, 'ignored/file.txt')} | ${undefined} | ${true}
${p(pkg, 'node_modules/bin')} | ${undefined} | ${true}
${__filename} | ${[p(samples, 'ignored')]} | ${false}
${p(samples, 'ignored/keepme.md')} | ${[p(samples, 'ignored')]} | ${false}
${p(samples, 'ignored/file.txt')} | ${[p(samples, 'ignored')]} | ${false}
${p(pkg, 'node_modules/bin')} | ${[p(samples, 'ignored')]} | ${true}
`('isIgnored $file $roots', async ({ file, roots, expected }) => {
const dir = path.dirname(file);
const gs = new GitIgnore();
const gs = new GitIgnore(roots);
const r = await gs.findGitIgnoreHierarchy(dir);
expect(r.isIgnored(file)).toEqual(expected);
});

// cspell:ignore keepme
test.each`
file | roots | expected
${__filename} | ${undefined} | ${undefined}
${p(samples, 'ignored/keepme.md')} | ${undefined} | ${undefined}
${p(samples, 'ignored/file.txt')} | ${undefined} | ${{ glob: 'ignored/**', matched: true, line: 3, root: samples, gitIgnoreFile: p(samples, '.gitignore') }}
${p(pkg, 'node_modules/bin')} | ${undefined} | ${{ glob: 'node_modules/', matched: true, line: 58, root: gitRoot, gitIgnoreFile: gitIgnoreFile }}
${__filename} | ${[p(samples, 'ignored')]} | ${undefined}
${p(samples, 'ignored/keepme.md')} | ${[p(samples, 'ignored')]} | ${undefined}
${p(samples, 'ignored/file.txt')} | ${[p(samples, 'ignored')]} | ${undefined}
${p(pkg, 'node_modules/bin')} | ${[p(samples, 'ignored')]} | ${{ glob: 'node_modules/', matched: true, line: 58, root: gitRoot, gitIgnoreFile: gitIgnoreFile }}
`('isIgnoredEx $file $roots', async ({ file, roots, expected }) => {
const dir = path.dirname(file);
const gs = new GitIgnore(roots);
const r = await gs.findGitIgnoreHierarchy(dir);
expect(r.isIgnoredEx(file)).toEqual(expected);
});

test('isIgnored files', async () => {
const files = [
__filename,
Expand All @@ -53,6 +78,38 @@ describe('GitIgnoreServer', () => {
expect(r).toEqual([__filename, p(samples, 'ignored/keepme.md')]);
});

test.each`
file | roots | addRoots | expectedBefore | expectedAfter
${__filename} | ${undefined} | ${[p(samples, 'ignored')]} | ${false} | ${false}
${p(samples, 'ignored/keepme.md')} | ${undefined} | ${[p(samples, 'ignored')]} | ${false} | ${false}
${p(samples, 'ignored/file.txt')} | ${undefined} | ${[p(samples, 'ignored')]} | ${true} | ${false}
${p(pkg, 'node_modules/bin')} | ${undefined} | ${[p(samples, 'ignored')]} | ${true} | ${true}
`('addRoots $file $addRoots', async ({ file, roots, addRoots, expectedBefore, expectedAfter }) => {
const gs = new GitIgnore(roots);
const before = await gs.isIgnored(file);
expect(before).toEqual(expectedBefore);
gs.addRoots(addRoots);
const after = await gs.isIgnored(file);
expect(after).toEqual(expectedAfter);
});

test('addRoots only reset cache if a new root is added', async () => {
const dir = p(samples, 'ignored');
const gs = new GitIgnore();
gs.findGitIgnoreHierarchy(dir);
const p0 = gs.peekGitIgnoreHierarchy(dir);
expect(p0).toBeDefined();
gs.addRoots([dir]);
expect(gs.peekGitIgnoreHierarchy(dir)).toBeUndefined();
gs.findGitIgnoreHierarchy(dir);
const p1 = gs.peekGitIgnoreHierarchy(dir);
expect(p1).not.toBe(p0);
gs.addRoots([dir]);
expect(gs.peekGitIgnoreHierarchy(dir)).toBe(p1);
gs.findGitIgnoreHierarchy(dir);
expect(gs.peekGitIgnoreHierarchy(dir)).toBe(p1);
});

function p(dir: string, ...dirs: string[]) {
return path.join(dir, ...dirs);
}
Expand Down
53 changes: 48 additions & 5 deletions packages/cspell-gitignore/src/GitIgnore.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,24 @@
import * as path from 'path';
import { contains } from '.';
import { GitIgnoreHierarchy, loadGitIgnore } from './GitIgnoreFile';
import { GitIgnoreHierarchy, IsIgnoredExResult, loadGitIgnore } from './GitIgnoreFile';

/**
* Class to cache and process `.gitignore` file queries.
*/
export class GitIgnore {
private resolvedGitIgnoreHierarchies = new Map<string, GitIgnoreHierarchy>();
private knownGitIgnoreHierarchies = new Map<string, Promise<GitIgnoreHierarchy>>();
readonly roots: string[];
private _roots: Set<string>;
private _sortedRoots: string[];

/**
* @param roots - (search roots) an optional array of root paths to prevent searching for `.gitignore` files above the root.
* If a file is under multiple roots, the closest root will apply. If a file is not under any root, then
* the search for `.gitignore` will go all the way to the system root of the file.
*/
constructor(roots: string[] = []) {
this.roots = roots.map((a) => path.resolve(a));
this.roots.sort((a, b) => a.length - b.length);
Object.freeze(this.roots);
this._sortedRoots = resolveAndSortRoots(roots);
this._roots = new Set(this._sortedRoots);
}

findResolvedGitIgnoreHierarchy(directory: string): GitIgnoreHierarchy | undefined {
Expand All @@ -35,6 +35,11 @@ export class GitIgnore {
return gh.isIgnored(file);
}

async isIgnoredEx(file: string): Promise<IsIgnoredExResult | undefined> {
const gh = await this.findGitIgnoreHierarchy(path.dirname(file));
return gh.isIgnoredEx(file);
}

async findGitIgnoreHierarchy(directory: string): Promise<GitIgnoreHierarchy> {
const known = this.knownGitIgnoreHierarchies.get(directory);
if (known) {
Expand All @@ -60,6 +65,28 @@ export class GitIgnore {
return result;
}

get roots(): string[] {
return this._sortedRoots;
}

addRoots(roots: string[]): void {
const rootsToAdd = roots.map((p) => path.resolve(p)).filter((r) => !this._roots.has(r));
if (!rootsToAdd.length) return;

rootsToAdd.forEach((r) => this._roots.add(r));
this._sortedRoots = resolveAndSortRoots([...this._roots]);
this.cleanCachedEntries();
}

peekGitIgnoreHierarchy(directory: string): Promise<GitIgnoreHierarchy> | undefined {
return this.knownGitIgnoreHierarchies.get(directory);
}

private cleanCachedEntries() {
this.knownGitIgnoreHierarchies.clear();
this.resolvedGitIgnoreHierarchies.clear();
}

private async _findGitIgnoreHierarchy(directory: string): Promise<GitIgnoreHierarchy> {
const root = this.determineRoot(directory);
const parent = path.dirname(directory);
Expand All @@ -82,3 +109,19 @@ export class GitIgnore {
return path.parse(directory).root;
}
}

function resolveAndSortRoots(roots: string[]): string[] {
const sortedRoots = roots.map((a) => path.resolve(a));
sortRoots(sortedRoots);
Object.freeze(sortedRoots);
return sortedRoots;
}

/**
* Sorts root paths based upon their length.
* @param roots - array to be sorted
*/
function sortRoots(roots: string[]): string[] {
roots.sort((a, b) => a.length - b.length);
return roots;
}
28 changes: 26 additions & 2 deletions packages/cspell-gitignore/src/GitIgnoreFile.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ import { GitIgnoreFile, GitIgnoreHierarchy, loadGitIgnore, __testing__ } from '.

const { mustBeHierarchical } = __testing__;

const pathPackage = path.resolve(__dirname, '..');
const pathRepo = path.resolve(pathPackage, '../..');
const gitIgnoreFile = path.resolve(pathRepo, '.gitignore');

describe('GitIgnoreFile', () => {
test('GitIgnoreFile', () => {
const gif = sampleGitIgnoreFile();
Expand Down Expand Up @@ -52,8 +56,24 @@ describe('GitIgnoreHierarchy', () => {
).toThrowError('Hierarchy violation - files are not nested');
});

function p(file: string): string {
return path.join(__dirname, file);
test.each`
file | expected
${__filename} | ${{ matched: true, gitIgnoreFile: p('./.gitignore'), line: undefined, glob: '*.test.*', root: __dirname }}
${p('GitIgnoreFiles.ts')} | ${undefined}
${require.resolve('jest')} | ${{ matched: true, gitIgnoreFile, glob: 'node_modules/', line: 58, root: pathRepo }}
${p('package-lock.json')} | ${undefined}
`('ignoreEx $file', async ({ file, expected }) => {
// cspell:ignore gifs
const gifs = [];
const gi = await loadGitIgnore(path.join(__dirname, '../../..'));
if (gi) gifs.push(gi);
gifs.push(sampleGitIgnoreFile());
const gih = new GitIgnoreHierarchy(gifs);
expect(gih.isIgnoredEx(file)).toEqual(expected);
});

function p(...files: string[]): string {
return path.resolve(__dirname, ...files);
}
});

Expand All @@ -74,3 +94,7 @@ function sampleGitIgnoreFile(): GitIgnoreFile {
const file = path.join(m.root, '.gitignore');
return new GitIgnoreFile(m, file);
}

// function oc<T>(v: Partial<T>): T {
// return expect.objectContaining(v);
// }
Loading

0 comments on commit 81d005e

Please sign in to comment.