Skip to content

Commit a98eced

Browse files
authored
Merge pull request #2071 from BTLzdravtech/main
feat: improvements for Gitea integration
2 parents 6d22618 + bf1cc50 commit a98eced

File tree

3 files changed

+162
-14
lines changed

3 files changed

+162
-14
lines changed

pr_agent/git_providers/gitea_provider.py

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import hashlib
21
import json
32
from typing import Any, Dict, List, Optional, Set, Tuple
43
from urllib.parse import urlparse
@@ -31,15 +30,15 @@ def __init__(self, url: Optional[str] = None):
3130
self.pr_url = ""
3231
self.issue_url = ""
3332

34-
gitea_access_token = get_settings().get("GITEA.PERSONAL_ACCESS_TOKEN", None)
35-
if not gitea_access_token:
33+
self.gitea_access_token = get_settings().get("GITEA.PERSONAL_ACCESS_TOKEN", None)
34+
if not self.gitea_access_token:
3635
self.logger.error("Gitea access token not found in settings.")
3736
raise ValueError("Gitea access token not found in settings.")
3837

3938
self.repo_settings = get_settings().get("GITEA.REPO_SETTING", None)
4039
configuration = giteapy.Configuration()
4140
configuration.host = "{}/api/v1".format(self.base_url)
42-
configuration.api_key['Authorization'] = f'token {gitea_access_token}'
41+
configuration.api_key['Authorization'] = f'token {self.gitea_access_token}'
4342

4443
if get_settings().get("GITEA.SKIP_SSL_VERIFICATION", False):
4544
configuration.verify_ssl = False
@@ -223,6 +222,19 @@ def get_pr_url(self) -> str:
223222
def get_issue_url(self) -> str:
224223
return self.issue_url
225224

225+
def get_latest_commit_url(self) -> str:
226+
return self.last_commit.html_url
227+
228+
def get_comment_url(self, comment) -> str:
229+
return comment.html_url
230+
231+
def publish_persistent_comment(self, pr_comment: str,
232+
initial_header: str,
233+
update_header: bool = True,
234+
name='review',
235+
final_update_message=True):
236+
self.publish_persistent_comment_full(pr_comment, initial_header, update_header, name, final_update_message)
237+
226238
def publish_comment(self, comment: str,is_temporary: bool = False) -> None:
227239
"""Publish a comment to the pull request"""
228240
if is_temporary and not get_settings().config.publish_output_progress:
@@ -308,7 +320,7 @@ def publish_inline_comments(self, comments: List[Dict[str, Any]],body : str = "I
308320

309321
if not response:
310322
self.logger.error("Failed to publish inline comment")
311-
return None
323+
return
312324

313325
self.logger.info("Inline comment published")
314326

@@ -515,6 +527,13 @@ def get_line_link(self, relevant_file, relevant_line_start, relevant_line_end =
515527
self.logger.info(f"Generated link: {link}")
516528
return link
517529

530+
def get_pr_id(self):
531+
try:
532+
pr_id = f"{self.repo}/{self.pr_number}"
533+
return pr_id
534+
except:
535+
return ""
536+
518537
def get_files(self) -> List[Dict[str, Any]]:
519538
"""Get all files in the PR"""
520539
return [file.get("filename","") for file in self.git_files]
@@ -551,7 +570,7 @@ def get_pr_branch(self) -> str:
551570
if not self.pr:
552571
self.logger.error("Failed to get PR branch")
553572
return ""
554-
573+
555574
if not self.pr.head:
556575
self.logger.error("PR head not found")
557576
return ""
@@ -611,6 +630,9 @@ def is_supported(self, capability) -> bool:
611630
"""Check if the provider is supported"""
612631
return True
613632

633+
def get_git_repo_url(self, issues_or_pr_url: str) -> str:
634+
return f"{self.base_url}/{self.owner}/{self.repo}.git" #base_url / <OWNER>/<REPO>.git
635+
614636
def publish_description(self, pr_title: str, pr_body: str) -> None:
615637
"""Publish PR description"""
616638
response = self.repo_api.edit_pull_request(
@@ -685,6 +707,35 @@ def remove_initial_comment(self) -> None:
685707
continue
686708
self.logger.info(f"Removed initial comment: {comment.get('comment_id')}")
687709

710+
#Clone related
711+
def _prepare_clone_url_with_token(self, repo_url_to_clone: str) -> str | None:
712+
#For example, to clone:
713+
#https://github.com/Codium-ai/pr-agent-pro.git
714+
#Need to embed inside the github token:
715+
#https://<token>@github.com/Codium-ai/pr-agent-pro.git
716+
717+
gitea_token = self.gitea_access_token
718+
gitea_base_url = self.base_url
719+
scheme = gitea_base_url.split("://")[0]
720+
scheme += "://"
721+
if not all([gitea_token, gitea_base_url]):
722+
get_logger().error("Either missing auth token or missing base url")
723+
return None
724+
base_url = gitea_base_url.split(scheme)[1]
725+
if not base_url:
726+
get_logger().error(f"Base url: {gitea_base_url} has an empty base url")
727+
return None
728+
if base_url not in repo_url_to_clone:
729+
get_logger().error(f"url to clone: {repo_url_to_clone} does not contain {base_url}")
730+
return None
731+
repo_full_name = repo_url_to_clone.split(base_url)[-1]
732+
if not repo_full_name:
733+
get_logger().error(f"url to clone: {repo_url_to_clone} is malformed")
734+
return None
735+
736+
clone_url = scheme
737+
clone_url += f"{gitea_token}@{base_url}{repo_full_name}"
738+
return clone_url
688739

689740
class RepoApi(giteapy.RepositoryApi):
690741
def __init__(self, client: giteapy.ApiClient):
@@ -693,7 +744,7 @@ def __init__(self, client: giteapy.ApiClient):
693744
self.logger = get_logger()
694745
super().__init__(client)
695746

696-
def create_inline_comment(self, owner: str, repo: str, pr_number: int, body : str ,commit_id : str, comments: List[Dict[str, Any]]) -> None:
747+
def create_inline_comment(self, owner: str, repo: str, pr_number: int, body : str ,commit_id : str, comments: List[Dict[str, Any]]):
697748
body = {
698749
"body": body,
699750
"comments": comments,

pr_agent/servers/gitea_app.py

Lines changed: 99 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import asyncio
21
import copy
32
import os
3+
import re
44
from typing import Any, Dict
55

66
from fastapi import APIRouter, FastAPI, HTTPException, Request, Response
@@ -10,7 +10,9 @@
1010
from starlette_context.middleware import RawContextMiddleware
1111

1212
from pr_agent.agent.pr_agent import PRAgent
13+
from pr_agent.algo.utils import update_settings_from_args
1314
from pr_agent.config_loader import get_settings, global_settings
15+
from pr_agent.git_providers.utils import apply_repo_settings
1416
from pr_agent.log import LoggingFormat, get_logger, setup_logger
1517
from pr_agent.servers.utils import verify_signature
1618

@@ -50,7 +52,7 @@ async def get_body(request: Request):
5052
if not signature_header:
5153
get_logger().error("Missing signature header")
5254
raise HTTPException(status_code=400, detail="Missing signature header")
53-
55+
5456
try:
5557
verify_signature(body_bytes, webhook_secret, f"sha256={signature_header}")
5658
except Exception as ex:
@@ -70,6 +72,9 @@ async def handle_request(body: Dict[str, Any], event: str):
7072

7173
# Handle different event types
7274
if event == "pull_request":
75+
if not should_process_pr_logic(body):
76+
get_logger().debug(f"Request ignored: PR logic filtering")
77+
return {}
7378
if action in ["opened", "reopened", "synchronized"]:
7479
await handle_pr_event(body, event, action, agent)
7580
elif event == "issue_comment":
@@ -90,12 +95,21 @@ async def handle_pr_event(body: Dict[str, Any], event: str, action: str, agent:
9095

9196
# Handle PR based on action
9297
if action in ["opened", "reopened"]:
93-
commands = get_settings().get("gitea.pr_commands", [])
94-
for command in commands:
95-
await agent.handle_request(api_url, command)
98+
# commands = get_settings().get("gitea.pr_commands", [])
99+
await _perform_commands_gitea("pr_commands", agent, body, api_url)
100+
# for command in commands:
101+
# await agent.handle_request(api_url, command)
96102
elif action == "synchronized":
97103
# Handle push to PR
98-
await agent.handle_request(api_url, "/review --incremental")
104+
commands_on_push = get_settings().get(f"gitea.push_commands", {})
105+
handle_push_trigger = get_settings().get(f"gitea.handle_push_trigger", False)
106+
if not commands_on_push or not handle_push_trigger:
107+
get_logger().info("Push event, but no push commands found or push trigger is disabled")
108+
return
109+
get_logger().debug(f'A push event has been received: {api_url}')
110+
await _perform_commands_gitea("push_commands", agent, body, api_url)
111+
# for command in commands_on_push:
112+
# await agent.handle_request(api_url, command)
99113

100114
async def handle_comment_event(body: Dict[str, Any], event: str, action: str, agent: PRAgent):
101115
"""Handle comment events"""
@@ -113,6 +127,85 @@ async def handle_comment_event(body: Dict[str, Any], event: str, action: str, ag
113127

114128
await agent.handle_request(pr_url, comment_body)
115129

130+
async def _perform_commands_gitea(commands_conf: str, agent: PRAgent, body: dict, api_url: str):
131+
apply_repo_settings(api_url)
132+
if commands_conf == "pr_commands" and get_settings().config.disable_auto_feedback: # auto commands for PR, and auto feedback is disabled
133+
get_logger().info(f"Auto feedback is disabled, skipping auto commands for PR {api_url=}")
134+
return
135+
if not should_process_pr_logic(body): # Here we already updated the configuration with the repo settings
136+
return {}
137+
commands = get_settings().get(f"gitea.{commands_conf}")
138+
if not commands:
139+
get_logger().info(f"New PR, but no auto commands configured")
140+
return
141+
get_settings().set("config.is_auto_command", True)
142+
for command in commands:
143+
split_command = command.split(" ")
144+
command = split_command[0]
145+
args = split_command[1:]
146+
other_args = update_settings_from_args(args)
147+
new_command = ' '.join([command] + other_args)
148+
get_logger().info(f"{commands_conf}. Performing auto command '{new_command}', for {api_url=}")
149+
await agent.handle_request(api_url, new_command)
150+
151+
def should_process_pr_logic(body) -> bool:
152+
try:
153+
pull_request = body.get("pull_request", {})
154+
title = pull_request.get("title", "")
155+
pr_labels = pull_request.get("labels", [])
156+
source_branch = pull_request.get("head", {}).get("ref", "")
157+
target_branch = pull_request.get("base", {}).get("ref", "")
158+
sender = body.get("sender", {}).get("login")
159+
repo_full_name = body.get("repository", {}).get("full_name", "")
160+
161+
# logic to ignore PRs from specific repositories
162+
ignore_repos = get_settings().get("CONFIG.IGNORE_REPOSITORIES", [])
163+
if ignore_repos and repo_full_name:
164+
if any(re.search(regex, repo_full_name) for regex in ignore_repos):
165+
get_logger().info(f"Ignoring PR from repository '{repo_full_name}' due to 'config.ignore_repositories' setting")
166+
return False
167+
168+
# logic to ignore PRs from specific users
169+
ignore_pr_users = get_settings().get("CONFIG.IGNORE_PR_AUTHORS", [])
170+
if ignore_pr_users and sender:
171+
if any(re.search(regex, sender) for regex in ignore_pr_users):
172+
get_logger().info(f"Ignoring PR from user '{sender}' due to 'config.ignore_pr_authors' setting")
173+
return False
174+
175+
# logic to ignore PRs with specific titles
176+
if title:
177+
ignore_pr_title_re = get_settings().get("CONFIG.IGNORE_PR_TITLE", [])
178+
if not isinstance(ignore_pr_title_re, list):
179+
ignore_pr_title_re = [ignore_pr_title_re]
180+
if ignore_pr_title_re and any(re.search(regex, title) for regex in ignore_pr_title_re):
181+
get_logger().info(f"Ignoring PR with title '{title}' due to config.ignore_pr_title setting")
182+
return False
183+
184+
# logic to ignore PRs with specific labels or source branches or target branches.
185+
ignore_pr_labels = get_settings().get("CONFIG.IGNORE_PR_LABELS", [])
186+
if pr_labels and ignore_pr_labels:
187+
labels = [label['name'] for label in pr_labels]
188+
if any(label in ignore_pr_labels for label in labels):
189+
labels_str = ", ".join(labels)
190+
get_logger().info(f"Ignoring PR with labels '{labels_str}' due to config.ignore_pr_labels settings")
191+
return False
192+
193+
# logic to ignore PRs with specific source or target branches
194+
ignore_pr_source_branches = get_settings().get("CONFIG.IGNORE_PR_SOURCE_BRANCHES", [])
195+
ignore_pr_target_branches = get_settings().get("CONFIG.IGNORE_PR_TARGET_BRANCHES", [])
196+
if pull_request and (ignore_pr_source_branches or ignore_pr_target_branches):
197+
if any(re.search(regex, source_branch) for regex in ignore_pr_source_branches):
198+
get_logger().info(
199+
f"Ignoring PR with source branch '{source_branch}' due to config.ignore_pr_source_branches settings")
200+
return False
201+
if any(re.search(regex, target_branch) for regex in ignore_pr_target_branches):
202+
get_logger().info(
203+
f"Ignoring PR with target branch '{target_branch}' due to config.ignore_pr_target_branches settings")
204+
return False
205+
except Exception as e:
206+
get_logger().error(f"Failed 'should_process_pr_logic': {e}")
207+
return True
208+
116209
# FastAPI app setup
117210
middleware = [Middleware(RawContextMiddleware)]
118211
app = FastAPI(middleware=middleware)

pr_agent/settings/configuration.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,14 +290,18 @@ push_commands = [
290290
# Configure SSL validation for GitLab. Can be either set to the path of a custom CA or disabled entirely.
291291
# ssl_verify = true
292292

293-
[gitea_app]
293+
[gitea]
294294
url = "https://gitea.com"
295295
handle_push_trigger = false
296296
pr_commands = [
297297
"/describe",
298298
"/review",
299299
"/improve",
300300
]
301+
push_commands = [
302+
"/describe",
303+
"/review",
304+
]
301305

302306
[bitbucket_app]
303307
pr_commands = [

0 commit comments

Comments
 (0)