diff --git a/src/review_classification/cli/app.py b/src/review_classification/cli/app.py index a1cf9ed..6f5d4bb 100644 --- a/src/review_classification/cli/app.py +++ b/src/review_classification/cli/app.py @@ -51,6 +51,7 @@ def _fetch_repo( reset_db: bool, verbose: bool, incremental: bool = False, + skip_empty_incremental: bool = False, ) -> None: """Fetch and store PRs for a single repository.""" repo = GitHubRepo.from_string(repo_name) @@ -75,6 +76,13 @@ def _fetch_repo( f"Incremental fetch: Found latest PR date {start_date} in database." ) else: + if skip_empty_incremental: + typer.echo( + f"Skipping {full_name}: no existing PR data found for " + "incremental fetch.", + err=True, + ) + return typer.echo( f"Error: Cannot use --incremental for {full_name} " "on an initial fetch or empty database.", @@ -307,6 +315,7 @@ def add_repo(rc: RepoConfig) -> None: # Create a RepoConfig inheriting from org_cfg, then global rc = RepoConfig( name=r, + from_organization=True, collate_start=( org_cfg.collate_start if org_cfg.collate_start is not None @@ -353,6 +362,7 @@ def add_repo(rc: RepoConfig) -> None: for r in org_repos: rc = RepoConfig( name=r, + from_organization=True, collate_start=default_collate_start, collate_end=default_collate_end, threshold=default_threshold, @@ -456,6 +466,7 @@ def fetch( reset_db, verbose, incremental=incremental, + skip_empty_incremental=target.from_organization, ) except (FileNotFoundError, ValueError) as e: diff --git a/src/review_classification/cli/config.py b/src/review_classification/cli/config.py index 9a20fab..92d2c22 100644 --- a/src/review_classification/cli/config.py +++ b/src/review_classification/cli/config.py @@ -16,6 +16,7 @@ class RepoConfig: min_samples: int | None = None start: str | None = None end: str | None = None + from_organization: bool = False @dataclass @@ -63,6 +64,7 @@ def resolve(self, repo: RepoConfig) -> RepoConfig: """ return RepoConfig( name=repo.name, + from_organization=repo.from_organization, collate_start=( repo.collate_start if repo.collate_start is not None diff --git a/tests/cli/test_fetch.py b/tests/cli/test_fetch.py new file mode 100644 index 0000000..eab8aaf --- /dev/null +++ b/tests/cli/test_fetch.py @@ -0,0 +1,102 @@ +"""Tests for fetch command behavior.""" + +from datetime import UTC, datetime +from importlib import import_module +from pathlib import Path +from typing import Any + +import pytest +import typer + +cli_app = import_module("review_classification.cli.app") + + +def _org_repos(_org_name: str) -> list[str]: + return ["my-org/existing", "my-org/new"] + + +def _save_pr(_pr: Any) -> None: + return None + + +def _no_latest_pr_date(_repo_name: str) -> None: + return None + + +def test_incremental_org_fetch_skips_repo_without_existing_data( + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], +) -> None: + """Org incremental fetches skip newly discovered repos without DB history.""" + fetched_repos: list[str] = [] + + def latest_pr_date(repo_name: str) -> datetime | None: + if repo_name == "my-org/existing": + return datetime(2024, 1, 15, tzinfo=UTC) + return None + + def fetch_prs( + repo_name: str, + start_date: str | None = None, + end_date: str | None = None, + token: str | None = None, + ) -> list[Any]: + del start_date, end_date, token + fetched_repos.append(repo_name) + return [] + + monkeypatch.setenv("GITHUB_TOKEN", "test-token") + monkeypatch.setattr( + "review_classification.queries.github_client.get_org_repos", _org_repos + ) + monkeypatch.setattr( + "review_classification.sqlite.database.get_latest_pr_date", latest_pr_date + ) + monkeypatch.setattr( + "review_classification.queries.github_client.fetch_prs", fetch_prs + ) + monkeypatch.setattr("review_classification.sqlite.database.init_db", lambda: None) + monkeypatch.setattr("review_classification.sqlite.database.save_pr", _save_pr) + + cli_app.fetch( + repo=None, + org=["my-org"], + config=None, + collate_start=None, + collate_end=None, + reset_db=False, + verbose=False, + incremental=True, + ) + + captured = capsys.readouterr() + assert fetched_repos == ["my-org/existing"] + assert ( + "Skipping my-org/new: no existing PR data found for incremental fetch." + in captured.err + ) + + +def test_incremental_repo_fetch_still_errors_without_existing_data( + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, +) -> None: + """Explicit repo incremental fetches still require existing DB history.""" + monkeypatch.chdir(tmp_path) + monkeypatch.setattr( + "review_classification.sqlite.database.get_latest_pr_date", _no_latest_pr_date + ) + + with pytest.raises(typer.Exit) as exc_info: + cli_app.fetch( + repo=["my-org/new"], + org=None, + config=None, + collate_start=None, + collate_end=None, + reset_db=False, + verbose=False, + incremental=True, + ) + + assert exc_info.value.exit_code == 1