feat(cli): add --repo and --exclude filtering (fixes #5)

Add argparse-based CLI parsing with repeatable --repo/-r (include) and
--exclude/-x (exclude) options. Filtering is case-insensitive substring
matching, applied post-fetch in collect_all() per ADR-005.

- parse_args() separated from main() for testability
- main(argv=None) accepts argv for test injection
- collect_all() gains optional include/exclude parameters
- 14 new tests (8 filtering + 6 CLI parsing/integration)
- All 51 tests pass, backward compatible (no args = v1.0.0 behavior)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
sylvain
2026-03-11 04:35:42 +01:00
parent 0f8e34edf3
commit 2232260821
5 changed files with 231 additions and 19 deletions

View File

@@ -128,3 +128,81 @@ class TestCollectAll:
assert result[0].is_fork is True
assert result[0].is_archived is True
assert result[0].is_mirror is True
class TestCollectAllFiltering:
"""Test collect_all filtering (include/exclude)."""
def _setup_client(self, repo_names: list[str]) -> MagicMock:
"""Create a mock client returning repos with the given names."""
client = MagicMock()
client.get_repos.return_value = [
_make_repo(name=n, full_name=f"admin/{n}") for n in repo_names
]
client.get_latest_release.return_value = None
client.get_milestones.return_value = []
return client
def test_no_filter_returns_all(self):
"""Without include/exclude, all repos are returned (backward compat)."""
client = self._setup_client(["alpha", "beta", "gamma"])
result = collect_all(client)
assert [r.name for r in result] == ["alpha", "beta", "gamma"]
def test_include_single(self):
"""Include filters repos by substring match."""
client = self._setup_client(["gitea-dashboard", "infra-core", "notes"])
result = collect_all(client, include=["dashboard"])
assert [r.name for r in result] == ["gitea-dashboard"]
def test_include_multiple(self):
"""Multiple include patterns are OR-combined."""
client = self._setup_client(["gitea-dashboard", "infra-core", "notes"])
result = collect_all(client, include=["dashboard", "infra"])
assert [r.name for r in result] == ["gitea-dashboard", "infra-core"]
def test_exclude_single(self):
"""Exclude removes repos matching the substring."""
client = self._setup_client(["gitea-dashboard", "old-fork", "notes"])
result = collect_all(client, exclude=["fork"])
assert [r.name for r in result] == ["gitea-dashboard", "notes"]
def test_include_and_exclude(self):
"""Include is applied first, then exclude."""
client = self._setup_client(["projet-web", "projet-old", "infra"])
result = collect_all(client, include=["projet"], exclude=["old"])
assert [r.name for r in result] == ["projet-web"]
def test_case_insensitive(self):
"""Filtering is case-insensitive."""
client = self._setup_client(["Gitea-Dashboard", "infra"])
result = collect_all(client, include=["dashboard"])
assert [r.name for r in result] == ["Gitea-Dashboard"]
def test_no_match_returns_empty(self):
"""Returns empty list when no repo matches include filter."""
client = self._setup_client(["alpha", "beta"])
result = collect_all(client, include=["inexistant"])
assert result == []
def test_exclude_all_returns_empty(self):
"""Returns empty list when all repos are excluded."""
client = self._setup_client(["alpha", "beta"])
result = collect_all(client, exclude=["alpha", "beta"])
assert result == []