Skip to content

Commit 6fb9518

Browse files
feat: implements ignore feature (#117)
Co-authored-by: seokju-na <seokju.me@gmail.com>
1 parent e3b19db commit 6fb9518

File tree

4 files changed

+302
-0
lines changed

4 files changed

+302
-0
lines changed

index.d.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3997,6 +3997,84 @@ export declare class Repository {
39973997
* staged deletes, tracked files, etc.
39983998
*/
39993999
diffTreeToWorkdirWithIndex(oldTree?: Tree | undefined | null, options?: DiffOptions | undefined | null): Diff
4000+
/**
4001+
* Add ignore rules for a repository.
4002+
*
4003+
* This adds ignore rules to the repository. The rules will be used
4004+
* in addition to any existing ignore rules (such as .gitignore files).
4005+
*
4006+
* @category Repository/Methods
4007+
* @signature
4008+
* ```ts
4009+
* class Repository {
4010+
* addIgnoreRule(rules: string): boolean;
4011+
* }
4012+
* ```
4013+
*
4014+
* @param {string} rules - Rules to add, separated by newlines.
4015+
*
4016+
* @example
4017+
* ```ts
4018+
* import { openRepository } from 'es-git';
4019+
*
4020+
* const repo = await openRepository('./path/to/repo');
4021+
* repo.addIgnoreRule("node_modules/");
4022+
* ```
4023+
*/
4024+
addIgnoreRule(rules: string): void
4025+
/**
4026+
* Clear ignore rules that were explicitly added.
4027+
*
4028+
* Resets to the default internal ignore rules.
4029+
* This will not turn off rules in .gitignore files that actually exist in the filesystem.
4030+
* The default internal ignores ignore ".", ".." and ".git" entries.
4031+
*
4032+
* @category Repository/Methods
4033+
* @signature
4034+
* ```ts
4035+
* class Repository {
4036+
* clearIgnoreRules(): void;
4037+
* }
4038+
* ```
4039+
*
4040+
* @example
4041+
* ```ts
4042+
* import { openRepository } from 'es-git';
4043+
*
4044+
* const repo = await openRepository('./path/to/repo');
4045+
* repo.addIgnoreRule("*.log");
4046+
* // Later, clear all added rules
4047+
* repo.clearIgnoreRules();
4048+
* ```
4049+
*/
4050+
clearIgnoreRules(): void
4051+
/**
4052+
* Test if the ignore rules apply to a given path.
4053+
*
4054+
* This function checks the ignore rules to see if they would apply to the given file.
4055+
* This indicates if the file would be ignored regardless of whether the file is already in the index or committed to the repository.
4056+
*
4057+
* @category Repository/Methods
4058+
* @signature
4059+
* ```ts
4060+
* class Repository {
4061+
* isPathIgnored(path: string): boolean;
4062+
* }
4063+
* ```
4064+
*
4065+
* @param {string} path - The path to check.
4066+
* @returns {boolean} - True if the path is ignored, false otherwise.
4067+
*
4068+
* @example
4069+
* ```ts
4070+
* import { openRepository } from 'es-git';
4071+
*
4072+
* const repo = await openRepository('./path/to/repo');
4073+
* const isIgnored = repo.isPathIgnored("node_modules/some-package");
4074+
* console.log(`Path is ${isIgnored ? "ignored" : "not ignored"}`);
4075+
* ```
4076+
*/
4077+
isPathIgnored(path: string): boolean
40004078
/**
40014079
* Get the Index file for this repository.
40024080
*

src/ignore.rs

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
use crate::repository::Repository;
2+
use napi_derive::napi;
3+
use std::path::Path;
4+
5+
#[napi]
6+
impl Repository {
7+
#[napi]
8+
/// Add ignore rules for a repository.
9+
///
10+
/// This adds ignore rules to the repository. The rules will be used
11+
/// in addition to any existing ignore rules (such as .gitignore files).
12+
///
13+
/// @category Repository/Methods
14+
/// @signature
15+
/// ```ts
16+
/// class Repository {
17+
/// addIgnoreRule(rules: string): void;
18+
/// }
19+
/// ```
20+
///
21+
/// @param {string} rules - Rules to add, separated by newlines.
22+
///
23+
/// @example
24+
/// ```ts
25+
/// import { openRepository } from 'es-git';
26+
///
27+
/// const repo = await openRepository('./path/to/repo');
28+
/// repo.addIgnoreRule("node_modules/");
29+
/// ```
30+
pub fn add_ignore_rule(&self, rules: String) -> crate::Result<()> {
31+
self.inner.add_ignore_rule(&rules)?;
32+
Ok(())
33+
}
34+
35+
#[napi]
36+
/// Clear ignore rules that were explicitly added.
37+
///
38+
/// Resets to the default internal ignore rules.
39+
/// This will not turn off rules in .gitignore files that actually exist in the filesystem.
40+
/// The default internal ignores ignore ".", ".." and ".git" entries.
41+
///
42+
/// @category Repository/Methods
43+
/// @signature
44+
/// ```ts
45+
/// class Repository {
46+
/// clearIgnoreRules(): void;
47+
/// }
48+
/// ```
49+
///
50+
/// @example
51+
/// ```ts
52+
/// import { openRepository } from 'es-git';
53+
///
54+
/// const repo = await openRepository('./path/to/repo');
55+
/// repo.addIgnoreRule("*.log");
56+
/// // Later, clear all added rules
57+
/// repo.clearIgnoreRules();
58+
/// ```
59+
pub fn clear_ignore_rules(&self) -> crate::Result<()> {
60+
self.inner.clear_ignore_rules()?;
61+
Ok(())
62+
}
63+
64+
#[napi]
65+
/// Test if the ignore rules apply to a given path.
66+
///
67+
/// This function checks the ignore rules to see if they would apply to the given file.
68+
/// This indicates if the file would be ignored regardless of whether the file is already in the index or committed to the repository.
69+
///
70+
/// @category Repository/Methods
71+
/// @signature
72+
/// ```ts
73+
/// class Repository {
74+
/// isPathIgnored(path: string): boolean;
75+
/// }
76+
/// ```
77+
///
78+
/// @param {string} path - The path to check.
79+
/// @returns {boolean} - True if the path is ignored, false otherwise.
80+
///
81+
/// @example
82+
/// ```ts
83+
/// import { openRepository } from 'es-git';
84+
///
85+
/// const repo = await openRepository('./path/to/repo');
86+
/// const isIgnored = repo.isPathIgnored("node_modules/some-package");
87+
/// console.log(`Path is ${isIgnored ? "ignored" : "not ignored"}`);
88+
/// ```
89+
pub fn is_path_ignored(&self, path: String) -> crate::Result<bool> {
90+
Ok(self.inner.is_path_ignored(Path::new(&path))?)
91+
}
92+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub mod commit;
77
pub mod config;
88
pub mod diff;
99
mod error;
10+
pub mod ignore;
1011
pub mod index;
1112
pub mod object;
1213
pub mod oid;

tests/ignore.spec.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import fs from 'node:fs/promises';
2+
import path from 'node:path';
3+
import { describe, expect, it } from 'vitest';
4+
import { openRepository } from '../index';
5+
import { useFixture } from './fixtures';
6+
7+
describe('ignore', () => {
8+
it('should handle basic ignore rules', async () => {
9+
const p = await useFixture('empty');
10+
const repo = await openRepository(p);
11+
12+
await fs.writeFile(path.join(p, 'server.log'), 'server.log');
13+
await fs.writeFile(path.join(p, 'index.js'), 'index.js');
14+
15+
repo.addIgnoreRule('*.log');
16+
expect(repo.isPathIgnored('server.log')).toBe(true);
17+
expect(repo.isPathIgnored('index.js')).toBe(false);
18+
19+
repo.clearIgnoreRules();
20+
expect(repo.isPathIgnored('server.log')).toBe(false);
21+
expect(repo.isPathIgnored('.git/config')).toBe(true);
22+
});
23+
24+
it('should handle multiple ignore patterns', async () => {
25+
const p = await useFixture('empty');
26+
const repo = await openRepository(p);
27+
28+
await fs.writeFile(path.join(p, 'server.log'), 'server.log');
29+
await fs.writeFile(path.join(p, 'index.js'), 'index.js');
30+
await fs.mkdir(path.join(p, 'node_modules'), { recursive: true });
31+
await fs.mkdir(path.join(p, 'dist'), { recursive: true });
32+
await fs.writeFile(path.join(p, 'node_modules/package.json'), '{}');
33+
await fs.writeFile(path.join(p, 'dist/bundle.js'), 'dist/bundle.js');
34+
35+
repo.addIgnoreRule('*.log\nnode_modules/\ndist/');
36+
37+
expect(repo.isPathIgnored('server.log')).toBe(true);
38+
expect(repo.isPathIgnored('node_modules/package.json')).toBe(true);
39+
expect(repo.isPathIgnored('dist/bundle.js')).toBe(true);
40+
expect(repo.isPathIgnored('index.js')).toBe(false);
41+
});
42+
43+
it('should respect .gitignore files', async () => {
44+
const p = await useFixture('empty');
45+
46+
await fs.mkdir(path.join(p, 'dist'), { recursive: true });
47+
await fs.mkdir(path.join(p, 'src'), { recursive: true });
48+
await fs.writeFile(path.join(p, 'dist/bundle.js'), 'dist/bundle.js');
49+
await fs.writeFile(path.join(p, 'src/main.js'), 'index.js');
50+
await fs.writeFile(path.join(p, '.gitignore'), 'dist/\n*.js\n!src/main.js');
51+
52+
const repo = await openRepository(p);
53+
54+
expect(repo.isPathIgnored('dist/bundle.js')).toBe(true);
55+
expect(repo.isPathIgnored('src/main.js')).toBe(false);
56+
});
57+
58+
it('should combine added rules with .gitignore rules', async () => {
59+
const p = await useFixture('empty');
60+
61+
await fs.mkdir(path.join(p, 'dist'), { recursive: true });
62+
await fs.mkdir(path.join(p, 'logs'), { recursive: true });
63+
await fs.writeFile(path.join(p, 'dist/bundle.js'), 'dist/bundle.js');
64+
await fs.writeFile(path.join(p, 'logs/server.log'), 'logs/server.log');
65+
await fs.writeFile(path.join(p, '.gitignore'), 'dist/');
66+
67+
const repo = await openRepository(p);
68+
69+
repo.addIgnoreRule('logs/');
70+
71+
expect(repo.isPathIgnored('dist/bundle.js')).toBe(true);
72+
expect(repo.isPathIgnored('logs/server.log')).toBe(true);
73+
});
74+
75+
it('should handle invalid inputs gracefully', async () => {
76+
const p = await useFixture('empty');
77+
const repo = await openRepository(p);
78+
await fs.writeFile(path.join(p, 'test.js'), 'test');
79+
80+
repo.addIgnoreRule('');
81+
expect(repo.isPathIgnored('test.js')).toBe(false);
82+
83+
expect(() => repo.isPathIgnored('')).not.toThrow();
84+
expect(() => repo.isPathIgnored('../outside')).not.toThrow();
85+
expect(() => repo.isPathIgnored('/absolute/path')).not.toThrow();
86+
87+
// @ts-expect-error
88+
expect(() => repo.isPathIgnored(null)).toThrow();
89+
// @ts-expect-error
90+
expect(() => repo.isPathIgnored(undefined)).toThrow();
91+
});
92+
93+
it('should handle special patterns correctly', async () => {
94+
const p = await useFixture('empty');
95+
const repo = await openRepository(p);
96+
97+
await fs.mkdir(path.join(p, 'logs'), { recursive: true });
98+
await fs.writeFile(path.join(p, 'logs/error.log'), 'error');
99+
await fs.writeFile(path.join(p, 'logs/important.log'), 'important');
100+
await fs.mkdir(path.join(p, 'a/b/c'), { recursive: true });
101+
await fs.writeFile(path.join(p, 'a/b/c/file.txt'), 'file');
102+
await fs.writeFile(path.join(p, 'test[1].js'), 'test');
103+
104+
repo.addIgnoreRule('*.log\n!logs/important.log\na/**/c/*.txt\ntest[[]*.js');
105+
106+
expect(repo.isPathIgnored('logs/error.log')).toBe(true);
107+
expect(repo.isPathIgnored('logs/important.log')).toBe(false);
108+
expect(repo.isPathIgnored('a/b/c/file.txt')).toBe(true);
109+
expect(repo.isPathIgnored('test[1].js')).toBe(true);
110+
});
111+
112+
it('should correctly handle path prefixes and comments', async () => {
113+
const p = await useFixture('empty');
114+
const repo = await openRepository(p);
115+
116+
await fs.mkdir(path.join(p, 'src/build'), { recursive: true });
117+
await fs.mkdir(path.join(p, 'build'), { recursive: true });
118+
await fs.mkdir(path.join(p, 'logs-2025-04-23'), { recursive: true });
119+
await fs.writeFile(path.join(p, 'src/build/output.js'), 'output');
120+
await fs.writeFile(path.join(p, 'build/output.js'), 'output');
121+
await fs.writeFile(path.join(p, 'logs-2025-04-23/app.log'), 'log');
122+
await fs.writeFile(path.join(p, 'temp.txt'), 'temp');
123+
124+
repo.addIgnoreRule('/build/\nlogs-*/\n# This is a comment\ntemp.txt');
125+
126+
expect(repo.isPathIgnored('build/output.js')).toBe(true);
127+
expect(repo.isPathIgnored('src/build/output.js')).toBe(false);
128+
expect(repo.isPathIgnored('logs-2025-04-23/app.log')).toBe(true);
129+
expect(repo.isPathIgnored('temp.txt')).toBe(true);
130+
});
131+
});

0 commit comments

Comments
 (0)