Skip to content

Commit 3ab611e

Browse files
authored
fix: don't exit with code 1 when there is nothing to review (#154)
1 parent 17b26b2 commit 3ab611e

19 files changed

Lines changed: 152 additions & 32 deletions

File tree

scripts/evaluate_review_quality.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def perform_review(
111111
technologies=("Python", "Django", "FastAPI"),
112112
),
113113
)
114-
review = code_reviewer.review_pull_request(target=url)
114+
review = code_reviewer.review(target=url)
115115
write_review_to_dir(model, output_dir, pr_name, sample, review)
116116

117117

src/lgtm_ai/__main__.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
)
1616
from lgtm_ai.ai.schemas import AgentSettings, CommentCategory, SupportedAIModelsList
1717
from lgtm_ai.base.constants import DEFAULT_HTTPX_TIMEOUT
18+
from lgtm_ai.base.exceptions import NothingToReviewError
1819
from lgtm_ai.base.schemas import IssuesPlatform, LocalRepository, OutputFormat, PRUrl
1920
from lgtm_ai.base.utils import git_source_supports_multiline_suggestions
2021
from lgtm_ai.config.constants import DEFAULT_INPUT_TOKEN_LIMIT
@@ -200,12 +201,18 @@ def review(target: PRUrl | LocalRepository, config: str | None, verbose: int, **
200201
git_client=git_client,
201202
config=resolved_config,
202203
)
203-
review = code_reviewer.review_pull_request(target=target)
204-
logger.info("Review completed, total comments: %d", len(review.review_response.comments))
205204

205+
formatter, printer = _get_formatter_and_printer(resolved_config.output_format)
206+
try:
207+
review = code_reviewer.review(target=target)
208+
except NothingToReviewError:
209+
if not resolved_config.silent:
210+
printer(formatter.empty_review_message())
211+
return
212+
213+
logger.info("Review completed, total comments: %d", len(review.review_response.comments))
206214
if not resolved_config.silent:
207215
logger.info("Printing review to console")
208-
formatter, printer = _get_formatter_and_printer(resolved_config.output_format)
209216
printer(formatter.format_review_summary_section(review))
210217
if review.review_response.comments:
211218
printer(formatter.format_review_comments_section(review.review_response.comments))
@@ -253,11 +260,18 @@ def guide(
253260
git_client=git_client,
254261
config=resolved_config,
255262
)
256-
guide = review_guide.generate_review_guide(pr_url=target)
263+
264+
formatter, printer = _get_formatter_and_printer(resolved_config.output_format)
265+
266+
try:
267+
guide = review_guide.generate_review_guide(pr_url=target)
268+
except NothingToReviewError:
269+
if not resolved_config.silent:
270+
printer(formatter.empty_guide_message())
271+
return
257272

258273
if not resolved_config.silent:
259274
logger.info("Printing review to console")
260-
formatter, printer = _get_formatter_and_printer(resolved_config.output_format)
261275
printer(formatter.format_guide(guide))
262276

263277
if resolved_config.publish and git_client:

src/lgtm_ai/config/handler.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,9 @@ class CliOptions(BaseModel):
6969
class ResolvedConfig(
7070
BaseSettings,
7171
):
72-
"""Resolved configuration class to hold the final configuration.
72+
"""Resolved configuration class to hold the final configuration used throghought the cli app.
7373
74-
All intrinsic values are non-nullable and have appropriate defaults. Optional settings passed toward pydantic-ai Agents are nullable, as they have their own defaults in the library.
75-
76-
This class uses Pydantic Settings to automatically load configuration from multiple sources:
77-
1. CLI arguments (highest priority)
78-
2. Configuration files (lgtm.toml, pyproject.toml)
79-
3. Environment variables (lowest priority)
74+
It will hold the merged configuration from all sources (CLI args, config files, env vars, defaults).
8075
"""
8176

8277
model_config = SettingsConfigDict(
@@ -135,6 +130,7 @@ class ResolvedConfig(
135130
"""If reviewing a local repository, what to compare against (branch, commit, or HEAD for working dir)."""
136131

137132
# Secrets - these will be loaded from environment variables with LGTM_ prefix
133+
# They are not displayed on logs or reprs.
138134
git_api_key: str = Field(repr=False)
139135
"""API key to interact with the git service (GitLab, GitHub, etc.)."""
140136

@@ -270,7 +266,7 @@ def resolve_config(self, target: PRUrl | LocalRepository) -> ResolvedConfig:
270266

271267
@classmethod
272268
def _create_dynamic_settings_class(cls, config_file_path: str | None) -> type[ResolvedConfig]:
273-
"""Dynamically create a ResolvedConfig subclass with custom settings sources if needed."""
269+
"""Dynamically create a ResolvedConfig subclass that uses a custom config file path if given."""
274270

275271
class DynamicResolvedConfig(ResolvedConfig):
276272
@override
@@ -284,6 +280,7 @@ def settings_customise_sources(
284280
file_secret_settings: PydanticBaseSettingsSource,
285281
) -> tuple[PydanticBaseSettingsSource, ...]:
286282
if not config_file_path:
283+
# No custom config file path provided, use default behavior
287284
return super().settings_customise_sources(
288285
settings_cls,
289286
init_settings,

src/lgtm_ai/formatters/base.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,9 @@ def format_review_comments_section(self, comments: list[ReviewComment]) -> _T: .
3030
def format_review_comment(self, comment: ReviewComment, *, with_footer: bool = True) -> _T: ...
3131

3232
def format_guide(self, guide: ReviewGuide) -> _T: ...
33+
34+
def empty_review_message(self) -> str:
35+
"""Message to display when nothing was reviewed."""
36+
37+
def empty_guide_message(self) -> str:
38+
"""Message to display when no guide was generated."""

src/lgtm_ai/formatters/json.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import json
2+
13
from lgtm_ai.ai.schemas import Review, ReviewComment, ReviewGuide
24
from lgtm_ai.formatters.base import Formatter
35

@@ -26,3 +28,9 @@ def format_review_comment(self, comment: ReviewComment, *, with_footer: bool = T
2628
def format_guide(self, guide: ReviewGuide) -> str:
2729
"""Format the review guide as JSON."""
2830
return guide.model_dump_json(indent=2, exclude={"pr_diff"})
31+
32+
def empty_review_message(self) -> str:
33+
return json.dumps({"review_response": None, "metadata": None}, indent=2)
34+
35+
def empty_guide_message(self) -> str:
36+
return json.dumps({"guide_response": None, "metadata": None}, indent=2)

src/lgtm_ai/formatters/markdown.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ def format_guide(self, guide: ReviewGuide) -> str:
7575
metadata=metadata,
7676
)
7777

78+
def empty_review_message(self) -> str:
79+
return "> ⚠️ No files to review (all files excluded by provided configuration)."
80+
81+
def empty_guide_message(self) -> str:
82+
return "> ⚠️ No files to generate a guide for (all files excluded by provided configuration)."
83+
7884
def _format_snippet(self, comment: ReviewComment) -> str:
7985
template = self._template_env.get_template(self.SNIPPET_TEMPLATE)
8086
return template.render(language=comment.programming_language.lower(), snippet=comment.quote_snippet)

src/lgtm_ai/formatters/pretty.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,9 @@ def format_guide(self, guide: ReviewGuide) -> Layout:
103103
*references,
104104
)
105105
return layout
106+
107+
def empty_review_message(self) -> str:
108+
return "✔ No files to review (all files excluded by provided configuration)."
109+
110+
def empty_guide_message(self) -> str:
111+
return "✔ No files to generate a guide for (all files excluded by provided configuration)."

src/lgtm_ai/git/parser.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class DiffResult(BaseModel):
3333

3434

3535
def parse_diff_patch(metadata: DiffFileMetadata, diff_text: object) -> DiffResult:
36+
"""Parse a unified diff patch and return the modified lines with their metadata."""
3637
if not isinstance(diff_text, str):
3738
raise GitDiffParseError("Diff text is not a string")
3839

src/lgtm_ai/git_client/gitlab.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ def _attempt_comment_at_positions(
230230
except gitlab.exceptions.GitlabError:
231231
comment_create_success = False
232232
logger.debug(
233-
"Failed to post the comment anywhere specific, it will go to general decription (hopefully)"
233+
"Failed to post the comment anywhere specific, it will go to general description (hopefully)"
234234
)
235235

236236
return comment_create_success

src/lgtm_ai/jira/jira.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ def __init__(self, issues_user: str, issues_api_key: str, httpx_client: httpx.Cl
1717
self._httpx_client = httpx_client
1818

1919
def get_issue_content(self, issues_url: HttpUrl, issue_id: str) -> IssueContent | None:
20+
"""Fetch the content of an issue from the base URL of the issues page.
21+
22+
Returns None if the issue cannot be fetched or parsed.
23+
"""
2024
api_url = f"https://{issues_url.host}/rest/api/{self.API_VERSION}/issue/{issue_id}"
2125

2226
try:

0 commit comments

Comments
 (0)