Skip to content

Commit

Permalink
feat: support gitignores out of cwd with @eslint/compat (#15)
Browse files Browse the repository at this point in the history
  • Loading branch information
Dimava committed Aug 29, 2024
1 parent a034c15 commit f09cf89
Show file tree
Hide file tree
Showing 10 changed files with 197 additions and 70 deletions.
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,9 @@ logs
node_modules
temp
!temp/.gitkeep

test/.out
!test/.in
test/dir/file1
!test/dir/file2
**/test/dir/file3
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@
"prepare": "simple-git-hooks"
},
"dependencies": {
"find-up-simple": "^1.0.0",
"parse-gitignore": "^2.0.0"
"@eslint/compat": "^1.1.1",
"find-up-simple": "^1.0.0"
},
"devDependencies": {
"@antfu/eslint-config": "^2.22.3",
Expand Down
12 changes: 9 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

76 changes: 65 additions & 11 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import fs from 'node:fs'
import process from 'node:process'
import path from 'node:path'
import { findUpSync } from 'find-up-simple'

// @ts-expect-error missing types
import parse from 'parse-gitignore'
import { convertIgnorePatternToMinimatch } from '@eslint/compat'

export interface FlatGitignoreOptions {
/**
Expand All @@ -12,10 +12,12 @@ export interface FlatGitignoreOptions {
name?: string
/**
* Path to `.gitignore` files, or files with compatible formats like `.eslintignore`.
* @default ['.gitignore'] // or findUpSync('.gitignore')
*/
files?: string | string[]
/**
* Throw an error if gitignore file not found.
* @default true
*/
strict?: boolean
/**
Expand All @@ -26,6 +28,13 @@ export interface FlatGitignoreOptions {
* @default false
*/
root?: boolean

/**
* Current working directory.
* Used to resolve relative paths.
* @default process.cwd()
*/
cwd?: string
}

export interface FlatConfigItem {
Expand All @@ -42,6 +51,7 @@ export default function ignore(options: FlatGitignoreOptions = {}): FlatConfigIt
root = false,
files: _files = root ? GITIGNORE : findUpSync(GITIGNORE) || [],
strict = true,
cwd = process.cwd(),
} = options

const files = Array.isArray(_files) ? _files : [_files]
Expand All @@ -56,14 +66,14 @@ export default function ignore(options: FlatGitignoreOptions = {}): FlatConfigIt
throw error
continue
}
const parsed = parse(`${content}\n`)
const globs = parsed.globs()
for (const glob of globs) {
if (glob.type === 'ignore')
ignores.push(...glob.patterns)
else if (glob.type === 'unignore')
ignores.push(...glob.patterns.map((pattern: string) => `!${pattern}`))
}
const relativePath = path.relative(cwd, path.dirname(file)).replaceAll('\\', '/')
const globs = content.split(/\r?\n/u)
.filter(line => line && !line.startsWith('#'))
.map(line => convertIgnorePatternToMinimatch(line))
.map(glob => relativyMinimatch(glob, relativePath, cwd))
.filter(glob => glob !== null)

ignores.push(...globs)
}

if (strict && files.length === 0)
Expand All @@ -75,3 +85,47 @@ export default function ignore(options: FlatGitignoreOptions = {}): FlatConfigIt
ignores,
}
}

function relativyMinimatch(pattern: string, relativePath: string, cwd: string) {
// if gitignore is in the current directory leave it as is
if (['', '.', '/'].includes(relativePath))
return pattern

const negated = pattern.startsWith('!') ? '!' : ''
let cleanPattern = negated ? pattern.slice(1) : pattern

if (!relativePath.endsWith('/'))
relativePath = `${relativePath}/`

const isParent = relativePath.startsWith('..')
// child directories need to just add path in start
if (!isParent)
return `${negated}${relativePath}${cleanPattern}`

// uncle directories don't make sence
if (!relativePath.match(/^(\.\.\/)+$/))
throw new Error('The ignore file location should be either a parent or child directory')

// if it has ** depth it may be left as is
if (cleanPattern.startsWith('**'))
return pattern

// if glob doesn't match the parent dirs it should be ignored
const parents = path.relative(path.resolve(cwd, relativePath), cwd).split(/[/\\]/)

while (parents.length && cleanPattern.startsWith(`${parents[0]}/`)) {
cleanPattern = cleanPattern.slice(parents[0].length + 1)
parents.shift()
}

// if it has ** depth it may be left as is
if (cleanPattern.startsWith('**'))
return `${negated}${cleanPattern}`

// if all parents are out, it's clean
if (parents.length === 0)
return `${negated}${cleanPattern}`

// otherwise it doesn't matches the current folder
return null
}
39 changes: 16 additions & 23 deletions test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,23 @@ describe('should execute tests in root folder', () => {
.toMatchInlineSnapshot(`
{
"ignores": [
".cache",
"**/.cache/**",
".DS_Store",
"**/.DS_Store/**",
".idea",
"**/.idea/**",
"*.log",
"**/*.log/**",
"*.tgz",
"**/*.tgz/**",
"coverage",
"**/coverage/**",
"dist",
"**/dist/**",
"lib-cov",
"**/lib-cov/**",
"logs",
"**/logs/**",
"node_modules",
"**/node_modules/**",
"temp",
"**/temp/**",
"**/.cache",
"**/.DS_Store",
"**/.idea",
"**/*.log",
"**/*.tgz",
"**/coverage",
"**/dist",
"**/lib-cov",
"**/logs",
"**/node_modules",
"**/temp",
"!temp/.gitkeep",
"!temp/.gitkeep/**",
"test/.out",
"!test/.in",
"test/dir/file1",
"!test/dir/file2",
"**/test/dir/file3",
],
}
`)
Expand Down
31 changes: 31 additions & 0 deletions test/parent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { describe, expect, it } from 'vitest'
import ignore from '../src/index'

describe('should execute tests in subfolder', () => {
process.chdir('test')
it('should properly work with parent dirs', () => {
expect(ignore({ root: false }))
.toMatchInlineSnapshot(`
{
"ignores": [
"**/.cache",
"**/.DS_Store",
"**/.idea",
"**/*.log",
"**/*.tgz",
"**/coverage",
"**/dist",
"**/lib-cov",
"**/logs",
"**/node_modules",
"**/temp",
".out",
"!.in",
"dir/file1",
"!dir/file2",
"**/test/dir/file3",
],
}
`)
})
})
52 changes: 46 additions & 6 deletions test/workspace-with-gitignore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@ describe('should execute tests in test/workspace-with-gitignore', () => {
.toMatchInlineSnapshot(`
{
"ignores": [
"gitignoretest",
"**/gitignoretest/**",
"rootfile",
"rootdir/",
"**/rootpath",
"rootfolder/file",
"rootfolder/dir/",
"rootfolder/path",
],
}
`)
Expand All @@ -35,8 +39,12 @@ describe('should execute tests in test/workspace-with-gitignore', () => {
.toMatchInlineSnapshot(`
{
"ignores": [
"gitignoretest",
"**/gitignoretest/**",
"rootfile",
"rootdir/",
"**/rootpath",
"rootfolder/file",
"rootfolder/dir/",
"rootfolder/path",
],
}
`)
Expand All @@ -47,8 +55,40 @@ describe('should execute tests in test/workspace-with-gitignore', () => {
.toMatchInlineSnapshot(`
{
"ignores": [
"gitignoretest",
"**/gitignoretest/**",
"rootfile",
"rootdir/",
"**/rootpath",
"rootfolder/file",
"rootfolder/dir/",
"rootfolder/path",
],
}
`)
})

it('should work properly with nested gitignore', () => {
/**
* https://git-scm.com/docs/gitignore#_pattern_format
* If there is a separator at the beginning or middle (or both) of the pattern,
* then the pattern is relative to the directory level of the particular .gitignore file itself.
* Otherwise the pattern may also match at any level below the .gitignore level.
*
* If there is a separator at the end of the pattern then the pattern will only match directories,
* otherwise the pattern can match both files and directories.
*/
expect(ignore({ files: ['.gitignore', 'folder/.gitignore'] }))
.toMatchInlineSnapshot(`
{
"ignores": [
"rootfile",
"rootdir/",
"**/rootpath",
"rootfolder/file",
"rootfolder/dir/",
"rootfolder/path",
"folder/file",
"folder/dir/",
"folder/**/path",
],
}
`)
Expand Down
8 changes: 7 additions & 1 deletion test/workspace-with-gitignore/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
gitignoretest
/rootfile
/rootdir/
rootpath

/rootfolder/file
/rootfolder/dir/
/rootfolder/path
3 changes: 3 additions & 0 deletions test/workspace-with-gitignore/folder/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/file
/dir/
path
36 changes: 12 additions & 24 deletions test/workspace-without-gitignore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,18 @@ describe('should execute tests in test/workspace-without-gitignore', () => {
.toMatchInlineSnapshot(`
{
"ignores": [
".cache",
"**/.cache/**",
".DS_Store",
"**/.DS_Store/**",
".idea",
"**/.idea/**",
"*.log",
"**/*.log/**",
"*.tgz",
"**/*.tgz/**",
"coverage",
"**/coverage/**",
"dist",
"**/dist/**",
"lib-cov",
"**/lib-cov/**",
"logs",
"**/logs/**",
"node_modules",
"**/node_modules/**",
"temp",
"**/temp/**",
"!temp/.gitkeep",
"!temp/.gitkeep/**",
"**/.cache",
"**/.DS_Store",
"**/.idea",
"**/*.log",
"**/*.tgz",
"**/coverage",
"**/dist",
"**/lib-cov",
"**/logs",
"**/node_modules",
"**/temp",
"**/test/dir/file3",
],
}
`)
Expand Down

0 comments on commit f09cf89

Please sign in to comment.