33namespace fs = std::filesystem;
47std::string trim(
const std::string& s) {
48 auto begin = s.find_first_not_of(
" \t\r\n");
49 auto end = s.find_last_not_of(
" \t\r\n");
50 return (begin == std::string::npos)
52 : s.substr(begin, end - begin + 1);
62std::string to_slash(
const fs::path& p) {
63 auto s = p.generic_string();
78bool is_regex_meta(
char c) {
80 case '.':
case '+':
case '(':
case ')':
81 case '|':
case '^':
case '$':
case '{':
106void emit_star(
const std::string& pattern,
size_t& i, std::string& out) {
107 bool double_star = (i + 1 < pattern.size())
108 && pattern[i + 1] ==
'*';
109 if (!double_star) { out +=
"[^/]*";
return; }
112 if (i + 1 < pattern.size() && pattern[i + 1] ==
'/') {
123void emit_bracket(
const std::string& pattern,
size_t& i,
127 while (i < pattern.size() && pattern[i] !=
']') {
139void emit_escape(
const std::string& pattern,
size_t& i,
141 char next = pattern[i + 1];
142 if (is_regex_meta(next)) { out +=
'\\'; }
170void emit_one(
const std::string& pattern,
size_t& i,
175 case '*': emit_star(pattern, i, out);
break;
176 case '?': out +=
"[^/]";
break;
177 case '[': emit_bracket(pattern, i, out);
break;
179 if (i + 1 < pattern.size()) {
180 emit_escape(pattern, i, out);
185 default: handled =
false;
break;
188 if (is_regex_meta(c)) { out +=
'\\'; }
207std::string IgnoreMatcher::pattern_to_regex(
const std::string& pattern) {
209 out.reserve(pattern.size() * 2);
210 for (
size_t i = 0; i < pattern.size(); ++i) {
211 emit_one(pattern, i, out);
235void strip_flags(std::string& body, IgnoreMatcher::Rule& rule) {
236 if (!body.empty() && body[0] ==
'!') {
240 if (!body.empty() && body.back() ==
'/') {
241 rule.dir_only =
true;
255std::string make_base_prefix(
const std::string& base) {
256 if (base.empty()) {
return {}; }
257 std::string raw = base +
"/";
260 if (is_regex_meta(c) || c ==
'*' || c ==
'?' || c ==
'[') {
274std::regex compile_or_never(
const std::string& src,
275 const std::string& original_pattern) {
277 return std::regex(src);
278 }
catch (
const std::regex_error& e) {
279 logger->warn(
"Skipping malformed ignore pattern '{}': {}",
280 original_pattern, e.what());
281 return std::regex(
"(?!)");
304IgnoreMatcher::Rule IgnoreMatcher::compile_pattern(
305 const std::string& pattern,
const std::string& base) {
307 rule.original = pattern;
310 std::string body = pattern;
311 strip_flags(body, rule);
312 bool root_anchored = !body.empty() && body[0] ==
'/';
313 if (root_anchored) { body.erase(0, 1); }
314 bool anchored = root_anchored
315 || body.find(
'/') != std::string::npos;
317 std::string regex_body = pattern_to_regex(body);
318 std::string base_prefix = make_base_prefix(base);
319 std::string anchor_left = anchored
320 ? (
"^" + base_prefix)
321 : (
"^" + base_prefix +
"(?:.*/)?");
323 rule.re_exact = compile_or_never(
324 anchor_left + regex_body +
"$", pattern);
325 rule.re_under = compile_or_never(
326 anchor_left + regex_body +
"/.*$", pattern);
338 const fs::path& base) {
339 std::string trimmed = trim(pattern);
340 if (trimmed.empty() || trimmed[0] ==
'#') {
return; }
341 rules_.push_back(compile_pattern(trimmed, to_slash(base)));
349void IgnoreMatcher::load_file(
const fs::path& path,
350 const std::string& base) {
351 std::ifstream in(path);
352 if (!in.is_open()) {
return; }
355 while (std::getline(in, line)) {
356 std::string trimmed = trim(line);
357 if (trimmed.empty() || trimmed[0] ==
'#') {
continue; }
358 rules_.push_back(compile_pattern(trimmed, base));
361 std::string base_label = base.empty() ? std::string(
"<root>") : base;
362 logger->info(
"Loaded {} ignore rules from {} (base='{}')",
363 loaded, path.string(), base_label);
373 if (!fs::exists(root) || !fs::is_directory(root)) {
374 logger->warn(
"IgnoreMatcher::load: root does not exist: {}",
379 auto canonical_root = fs::weakly_canonical(root);
380 fs::path root_gi = canonical_root /
".gitignore";
381 if (fs::exists(root_gi)) { load_file(root_gi,
""); }
383 load_nested_gitignores(canonical_root, root_gi);
385 fs::path explorer = canonical_root /
".explorerignore";
386 if (fs::exists(explorer)) { load_file(explorer,
""); }
396void IgnoreMatcher::load_nested_gitignores(
397 const fs::path& canonical_root,
const fs::path& root_gi) {
403 auto it = fs::recursive_directory_iterator(
405 fs::directory_options::skip_permission_denied);
406 for (
auto& entry : it) {
407 if (!entry.is_regular_file()) {
continue; }
408 if (entry.path().filename() !=
".gitignore") {
continue; }
409 if (entry.path() == root_gi) {
continue; }
410 auto rel_dir = fs::relative(entry.path().parent_path(),
412 load_file(entry.path(), to_slash(rel_dir));
414 }
catch (
const std::exception& e) {
415 logger->warn(
"Recursive gitignore scan aborted: {}", e.what());
426 bool ignored =
false;
427 for (
const auto& rule : rules_) {
428 bool match_under = std::regex_match(rel_path, rule.re_under);
429 bool match_exact = std::regex_match(rel_path, rule.re_exact);
434 bool exact_counts = match_exact
435 && (!rule.dir_only || is_dir);
436 if (match_under || exact_counts) {
437 ignored = !rule.negate;
void load(const std::filesystem::path &root)
Load gitignore + explorerignore from a workspace root.
void add_pattern(const std::string &pattern, const std::filesystem::path &base={})
Add a single pattern programmatically (test surface).
bool is_ignored(const std::string &rel_path, bool is_dir) const
Test whether a path is ignored.
Path-relative ignore matching honoring .gitignore + .explorerignore.
spdlog initialization and logger access.
ENTROPIC_EXPORT std::shared_ptr< spdlog::logger > get(const std::string &name)
Get or create a named logger.
Activate model on GPU (WARM → ACTIVE).