Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions src/dippy/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -672,7 +672,9 @@ def _match_words(
matched = fnmatch.fnmatch(normalized_cmd, normalized_pattern)
# Trailing ' *' also matches bare command (no args)
if not matched and normalized_pattern.endswith(" *"):
matched = normalized_cmd == normalized_pattern[:-2]
base = normalized_pattern[:-2]
if not fnmatch.fnmatch("", base):
matched = fnmatch.fnmatch(normalized_cmd, base)
if matched:
result = Match(
decision=rule.decision,
Expand Down Expand Up @@ -820,7 +822,9 @@ def match_after(words: list[str], config: Config, cwd: Path) -> str | None:
matched = fnmatch.fnmatch(normalized_cmd, normalized_pattern)
# Trailing ' *' also matches bare command (no args)
if not matched and normalized_pattern.endswith(" *"):
matched = normalized_cmd == normalized_pattern[:-2]
base = normalized_pattern[:-2]
if not fnmatch.fnmatch("", base):
matched = fnmatch.fnmatch(normalized_cmd, base)
if matched:
# message is None for pattern-only rules, "" for explicit empty
result = rule.message if rule.message is not None else ""
Expand Down
48 changes: 48 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1162,6 +1162,40 @@ def test_trailing_star_matches_rest(self, tmp_path):
match_command(cmd("git status --short --branch"), cfg, tmp_path) is not None
)

def test_trailing_star_fallback_uses_fnmatch(self, tmp_path):
"""Trailing ' *' fallback for bare commands should use fnmatch, not == (issue #110).

Pattern 'tea issue* close *' should match bare 'tea issues close'
because the base pattern 'tea issue* close' contains a glob.
"""
cfg = Config(
rules=[Rule("ask", "tea issue* close *", message="Confirm closing issue")]
)
# Full pattern matches with args
assert match_command(cmd("tea issues close 42"), cfg, tmp_path) is not None
# Trailing ' *' fallback should use fnmatch on 'tea issue* close'
assert match_command(cmd("tea issues close"), cfg, tmp_path) is not None

def test_trailing_star_fallback_degenerate_base(self, tmp_path):
"""Pattern '* *' should NOT match bare commands — base '*' is too permissive."""
cfg = Config(rules=[Rule("allow", "* *")])
assert match_command(cmd("git status"), cfg, tmp_path) is not None # has args
assert match_command(cmd("ls"), cfg, tmp_path) is None # bare, should not match

def test_trailing_star_fallback_glob_suffix(self, tmp_path):
"""Pattern '*foo *' should match bare 'barfoo' via fnmatch fallback."""
cfg = Config(rules=[Rule("allow", "*foo *")])
assert match_command(cmd("barfoo baz"), cfg, tmp_path) is not None
assert match_command(cmd("barfoo"), cfg, tmp_path) is not None

def test_trailing_star_fallback_char_class(self, tmp_path):
"""Pattern 'git [cp]* *' should match bare 'git clone' via fnmatch fallback."""
cfg = Config(rules=[Rule("ask", "git [cp]* *")])
assert match_command(cmd("git clone repo"), cfg, tmp_path) is not None
assert match_command(cmd("git clone"), cfg, tmp_path) is not None
assert match_command(cmd("git push"), cfg, tmp_path) is not None
assert match_command(cmd("git status"), cfg, tmp_path) is None # s not in [cp]


class TestPatternNormalization:
"""Test that patterns are normalized against cwd for matching."""
Expand Down Expand Up @@ -1432,6 +1466,20 @@ def test_trailing_star_matches_bare_command(self, tmp_path):
result = match_after(["python"], cfg, tmp_path)
assert result == "Python ran"

def test_trailing_star_fallback_uses_fnmatch(self, tmp_path):
"""After rule trailing ' *' fallback should use fnmatch (issue #110)."""
cfg = Config(
after_rules=[Rule("after", "tea issue* close *", message="Issue closed")]
)
result = match_after(["tea", "issues", "close"], cfg, tmp_path)
assert result == "Issue closed"

def test_trailing_star_fallback_degenerate_base(self, tmp_path):
"""After rule '* *' should NOT match bare commands."""
cfg = Config(after_rules=[Rule("after", "* *", message="ran")])
assert match_after(["git", "status"], cfg, tmp_path) == "ran"
assert match_after(["ls"], cfg, tmp_path) is None

def test_path_normalization(self, tmp_path):
home = str(Path.home())
cfg = Config(after_rules=[Rule("after", f"{home}/bin/*", message="custom bin")])
Expand Down
Loading