offgrep 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {offgrep-0.1.0 → offgrep-0.2.0}/PKG-INFO +2 -2
- {offgrep-0.1.0 → offgrep-0.2.0}/README.md +1 -1
- {offgrep-0.1.0 → offgrep-0.2.0}/pyproject.toml +1 -1
- {offgrep-0.1.0 → offgrep-0.2.0}/src/offgrep/__main__.py +56 -18
- offgrep-0.2.0/src/offgrep/filesystem.py +343 -0
- {offgrep-0.1.0 → offgrep-0.2.0}/src/offgrep/git.py +0 -3
- offgrep-0.1.0/src/offgrep/ignore.py → offgrep-0.2.0/src/offgrep/path_matching.py +209 -93
- offgrep-0.1.0/src/offgrep/filesystem.py +0 -182
- {offgrep-0.1.0 → offgrep-0.2.0}/LICENSE +0 -0
- {offgrep-0.1.0 → offgrep-0.2.0}/src/offgrep/__init__.py +0 -0
- {offgrep-0.1.0 → offgrep-0.2.0}/src/offgrep/commons.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: offgrep
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A lousy ripgrep rip-off in pure Python
|
|
5
5
|
Keywords: grep,recursive
|
|
6
6
|
Author: Rainer Schwarzbach
|
|
@@ -68,4 +68,4 @@ instead of **offgrep**.
|
|
|
68
68
|
If you – however – are in a super restricted network (eg. government)
|
|
69
69
|
without a chance to install **ripgrep**,
|
|
70
70
|
but are able to install Python packages, **offgrep** might be an interesting
|
|
71
|
-
|
|
71
|
+
alternative.
|
|
@@ -14,7 +14,7 @@ from re import compile as re_compile
|
|
|
14
14
|
from tomllib import loads as tomli_loads
|
|
15
15
|
|
|
16
16
|
from .commons import UTF_8
|
|
17
|
-
from .filesystem import
|
|
17
|
+
from .filesystem import DEFAULT_IGNORED_DIRS, Priority, DirectoryWalker
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
PACKAGE_NAME = "offgrep"
|
|
@@ -75,8 +75,10 @@ def find_in_files(
|
|
|
75
75
|
#
|
|
76
76
|
#
|
|
77
77
|
#
|
|
78
|
-
except UnicodeDecodeError
|
|
79
|
-
debug(
|
|
78
|
+
except UnicodeDecodeError:
|
|
79
|
+
debug(
|
|
80
|
+
"assuming binary file %s because of unicode decode error", single_file
|
|
81
|
+
)
|
|
80
82
|
except OSError as os_error:
|
|
81
83
|
error(str(os_error))
|
|
82
84
|
continue
|
|
@@ -102,6 +104,7 @@ def find_in_files(
|
|
|
102
104
|
def grep_files(arguments: Namespace) -> int:
|
|
103
105
|
"""Search for text in files"""
|
|
104
106
|
# TODO: arguments.file → read regexps from the provided file
|
|
107
|
+
default_ignore_dirs = arguments.default_ignore_dirs.split(",")
|
|
105
108
|
pattern_and_or_paths = list(arguments.pattern_and_or_paths)
|
|
106
109
|
try:
|
|
107
110
|
pattern = pattern_and_or_paths.pop(0)
|
|
@@ -127,11 +130,12 @@ def grep_files(arguments: Namespace) -> int:
|
|
|
127
130
|
debug("Omitting ignore rules for file %s", single_path)
|
|
128
131
|
target_paths.append(single_path)
|
|
129
132
|
elif single_path.is_dir():
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
walker = DirectoryWalker(
|
|
134
|
+
single_path,
|
|
135
|
+
default_ignore_dirs=default_ignore_dirs,
|
|
136
|
+
default_ignore_priority=arguments.default_ignore_priority,
|
|
137
|
+
)
|
|
138
|
+
target_paths.extend(walker.iter_files())
|
|
135
139
|
#
|
|
136
140
|
#
|
|
137
141
|
find_in_files(target_paths, pattern, show_files=show_files)
|
|
@@ -140,6 +144,7 @@ def grep_files(arguments: Namespace) -> int:
|
|
|
140
144
|
|
|
141
145
|
def list_files(arguments: Namespace) -> int:
|
|
142
146
|
"""List files that would be searched"""
|
|
147
|
+
default_ignore_dirs = arguments.default_ignore_dirs.split(",")
|
|
143
148
|
paths = [Path(item) for item in arguments.pattern_and_or_paths]
|
|
144
149
|
if not paths:
|
|
145
150
|
paths.append(Path.cwd())
|
|
@@ -149,12 +154,13 @@ def list_files(arguments: Namespace) -> int:
|
|
|
149
154
|
debug("Omitting ignore rules for file %s", single_path)
|
|
150
155
|
print(single_path)
|
|
151
156
|
elif single_path.is_dir():
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
walker = DirectoryWalker(
|
|
158
|
+
single_path,
|
|
159
|
+
default_ignore_dirs=default_ignore_dirs,
|
|
160
|
+
default_ignore_priority=arguments.default_ignore_priority,
|
|
161
|
+
)
|
|
162
|
+
for target_path in walker.iter_files():
|
|
163
|
+
print(target_path)
|
|
158
164
|
#
|
|
159
165
|
else:
|
|
160
166
|
warning("Ingored %s, neither a regular file nor a directory", single_path)
|
|
@@ -163,7 +169,10 @@ def list_files(arguments: Namespace) -> int:
|
|
|
163
169
|
return 0
|
|
164
170
|
|
|
165
171
|
|
|
166
|
-
|
|
172
|
+
def print_supported_file_tyoes() -> int:
|
|
173
|
+
"""Print the list of supported file types"""
|
|
174
|
+
print("Not implemented yet")
|
|
175
|
+
return 0
|
|
167
176
|
|
|
168
177
|
|
|
169
178
|
def get_arguments() -> Namespace:
|
|
@@ -180,7 +189,11 @@ def get_arguments() -> Namespace:
|
|
|
180
189
|
main_parser = ArgumentParser(
|
|
181
190
|
prog="offgrep", description="searches text in (text) files recursively"
|
|
182
191
|
)
|
|
183
|
-
main_parser.set_defaults(
|
|
192
|
+
main_parser.set_defaults(
|
|
193
|
+
loglevel=WARNING,
|
|
194
|
+
default_ignore_dirs=",".join(DEFAULT_IGNORED_DIRS),
|
|
195
|
+
default_ignore_priority=Priority.LOW,
|
|
196
|
+
)
|
|
184
197
|
main_parser.add_argument("-V", "--version", action="version", version=version)
|
|
185
198
|
main_parser.add_argument(
|
|
186
199
|
"--debug",
|
|
@@ -189,11 +202,32 @@ def get_arguments() -> Namespace:
|
|
|
189
202
|
dest="loglevel",
|
|
190
203
|
help="log with debug level",
|
|
191
204
|
)
|
|
205
|
+
main_parser.add_argument(
|
|
206
|
+
"--default-ignore-dirs",
|
|
207
|
+
help="Always ignore the directories named here, separated by commas."
|
|
208
|
+
" See also ---default-ignore-priority to specify when to ignore them."
|
|
209
|
+
" The preset is %(default)r – for various version control systems.",
|
|
210
|
+
)
|
|
211
|
+
main_parser.add_argument(
|
|
212
|
+
"--default-ignore-priority",
|
|
213
|
+
choices=list(Priority),
|
|
214
|
+
type=Priority,
|
|
215
|
+
help="Priority for the ignore rules specified through -default-ignore-dirs."
|
|
216
|
+
f" Choices: %(choices)s ({str(Priority.LOW)!r} is applied first but might"
|
|
217
|
+
" be overridden by eg. directory defined rules,"
|
|
218
|
+
f" while {str(Priority.HIGH)!r} is applied last but cannot be overridden anymore)."
|
|
219
|
+
f" The preset is {str(Priority.LOW)!r}.",
|
|
220
|
+
)
|
|
192
221
|
main_parser.add_argument(
|
|
193
222
|
"--files",
|
|
194
223
|
action="store_true",
|
|
195
|
-
help="
|
|
196
|
-
" performing the search.",
|
|
224
|
+
help="Print the path of each file that would be searched,"
|
|
225
|
+
" without actually performing the search.",
|
|
226
|
+
)
|
|
227
|
+
main_parser.add_argument(
|
|
228
|
+
"--type-list",
|
|
229
|
+
action="store_true",
|
|
230
|
+
help="Print the list of supported file types.",
|
|
197
231
|
)
|
|
198
232
|
main_parser.add_argument(
|
|
199
233
|
"pattern_and_or_paths",
|
|
@@ -216,6 +250,10 @@ def main() -> int:
|
|
|
216
250
|
the script rteurncode
|
|
217
251
|
"""
|
|
218
252
|
arguments = get_arguments()
|
|
253
|
+
if arguments.type_list:
|
|
254
|
+
print_supported_file_tyoes()
|
|
255
|
+
return 0
|
|
256
|
+
#
|
|
219
257
|
if arguments.files:
|
|
220
258
|
return list_files(arguments)
|
|
221
259
|
#
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Filesystem related functionality
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from collections.abc import Iterable, Iterator
|
|
8
|
+
from enum import StrEnum
|
|
9
|
+
from logging import debug, warning
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from .commons import EMPTY, SLASH
|
|
13
|
+
from .git import get_configured_global_gitignore_path
|
|
14
|
+
from .path_matching import NoRulesFound, RuleSet, iter_gitignore_rules_from_lines
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
GIT_IGNORE_FILE = ".gitignore"
|
|
18
|
+
MERCURIAL_IGNORE_FILE = ".hgignore"
|
|
19
|
+
GENERIC_IGNORE_FILE = ".ignore"
|
|
20
|
+
RIPGREP_IGNORE_FILE = ".rgignore"
|
|
21
|
+
|
|
22
|
+
DEFAULT_IGNORE_FILES = (GIT_IGNORE_FILE,)
|
|
23
|
+
|
|
24
|
+
IGNORE_FILES_SEQUENCE = (
|
|
25
|
+
GIT_IGNORE_FILE,
|
|
26
|
+
MERCURIAL_IGNORE_FILE,
|
|
27
|
+
GENERIC_IGNORE_FILE,
|
|
28
|
+
RIPGREP_IGNORE_FILE,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
GIT_DIR = ".git"
|
|
33
|
+
MERCURIAL_DIR = ".hg"
|
|
34
|
+
BREEZY_DIR = ".bzr"
|
|
35
|
+
DARCS_DIR = "_darcs"
|
|
36
|
+
SUBVERSION_DIR = ".svn"
|
|
37
|
+
|
|
38
|
+
DEFAULT_IGNORED_DIRS = (GIT_DIR, MERCURIAL_DIR, BREEZY_DIR, DARCS_DIR, SUBVERSION_DIR)
|
|
39
|
+
|
|
40
|
+
IN_GIT_REPO = 0x1
|
|
41
|
+
IN_MERCURIAL_REPO = 0x2
|
|
42
|
+
|
|
43
|
+
_VCS_LOOKUP: dict[str, int] = {GIT_DIR: IN_GIT_REPO, MERCURIAL_DIR: IN_MERCURIAL_REPO}
|
|
44
|
+
_VCS_IGNORE_FILES: dict[int, str] = {
|
|
45
|
+
IN_GIT_REPO: GIT_IGNORE_FILE,
|
|
46
|
+
IN_MERCURIAL_REPO: MERCURIAL_IGNORE_FILE,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class IgnoreFileMode(StrEnum):
|
|
51
|
+
"""Ignore file handling mode"""
|
|
52
|
+
|
|
53
|
+
ALWAYS = "always"
|
|
54
|
+
"""Use all ignore files, always"""
|
|
55
|
+
|
|
56
|
+
AUTOMATIC = "automatic"
|
|
57
|
+
"""Use .gitignore only in Git repositories,
|
|
58
|
+
.hgignore only in Mercurial repositories,
|
|
59
|
+
and all others always
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
NEVER = "never"
|
|
63
|
+
"""Never use any ignore files"""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class Priority(StrEnum):
|
|
67
|
+
"""Possible priorities of the default directory ruleset"""
|
|
68
|
+
|
|
69
|
+
LOW = "low"
|
|
70
|
+
HIGH = "high"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class DirectoryWalker:
|
|
74
|
+
"""Walks through a directory recursively"""
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
start_directory: Path,
|
|
79
|
+
default_ignore_dirs: list[str] | tuple[str, ...] = DEFAULT_IGNORED_DIRS,
|
|
80
|
+
default_ignore_priority: Priority = Priority.LOW,
|
|
81
|
+
ignore_file_mode: IgnoreFileMode = IgnoreFileMode.AUTOMATIC,
|
|
82
|
+
) -> None:
|
|
83
|
+
"""Initialize basic values"""
|
|
84
|
+
self.start_directory = start_directory.resolve()
|
|
85
|
+
debug("Start directory was resolved to %s", self.start_directory)
|
|
86
|
+
self.default_ignore_ruleset = RuleSet(
|
|
87
|
+
title=f"Default ignored directories (Priority {default_ignore_priority})",
|
|
88
|
+
anchored_at_dir=self.start_directory,
|
|
89
|
+
rules=tuple(
|
|
90
|
+
iter_gitignore_rules_from_lines(
|
|
91
|
+
f"{dirname.rstrip(SLASH)}{SLASH}" for dirname in default_ignore_dirs
|
|
92
|
+
)
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
debug(str(self.default_ignore_ruleset))
|
|
96
|
+
self.default_ignore_priority = default_ignore_priority
|
|
97
|
+
self.cwd = Path.cwd()
|
|
98
|
+
self.use_relative_paths = self.start_directory.is_relative_to(self.cwd)
|
|
99
|
+
self.ignore_file_mode = ignore_file_mode
|
|
100
|
+
|
|
101
|
+
def iter_files(self) -> Iterator[Path]:
|
|
102
|
+
"""Iterate over all files"""
|
|
103
|
+
vcs_bitmap, base_rulesets = self.compile_base_rulesets()
|
|
104
|
+
debug("Walking down from %s …", self.start_directory)
|
|
105
|
+
yield from self.walk_recursively(
|
|
106
|
+
self.start_directory, base_rulesets, vcs_bitmap
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def compile_base_rulesets(self) -> tuple[int, tuple[RuleSet, ...]]:
|
|
110
|
+
"""Compile the base rulesets"""
|
|
111
|
+
if self.ignore_file_mode == IgnoreFileMode.NEVER:
|
|
112
|
+
return 0, ()
|
|
113
|
+
#
|
|
114
|
+
vcs_bitmap = 0
|
|
115
|
+
vcs_rulesets: list[RuleSet] = [self.default_ignore_ruleset]
|
|
116
|
+
for indicator, check_for_dir, ignore_file_name in (
|
|
117
|
+
(IN_GIT_REPO, GIT_DIR, GIT_IGNORE_FILE),
|
|
118
|
+
(IN_MERCURIAL_REPO, MERCURIAL_DIR, MERCURIAL_IGNORE_FILE),
|
|
119
|
+
):
|
|
120
|
+
additional_bitmap, additional_rulesets = self._compile_base_vcs_rulesets(
|
|
121
|
+
indicator,
|
|
122
|
+
check_for_dir=check_for_dir,
|
|
123
|
+
ignore_file_name=ignore_file_name,
|
|
124
|
+
)
|
|
125
|
+
vcs_bitmap |= additional_bitmap
|
|
126
|
+
vcs_rulesets.extend(additional_rulesets)
|
|
127
|
+
#
|
|
128
|
+
return vcs_bitmap, tuple(vcs_rulesets)
|
|
129
|
+
|
|
130
|
+
def _compile_base_vcs_rulesets(
|
|
131
|
+
self, vcs_bitmap_on_success: int, check_for_dir: str, ignore_file_name: str
|
|
132
|
+
) -> tuple[int, tuple[RuleSet, ...]]:
|
|
133
|
+
"""Compile the base VCS related rulesets"""
|
|
134
|
+
vcs_bitmap = 0
|
|
135
|
+
base_vcs_rulesets: list[RuleSet] = []
|
|
136
|
+
dirs_to_check = [self.start_directory, *self.start_directory.parents]
|
|
137
|
+
checked_dirs = []
|
|
138
|
+
for pointer in dirs_to_check:
|
|
139
|
+
debug("Checking %s for %s …", pointer, check_for_dir)
|
|
140
|
+
checked_dirs.append(pointer)
|
|
141
|
+
candidate = pointer / check_for_dir
|
|
142
|
+
if candidate.is_dir():
|
|
143
|
+
debug("… found at %s!", pointer)
|
|
144
|
+
vcs_bitmap = vcs_bitmap_on_success
|
|
145
|
+
if check_for_dir == GIT_DIR:
|
|
146
|
+
base_vcs_rulesets.extend(
|
|
147
|
+
compile_default_git_rulesets(
|
|
148
|
+
repository_root=pointer,
|
|
149
|
+
with_user_global_gitignore=True,
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
#
|
|
153
|
+
break
|
|
154
|
+
#
|
|
155
|
+
else:
|
|
156
|
+
debug(
|
|
157
|
+
"%s: No repository found in any parent,"
|
|
158
|
+
" continuing without (%s specific) exclusion rules",
|
|
159
|
+
check_for_dir,
|
|
160
|
+
ignore_file_name,
|
|
161
|
+
)
|
|
162
|
+
return 0, ()
|
|
163
|
+
#
|
|
164
|
+
# walk back up and handle intermediate ignore files,
|
|
165
|
+
# except for the start directory itself (that is done when walking it)
|
|
166
|
+
for idx in range(len(checked_dirs) - 1, 0, -1):
|
|
167
|
+
current_dir = checked_dirs[idx]
|
|
168
|
+
append_ruleset_from_path(
|
|
169
|
+
base_vcs_rulesets,
|
|
170
|
+
current_dir / ignore_file_name,
|
|
171
|
+
current_dir,
|
|
172
|
+
)
|
|
173
|
+
dir_to_check = checked_dirs[idx - 1]
|
|
174
|
+
is_excluded = False
|
|
175
|
+
for current_ignore_rule in base_vcs_rulesets:
|
|
176
|
+
is_excluded = current_ignore_rule.excludes_path(
|
|
177
|
+
dir_to_check, pre_excluded=is_excluded
|
|
178
|
+
)
|
|
179
|
+
#
|
|
180
|
+
if is_excluded:
|
|
181
|
+
warning(
|
|
182
|
+
"%s is excluded by the rulesets collected so far,"
|
|
183
|
+
" clearing rulesets",
|
|
184
|
+
dir_to_check,
|
|
185
|
+
)
|
|
186
|
+
return 0, ()
|
|
187
|
+
#
|
|
188
|
+
#
|
|
189
|
+
return vcs_bitmap, tuple(base_vcs_rulesets)
|
|
190
|
+
|
|
191
|
+
def _compile_local_ignore_files(
|
|
192
|
+
self,
|
|
193
|
+
directory_contents: Iterable[Path],
|
|
194
|
+
vcs_bitmap: int,
|
|
195
|
+
) -> tuple[int, set[str]]:
|
|
196
|
+
"""Compile local ignore files from the provided directory contents"""
|
|
197
|
+
match self.ignore_file_mode:
|
|
198
|
+
case IgnoreFileMode.ALWAYS:
|
|
199
|
+
return vcs_bitmap, set(IGNORE_FILES_SEQUENCE)
|
|
200
|
+
case IgnoreFileMode.NEVER:
|
|
201
|
+
return 0, set()
|
|
202
|
+
#
|
|
203
|
+
#
|
|
204
|
+
dir_names: set[str] = set()
|
|
205
|
+
file_names: set[str] = set()
|
|
206
|
+
for current_path in directory_contents:
|
|
207
|
+
item_name = current_path.name
|
|
208
|
+
if current_path.is_dir():
|
|
209
|
+
dir_names.add(item_name)
|
|
210
|
+
elif current_path.is_file():
|
|
211
|
+
file_names.add(item_name)
|
|
212
|
+
#
|
|
213
|
+
local_ignore_files = {GENERIC_IGNORE_FILE, RIPGREP_IGNORE_FILE}
|
|
214
|
+
for vcs_dir_name, indicator in _VCS_LOOKUP.items():
|
|
215
|
+
if vcs_dir_name in dir_names:
|
|
216
|
+
vcs_bitmap |= indicator
|
|
217
|
+
#
|
|
218
|
+
if vcs_bitmap & indicator:
|
|
219
|
+
local_ignore_files.add(_VCS_IGNORE_FILES[indicator])
|
|
220
|
+
#
|
|
221
|
+
#
|
|
222
|
+
return vcs_bitmap, local_ignore_files & file_names
|
|
223
|
+
|
|
224
|
+
def walk_recursively(
|
|
225
|
+
self,
|
|
226
|
+
working_directory: Path,
|
|
227
|
+
ignore_rulesets: tuple[RuleSet, ...],
|
|
228
|
+
vcs_bitmap: int,
|
|
229
|
+
) -> Iterator[Path]:
|
|
230
|
+
"""Walk through all contained files, respecting the ignore rules.
|
|
231
|
+
|
|
232
|
+
Yield paths relative to the start directory if the start directory
|
|
233
|
+
is the current working directory.
|
|
234
|
+
"""
|
|
235
|
+
absolute_workdir = working_directory.resolve()
|
|
236
|
+
effective_ignore_rulesets = list(ignore_rulesets)
|
|
237
|
+
directory_contents = sorted(absolute_workdir.glob("*"), key=Path.as_posix)
|
|
238
|
+
vcs_bitmap, local_ignore_files = self._compile_local_ignore_files(
|
|
239
|
+
directory_contents, vcs_bitmap
|
|
240
|
+
)
|
|
241
|
+
for current_ignore_file in IGNORE_FILES_SEQUENCE:
|
|
242
|
+
if current_ignore_file not in local_ignore_files:
|
|
243
|
+
continue
|
|
244
|
+
#
|
|
245
|
+
append_ruleset_from_path(
|
|
246
|
+
effective_ignore_rulesets,
|
|
247
|
+
absolute_workdir / current_ignore_file,
|
|
248
|
+
absolute_workdir,
|
|
249
|
+
)
|
|
250
|
+
#
|
|
251
|
+
rulesets_to_pass: tuple[RuleSet, ...] = tuple(effective_ignore_rulesets)
|
|
252
|
+
match self.default_ignore_priority:
|
|
253
|
+
case Priority.LOW:
|
|
254
|
+
effective_ignore_rulesets.insert(0, self.default_ignore_ruleset)
|
|
255
|
+
case Priority.HIGH:
|
|
256
|
+
effective_ignore_rulesets.append(self.default_ignore_ruleset)
|
|
257
|
+
#
|
|
258
|
+
#
|
|
259
|
+
for contained_path in directory_contents:
|
|
260
|
+
# name = contained_path.name
|
|
261
|
+
if not (contained_path.is_dir() or contained_path.is_file()):
|
|
262
|
+
debug("Skipping %s, not a directory or regular file", contained_path)
|
|
263
|
+
continue
|
|
264
|
+
#
|
|
265
|
+
is_excluded = False
|
|
266
|
+
for current_ruleset in effective_ignore_rulesets:
|
|
267
|
+
is_excluded = current_ruleset.excludes_path(
|
|
268
|
+
contained_path, pre_excluded=is_excluded
|
|
269
|
+
)
|
|
270
|
+
#
|
|
271
|
+
if is_excluded:
|
|
272
|
+
debug("Ignoring %s", contained_path)
|
|
273
|
+
continue
|
|
274
|
+
#
|
|
275
|
+
if contained_path.is_file():
|
|
276
|
+
if self.use_relative_paths:
|
|
277
|
+
yield contained_path.relative_to(self.cwd)
|
|
278
|
+
else:
|
|
279
|
+
yield contained_path
|
|
280
|
+
#
|
|
281
|
+
#
|
|
282
|
+
if contained_path.is_dir():
|
|
283
|
+
debug("Descending into directory %s", contained_path.name)
|
|
284
|
+
yield from self.walk_recursively(
|
|
285
|
+
contained_path,
|
|
286
|
+
rulesets_to_pass,
|
|
287
|
+
vcs_bitmap,
|
|
288
|
+
)
|
|
289
|
+
#
|
|
290
|
+
#
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def append_ruleset_from_path(
|
|
294
|
+
rulesets: list[RuleSet],
|
|
295
|
+
/,
|
|
296
|
+
path: Path,
|
|
297
|
+
anchored_at_dir: Path,
|
|
298
|
+
title: str = EMPTY,
|
|
299
|
+
) -> bool:
|
|
300
|
+
"""load a ruleset and append it to the list"""
|
|
301
|
+
if path.is_file():
|
|
302
|
+
try:
|
|
303
|
+
if path.name == MERCURIAL_IGNORE_FILE:
|
|
304
|
+
new_ruleset = RuleSet.from_hgignore_file(
|
|
305
|
+
path, anchored_at_dir, title=title
|
|
306
|
+
)
|
|
307
|
+
else:
|
|
308
|
+
new_ruleset = RuleSet.from_gitignore_file(
|
|
309
|
+
path, anchored_at_dir, title=title
|
|
310
|
+
)
|
|
311
|
+
#
|
|
312
|
+
except OSError as os_error:
|
|
313
|
+
debug(str(os_error))
|
|
314
|
+
return False
|
|
315
|
+
except NoRulesFound:
|
|
316
|
+
return False
|
|
317
|
+
#
|
|
318
|
+
rulesets.append(new_ruleset)
|
|
319
|
+
return True
|
|
320
|
+
#
|
|
321
|
+
return False
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def compile_default_git_rulesets(
|
|
325
|
+
repository_root: Path,
|
|
326
|
+
with_user_global_gitignore: bool = False,
|
|
327
|
+
) -> list[RuleSet]:
|
|
328
|
+
"""Compile the default git rulesets"""
|
|
329
|
+
default_git_rulesets: list[RuleSet] = []
|
|
330
|
+
if with_user_global_gitignore:
|
|
331
|
+
append_ruleset_from_path(
|
|
332
|
+
default_git_rulesets,
|
|
333
|
+
path=get_configured_global_gitignore_path(),
|
|
334
|
+
anchored_at_dir=repository_root,
|
|
335
|
+
title="User global gitignore file",
|
|
336
|
+
)
|
|
337
|
+
#
|
|
338
|
+
append_ruleset_from_path(
|
|
339
|
+
default_git_rulesets,
|
|
340
|
+
path=repository_root / GIT_DIR / "info" / "exclude",
|
|
341
|
+
anchored_at_dir=repository_root,
|
|
342
|
+
)
|
|
343
|
+
return default_git_rulesets
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
Ignore (gitignore, tba: hgignore) implementation
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
from collections.abc import Iterable, Iterator
|
|
7
8
|
from dataclasses import dataclass
|
|
8
9
|
from enum import StrEnum
|
|
9
10
|
from functools import lru_cache
|
|
10
11
|
from logging import debug, warning
|
|
11
|
-
from os.path import normcase
|
|
12
12
|
from pathlib import Path
|
|
13
|
-
from re import compile as re_compile, escape
|
|
13
|
+
from re import IGNORECASE, compile as re_compile, escape
|
|
14
14
|
from typing import Self
|
|
15
15
|
|
|
16
16
|
from .commons import (
|
|
@@ -50,6 +50,10 @@ Suitable for matching relative path segments.
|
|
|
50
50
|
"""
|
|
51
51
|
|
|
52
52
|
|
|
53
|
+
class NoRulesFound(Exception):
|
|
54
|
+
"""Raised when no rules were found in a file"""
|
|
55
|
+
|
|
56
|
+
|
|
53
57
|
class Style(StrEnum):
|
|
54
58
|
"""Ignore style"""
|
|
55
59
|
|
|
@@ -62,30 +66,45 @@ class Style(StrEnum):
|
|
|
62
66
|
ROOTGLOB = "rootglob"
|
|
63
67
|
"""Mercurial ignore entry, anchored glob style"""
|
|
64
68
|
|
|
65
|
-
|
|
66
|
-
"""Mercurial ignore entry,
|
|
69
|
+
REGEXP = "regexp"
|
|
70
|
+
"""Mercurial ignore entry, regexp style"""
|
|
67
71
|
|
|
68
72
|
|
|
69
|
-
class
|
|
70
|
-
"""
|
|
73
|
+
class ExclusionRule:
|
|
74
|
+
"""Rule to match relative file paths"""
|
|
71
75
|
|
|
72
76
|
def __init__(
|
|
73
|
-
self,
|
|
77
|
+
self, original_pattern: str, style: Style, case_sensitive: bool = True
|
|
74
78
|
) -> None:
|
|
75
79
|
"""compile the RE pattern
|
|
76
80
|
|
|
77
81
|
Args:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
defaults (`True`), or to always match case sensitive (`False`)
|
|
82
|
+
original_pattern: the roriginal pattern
|
|
83
|
+
style: the ignore rule style
|
|
84
|
+
case_sensitive: Flag indicating whether to use case sensitive
|
|
85
|
+
file name matching or not. Converts regex_pattern to lower case
|
|
86
|
+
if set `False`.
|
|
84
87
|
"""
|
|
85
|
-
self.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
88
|
+
self._negated = False
|
|
89
|
+
stripped_pattern = original_pattern.rstrip(CRLF)
|
|
90
|
+
if stripped_pattern.endswith(BACKSLASH) and not stripped_pattern[:-1].endswith(
|
|
91
|
+
BACKSLASH
|
|
92
|
+
):
|
|
93
|
+
raise ValueError("Lines ending in a single backslash are not allowed")
|
|
94
|
+
#
|
|
95
|
+
self._original_pattern = stripped_pattern
|
|
96
|
+
effective_pattern = stripped_pattern
|
|
97
|
+
if style is Style.GITIGNORE and effective_pattern[0] == EXCLAMATION_MARK:
|
|
98
|
+
effective_pattern = effective_pattern[1:]
|
|
99
|
+
self._negated = True
|
|
100
|
+
#
|
|
101
|
+
self._style = style
|
|
102
|
+
regex_pattern = translate(effective_pattern, style=style)
|
|
103
|
+
self._cre = (
|
|
104
|
+
re_compile(regex_pattern)
|
|
105
|
+
if case_sensitive
|
|
106
|
+
else re_compile(regex_pattern, IGNORECASE)
|
|
107
|
+
)
|
|
89
108
|
|
|
90
109
|
def matches(self, relative_path: str) -> bool:
|
|
91
110
|
"""Indicate if the pattern matches the relative path
|
|
@@ -96,110 +115,207 @@ class CompiledPattern:
|
|
|
96
115
|
Returns:
|
|
97
116
|
`True` if the path matches the pattern, `False` if not
|
|
98
117
|
"""
|
|
99
|
-
if self.
|
|
100
|
-
relative_path
|
|
118
|
+
if self._cre.match(relative_path):
|
|
119
|
+
debug(f"{relative_path!r} matched rule {self}")
|
|
120
|
+
return True
|
|
101
121
|
#
|
|
102
|
-
return
|
|
122
|
+
return False
|
|
103
123
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
124
|
+
def flip(self, relative_path: str, pre_excluded: bool) -> bool:
|
|
125
|
+
"""Flips the exclude flag if required
|
|
126
|
+
|
|
127
|
+
Respecting the current exclusion state, matching is only performed
|
|
128
|
+
if required (no mathing of an already-excluded path against a rule that
|
|
129
|
+
could exclude it, and also no matching of an included path against a rule
|
|
130
|
+
that could re-include it).
|
|
131
|
+
|
|
132
|
+
* pre-excluded and self._negated -> flip if the rule matches
|
|
133
|
+
* not pre-excluded, self not negated -> flip if the rule matches
|
|
109
134
|
|
|
110
135
|
Args:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
`core.excludesFile` configuration directive)
|
|
114
|
-
use_normcase: Flag indicating whether to normalize the pattern
|
|
115
|
-
and the relative paths according to the operating system
|
|
116
|
-
defaults (`True`), or to always match case sensitive (`False`)
|
|
136
|
+
relative_path: the pathc to match
|
|
137
|
+
pre_excluded: exclusion state before applying the rule
|
|
117
138
|
|
|
118
139
|
Returns:
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
Raises:
|
|
122
|
-
ValueError: if the line ends in a single backslash
|
|
140
|
+
the new exclusion state
|
|
123
141
|
"""
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
BACKSLASH
|
|
127
|
-
):
|
|
128
|
-
raise ValueError("Lines ending in a single backslash are not allowed")
|
|
129
|
-
#
|
|
130
|
-
_negated = False
|
|
131
|
-
if gitignore_line.startswith("!"):
|
|
132
|
-
_negated = True
|
|
133
|
-
gitignore_line = gitignore_line[1:]
|
|
142
|
+
if self._negated != pre_excluded or not self.matches(relative_path):
|
|
143
|
+
return pre_excluded
|
|
134
144
|
#
|
|
135
|
-
return
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
145
|
+
return not pre_excluded
|
|
146
|
+
|
|
147
|
+
def __str__(self) -> str:
|
|
148
|
+
"""String display
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
the representation of the original pattern, with an explicit
|
|
152
|
+
abbotation if it is case INsensitive.
|
|
153
|
+
"""
|
|
154
|
+
cs_display = " (CASE INSENSITIVE)" if self._cre.flags & IGNORECASE else EMPTY
|
|
155
|
+
return f"{self._original_pattern!r}{cs_display}"
|
|
156
|
+
|
|
157
|
+
def __repr__(self) -> str:
|
|
158
|
+
"""String representation
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
the Python expression that would create an equivalent instance
|
|
162
|
+
"""
|
|
163
|
+
cs_display = not self._cre.flags & IGNORECASE
|
|
164
|
+
return (
|
|
165
|
+
f"self.__class__.__name__({self._original_pattern!r},"
|
|
166
|
+
f" style={self._style}, case_sensitive={cs_display})"
|
|
139
167
|
)
|
|
140
168
|
|
|
141
169
|
|
|
142
170
|
@dataclass(frozen=True)
|
|
143
|
-
class
|
|
144
|
-
"""
|
|
171
|
+
class RuleSet:
|
|
172
|
+
"""Collection of ignore rules"""
|
|
173
|
+
|
|
174
|
+
title: str
|
|
175
|
+
"""Ruleset title"""
|
|
145
176
|
|
|
146
|
-
|
|
147
|
-
"""The
|
|
177
|
+
anchored_at_dir: Path
|
|
178
|
+
"""The directory where the rules are anchored"""
|
|
148
179
|
|
|
149
|
-
rules: tuple[
|
|
180
|
+
rules: tuple[ExclusionRule, ...]
|
|
150
181
|
"""The ignore rules set"""
|
|
151
182
|
|
|
152
183
|
@classmethod
|
|
153
|
-
def
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
184
|
+
def from_gitignore_file(
|
|
185
|
+
cls, source: Path, anchored_at_dir: Path, title: str = EMPTY
|
|
186
|
+
) -> Self:
|
|
187
|
+
"""Factory method: Create a new instance from the provided gitignore file
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
source: the gitignore file path
|
|
191
|
+
anchored_at_dir: the directory where the rules are anchored at
|
|
192
|
+
title: the title of the new ruleset
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
the new instance
|
|
196
|
+
|
|
197
|
+
Raises:
|
|
198
|
+
NoRulesFound: if no rules were defined in the file
|
|
199
|
+
"""
|
|
157
200
|
with open(source, mode="rt", encoding=UTF_8) as gitignore_file:
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
warning(str(value_error))
|
|
166
|
-
#
|
|
167
|
-
#
|
|
201
|
+
defined_rules = tuple(iter_gitignore_rules_from_lines(gitignore_file))
|
|
202
|
+
#
|
|
203
|
+
if not defined_rules:
|
|
204
|
+
raise NoRulesFound
|
|
205
|
+
#
|
|
206
|
+
if not title:
|
|
207
|
+
title = str(source)
|
|
168
208
|
#
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
209
|
+
debug("%s: defined %s rules", title, len(defined_rules))
|
|
210
|
+
return cls(title=title, anchored_at_dir=anchored_at_dir, rules=defined_rules)
|
|
211
|
+
|
|
212
|
+
@classmethod
|
|
213
|
+
def from_hgignore_file(
|
|
214
|
+
cls, source: Path, anchored_at_dir: Path, title: str = EMPTY
|
|
215
|
+
) -> Self:
|
|
216
|
+
"""Factory method: Create a new instance from the provided .hgignore file
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
source: the .hgignore file path
|
|
220
|
+
anchored_at_dir: the directory where the rules are anchored at
|
|
221
|
+
title: the title of the new ruleset
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
the new instance
|
|
225
|
+
|
|
226
|
+
Raises:
|
|
227
|
+
NoRulesFound: if no rules were defined in the file
|
|
228
|
+
"""
|
|
229
|
+
with open(source, mode="rt", encoding=UTF_8) as gitignore_file:
|
|
230
|
+
defined_rules = tuple(iter_hgignore_rules_from_lines(gitignore_file))
|
|
231
|
+
#
|
|
232
|
+
if not defined_rules:
|
|
233
|
+
raise NoRulesFound
|
|
234
|
+
#
|
|
235
|
+
if not title:
|
|
236
|
+
title = str(source)
|
|
237
|
+
#
|
|
238
|
+
debug("%s: defined %s rules", title, len(defined_rules))
|
|
239
|
+
return cls(title=title, anchored_at_dir=anchored_at_dir, rules=defined_rules)
|
|
172
240
|
|
|
173
241
|
def excludes_path(self, file_or_dir: Path, pre_excluded: bool = False) -> bool:
|
|
174
|
-
"""Return True if the named
|
|
242
|
+
"""Return True if the named file or dir is excluded"""
|
|
243
|
+
# fast path if no roles are stored
|
|
244
|
+
if not self.rules:
|
|
245
|
+
return pre_excluded
|
|
246
|
+
#
|
|
175
247
|
is_excluded = pre_excluded
|
|
176
|
-
relative_path = file_or_dir.relative_to(self.
|
|
248
|
+
relative_path = file_or_dir.relative_to(self.anchored_at_dir)
|
|
177
249
|
comparable = relative_path.as_posix()
|
|
178
|
-
if
|
|
250
|
+
if file_or_dir.is_dir():
|
|
179
251
|
comparable = f"{comparable}/"
|
|
180
252
|
#
|
|
253
|
+
last_matching_rule = self.rules[0]
|
|
181
254
|
for single_rule in self.rules:
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
file_or_dir,
|
|
187
|
-
single_rule.regex_pattern,
|
|
188
|
-
)
|
|
189
|
-
is_excluded = False
|
|
190
|
-
else:
|
|
191
|
-
debug(
|
|
192
|
-
"Excluding %s because of %s",
|
|
193
|
-
file_or_dir,
|
|
194
|
-
single_rule.regex_pattern,
|
|
195
|
-
)
|
|
196
|
-
is_excluded = True
|
|
197
|
-
#
|
|
255
|
+
flipped = single_rule.flip(comparable, is_excluded)
|
|
256
|
+
if flipped ^ is_excluded:
|
|
257
|
+
last_matching_rule = single_rule
|
|
258
|
+
is_excluded = flipped
|
|
198
259
|
#
|
|
199
260
|
#
|
|
261
|
+
if is_excluded ^ pre_excluded:
|
|
262
|
+
new_state = "excluded" if is_excluded else "re-included"
|
|
263
|
+
debug(f"{new_state} by rule {last_matching_rule} from {self.title}")
|
|
264
|
+
#
|
|
200
265
|
return is_excluded
|
|
201
266
|
|
|
202
267
|
|
|
268
|
+
def iter_gitignore_rules_from_lines(lines: Iterable[str]) -> Iterator[ExclusionRule]:
|
|
269
|
+
"""Create rules from the provided lines
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
lines: an Iterable of gitignore-style lines
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
an Iterator over the created rules
|
|
276
|
+
"""
|
|
277
|
+
for current_line in lines:
|
|
278
|
+
if current_line.startswith("#") or not current_line.strip():
|
|
279
|
+
continue
|
|
280
|
+
#
|
|
281
|
+
try:
|
|
282
|
+
yield ExclusionRule(current_line, style=Style.GITIGNORE)
|
|
283
|
+
except ValueError as value_error:
|
|
284
|
+
warning(str(value_error))
|
|
285
|
+
#
|
|
286
|
+
#
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def iter_hgignore_rules_from_lines(lines: Iterable[str]) -> Iterator[ExclusionRule]:
|
|
290
|
+
"""Create rules from the provided lines
|
|
291
|
+
|
|
292
|
+
Args:
|
|
293
|
+
lines: an Iterable of mercurial-style lines
|
|
294
|
+
|
|
295
|
+
Returns:
|
|
296
|
+
an Iterator over the created rules
|
|
297
|
+
"""
|
|
298
|
+
current_style = Style.REGEXP
|
|
299
|
+
for current_line in lines:
|
|
300
|
+
if current_line.startswith("#") or not current_line.strip():
|
|
301
|
+
continue
|
|
302
|
+
#
|
|
303
|
+
if current_line.startswith("syntax: "):
|
|
304
|
+
try:
|
|
305
|
+
current_style = Style(current_line.split()[1])
|
|
306
|
+
except ValueError as value_error:
|
|
307
|
+
warning("Invalid syntax: %s", value_error)
|
|
308
|
+
#
|
|
309
|
+
continue
|
|
310
|
+
#
|
|
311
|
+
try:
|
|
312
|
+
yield ExclusionRule(current_line, style=current_style)
|
|
313
|
+
except ValueError as value_error:
|
|
314
|
+
warning(str(value_error))
|
|
315
|
+
#
|
|
316
|
+
#
|
|
317
|
+
|
|
318
|
+
|
|
203
319
|
def translate(pattern: str, style: Style = Style.GITIGNORE) -> str:
|
|
204
320
|
"""Dispatch pattern to the appropriate translation function,
|
|
205
321
|
according to the style argument
|
|
@@ -218,7 +334,7 @@ def translate(pattern: str, style: Style = Style.GITIGNORE) -> str:
|
|
|
218
334
|
match style:
|
|
219
335
|
case Style.GITIGNORE:
|
|
220
336
|
return gitignore_pattern_to_full_regex(pattern)
|
|
221
|
-
case Style.
|
|
337
|
+
case Style.REGEXP:
|
|
222
338
|
return pattern
|
|
223
339
|
case _:
|
|
224
340
|
raise NotImplementedError
|
|
@@ -366,7 +482,7 @@ def dissect_glob_segment(
|
|
|
366
482
|
the sequence of regex chunks and the sequence of star positions
|
|
367
483
|
in the chunks sequence.
|
|
368
484
|
This result is post-processed in the
|
|
369
|
-
[glob_segment_to_regex_segment][offgrep.
|
|
485
|
+
[glob_segment_to_regex_segment][offgrep.path_matching.glob_segment_to_regex_segment]
|
|
370
486
|
function which calls `dissect_glob_segment()` internally.
|
|
371
487
|
"""
|
|
372
488
|
re_chunks: list[str] = []
|
|
@@ -431,7 +547,7 @@ def bracketed_glob_pattern_part_to_partial_regex(
|
|
|
431
547
|
|
|
432
548
|
!!! note
|
|
433
549
|
Factored out from the
|
|
434
|
-
[dissect_glob_segment()][offgrep.
|
|
550
|
+
[dissect_glob_segment()][offgrep.path_matching.dissect_glob_segment]
|
|
435
551
|
function
|
|
436
552
|
|
|
437
553
|
Args:
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
# -*- coding: utf-8 -*-
|
|
2
|
-
|
|
3
|
-
"""
|
|
4
|
-
Filesystem related functionality
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from collections.abc import Iterator
|
|
8
|
-
from logging import debug
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
|
|
11
|
-
from .git import GIT_DIR, get_configured_global_gitignore_path
|
|
12
|
-
from .ignore import CompiledPattern, IgnoreFile
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
GIT_IGNORE_FILE = ".gitignore"
|
|
16
|
-
|
|
17
|
-
DEFAULT_IGNORE_FILES = (GIT_IGNORE_FILE,)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class IgnoredDirectoryError(Exception):
|
|
21
|
-
"""raised if a directory was explicitly excluded"""
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def load_gitignore_file(path: Path, handles: Path, description: str) -> IgnoreFile:
|
|
25
|
-
"""load an ignore file"""
|
|
26
|
-
if path.is_file():
|
|
27
|
-
return IgnoreFile.from_gitignore(path, handles)
|
|
28
|
-
#
|
|
29
|
-
raise ValueError(f"{description} ignore file {path} not found")
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
def iter_target_files(start_directory: Path) -> Iterator[Path]:
|
|
33
|
-
"""Iterate over target files"""
|
|
34
|
-
absolute_start = start_directory.resolve()
|
|
35
|
-
debug("Compiling ignore files for directory %s …", absolute_start)
|
|
36
|
-
ignore_files = compile_ignore_files(absolute_start)
|
|
37
|
-
debug("Walking down directory %s", absolute_start)
|
|
38
|
-
yield from walk_files(absolute_start, absolute_start, ignore_files)
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
# pylint: disable=too-many-locals ; refactoring candidate
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def compile_ignore_files(
|
|
45
|
-
start_directory: Path, check_git: bool = True
|
|
46
|
-
) -> list[IgnoreFile]:
|
|
47
|
-
"""Compile ignore files for start_directory"""
|
|
48
|
-
absolute_start = start_directory.resolve()
|
|
49
|
-
assert absolute_start.is_dir()
|
|
50
|
-
git_ignore_files: list[IgnoreFile] = []
|
|
51
|
-
if check_git:
|
|
52
|
-
dirs_to_check = [absolute_start, *absolute_start.parents]
|
|
53
|
-
checked_dirs = []
|
|
54
|
-
for pointer in dirs_to_check:
|
|
55
|
-
debug("Checking %s for %s …", pointer, GIT_DIR)
|
|
56
|
-
checked_dirs.append(pointer)
|
|
57
|
-
candidate = pointer / GIT_DIR
|
|
58
|
-
if candidate.is_dir():
|
|
59
|
-
debug("… found! Compiling ignore rules …")
|
|
60
|
-
git_ignore_files.append(
|
|
61
|
-
IgnoreFile(
|
|
62
|
-
handles_directory=pointer,
|
|
63
|
-
rules=(CompiledPattern.from_gitignore_line(f"{GIT_DIR}/"),),
|
|
64
|
-
)
|
|
65
|
-
)
|
|
66
|
-
for description, file_path in (
|
|
67
|
-
("user global", get_configured_global_gitignore_path()),
|
|
68
|
-
("repo private", candidate / "info" / "exclude"),
|
|
69
|
-
):
|
|
70
|
-
try:
|
|
71
|
-
git_ignore_files.append(
|
|
72
|
-
load_gitignore_file(file_path, pointer, description)
|
|
73
|
-
)
|
|
74
|
-
except (OSError, ValueError) as error:
|
|
75
|
-
debug(str(error))
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
break
|
|
79
|
-
#
|
|
80
|
-
else:
|
|
81
|
-
debug("No git repository found in any parent, continuing without …")
|
|
82
|
-
#
|
|
83
|
-
# walk back up and handle intermediate ignore files,
|
|
84
|
-
# except for the start directory itself (that is done when walking it)
|
|
85
|
-
if git_ignore_files:
|
|
86
|
-
for idx in range(len(checked_dirs) - 1, 0, -1):
|
|
87
|
-
current_dir = checked_dirs[idx]
|
|
88
|
-
try:
|
|
89
|
-
intermediate_ignore_file = load_gitignore_file(
|
|
90
|
-
current_dir / GIT_IGNORE_FILE,
|
|
91
|
-
current_dir,
|
|
92
|
-
"intermediate",
|
|
93
|
-
)
|
|
94
|
-
except (OSError, ValueError) as error:
|
|
95
|
-
debug(str(error))
|
|
96
|
-
else:
|
|
97
|
-
git_ignore_files.append(intermediate_ignore_file)
|
|
98
|
-
#
|
|
99
|
-
dir_to_check = checked_dirs[idx - 1]
|
|
100
|
-
is_excluded = False
|
|
101
|
-
for current_ignore_file in git_ignore_files:
|
|
102
|
-
is_excluded = current_ignore_file.excludes_path(
|
|
103
|
-
dir_to_check, pre_excluded=is_excluded
|
|
104
|
-
)
|
|
105
|
-
#
|
|
106
|
-
if is_excluded:
|
|
107
|
-
raise IgnoredDirectoryError(
|
|
108
|
-
f"gitignore config excludes directory {dir_to_check},"
|
|
109
|
-
" cannot continue"
|
|
110
|
-
)
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
#
|
|
114
|
-
#
|
|
115
|
-
return git_ignore_files
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def walk_files(
|
|
119
|
-
start_directory: Path,
|
|
120
|
-
working_directory: Path,
|
|
121
|
-
ignore_files: list[IgnoreFile],
|
|
122
|
-
use_ignore_files: tuple[str, ...] = DEFAULT_IGNORE_FILES,
|
|
123
|
-
) -> Iterator[Path]:
|
|
124
|
-
"""Walk through all contained files, respecting the ignore rules.
|
|
125
|
-
|
|
126
|
-
Yield paths relative to the start directory if the start directory
|
|
127
|
-
is the current working directory.
|
|
128
|
-
"""
|
|
129
|
-
assert start_directory.is_absolute()
|
|
130
|
-
use_relative_paths = False
|
|
131
|
-
cwd = Path.cwd()
|
|
132
|
-
anchor_directory = cwd
|
|
133
|
-
if start_directory.is_relative_to(Path.cwd()):
|
|
134
|
-
use_relative_paths = True
|
|
135
|
-
#
|
|
136
|
-
absolute_workdir = working_directory.resolve()
|
|
137
|
-
effective_ignore_files = list(ignore_files)
|
|
138
|
-
for ignore_file_candidate in use_ignore_files:
|
|
139
|
-
try:
|
|
140
|
-
effective_ignore_files.append(
|
|
141
|
-
load_gitignore_file(
|
|
142
|
-
absolute_workdir / ignore_file_candidate,
|
|
143
|
-
absolute_workdir,
|
|
144
|
-
"Directory level",
|
|
145
|
-
)
|
|
146
|
-
)
|
|
147
|
-
except (OSError, ValueError) as error:
|
|
148
|
-
debug(str(error))
|
|
149
|
-
#
|
|
150
|
-
#
|
|
151
|
-
for contained_path in sorted(absolute_workdir.glob("*"), key=Path.as_posix):
|
|
152
|
-
# name = contained_path.name
|
|
153
|
-
if not (contained_path.is_dir() or contained_path.is_file()):
|
|
154
|
-
debug("Skipping %s, not a directory or regular file", contained_path)
|
|
155
|
-
continue
|
|
156
|
-
#
|
|
157
|
-
is_excluded = False
|
|
158
|
-
for current_ignore_file in effective_ignore_files:
|
|
159
|
-
is_excluded = current_ignore_file.excludes_path(
|
|
160
|
-
contained_path, pre_excluded=is_excluded
|
|
161
|
-
)
|
|
162
|
-
#
|
|
163
|
-
if is_excluded:
|
|
164
|
-
continue
|
|
165
|
-
#
|
|
166
|
-
if contained_path.is_file():
|
|
167
|
-
if use_relative_paths:
|
|
168
|
-
yield contained_path.relative_to(anchor_directory)
|
|
169
|
-
else:
|
|
170
|
-
yield contained_path
|
|
171
|
-
#
|
|
172
|
-
#
|
|
173
|
-
if contained_path.is_dir():
|
|
174
|
-
debug("Descending into directory %s", contained_path.name)
|
|
175
|
-
yield from walk_files(
|
|
176
|
-
start_directory,
|
|
177
|
-
contained_path,
|
|
178
|
-
effective_ignore_files,
|
|
179
|
-
use_ignore_files=use_ignore_files,
|
|
180
|
-
)
|
|
181
|
-
#
|
|
182
|
-
#
|
|
File without changes
|
|
File without changes
|
|
File without changes
|