Improve file exclude integration with context-specific exclude patterns

This commit is contained in:
Carlos de Paula 2025-10-20 10:34:24 -03:00
parent 81c0b230f5
commit 545aad9eea
No known key found for this signature in database
9 changed files with 419 additions and 128 deletions

View file

@ -16,14 +16,18 @@ impl Actor for Ignore {
const NAME: &str = "ignore";
fn act(cx: &mut Ctx, _: Self::Options) -> Result<Data> {
let gitignore_enabled = YAZI.mgr.gitignore_enable;
let override_patterns = &YAZI.mgr.ignore_override;
let gitignores = YAZI.files.gitignores;
// If gitignore is disabled but we have override patterns, apply them
if !gitignore_enabled && !override_patterns.is_empty() {
// Load ignore filter from override patterns only
// Get exclude patterns for the current context
// Use path string for context matching
let cwd_str = cx.cwd().as_path().map(|p| p.display().to_string()).unwrap_or_default();
let exclude_patterns = YAZI.files.excludes_for_context(&cwd_str);
// If gitignores is disabled but we have exclude patterns, apply them
if !gitignores && !exclude_patterns.is_empty() {
// Load ignore filter from exclude patterns only
let ignore_filter = if let Some(path) = cx.cwd().as_path() {
IgnoreFilter::from_patterns(path, override_patterns)
IgnoreFilter::from_patterns(path, &exclude_patterns)
} else {
None
};
@ -48,8 +52,10 @@ impl Actor for Ignore {
// Apply to hovered
if let Some(h) = cx.hovered_folder_mut() {
let hovered_str = h.url.as_path().map(|p| p.display().to_string()).unwrap_or_default();
let hovered_excludes = YAZI.files.excludes_for_context(&hovered_str);
let hovered_filter = if let Some(path) = h.url.as_path() {
IgnoreFilter::from_patterns(path, override_patterns)
IgnoreFilter::from_patterns(path, &hovered_excludes)
} else {
None
};
@ -66,8 +72,8 @@ impl Actor for Ignore {
succ!();
}
// If gitignore is disabled and no override patterns, remove any ignore filter
if !gitignore_enabled {
// If gitignores is disabled and no exclude patterns, remove any ignore filter
if !gitignores {
let hovered = cx.hovered().map(|f| f.urn().to_owned());
let apply = |f: &mut Folder| {
// Always clear the filter, even when loading
@ -104,7 +110,7 @@ impl Actor for Ignore {
// Load ignore filter from the current directory
let ignore_filter = if let Some(path) = cx.cwd().as_path() {
IgnoreFilter::from_dir(path, override_patterns)
IgnoreFilter::from_dir(path, &exclude_patterns, gitignores)
} else {
None
};
@ -135,8 +141,10 @@ impl Actor for Ignore {
// Apply to hovered
if let Some(h) = cx.hovered_folder_mut() {
// Load ignore filter for hovered directory if it's a directory
let hovered_str = h.url.as_path().map(|p| p.display().to_string()).unwrap_or_default();
let hovered_excludes = YAZI.files.excludes_for_context(&hovered_str);
let hovered_filter = if let Some(path) = h.url.as_path() {
IgnoreFilter::from_dir(path, override_patterns)
IgnoreFilter::from_dir(path, &hovered_excludes, gitignores)
} else {
None
};

View file

@ -16,90 +16,128 @@ scrolloff = 5
mouse_events = [ "click", "scroll" ]
title_format = "Yazi: {cwd}"
# Git ignore integration
# gitignore_enable = false
# ignore_override = [
# "*.log", # Hide all .log files
# "tmp/", # Hide tmp directory
# "!target/", # Show target/ even if gitignored (negation)
[files]
# Enable git ignore integration
# gitignores = false
# Context-specific exclude patterns
# Patterns are compiled into glob matchers for efficient matching
# Patterns starting with '!' negate (whitelist) previous matches
# excludes = [
# # SFTP temporary files
# { urn = "*.tmp", in = "sftp://**" },
# # Python cache in search results
# { urn = "/root/**/*.pyc", in = "search://**" },
# # Multiple patterns for /code directory (supports arrays)
# { urn = [".git", ".DS_Store", "__pycache__"], in = "/code/**" },
# # Negation: show target/ even if previously ignored
# { urn = "!target/", in = "/code/**" },
# # Fallback rule for all contexts
# { urn = ".DS_Store", in = "*" },
# ]
[preview]
wrap = "no"
tab_size = 2
max_width = 600
max_height = 900
cache_dir = ""
image_delay = 30
image_filter = "triangle"
image_quality = 75
ueberzug_scale = 1
ueberzug_offset = [ 0, 0, 0, 0 ]
wrap = "no"
tab_size = 2
max_width = 600
max_height = 900
cache_dir = ""
image_delay = 30
image_filter = "triangle"
image_quality = 75
ueberzug_scale = 1
ueberzug_offset = [0, 0, 0, 0]
[opener]
edit = [
{ run = "${EDITOR:-vi} %s", desc = "$EDITOR", for = "unix", block = true },
{ run = "code %s", desc = "code", for = "windows", orphan = true },
{ run = "code -w %s", desc = "code (block)", for = "windows", block = true },
{ run = "${EDITOR:-vi} %s", desc = "$EDITOR", for = "unix", block = true },
{ run = "code %s", desc = "code", for = "windows", orphan = true },
{ run = "code -w %s", desc = "code (block)", for = "windows", block = true },
]
play = [
{ run = "xdg-open %s1", desc = "Play", for = "linux" },
{ run = "open %s", desc = "Play", for = "macos" },
{ run = "xdg-open %s1", desc = "Play", for = "linux" },
{ run = "open %s", desc = "Play", for = "macos" },
{ run = 'start "" %s1', orphan = true, desc = "Play", for = "windows" },
{ run = "termux-open %s1", desc = "Play", for = "android" },
{ run = "termux-open %s1", desc = "Play", for = "android" },
{ run = "mediainfo %s1; echo 'Press enter to exit'; read _", block = true, desc = "Show media info", for = "unix" },
{ run = "mediainfo %s1 & pause", block = true, desc = "Show media info", for = "windows" },
]
open = [
{ run = "xdg-open %s1", desc = "Open", for = "linux" },
{ run = "open %s", desc = "Open", for = "macos" },
{ run = 'start "" %s1', desc = "Open", for = "windows", orphan = true },
{ run = "xdg-open %s1", desc = "Open", for = "linux" },
{ run = "open %s", desc = "Open", for = "macos" },
{ run = 'start "" %s1', desc = "Open", for = "windows", orphan = true },
{ run = "termux-open %s1", desc = "Open", for = "android" },
]
reveal = [
{ run = "xdg-open %d1", desc = "Reveal", for = "linux" },
{ run = "open -R %s1", desc = "Reveal", for = "macos" },
{ run = "xdg-open %d1", desc = "Reveal", for = "linux" },
{ run = "open -R %s1", desc = "Reveal", for = "macos" },
{ run = "explorer /select,%s1", desc = "Reveal", for = "windows", orphan = true },
{ run = "termux-open %d1", desc = "Reveal", for = "android" },
{ run = "termux-open %d1", desc = "Reveal", for = "android" },
{ run = "clear; exiftool %s1; echo 'Press enter to exit'; read _", desc = "Show EXIF", for = "unix", block = true },
]
extract = [
{ run = "ya pub extract --list %s", desc = "Extract here" },
]
extract = [{ run = "ya pub extract --list %s", desc = "Extract here" }]
download = [
{ run = "ya emit download --open %S", desc = "Download and open" },
{ run = "ya emit download %S", desc = "Download" },
{ run = "ya emit download %S", desc = "Download" },
]
[open]
rules = [
# Folder
{ url = "*/", use = [ "edit", "open", "reveal" ] },
{ url = "*/", use = [
"edit",
"open",
"reveal",
] },
# Text
{ mime = "text/*", use = [ "edit", "reveal" ] },
{ mime = "text/*", use = [
"edit",
"reveal",
] },
# Image
{ mime = "image/*", use = [ "open", "reveal" ] },
{ mime = "image/*", use = [
"open",
"reveal",
] },
# Media
{ mime = "{audio,video}/*", use = [ "play", "reveal" ] },
{ mime = "{audio,video}/*", use = [
"play",
"reveal",
] },
# Archive
{ mime = "application/{zip,rar,7z*,tar,gzip,xz,zstd,bzip*,lzma,compress,archive,cpio,arj,xar,ms-cab*}", use = [ "extract", "reveal" ] },
{ mime = "application/{zip,rar,7z*,tar,gzip,xz,zstd,bzip*,lzma,compress,archive,cpio,arj,xar,ms-cab*}", use = [
"extract",
"reveal",
] },
# JSON
{ mime = "application/{json,ndjson}", use = [ "edit", "reveal" ] },
{ mime = "*/javascript", use = [ "edit", "reveal" ] },
{ mime = "application/{json,ndjson}", use = [
"edit",
"reveal",
] },
{ mime = "*/javascript", use = [
"edit",
"reveal",
] },
# Empty file
{ mime = "inode/empty", use = [ "edit", "reveal" ] },
{ mime = "inode/empty", use = [
"edit",
"reveal",
] },
# Virtual file system
{ mime = "vfs/{absent,stale}", use = "download" },
# Fallback
{ url = "*", use = [ "open", "reveal" ] },
{ url = "*", use = [
"open",
"reveal",
] },
]
[tasks]
micro_workers = 10
macro_workers = 10
bizarre_retry = 3
image_alloc = 536870912 # 512MB
image_bound = [ 10000, 10000 ]
micro_workers = 10
macro_workers = 10
bizarre_retry = 3
image_alloc = 536870912 # 512MB
image_bound = [10000, 10000]
suppress_preload = false
[plugin]
@ -176,70 +214,70 @@ previewers = [
cursor_blink = false
# cd
cd_title = "Change directory:"
cd_title = "Change directory:"
cd_origin = "top-center"
cd_offset = [ 0, 2, 50, 3 ]
cd_offset = [0, 2, 50, 3]
# create
create_title = [ "Create:", "Create (dir):" ]
create_title = ["Create:", "Create (dir):"]
create_origin = "top-center"
create_offset = [ 0, 2, 50, 3 ]
create_offset = [0, 2, 50, 3]
# rename
rename_title = "Rename:"
rename_title = "Rename:"
rename_origin = "hovered"
rename_offset = [ 0, 1, 50, 3 ]
rename_offset = [0, 1, 50, 3]
# filter
filter_title = "Filter:"
filter_title = "Filter:"
filter_origin = "top-center"
filter_offset = [ 0, 2, 50, 3 ]
filter_offset = [0, 2, 50, 3]
# find
find_title = [ "Find next:", "Find previous:" ]
find_title = ["Find next:", "Find previous:"]
find_origin = "top-center"
find_offset = [ 0, 2, 50, 3 ]
find_offset = [0, 2, 50, 3]
# search
search_title = "Search via {n}:"
search_title = "Search via {n}:"
search_origin = "top-center"
search_offset = [ 0, 2, 50, 3 ]
search_offset = [0, 2, 50, 3]
# shell
shell_title = [ "Shell:", "Shell (block):" ]
shell_title = ["Shell:", "Shell (block):"]
shell_origin = "top-center"
shell_offset = [ 0, 2, 50, 3 ]
shell_offset = [0, 2, 50, 3]
[confirm]
# trash
trash_title = "Trash {n} selected file{s}?"
trash_origin = "center"
trash_offset = [ 0, 0, 70, 20 ]
trash_title = "Trash {n} selected file{s}?"
trash_origin = "center"
trash_offset = [0, 0, 70, 20]
# delete
delete_title = "Permanently delete {n} selected file{s}?"
delete_origin = "center"
delete_offset = [ 0, 0, 70, 20 ]
delete_title = "Permanently delete {n} selected file{s}?"
delete_origin = "center"
delete_offset = [0, 0, 70, 20]
# overwrite
overwrite_title = "Overwrite file?"
overwrite_body = "Will overwrite the following file:"
overwrite_title = "Overwrite file?"
overwrite_body = "Will overwrite the following file:"
overwrite_origin = "center"
overwrite_offset = [ 0, 0, 50, 15 ]
overwrite_offset = [0, 0, 50, 15]
# quit
quit_title = "Quit?"
quit_body = "The following tasks are still running, are you sure you want to quit?"
quit_title = "Quit?"
quit_body = "The following tasks are still running, are you sure you want to quit?"
quit_origin = "center"
quit_offset = [ 0, 0, 50, 15 ]
quit_offset = [0, 0, 50, 15]
[pick]
open_title = "Open with:"
open_title = "Open with:"
open_origin = "hovered"
open_offset = [ 0, 1, 50, 7 ]
open_offset = [0, 1, 50, 7]
[which]
sort_by = "none"
sort_by = "none"
sort_sensitive = false
sort_reverse = false
sort_translit = false
sort_reverse = false
sort_translit = false

View file

@ -0,0 +1,109 @@
use std::path::Path;
use globset::{Glob, GlobSet, GlobSetBuilder};
use serde::Deserialize;
/// Represents a single exclude rule with patterns and context
#[derive(Debug, Clone, Deserialize)]
pub struct Exclude {
/// Pattern(s) to match files/directories against
/// Can be a single glob pattern string or an array of glob patterns
/// Patterns starting with '!' negate (whitelist) previously matched patterns
#[serde(deserialize_with = "deserialize_urn")]
pub urn: Vec<String>,
/// Context where this exclude rule applies
/// Supports glob patterns like "/code/**", "sftp://**", "search://**", or "*" for all
#[serde(rename = "in")]
pub context: String,
#[serde(skip)]
compiled: Option<CompiledPatterns>,
}
#[derive(Debug, Clone)]
struct CompiledPatterns {
/// Regular patterns (to ignore)
ignores: GlobSet,
/// Negated patterns (to whitelist/un-ignore)
whitelists: GlobSet,
}
fn deserialize_urn<'de, D>(deserializer: D) -> Result<Vec<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum UrnOrUrns {
Single(String),
Multiple(Vec<String>),
}
match UrnOrUrns::deserialize(deserializer)? {
UrnOrUrns::Single(s) => Ok(vec![s]),
UrnOrUrns::Multiple(v) => Ok(v),
}
}
impl Exclude {
/// Compile the glob patterns into GlobSets for efficient matching
pub fn compile(&mut self) -> Result<(), globset::Error> {
let mut ignore_builder = GlobSetBuilder::new();
let mut whitelist_builder = GlobSetBuilder::new();
for pattern in &self.urn {
if let Some(negated) = pattern.strip_prefix('!') {
// Negation pattern - add to whitelist
let glob = Glob::new(negated)?;
whitelist_builder.add(glob);
} else {
// Regular pattern - add to ignore list
let glob = Glob::new(pattern)?;
ignore_builder.add(glob);
}
}
self.compiled = Some(CompiledPatterns {
ignores: ignore_builder.build()?,
whitelists: whitelist_builder.build()?,
});
Ok(())
}
/// Check if a path matches this exclude rule
/// Returns Some(true) if path should be ignored, Some(false) if whitelisted,
/// None if no match
pub fn matches_path(&self, path: &Path) -> Option<bool> {
let compiled = self.compiled.as_ref()?;
// Check whitelist first (negation takes precedence)
if compiled.whitelists.is_match(path) {
return Some(false); // Explicitly NOT ignored
}
// Check ignore patterns
if compiled.ignores.is_match(path) {
return Some(true); // Should be ignored
}
None // No match
}
/// Check if this exclude rule applies to the given path context
pub fn matches_context(&self, path: &str) -> bool {
// Wildcard matches everything
if self.context == "*" {
return true;
}
// Handle glob patterns
if self.context.ends_with("/**") {
let prefix = &self.context[..self.context.len() - 3];
path.starts_with(prefix)
} else {
path == self.context || path.starts_with(&format!("{}/", self.context))
}
}
}

View file

@ -0,0 +1,56 @@
use std::path::Path;
use serde::Deserialize;
use yazi_codegen::DeserializeOver2;
use super::Exclude;
/// Configuration for file filtering
#[derive(Debug, Deserialize, DeserializeOver2, Default)]
pub struct Files {
/// Enable git ignore integration
#[serde(default)]
pub gitignores: bool,
/// List of exclude rules with context-specific patterns
#[serde(default)]
pub excludes: Vec<Exclude>,
}
impl Files {
/// Compile all glob patterns in exclude rules
pub fn compile(&mut self) -> Result<(), String> {
for exclude in &mut self.excludes {
exclude.compile().map_err(|e| format!("Failed to compile glob pattern: {}", e))?;
}
Ok(())
}
/// Get all exclude patterns that apply to a given context
pub fn excludes_for_context(&self, context: &str) -> Vec<String> {
self
.excludes
.iter()
.filter(|e| e.matches_context(context))
.flat_map(|e| e.urn.iter().cloned())
.collect()
}
/// Check if a path should be excluded based on compiled patterns for a given
/// context Returns Some(true) if should be ignored, Some(false) if
/// whitelisted, None if no match
pub fn matches_path(&self, path: &Path, context: &str) -> Option<bool> {
// Process rules in order, last match wins
let mut result = None;
for exclude in &self.excludes {
if exclude.matches_context(context) {
if let Some(should_ignore) = exclude.matches_path(path) {
result = Some(should_ignore);
}
}
}
result
}
}

View file

@ -0,0 +1,5 @@
mod exclude;
mod files;
pub use exclude::*;
pub use files::*;

View file

@ -1,4 +1,4 @@
yazi_macro::mod_pub!(keymap mgr open opener plugin popup preview tasks theme which vfs);
yazi_macro::mod_pub!(files keymap mgr open opener plugin popup preview tasks theme which vfs);
yazi_macro::mod_flat!(color icon layout pattern platform preset priority style yazi);

View file

@ -24,12 +24,6 @@ pub struct Mgr {
pub scrolloff: SyncCell<u8>,
pub mouse_events: SyncCell<MouseEvents>,
pub title_format: String,
// Filtering
#[serde(default)]
pub gitignore_enable: bool,
#[serde(default)]
pub ignore_override: Vec<String>,
}
impl Mgr {

View file

@ -3,11 +3,13 @@ use serde::Deserialize;
use yazi_codegen::DeserializeOver1;
use yazi_fs::{Xdg, ok_or_not_found};
use crate::{mgr, open, opener, plugin, popup, preview, tasks, which};
use crate::{files, mgr, open, opener, plugin, popup, preview, tasks, which};
#[derive(Deserialize, DeserializeOver1)]
pub struct Yazi {
pub mgr: mgr::Mgr,
#[serde(default)]
pub files: files::Files,
pub preview: preview::Preview,
pub opener: opener::Opener,
pub open: open::Open,
@ -26,9 +28,13 @@ impl Yazi {
.with_context(|| format!("Failed to read config {p:?}"))
}
pub(super) fn reshape(self) -> Result<Self> {
pub(super) fn reshape(mut self) -> Result<Self> {
// Compile glob patterns in exclude rules
self.files.compile().map_err(|e| anyhow::anyhow!(e))?;
Ok(Self {
mgr: self.mgr.reshape()?,
files: self.files,
preview: self.preview.reshape()?,
opener: self.opener.reshape()?,
open: self.open.reshape()?,

View file

@ -2,51 +2,93 @@ use std::{collections::HashSet, path::{Path, PathBuf}};
use yazi_shared::url::AsUrl;
/// Filter for ignoring files based on git ignore rules and custom exclude
/// patterns.
///
/// This filter combines git's native ignore functionality with custom exclude
/// patterns that follow gitignore syntax. Exclude patterns can be
/// context-specific and take precedence over git's ignore rules using the `!`
/// prefix for negation.
#[derive(Clone, Debug)]
pub struct IgnoreFilter {
/// Set of paths ignored by git (from git status)
ignored_paths: HashSet<PathBuf>,
/// Custom gitignore matcher for exclude patterns
gitignore: Option<ignore::gitignore::Gitignore>,
}
impl IgnoreFilter {
/// Create a new IgnoreFilter by checking git ignore status for files in the
/// given directory
pub fn from_dir(dir: impl AsRef<Path>, override_patterns: &[String]) -> Option<Self> {
/// Creates a new `IgnoreFilter` by checking git ignore status for the given
/// directory.
///
/// # Arguments
///
/// * `dir` - Directory to check for git ignore status
/// * `exclude_patterns` - Custom gitignore patterns that override git's rules
/// * `use_git` - Whether to integrate with git ignore rules
///
/// # Returns
///
/// `Some(IgnoreFilter)` if git repository is found (when use_git=true) or
/// exclude patterns exist, `None` if no git repository and no exclude
/// patterns.
///
/// # Behavior
///
/// - Discovers the git repository containing `dir` (if use_git=true)
/// - Collects all ignored paths from git status
/// - Builds a custom gitignore matcher from exclude patterns (relative to
/// repo root)
/// - Exclude patterns are checked first and take precedence over git ignore
/// rules
/// - Negation patterns (starting with `!`) can whitelist git-ignored files
pub fn from_dir(
dir: impl AsRef<Path>,
exclude_patterns: &[String],
use_git: bool,
) -> Option<Self> {
let dir = dir.as_ref();
// Try to open the git repository for this directory
let repo = git2::Repository::discover(dir).ok()?;
let (workdir, ignored_paths) = if use_git {
// Try to open the git repository for this directory
let repo = git2::Repository::discover(dir).ok()?;
// Get the workdir (root of the git repository)
let workdir = repo.workdir()?;
// Get the workdir (root of the git repository)
let workdir = repo.workdir()?.to_path_buf();
// Get git statuses for the repository
let statuses = match repo.statuses(None) {
Ok(s) => s,
Err(_) => return None,
};
// Get git statuses for the repository
let statuses = match repo.statuses(None) {
Ok(s) => s,
Err(_) => return None,
};
// Build a set of ALL ignored paths from git status
let mut ignored_paths = HashSet::new();
// Build a set of ALL ignored paths from git status
let mut ignored_paths = HashSet::new();
// Manually add .git directory as ignored (like eza does)
ignored_paths.insert(workdir.join(".git"));
// Manually add .git directory as ignored (like eza does)
ignored_paths.insert(workdir.join(".git"));
// Add all ignored files from git status
for status in statuses.iter() {
if status.status() == git2::Status::IGNORED {
if let Some(path) = status.path() {
// Add all ignored files from git status
for status in statuses.iter() {
if status.status() == git2::Status::IGNORED
&& let Some(path) = status.path()
{
ignored_paths.insert(workdir.join(path));
}
}
}
// Build custom gitignore from override patterns if provided
let gitignore = if !override_patterns.is_empty() {
let mut builder = ignore::gitignore::GitignoreBuilder::new(workdir);
(workdir, ignored_paths)
} else {
// No git integration, use current directory as base
(dir.to_path_buf(), HashSet::new())
};
// Add each override pattern
for pattern in override_patterns {
// Build custom gitignore from exclude patterns if provided
let gitignore = if !exclude_patterns.is_empty() {
let mut builder = ignore::gitignore::GitignoreBuilder::new(&workdir);
// Add each exclude pattern
for pattern in exclude_patterns {
let _ = builder.add_line(None, pattern);
}
@ -58,7 +100,7 @@ impl IgnoreFilter {
if ignored_paths.is_empty()
|| (ignored_paths.len() == 1 && ignored_paths.contains(&workdir.join(".git")))
{
// If we have no git-ignored paths but we have override patterns, still create
// If we have no git-ignored paths but we have exclude patterns, still create
// the filter
if gitignore.is_some() {
return Some(Self { ignored_paths, gitignore });
@ -71,7 +113,21 @@ impl IgnoreFilter {
Some(Self { ignored_paths, gitignore })
}
/// Create a new IgnoreFilter from only override patterns (no git integration)
/// Creates a new `IgnoreFilter` from only exclude patterns without git
/// integration.
///
/// # Arguments
///
/// * `dir` - Base directory for pattern matching
/// * `patterns` - Gitignore-style patterns to apply
///
/// # Returns
///
/// `Some(IgnoreFilter)` if patterns are provided, `None` if patterns is
/// empty.
///
/// This is useful when `gitignores = false` but custom exclude patterns
/// are still desired.
pub fn from_patterns(dir: impl AsRef<Path>, patterns: &[String]) -> Option<Self> {
if patterns.is_empty() {
return None;
@ -90,7 +146,28 @@ impl IgnoreFilter {
Some(Self { ignored_paths: HashSet::new(), gitignore: Some(gitignore) })
}
/// Check if a file should be ignored based on its URL
/// Checks if a file should be ignored based on its URL.
///
/// # Arguments
///
/// * `url` - URL of the file to check
///
/// # Returns
///
/// `true` if the file should be ignored, `false` otherwise.
///
/// # Matching Logic
///
/// 1. Check override patterns first:
/// - If matched as whitelist (negation `!`), return `false` (not ignored)
/// - If matched as ignore, return `true` (ignored)
/// 2. Check if path is directly in git's ignored set
/// 3. Check if any parent directory is ignored by git
/// - If parent is ignored, also check if override patterns whitelist it
/// - If parent is whitelisted, children are not ignored
///
/// This ensures override patterns take precedence and negation patterns work
/// correctly even when navigating inside ignored directories.
pub fn matches_url(&self, url: impl AsUrl) -> bool {
let url = url.as_url();
let path = url.loc.as_path();
@ -147,8 +224,6 @@ impl IgnoreFilter {
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_git_repo_discovery() {
// This test assumes we're running in the yazi git repo