Skip to content

Commit 53bb228

Browse files
committed
fix: .gitignore handling with hierarchical loading
Load .gitignore files from all directories in the path hierarchy instead of just the repo root. This properly respects Git's pattern override behavior where deeper patterns take precedence over parent patterns. The gitignore rules are now dynamically reloaded as users navigate between directories to ensure accurate filtering.
1 parent 3590d2c commit 53bb228

File tree

1 file changed

+38
-9
lines changed

1 file changed

+38
-9
lines changed

src/tokdu/tokdu.py

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,14 +26,41 @@ def find_git_root(starting_dir):
2626
current = os.path.dirname(current)
2727
return starting_dir
2828

29-
def load_gitignore(git_root):
30-
gitignore_path = os.path.join(git_root, '.gitignore')
31-
if os.path.exists(gitignore_path):
32-
with open(gitignore_path, 'r', encoding='utf-8', errors='ignore') as f:
33-
patterns = f.readlines()
34-
spec = pathspec.PathSpec.from_lines('gitwildmatch', patterns)
35-
else:
36-
spec = pathspec.PathSpec.from_lines('gitwildmatch', [])
29+
def load_gitignore(git_root, current_dir):
30+
"""
31+
Load .gitignore files from git_root up to and including current_dir.
32+
Patterns in deeper directories override those in parent directories,
33+
matching Git's actual behavior.
34+
"""
35+
git_root = os.path.abspath(git_root)
36+
current_dir = os.path.abspath(current_dir)
37+
38+
# Collect all directories from git_root to current_dir
39+
directories = []
40+
path = current_dir
41+
while os.path.commonpath([path, git_root]) == git_root:
42+
directories.append(path)
43+
if path == git_root:
44+
break
45+
path = os.path.dirname(path)
46+
47+
# Process directories from root to current (so deeper patterns override parent patterns)
48+
directories.reverse()
49+
50+
# Collect all patterns
51+
all_patterns = []
52+
for directory in directories:
53+
gitignore_path = os.path.join(directory, '.gitignore')
54+
if os.path.exists(gitignore_path):
55+
try:
56+
with open(gitignore_path, 'r', encoding='utf-8', errors='ignore') as f:
57+
patterns = f.readlines()
58+
all_patterns.extend(patterns)
59+
except Exception:
60+
pass # Skip if we can't read the file
61+
62+
# Create spec with all collected patterns
63+
spec = pathspec.PathSpec.from_lines('gitwildmatch', all_patterns)
3764
return spec
3865

3966
def is_binary(filepath):
@@ -231,7 +258,6 @@ def tui(stdscr, start_path, encoding_name, model_name):
231258

232259
encoder = get_encoder(encoding_name, model_name)
233260
repo_root = find_git_root(start_path)
234-
git_spec = load_gitignore(repo_root)
235261

236262
# Save the absolute starting directory.
237263
root_dir = os.path.abspath(start_path)
@@ -245,6 +271,9 @@ def tui(stdscr, start_path, encoding_name, model_name):
245271
root_message = "" # Message to display when at root boundary
246272

247273
while True:
274+
# Load or reload gitignore for the current directory
275+
git_spec = load_gitignore(repo_root, current_path)
276+
248277
items = cached_scan_directory(current_path, encoder, git_spec, repo_root)
249278
scanning = items is None
250279

0 commit comments

Comments
 (0)