From 460db83c957498ffc498ecfef94d0272ee8bd5eb Mon Sep 17 00:00:00 2001 From: nankingjing <1079826437@qq.com> Date: Sat, 11 Jul 2026 19:56:01 +0800 Subject: [PATCH] fix(gitignore): anchor directory-only patterns correctly A trailing slash (e.g. build/) counted as an internal '/', so _transform_gitignore_line anchored the pattern to base_rel instead of matching it at any depth. Strip the trailing slash before the anchoring decision and re-append it afterward so directory-only markers keep their unanchored semantics. --- openviking/parse/gitignore.py | 18 +++++++---- tests/parse/test_gitignore_dir_anchoring.py | 34 +++++++++++++++++++++ 2 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 tests/parse/test_gitignore_dir_anchoring.py diff --git a/openviking/parse/gitignore.py b/openviking/parse/gitignore.py index 3351cdb302..e3a1070541 100644 --- a/openviking/parse/gitignore.py +++ b/openviking/parse/gitignore.py @@ -41,13 +41,19 @@ def _transform_gitignore_line(line: str, base_rel: str) -> str: return raw scoped = pattern - if scoped.startswith("/"): - scoped = scoped.lstrip("/") - scoped = f"{base_rel}/{scoped}" if scoped else base_rel - elif "/" in scoped: - scoped = f"{base_rel}/{scoped}" + is_dir_only = scoped.endswith("/") + body = scoped.rstrip("/") if is_dir_only else scoped + + if body.startswith("/"): + body = body.lstrip("/") + scoped = f"{base_rel}/{body}" if body else base_rel + elif "/" in body: + scoped = f"{base_rel}/{body}" else: - scoped = f"{base_rel}/**/{scoped}" if scoped else base_rel + scoped = f"{base_rel}/**/{body}" if body else base_rel + + if is_dir_only and scoped: + scoped = f"{scoped}/" return f"!{scoped}" if negated else scoped diff --git a/tests/parse/test_gitignore_dir_anchoring.py b/tests/parse/test_gitignore_dir_anchoring.py new file mode 100644 index 0000000000..1358e079ed --- /dev/null +++ b/tests/parse/test_gitignore_dir_anchoring.py @@ -0,0 +1,34 @@ +# Copyright (c) 2026 Beijing Volcano Engine Technology Co., Ltd. +# SPDX-License-Identifier: AGPL-3.0 +"""Tests for _transform_gitignore_line directory-pattern anchoring. + +A trailing-slash directory pattern (e.g. ``build/``) from a nested .gitignore +must remain unanchored (match anywhere under base_rel) instead of being scoped +directly to base_rel because the trailing ``/`` made it look like a path with +a separator. +""" + +from openviking.parse.gitignore import _transform_gitignore_line + + +class TestTransformGitignoreDirAnchoring: + def test_dir_only_pattern_stays_unanchored(self): + """``build/`` must expand to ``src/**/build/`` (match anywhere), not ``src/build/``.""" + result = _transform_gitignore_line("build/", "src") + assert result == "src/**/build/" + + def test_plain_pattern_without_slash_is_unanchored(self): + """``build`` (no slash) matches anywhere under base_rel.""" + assert _transform_gitignore_line("build", "src") == "src/**/build" + + def test_leading_slash_pattern_is_anchored(self): + """``/build`` is anchored directly to base_rel.""" + assert _transform_gitignore_line("/build", "src") == "src/build" + + def test_dir_only_with_leading_slash_stays_anchored(self): + """``/build/`` stays anchored to base_rel but preserves the dir-only slash.""" + assert _transform_gitignore_line("/build/", "src") == "src/build/" + + def test_negated_dir_only_pattern_preserves_bang(self): + """Negation prefix is preserved through the unanchored transform.""" + assert _transform_gitignore_line("!build/", "src") == "!src/**/build/"