diff --git a/README.md b/README.md index 522ccca..085a3fc 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ MR Reviewer automatise la revue de code sur GitLab en s'appuyant sur un moteur d ## Fonctionnalités principales - Interrogation périodique de GitLab pour récupérer les MRs ouvertes +- Possibilité de ne traiter qu'un projet spécifique via `PROJECT_ID` - Analyse du diff via Mistral et publication automatisée des commentaires - Gestion du nombre maximal de tokens et filtrage des MRs trop volumineuses - Système de cache pour éviter les doublons @@ -33,6 +34,7 @@ GITLAB_URL=https://gitlab.com/api/v4 MISTRAL_API_KEY=sk-xxxx CACHE_FILE=cache.json POLLING_INTERVAL=300 +PROJECT_ID=123 # Laisser vide pour traiter toutes vos MR MAX_TOKENS=8000 LOG_LEVEL=INFO # Optionnel : fichier où enregistrer les logs @@ -47,7 +49,7 @@ python -m app.main ## Comportement général -- Le scheduler déclenche `review_merge_requests()` à l'intervalle défini +- Le scheduler déclenche immédiatement `review_merge_requests()` puis à l'intervalle défini - Pour chaque MR détectée : - si elle a déjà été traitée, elle est ignorée - si le diff dépasse la limite autorisée, un message explicatif est envoyé diff --git a/app/adapters/gitlab_adapter.py b/app/adapters/gitlab_adapter.py index e98435a..8d90dc7 100644 --- a/app/adapters/gitlab_adapter.py +++ b/app/adapters/gitlab_adapter.py @@ -42,14 +42,15 @@ def _request(self, method: str, url: str, **kwargs) -> tuple[any, dict]: raise return resp.json(), resp.headers - def get_open_merge_requests(self) -> Iterable[MergeRequest]: - """ - Retrieve all open merge requests from GitLab. + def get_open_merge_requests(self, project_id: int | None = None) -> Iterable[MergeRequest]: + """Retrieve open merge requests. - Returns: - Iterable[MergeRequest]: An iterable of MergeRequest objects representing open merge requests. + When ``project_id`` is provided, only MRs of that project are returned. """ - url = f"{self.base_url}/merge_requests" + if project_id is not None: + url = f"{self.base_url}/projects/{project_id}/merge_requests" + else: + url = f"{self.base_url}/merge_requests" params = {"state": "opened", "per_page": 100, "page": 1} results: List[MergeRequest] = [] while True: diff --git a/app/main.py b/app/main.py index 9586381..ff920eb 100644 --- a/app/main.py +++ b/app/main.py @@ -1,4 +1,5 @@ import time + from app.adapters.gitlab_adapter import GitLabAdapter from app.adapters.mistral_adapter import MistralAdapter from app.adapters.cache_adapter import JSONCacheAdapter @@ -9,25 +10,23 @@ def main(): - """ - Main entry point for the application. - - Initializes the logging, adapters, and starts the scheduler to review merge requests. - This function is responsible for setting up the environment and starting the review process. - """ + """Main entry point for the application.""" configure_logging() gitlab = GitLabAdapter(token=settings.gitlab_token, base_url=settings.gitlab_url) ai = MistralAdapter(api_key=settings.mistral_api_key, model=settings.mistral_model) cache = JSONCacheAdapter(cache_file=settings.cache_file) start_scheduler( - lambda: run_merge_request_review(gitlab, ai, cache), settings.polling_interval + lambda: run_merge_request_review(gitlab, ai, cache, project_id=settings.project_id), + settings.polling_interval, ) try: while True: time.sleep(1) except KeyboardInterrupt: + pass + finally: stop_scheduler() diff --git a/app/ports/output/gitlab_port.py b/app/ports/output/gitlab_port.py index af93e2e..1b8f305 100644 --- a/app/ports/output/gitlab_port.py +++ b/app/ports/output/gitlab_port.py @@ -10,8 +10,12 @@ class GitLabPort(ABC): """Interface used to interact with GitLab.""" @abstractmethod - def get_open_merge_requests(self) -> Iterable[MergeRequest]: - """Return currently opened merge requests.""" + def get_open_merge_requests(self, project_id: int | None = None) -> Iterable[MergeRequest]: + """Return currently opened merge requests. + + Args: + project_id: restrict search to this project when provided. + """ raise NotImplementedError diff --git a/app/scheduler/polling.py b/app/scheduler/polling.py index ffb4a71..df700eb 100644 --- a/app/scheduler/polling.py +++ b/app/scheduler/polling.py @@ -21,6 +21,7 @@ def start_scheduler(task: Callable[[], None], interval: int) -> None: global _scheduler scheduler = BackgroundScheduler() scheduler.add_job(task, "interval", seconds=interval) + task() scheduler.start() _scheduler = scheduler logger.info("Scheduler démarré (intervalle=%ss)", interval) diff --git a/app/usecases/review_merge_requests.py b/app/usecases/review_merge_requests.py index 609de77..9f5b62c 100644 --- a/app/usecases/review_merge_requests.py +++ b/app/usecases/review_merge_requests.py @@ -10,12 +10,14 @@ from config.settings import settings -def run_merge_request_review(gitlab: GitLabPort, ai: AIPort, cache: CachePort) -> None: +def run_merge_request_review( + gitlab: GitLabPort, ai: AIPort, cache: CachePort, project_id: int | None = None +) -> None: """Fetch opened MRs and post an automated review when possible.""" logger = logging.getLogger("Review") engine = ReviewEngine(ai, settings.max_tokens) - for mr in gitlab.get_open_merge_requests(): + for mr in gitlab.get_open_merge_requests(project_id=project_id): if cache.is_up_to_date(mr.id, mr.sha): logger.debug("MR %s déjà analysée", mr.id) continue diff --git a/config/settings.py b/config/settings.py index 64ffda7..157fb3d 100644 --- a/config/settings.py +++ b/config/settings.py @@ -12,6 +12,7 @@ class Settings(BaseSettings): cache_file: str = Field(default="cache.json", env="CACHE_FILE") polling_interval: int = Field(default=300, env="POLLING_INTERVAL") + project_id: int | None = Field(default=None, env="PROJECT_ID") max_tokens: int = Field(default=8000, env="MAX_TOKENS") log_level: str = Field(default="INFO", env="LOG_LEVEL") log_file: str | None = Field(default=None, env="LOG_FILE") diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 185e7d0..65ec149 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -77,7 +77,7 @@ Définit les interfaces abstraites que les adaptateurs techniques doivent implé Interface : `review_diff(diff: str) -> str` - **gitlab_port.py** - Interface : `get_open_merge_requests()`, `get_diff(mr_id)`, `post_comment(mr_id, text)` + Interface : `get_open_merge_requests(project_id=None)`, `get_diff(mr_id)`, `post_comment(mr_id, text)` - **cache_port.py** Interface : `is_up_to_date(mr_id, sha)`, `update_reviewed(mr_id, sha)` diff --git a/tests/integration/test_usecase_run_review.py b/tests/integration/test_usecase_run_review.py index df1219a..653fa0a 100644 --- a/tests/integration/test_usecase_run_review.py +++ b/tests/integration/test_usecase_run_review.py @@ -5,7 +5,8 @@ class DummyGitLab: def __init__(self): self.comments = [] - def get_open_merge_requests(self): + + def get_open_merge_requests(self, project_id=None): return [MergeRequest(id=1, project_id=1, sha="s1")] def get_diff(self, project_id, mr_id): return "good diff" @@ -33,7 +34,7 @@ def test_run_merge_request_review(monkeypatch): ai = DummyAI() cache = DummyCache() monkeypatch.setattr(usecase, "settings", SimpleNamespace(max_tokens=100)) - usecase.run_merge_request_review(gitlab, ai, cache) + usecase.run_merge_request_review(gitlab, ai, cache, project_id=None) assert ai.called == "good diff" assert gitlab.comments == ["reviewed"] assert cache.data[1] == "s1" @@ -44,5 +45,5 @@ def test_skip_when_diff_too_large(monkeypatch): ai = DummyAI() cache = DummyCache() monkeypatch.setattr(usecase, "settings", SimpleNamespace(max_tokens=1)) - usecase.run_merge_request_review(gitlab, ai, cache) + usecase.run_merge_request_review(gitlab, ai, cache, project_id=None) assert gitlab.comments == ["Diff trop volumineux pour analyse automatique."] diff --git a/tests/unit/test_gitlab_adapter.py b/tests/unit/test_gitlab_adapter.py index 5f1a57a..1d332b1 100644 --- a/tests/unit/test_gitlab_adapter.py +++ b/tests/unit/test_gitlab_adapter.py @@ -37,6 +37,22 @@ def fake_request(method, url, **kwargs): assert calls[-1][2]["params"]["page"] == "2" +def test_get_open_merge_requests_for_project(monkeypatch): + resp = DummyResponse([], headers={}) + captured = {} + + def fake_request(method, url, **kwargs): + captured["url"] = url + captured["params"] = kwargs["params"] + return resp + + monkeypatch.setattr(requests, "request", fake_request) + adapter = GitLabAdapter(token="t", base_url="http://x") + list(adapter.get_open_merge_requests(project_id=5)) + assert captured["url"].endswith("/projects/5/merge_requests") + assert captured["params"]["state"] == "opened" + + def test_get_diff_and_post_comment(monkeypatch): def fake_request(method, url, **kwargs): if method == "GET": diff --git a/tests/unit/test_main.py b/tests/unit/test_main.py index 0f9a17a..25e3867 100644 --- a/tests/unit/test_main.py +++ b/tests/unit/test_main.py @@ -24,6 +24,7 @@ def test_main_calls_dependencies( mock_settings.mistral_model = "model" mock_settings.cache_file = "cache.json" mock_settings.polling_interval = 42 + mock_settings.project_id = None with ( patch("app.main.time.sleep", side_effect=KeyboardInterrupt), @@ -66,10 +67,13 @@ def test_main_runs_lambda(mock_start_scheduler): mock_settings.mistral_model = "model" mock_settings.cache_file = "cache.json" mock_settings.polling_interval = 42 + mock_settings.project_id = 123 app.main.main() # Extract the lambda and call it lambda_func = mock_start_scheduler.call_args[0][0] lambda_func() - mock_review.assert_called_once_with(fake_gitlab, fake_ai, fake_cache) + mock_review.assert_called_once_with( + fake_gitlab, fake_ai, fake_cache, project_id=123 + ) diff --git a/tests/unit/test_polling_scheduler.py b/tests/unit/test_polling_scheduler.py index 36a7d4e..94ff9a1 100644 --- a/tests/unit/test_polling_scheduler.py +++ b/tests/unit/test_polling_scheduler.py @@ -30,6 +30,7 @@ def task(): called.append(True) polling.start_scheduler(task, interval=5) + assert called == [True] assert dummy.started assert dummy.jobs[0][1] == "interval" and dummy.jobs[0][2] == 5 assert polling._scheduler is dummy