mirror of
https://github.com/TagStudioDev/TagStudio.git
synced 2026-09-15 06:59:34 +02:00
fix: fix wcmatch matching '*' to '/'
This commit is contained in:
+6
-7
@@ -8,12 +8,6 @@ icon: material/file-document-remove
|
||||
|
||||
# :material-file-document-remove: Ignoring Files & Directories
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
!!! warning "Legacy File Extension Ignoring"
|
||||
TagStudio versions prior to v9.5.4 use a different, more limited method to exclude or include file extensions from your library and subsequent searches. Opening a pre-exiting library in v9.5.4 or later will non-destructively convert this to the newer, more extensive `.ts_ignore` format.
|
||||
|
||||
If you're still running an older version of TagStudio in the meantime, you can access the legacy system by going to "Edit -> Manage File Extensions" in the menubar.
|
||||
|
||||
TagStudio offers the ability to ignore specific files and directories via a `.ts_ignore` file located inside your [library's](libraries.md) `.TagStudio` folder. This file is designed to use very similar [glob](<https://en.wikipedia.org/wiki/Glob_(programming)>)-style pattern matching as the [`.gitignore`](https://git-scm.com/docs/gitignore) file used by Git™[^1]. It can be edited within TagStudio or opened to edit with an external program by going to the "Edit -> Ignore Files" option in the menubar.
|
||||
|
||||
This file is only referenced when scanning directories for new files to add to your library, and does not apply to files that have already been added to your library.
|
||||
@@ -131,7 +125,8 @@ The forward slash "`/`" is used as the directory separator. Separators may occur
|
||||
A `!` prefix before a pattern negates the pattern, allowing any files matched matched by previous patterns to be un-matched.
|
||||
|
||||
- Any matching file excluded by a previous pattern will become included again.
|
||||
- **It is not possible to re-include a file if a parent directory of that file is excluded.**
|
||||
- **An excluded parent directory can not have files within it reincluded!** (e.g. `Photos/`)
|
||||
- Alternatively, if one or more files within a directory is excluded (e.g. `!Photos/*.jpg`) then files within can be reincluded from there (e.g. `Photos/a.jpg`).
|
||||
|
||||
<!-- prettier-ignore-start -->
|
||||
=== "Example negation"
|
||||
@@ -147,6 +142,10 @@ A `!` prefix before a pattern negates the pattern, allowing any files matched ma
|
||||
```
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
<!-- prettier-ignore -->
|
||||
!!! bug "Directory Exclusion Negation"
|
||||
TagStudio attempts to match the behavior of a `.gitignore` file 1:1, however if you don't have `ripgrep` installed on your system and TagStudio falls back to its internal pattern matcher, excluded directories can be overwritten by further negations, unlike `.gitignore` behavior. Be wary that this is **not officially supported**, and this behavior may be removed at any time.
|
||||
|
||||
---
|
||||
|
||||
### Wildcards
|
||||
|
||||
@@ -5,15 +5,14 @@
|
||||
from pathlib import Path
|
||||
|
||||
import structlog
|
||||
import wcmatch.fnmatch as fnmatch
|
||||
from wcmatch import glob, pathlib
|
||||
from wcmatch import glob
|
||||
|
||||
from tagstudio.core.constants import IGNORE_NAME, TS_FOLDER_NAME
|
||||
from tagstudio.core.utils.singleton import Singleton
|
||||
|
||||
logger = structlog.get_logger()
|
||||
|
||||
PATH_GLOB_FLAGS: int = glob.GLOBSTARLONG | glob.DOTGLOB | glob.NEGATE | pathlib.MATCHBASE
|
||||
PATH_GLOB_FLAGS: int = glob.GLOBSTARLONG | glob.DOTGLOB | glob.NEGATE
|
||||
|
||||
|
||||
GLOBAL_IGNORE = [
|
||||
@@ -119,7 +118,7 @@ class Ignore(metaclass=Singleton):
|
||||
|
||||
_last_loaded: tuple[Path, float] | None = None
|
||||
_patterns: list[str] = []
|
||||
compiled_patterns: fnmatch.WcMatcher | None = None
|
||||
compiled_patterns: glob.WcMatcher | None = None
|
||||
|
||||
@staticmethod
|
||||
def read_ignore_file(library_dir: Path) -> list[str]:
|
||||
@@ -185,9 +184,9 @@ class Ignore(metaclass=Singleton):
|
||||
new_mtime=loaded[1],
|
||||
)
|
||||
Ignore._patterns = patterns + Ignore._load_ignore_file(ts_ignore_path)
|
||||
Ignore.compiled_patterns = fnmatch.compile(
|
||||
ignore_to_glob(Ignore._patterns),
|
||||
PATH_GLOB_FLAGS,
|
||||
Ignore.compiled_patterns = glob.compile(
|
||||
patterns=ignore_to_glob(Ignore._patterns),
|
||||
flags=PATH_GLOB_FLAGS,
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
|
||||
@@ -9,7 +9,7 @@ from collections.abc import Iterator
|
||||
from pathlib import Path
|
||||
|
||||
import structlog
|
||||
import wcmatch.fnmatch as fnmatch
|
||||
from wcmatch import glob
|
||||
|
||||
from tagstudio.core.constants import TS_FOLDER_NAME
|
||||
from tagstudio.core.library.ignore import PATH_GLOB_FLAGS, ignore_to_glob
|
||||
@@ -98,7 +98,7 @@ def _scan_with_ripgrep(scan_dir: Path, ignore_patterns: list[str]) -> Iterator[P
|
||||
def _scan_with_internal_scanner(scan_dir: Path, ignore_patterns: list[str]) -> Iterator[Path]:
|
||||
"""Scan for files with the internal scanner."""
|
||||
logger.info("[Scanners] Using internal scanner for scanning", path=scan_dir)
|
||||
matcher = fnmatch.compile(ignore_to_glob(ignore_patterns), PATH_GLOB_FLAGS)
|
||||
matcher = glob.compile(patterns=ignore_to_glob(ignore_patterns), flags=PATH_GLOB_FLAGS)
|
||||
|
||||
def walk(dir_path: Path, ancestors: frozenset[tuple[int, int]]) -> Iterator[Path]:
|
||||
try:
|
||||
|
||||
@@ -8,7 +8,6 @@ from typing import cast
|
||||
from warnings import deprecated
|
||||
|
||||
import structlog
|
||||
import wcmatch.fnmatch as fnmatch
|
||||
from PySide6.QtCore import QObject, Qt, QThreadPool, Signal
|
||||
from PySide6.QtWidgets import (
|
||||
QApplication,
|
||||
@@ -24,6 +23,7 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from wcmatch import glob
|
||||
|
||||
from tagstudio.core.constants import (
|
||||
IGNORE_NAME,
|
||||
@@ -512,13 +512,13 @@ class JsonMigrationModal(QObject):
|
||||
return str(f"<b><a style='color: {color}'>{new_value}</a></b>")
|
||||
|
||||
def assert_ignore_parity(self) -> None:
|
||||
compiled_pats = fnmatch.compile(
|
||||
compiled_pats = glob.compile(
|
||||
ignore_to_glob(
|
||||
Ignore._load_ignore_file( # pyright: ignore[reportPrivateUsage]
|
||||
unwrap(self.json_lib.library_dir) / TS_FOLDER_NAME / IGNORE_NAME
|
||||
)
|
||||
),
|
||||
PATH_GLOB_FLAGS,
|
||||
flags=PATH_GLOB_FLAGS,
|
||||
) # copied from Ignore.get_patterns since that method modifies singleton state
|
||||
path = self.json_lib.library_dir / "filename"
|
||||
for ext in self.json_lib.ext_list:
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# SPDX-FileCopyrightText: (c) TagStudio Contributors
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import wcmatch.fnmatch as fnmatch
|
||||
from wcmatch import glob
|
||||
|
||||
from tagstudio.core.library.ignore import PATH_GLOB_FLAGS, ignore_to_glob
|
||||
|
||||
|
||||
def matches(patterns: list[str], path: str) -> bool:
|
||||
return fnmatch.compile(ignore_to_glob(patterns), PATH_GLOB_FLAGS).match(path)
|
||||
return glob.compile(ignore_to_glob(patterns), flags=PATH_GLOB_FLAGS).match(path)
|
||||
|
||||
|
||||
def test_ignore_to_glob_does_not_crash_on_negated_root_anchored_pattern():
|
||||
@@ -84,3 +84,21 @@ def test_ignore_to_glob_output_has_no_duplicates():
|
||||
"""Output must be deduplicated."""
|
||||
glob_patterns = ignore_to_glob(["*.jpg", "Photos/", "**/foo"])
|
||||
assert len(glob_patterns) == len(set(glob_patterns))
|
||||
|
||||
|
||||
def test_single_asterisk_does_not_match_slash():
|
||||
"""A single "*" must not match a single "/".
|
||||
|
||||
fnmatch will still match "*" to a "/", when gitignore and wcmatch.glob will not.
|
||||
"""
|
||||
patterns = ["Images/*.png"]
|
||||
assert matches(patterns, "Images/mario.png") is True
|
||||
assert matches(patterns, "Images/Mario/cat.png") is False
|
||||
|
||||
|
||||
def test_negation_does_not_extend_to_deeper_subfolder():
|
||||
"""A negation must not extend into a deeper subfolder its pattern doesn't match."""
|
||||
patterns = ["*.jpg", "!Photos/*.jpg", "Photos/Private/*.jpg"]
|
||||
assert matches(patterns, "a.jpg") is True
|
||||
assert matches(patterns, "Photos/a.jpg") is False
|
||||
assert matches(patterns, "Photos/Private/a.jpg") is True
|
||||
|
||||
Reference in New Issue
Block a user