diff --git a/openviking/parse/gitignore.py b/openviking/parse/gitignore.py index 3351cdb30..e3a107054 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 000000000..1358e079e --- /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/"