Add global gitignore support to file exclusion logic
This commit is contained in:
parent
520606ccb9
commit
365b5d2365
|
|
@ -1,11 +1,12 @@
|
||||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||||
import { mkdirSync, rmSync, writeFileSync } from 'fs'
|
import { mkdirSync, rmSync, writeFileSync } from 'fs'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
import { loadGitignore } from './gitignore'
|
import { _resetGlobalCache, loadGitignore } from './gitignore'
|
||||||
|
|
||||||
const TEST_DIR = '/tmp/toes-gitignore-test'
|
const TEST_DIR = '/tmp/toes-gitignore-test'
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
_resetGlobalCache()
|
||||||
mkdirSync(TEST_DIR, { recursive: true })
|
mkdirSync(TEST_DIR, { recursive: true })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
@ -171,4 +172,64 @@ describe('loadGitignore', () => {
|
||||||
expect(checker.shouldExclude('build/output')).toBe(true)
|
expect(checker.shouldExclude('build/output')).toBe(true)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('global gitignore', () => {
|
||||||
|
const GLOBAL_IGNORE = '/tmp/toes-global-gitignore-test'
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mkdirSync(GLOBAL_IGNORE, { recursive: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
rmSync(GLOBAL_IGNORE, { recursive: true, force: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should include patterns from global gitignore', () => {
|
||||||
|
const globalIgnorePath = join(GLOBAL_IGNORE, 'ignore')
|
||||||
|
writeFileSync(globalIgnorePath, '*.bak\n.idea/')
|
||||||
|
|
||||||
|
const origSpawnSync = require('child_process').spawnSync
|
||||||
|
const spawnSyncMock = mock((cmd: string, args: string[], opts: any) => {
|
||||||
|
if (cmd === 'git' && args.includes('core.excludesFile')) {
|
||||||
|
return { status: 0, stdout: globalIgnorePath + '\n', stderr: '' }
|
||||||
|
}
|
||||||
|
return origSpawnSync(cmd, args, opts)
|
||||||
|
})
|
||||||
|
mock.module('child_process', () => ({ spawnSync: spawnSyncMock }))
|
||||||
|
_resetGlobalCache()
|
||||||
|
|
||||||
|
const checker = loadGitignore(TEST_DIR)
|
||||||
|
|
||||||
|
expect(checker.shouldExclude('backup.bak')).toBe(true)
|
||||||
|
expect(checker.shouldExclude('src/old.bak')).toBe(true)
|
||||||
|
expect(checker.shouldExclude('.idea')).toBe(true)
|
||||||
|
expect(checker.shouldExclude('src/index.ts')).toBe(false)
|
||||||
|
|
||||||
|
mock.module('child_process', () => ({ spawnSync: origSpawnSync }))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('should merge global and local patterns', () => {
|
||||||
|
const globalIgnorePath = join(GLOBAL_IGNORE, 'ignore')
|
||||||
|
writeFileSync(globalIgnorePath, '*.bak')
|
||||||
|
writeFileSync(join(TEST_DIR, '.gitignore'), '*.log')
|
||||||
|
|
||||||
|
const origSpawnSync = require('child_process').spawnSync
|
||||||
|
const spawnSyncMock = mock((cmd: string, args: string[], opts: any) => {
|
||||||
|
if (cmd === 'git' && args.includes('core.excludesFile')) {
|
||||||
|
return { status: 0, stdout: globalIgnorePath + '\n', stderr: '' }
|
||||||
|
}
|
||||||
|
return origSpawnSync(cmd, args, opts)
|
||||||
|
})
|
||||||
|
mock.module('child_process', () => ({ spawnSync: spawnSyncMock }))
|
||||||
|
_resetGlobalCache()
|
||||||
|
|
||||||
|
const checker = loadGitignore(TEST_DIR)
|
||||||
|
|
||||||
|
expect(checker.shouldExclude('backup.bak')).toBe(true)
|
||||||
|
expect(checker.shouldExclude('debug.log')).toBe(true)
|
||||||
|
expect(checker.shouldExclude('src/index.ts')).toBe(false)
|
||||||
|
|
||||||
|
mock.module('child_process', () => ({ spawnSync: origSpawnSync }))
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
|
import { spawnSync } from 'child_process'
|
||||||
import { existsSync, readFileSync } from 'fs'
|
import { existsSync, readFileSync } from 'fs'
|
||||||
|
import { homedir } from 'os'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
|
||||||
const ALWAYS_EXCLUDE = [
|
const ALWAYS_EXCLUDE = [
|
||||||
|
|
@ -24,15 +26,46 @@ export interface GitignoreChecker {
|
||||||
|
|
||||||
export function loadGitignore(appPath: string): GitignoreChecker {
|
export function loadGitignore(appPath: string): GitignoreChecker {
|
||||||
const gitignorePath = join(appPath, '.gitignore')
|
const gitignorePath = join(appPath, '.gitignore')
|
||||||
|
const globalPatterns = loadGlobalPatterns()
|
||||||
const patterns = existsSync(gitignorePath)
|
const patterns = existsSync(gitignorePath)
|
||||||
? [...ALWAYS_EXCLUDE, ...parseGitignore(readFileSync(gitignorePath, 'utf-8'))]
|
? [...ALWAYS_EXCLUDE, ...globalPatterns, ...parseGitignore(readFileSync(gitignorePath, 'utf-8'))]
|
||||||
: DEFAULT_PATTERNS
|
: [...DEFAULT_PATTERNS, ...globalPatterns]
|
||||||
|
|
||||||
return {
|
return {
|
||||||
shouldExclude: (path: string) => matchesPattern(path, patterns),
|
shouldExclude: (path: string) => matchesPattern(path, patterns),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const _resetGlobalCache = () => {
|
||||||
|
cachedGlobalPatterns = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachedGlobalPatterns: string[] | undefined
|
||||||
|
|
||||||
|
const expandHome = (filepath: string) =>
|
||||||
|
filepath.startsWith('~/') ? join(homedir(), filepath.slice(2)) : filepath
|
||||||
|
|
||||||
|
function getGlobalExcludesPath(): string | null {
|
||||||
|
const result = spawnSync('git', ['config', '--global', 'core.excludesFile'], {
|
||||||
|
encoding: 'utf-8',
|
||||||
|
})
|
||||||
|
if (result.status === 0 && result.stdout.trim()) {
|
||||||
|
return expandHome(result.stdout.trim())
|
||||||
|
}
|
||||||
|
const xdgHome = process.env.XDG_CONFIG_HOME || join(homedir(), '.config')
|
||||||
|
const defaultPath = join(xdgHome, 'git', 'ignore')
|
||||||
|
if (existsSync(defaultPath)) return defaultPath
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadGlobalPatterns(): string[] {
|
||||||
|
if (cachedGlobalPatterns !== undefined) return cachedGlobalPatterns
|
||||||
|
const path = getGlobalExcludesPath()
|
||||||
|
cachedGlobalPatterns =
|
||||||
|
path && existsSync(path) ? parseGitignore(readFileSync(path, 'utf-8')) : []
|
||||||
|
return cachedGlobalPatterns
|
||||||
|
}
|
||||||
|
|
||||||
function parseGitignore(content: string): string[] {
|
function parseGitignore(content: string): string[] {
|
||||||
return content
|
return content
|
||||||
.split('\n')
|
.split('\n')
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user