Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(filter): Honor .gitignore files in subdirectories #35

Merged
merged 1 commit into from
Aug 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/utils/.repopackignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#*.ts
26 changes: 10 additions & 16 deletions src/utils/filterUtils.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
import path from 'path';
import { globby } from 'globby';
import { logger } from './logger.js';
import type { RepopackConfigMerged } from '../config/configTypes.js';
import * as fs from 'node:fs/promises';
import { defaultIgnoreList } from './defaultIgnore.js';

export async function filterFiles(rootDir: string, config: RepopackConfigMerged): Promise<string[]> {
const includePatterns = config.include.length > 0 ? config.include : ['**/*'];

const ignorePatterns = await getIgnorePatterns(config);
const ignoreFilePaths = await getIgnoreFilePaths(rootDir, config);
const ignoreFilePatterns = await getIgnoreFilePatterns(rootDir, config);

logger.trace('Include patterns:', includePatterns);
logger.trace('Ignore patterns:', ignorePatterns);
logger.trace('Ignore files:', ignoreFilePaths);
logger.trace('Ignore file patterns: ', ignoreFilePatterns);

try {
const filePaths = await globby(includePatterns, {
cwd: rootDir,
ignore: ignorePatterns,
ignoreFiles: ignoreFilePaths,
ignoreFiles: ignoreFilePatterns,
// result options
onlyFiles: true,
absolute: false,
Expand All @@ -28,6 +26,8 @@ export async function filterFiles(rootDir: string, config: RepopackConfigMerged)
followSymbolicLinks: false,
});

// HACK: globby is not filtering sub directories correctly.

logger.trace(`Filtered ${filePaths.length} files`);
return filePaths;
} catch (error) {
Expand All @@ -43,22 +43,16 @@ export function parseIgnoreContent(content: string): string[] {
.filter((line) => line && !line.startsWith('#'));
}

export async function getIgnoreFilePaths(rootDir: string, config: RepopackConfigMerged): Promise<string[]> {
let ignoreFilePaths: string[] = [];
export async function getIgnoreFilePatterns(rootDir: string, config: RepopackConfigMerged): Promise<string[]> {
let ignoreFilePatterns: string[] = [];

if (config.ignore.useGitignore) {
ignoreFilePaths.push(path.join(rootDir, '.gitignore'));
ignoreFilePatterns.push('**/.gitignore');
}

const existsRepopackIgnore = await fs
.access(path.join(rootDir, '.repopackignore'))
.then(() => true)
.catch(() => false);
if (existsRepopackIgnore) {
ignoreFilePaths.push(path.join(rootDir, '.repopackignore'));
}
ignoreFilePatterns.push('**/.repopackignore');

return ignoreFilePaths;
return ignoreFilePatterns;
}

export async function getIgnorePatterns(config: RepopackConfigMerged): Promise<string[]> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ignored-data.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dummy data
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dummy data
13 changes: 13 additions & 0 deletions tests/fixtures/packager/outputs/simple-project-output.txt
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ This repository is simple-project
================================================================
Repository Structure
================================================================
resources/
.repopackignore
data.txt
src/
index.js
utils.js
Expand Down Expand Up @@ -113,6 +116,16 @@ File: repopack.config.json
}
}

================
File: resources/.repopackignore
================
ignored-data.txt

================
File: resources/data.txt
================
dummy data

================
File: src/index.js
================
Expand Down
130 changes: 90 additions & 40 deletions tests/utils/filterUtils.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { expect, test, vi, describe, beforeEach } from 'vitest';
import { getIgnorePatterns, parseIgnoreContent, getIgnoreFilePaths, filterFiles } from '../../src/utils/filterUtils.js';
import {
getIgnorePatterns,
parseIgnoreContent,
getIgnoreFilePatterns,
filterFiles,
} from '../../src/utils/filterUtils.js';
import path from 'path';
import * as fs from 'fs/promises';
import { createMockConfig } from '../testing/testUtils.js';
import { createMockConfig, isWindows } from '../testing/testUtils.js';
import { globby } from 'globby';
import { minimatch } from 'minimatch';

vi.mock('fs/promises');
vi.mock('globby');
Expand All @@ -15,56 +21,29 @@ describe('filterUtils', () => {

describe('getIgnoreFilePaths', () => {
test('should return correct paths when .gitignore and .repopackignore exist', async () => {
vi.mocked(fs.access).mockResolvedValue(undefined);
const mockConfig = createMockConfig({
ignore: {
useGitignore: true,
useDefaultPatterns: true,
customPatterns: [],
},
});

vi.mocked(fs.access).mockResolvedValue(undefined);

const paths = await getIgnoreFilePaths('/mock/root', mockConfig);

expect(paths).toEqual([path.join('/mock/root', '.gitignore'), path.join('/mock/root', '.repopackignore')]);
const filePatterns = await getIgnoreFilePatterns('/mock/root', mockConfig);
expect(filePatterns).toEqual(['**/.gitignore', '**/.repopackignore']);
});

test('should not include .gitignore when useGitignore is false', async () => {
const mockConfig = createMockConfig({
ignore: {
useGitignore: false,
useDefaultPatterns: true,
customPatterns: [],
},
});

vi.mocked(fs.access).mockResolvedValue(undefined);

const paths = await getIgnoreFilePaths('/mock/root', mockConfig);

expect(paths).toEqual([path.join('/mock/root', '.repopackignore')]);
});

test('should handle missing .repopackignore', async () => {
const mockConfig = createMockConfig({
ignore: {
useGitignore: true,
useGitignore: false,
useDefaultPatterns: true,
customPatterns: [],
},
});

vi.mocked(fs.access).mockImplementation((path) => {
if (path.toString().includes('.repopackignore')) {
return Promise.reject(new Error('File not found'));
}
return Promise.resolve(undefined);
});

const paths = await getIgnoreFilePaths('/mock/root', mockConfig);

expect(paths).toEqual([path.join('/mock/root', '.gitignore')]);
const filePatterns = await getIgnoreFilePatterns('/mock/root', mockConfig);
expect(filePatterns).toEqual(['**/.repopackignore']);
});
});

Expand Down Expand Up @@ -140,12 +119,16 @@ node_modules
});

describe('filterFiles', () => {
beforeEach(() => {
vi.resetAllMocks();
});

test('should call globby with correct parameters', async () => {
const mockConfig = createMockConfig({
include: ['**/*.js'],
ignore: {
useGitignore: true,
useDefaultPatterns: true,
useDefaultPatterns: false,
customPatterns: ['*.custom'],
},
});
Expand All @@ -160,16 +143,83 @@ node_modules
expect.objectContaining({
cwd: '/mock/root',
ignore: expect.arrayContaining(['*.custom']),
ignoreFiles: expect.arrayContaining([
path.join('/mock/root', '.gitignore'),
path.join('/mock/root', '.repopackignore'),
]),
ignoreFiles: expect.arrayContaining(['**/.gitignore', '**/.repopackignore']),
onlyFiles: true,
absolute: false,
dot: true,
followSymbolicLinks: false,
}),
);
});

test.runIf(!isWindows)('Honor .gitignore files in subdirectories', async () => {
const mockConfig = createMockConfig({
include: ['**/*.js'],
ignore: {
useGitignore: true,
useDefaultPatterns: false,
customPatterns: [],
},
});

const mockFileStructure = [
'root/file1.js',
'root/subdir/file2.js',
'root/subdir/ignored.js',
'root/another/file3.js',
];

const mockGitignoreContent = {
'/mock/root/.gitignore': '*.log',
'/mock/root/subdir/.gitignore': 'ignored.js',
};

vi.mocked(globby).mockImplementation(async () => {
// Simulate filtering files based on .gitignore
return mockFileStructure.filter((file) => {
const relativePath = file.replace('root/', '');
const dir = path.dirname(relativePath);
const gitignorePath = path.join('/mock/root', dir, '.gitignore');
const gitignoreContent = mockGitignoreContent[gitignorePath as keyof typeof mockGitignoreContent];
if (gitignoreContent && minimatch(path.basename(file), gitignoreContent)) {
return false;
}
return true;
});
});

vi.mocked(fs.readFile).mockImplementation(async (filePath) => {
return mockGitignoreContent[filePath as keyof typeof mockGitignoreContent] || '';
});

const result = await filterFiles('/mock/root', mockConfig);
expect(result).toEqual(['root/file1.js', 'root/subdir/file2.js', 'root/another/file3.js']);
expect(result).not.toContain('root/subdir/ignored.js');
});

test('should not apply .gitignore when useGitignore is false', async () => {
const mockConfig = createMockConfig({
include: ['**/*.js'],
ignore: {
useGitignore: false,
useDefaultPatterns: false,
customPatterns: [],
},
});

const mockFileStructure = [
'root/file1.js',
'root/subdir/file2.js',
'root/subdir/ignored.js',
'root/another/file3.js',
];

vi.mocked(globby).mockResolvedValue(mockFileStructure);

const result = await filterFiles('/mock/root', mockConfig);

expect(result).toEqual(mockFileStructure);
expect(result).toContain('root/subdir/ignored.js');
});
});
});
Loading