offgrep 0.1.1__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.1 → offgrep-0.2.0}/PKG-INFO +2 -2
- {offgrep-0.1.1 → offgrep-0.2.0}/README.md +1 -1
- {offgrep-0.1.1 → offgrep-0.2.0}/pyproject.toml +1 -1
- {offgrep-0.1.1 → offgrep-0.2.0}/src/offgrep/__main__.py +52 -16
- offgrep-0.2.0/src/offgrep/filesystem.py +343 -0
- {offgrep-0.1.1 → offgrep-0.2.0}/src/offgrep/git.py +0 -3
- offgrep-0.1.1/src/offgrep/ignore.py → offgrep-0.2.0/src/offgrep/path_matching.py +206 -88
- offgrep-0.1.1/src/offgrep/filesystem.py +0 -182
- {offgrep-0.1.1 → offgrep-0.2.0}/LICENSE +0 -0
- {offgrep-0.1.1 → offgrep-0.2.0}/src/offgrep/__init__.py +0 -0
- {offgrep-0.1.1 → 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"
|
|
@@ -104,6 +104,7 @@ def find_in_files(
|
|
|
104
104
|
def grep_files(arguments: Namespace) -> int:
|
|
105
105
|
"""Search for text in files"""
|
|
106
106
|
# TODO: arguments.file → read regexps from the provided file
|
|
107
|
+
default_ignore_dirs = arguments.default_ignore_dirs.split(",")
|
|
107
108
|
pattern_and_or_paths = list(arguments.pattern_and_or_paths)
|
|
108
109
|
try:
|
|
109
110
|
pattern = pattern_and_or_paths.pop(0)
|
|
@@ -129,11 +130,12 @@ def grep_files(arguments: Namespace) -> int:
|
|
|
129
130
|
debug("Omitting ignore rules for file %s", single_path)
|
|
130
131
|
target_paths.append(single_path)
|
|
131
132
|
elif single_path.is_dir():
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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())
|
|
137
139
|
#
|
|
138
140
|
#
|
|
139
141
|
find_in_files(target_paths, pattern, show_files=show_files)
|
|
@@ -142,6 +144,7 @@ def grep_files(arguments: Namespace) -> int:
|
|
|
142
144
|
|
|
143
145
|
def list_files(arguments: Namespace) -> int:
|
|
144
146
|
"""List files that would be searched"""
|
|
147
|
+
default_ignore_dirs = arguments.default_ignore_dirs.split(",")
|
|
145
148
|
paths = [Path(item) for item in arguments.pattern_and_or_paths]
|
|
146
149
|
if not paths:
|
|
147
150
|
paths.append(Path.cwd())
|
|
@@ -151,12 +154,13 @@ def list_files(arguments: Namespace) -> int:
|
|
|
151
154
|
debug("Omitting ignore rules for file %s", single_path)
|
|
152
155
|
print(single_path)
|
|
153
156
|
elif single_path.is_dir():
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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)
|
|
160
164
|
#
|
|
161
165
|
else:
|
|
162
166
|
warning("Ingored %s, neither a regular file nor a directory", single_path)
|
|
@@ -165,7 +169,10 @@ def list_files(arguments: Namespace) -> int:
|
|
|
165
169
|
return 0
|
|
166
170
|
|
|
167
171
|
|
|
168
|
-
|
|
172
|
+
def print_supported_file_tyoes() -> int:
|
|
173
|
+
"""Print the list of supported file types"""
|
|
174
|
+
print("Not implemented yet")
|
|
175
|
+
return 0
|
|
169
176
|
|
|
170
177
|
|
|
171
178
|
def get_arguments() -> Namespace:
|
|
@@ -182,7 +189,11 @@ def get_arguments() -> Namespace:
|
|
|
182
189
|
main_parser = ArgumentParser(
|
|
183
190
|
prog="offgrep", description="searches text in (text) files recursively"
|
|
184
191
|
)
|
|
185
|
-
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
|
+
)
|
|
186
197
|
main_parser.add_argument("-V", "--version", action="version", version=version)
|
|
187
198
|
main_parser.add_argument(
|
|
188
199
|
"--debug",
|
|
@@ -191,11 +202,32 @@ def get_arguments() -> Namespace:
|
|
|
191
202
|
dest="loglevel",
|
|
192
203
|
help="log with debug level",
|
|
193
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
|
+
)
|
|
194
221
|
main_parser.add_argument(
|
|
195
222
|
"--files",
|
|
196
223
|
action="store_true",
|
|
197
|
-
help="
|
|
198
|
-
" 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.",
|
|
199
231
|
)
|
|
200
232
|
main_parser.add_argument(
|
|
201
233
|
"pattern_and_or_paths",
|
|
@@ -218,6 +250,10 @@ def main() -> int:
|
|
|
218
250
|
the script rteurncode
|
|
219
251
|
"""
|
|
220
252
|
arguments = get_arguments()
|
|
253
|
+
if arguments.type_list:
|
|
254
|
+
print_supported_file_tyoes()
|
|
255
|
+
return 0
|
|
256
|
+
#
|
|
221
257
|
if arguments.files:
|
|
222
258
|
return list_files(arguments)
|
|
223
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,12 +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
12
|
from pathlib import Path
|
|
12
|
-
from re import compile as re_compile, escape
|
|
13
|
+
from re import IGNORECASE, compile as re_compile, escape
|
|
13
14
|
from typing import Self
|
|
14
15
|
|
|
15
16
|
from .commons import (
|
|
@@ -49,6 +50,10 @@ Suitable for matching relative path segments.
|
|
|
49
50
|
"""
|
|
50
51
|
|
|
51
52
|
|
|
53
|
+
class NoRulesFound(Exception):
|
|
54
|
+
"""Raised when no rules were found in a file"""
|
|
55
|
+
|
|
56
|
+
|
|
52
57
|
class Style(StrEnum):
|
|
53
58
|
"""Ignore style"""
|
|
54
59
|
|
|
@@ -61,30 +66,45 @@ class Style(StrEnum):
|
|
|
61
66
|
ROOTGLOB = "rootglob"
|
|
62
67
|
"""Mercurial ignore entry, anchored glob style"""
|
|
63
68
|
|
|
64
|
-
|
|
65
|
-
"""Mercurial ignore entry,
|
|
69
|
+
REGEXP = "regexp"
|
|
70
|
+
"""Mercurial ignore entry, regexp style"""
|
|
66
71
|
|
|
67
72
|
|
|
68
|
-
class
|
|
69
|
-
"""
|
|
73
|
+
class ExclusionRule:
|
|
74
|
+
"""Rule to match relative file paths"""
|
|
70
75
|
|
|
71
76
|
def __init__(
|
|
72
|
-
self,
|
|
77
|
+
self, original_pattern: str, style: Style, case_sensitive: bool = True
|
|
73
78
|
) -> None:
|
|
74
79
|
"""compile the RE pattern
|
|
75
80
|
|
|
76
81
|
Args:
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
or re-includes a previously excluded path (`True`)
|
|
82
|
+
original_pattern: the roriginal pattern
|
|
83
|
+
style: the ignore rule style
|
|
80
84
|
case_sensitive: Flag indicating whether to use case sensitive
|
|
81
85
|
file name matching or not. Converts regex_pattern to lower case
|
|
82
86
|
if set `False`.
|
|
83
87
|
"""
|
|
84
|
-
self.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
+
)
|
|
88
108
|
|
|
89
109
|
def matches(self, relative_path: str) -> bool:
|
|
90
110
|
"""Indicate if the pattern matches the relative path
|
|
@@ -95,109 +115,207 @@ class CompiledPattern:
|
|
|
95
115
|
Returns:
|
|
96
116
|
`True` if the path matches the pattern, `False` if not
|
|
97
117
|
"""
|
|
98
|
-
if self.
|
|
99
|
-
relative_path
|
|
118
|
+
if self._cre.match(relative_path):
|
|
119
|
+
debug(f"{relative_path!r} matched rule {self}")
|
|
120
|
+
return True
|
|
100
121
|
#
|
|
101
|
-
return
|
|
122
|
+
return False
|
|
102
123
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
108
134
|
|
|
109
135
|
Args:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
`core.excludesFile` configuration directive)
|
|
113
|
-
case_sensitive: Flag indicating whether to use case sensitive
|
|
114
|
-
file name matching or not
|
|
136
|
+
relative_path: the pathc to match
|
|
137
|
+
pre_excluded: exclusion state before applying the rule
|
|
115
138
|
|
|
116
139
|
Returns:
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
Raises:
|
|
120
|
-
ValueError: if the line ends in a single backslash
|
|
140
|
+
the new exclusion state
|
|
121
141
|
"""
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
BACKSLASH
|
|
125
|
-
):
|
|
126
|
-
raise ValueError("Lines ending in a single backslash are not allowed")
|
|
142
|
+
if self._negated != pre_excluded or not self.matches(relative_path):
|
|
143
|
+
return pre_excluded
|
|
127
144
|
#
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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})"
|
|
137
167
|
)
|
|
138
168
|
|
|
139
169
|
|
|
140
170
|
@dataclass(frozen=True)
|
|
141
|
-
class
|
|
142
|
-
"""
|
|
171
|
+
class RuleSet:
|
|
172
|
+
"""Collection of ignore rules"""
|
|
173
|
+
|
|
174
|
+
title: str
|
|
175
|
+
"""Ruleset title"""
|
|
143
176
|
|
|
144
|
-
|
|
145
|
-
"""The
|
|
177
|
+
anchored_at_dir: Path
|
|
178
|
+
"""The directory where the rules are anchored"""
|
|
146
179
|
|
|
147
|
-
rules: tuple[
|
|
180
|
+
rules: tuple[ExclusionRule, ...]
|
|
148
181
|
"""The ignore rules set"""
|
|
149
182
|
|
|
150
183
|
@classmethod
|
|
151
|
-
def
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
+
"""
|
|
155
200
|
with open(source, mode="rt", encoding=UTF_8) as gitignore_file:
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
try:
|
|
161
|
-
rules_collector.append(CompiledPattern.from_gitignore_line(line))
|
|
162
|
-
except ValueError as value_error:
|
|
163
|
-
warning(str(value_error))
|
|
164
|
-
#
|
|
165
|
-
#
|
|
201
|
+
defined_rules = tuple(iter_gitignore_rules_from_lines(gitignore_file))
|
|
202
|
+
#
|
|
203
|
+
if not defined_rules:
|
|
204
|
+
raise NoRulesFound
|
|
166
205
|
#
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
206
|
+
if not title:
|
|
207
|
+
title = str(source)
|
|
208
|
+
#
|
|
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)
|
|
170
240
|
|
|
171
241
|
def excludes_path(self, file_or_dir: Path, pre_excluded: bool = False) -> bool:
|
|
172
|
-
"""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
|
+
#
|
|
173
247
|
is_excluded = pre_excluded
|
|
174
|
-
relative_path = file_or_dir.relative_to(self.
|
|
248
|
+
relative_path = file_or_dir.relative_to(self.anchored_at_dir)
|
|
175
249
|
comparable = relative_path.as_posix()
|
|
176
|
-
if
|
|
250
|
+
if file_or_dir.is_dir():
|
|
177
251
|
comparable = f"{comparable}/"
|
|
178
252
|
#
|
|
253
|
+
last_matching_rule = self.rules[0]
|
|
179
254
|
for single_rule in self.rules:
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
file_or_dir,
|
|
185
|
-
single_rule.regex_pattern,
|
|
186
|
-
)
|
|
187
|
-
is_excluded = False
|
|
188
|
-
else:
|
|
189
|
-
debug(
|
|
190
|
-
"Excluding %s because of %s",
|
|
191
|
-
file_or_dir,
|
|
192
|
-
single_rule.regex_pattern,
|
|
193
|
-
)
|
|
194
|
-
is_excluded = True
|
|
195
|
-
#
|
|
255
|
+
flipped = single_rule.flip(comparable, is_excluded)
|
|
256
|
+
if flipped ^ is_excluded:
|
|
257
|
+
last_matching_rule = single_rule
|
|
258
|
+
is_excluded = flipped
|
|
196
259
|
#
|
|
197
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
|
+
#
|
|
198
265
|
return is_excluded
|
|
199
266
|
|
|
200
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
|
+
|
|
201
319
|
def translate(pattern: str, style: Style = Style.GITIGNORE) -> str:
|
|
202
320
|
"""Dispatch pattern to the appropriate translation function,
|
|
203
321
|
according to the style argument
|
|
@@ -216,7 +334,7 @@ def translate(pattern: str, style: Style = Style.GITIGNORE) -> str:
|
|
|
216
334
|
match style:
|
|
217
335
|
case Style.GITIGNORE:
|
|
218
336
|
return gitignore_pattern_to_full_regex(pattern)
|
|
219
|
-
case Style.
|
|
337
|
+
case Style.REGEXP:
|
|
220
338
|
return pattern
|
|
221
339
|
case _:
|
|
222
340
|
raise NotImplementedError
|
|
@@ -364,7 +482,7 @@ def dissect_glob_segment(
|
|
|
364
482
|
the sequence of regex chunks and the sequence of star positions
|
|
365
483
|
in the chunks sequence.
|
|
366
484
|
This result is post-processed in the
|
|
367
|
-
[glob_segment_to_regex_segment][offgrep.
|
|
485
|
+
[glob_segment_to_regex_segment][offgrep.path_matching.glob_segment_to_regex_segment]
|
|
368
486
|
function which calls `dissect_glob_segment()` internally.
|
|
369
487
|
"""
|
|
370
488
|
re_chunks: list[str] = []
|
|
@@ -429,7 +547,7 @@ def bracketed_glob_pattern_part_to_partial_regex(
|
|
|
429
547
|
|
|
430
548
|
!!! note
|
|
431
549
|
Factored out from the
|
|
432
|
-
[dissect_glob_segment()][offgrep.
|
|
550
|
+
[dissect_glob_segment()][offgrep.path_matching.dissect_glob_segment]
|
|
433
551
|
function
|
|
434
552
|
|
|
435
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
|