diff --git a/pr_agent/git_providers/azuredevops_provider.py b/pr_agent/git_providers/azuredevops_provider.py index 426953ae..d77efc27 100644 --- a/pr_agent/git_providers/azuredevops_provider.py +++ b/pr_agent/git_providers/azuredevops_provider.py @@ -21,7 +21,7 @@ try: # noinspection PyUnresolvedReferences from azure.devops.connection import Connection # noinspection PyUnresolvedReferences - from azure.devops.released.git import (Comment, CommentThread, GitPullRequest, GitVersionDescriptor, GitClient) + from azure.devops.released.git import (Comment, CommentThread, GitPullRequest, GitVersionDescriptor, GitClient, CommentThreadContext) # noinspection PyUnresolvedReferences from azure.identity import DefaultAzureCredential from msrest.authentication import BasicAuthentication @@ -564,6 +564,13 @@ class AzureDevopsProvider(GitProvider): except Exception as e: get_logger().exception(f"Failed to reply to thread, error: {e}") + def get_thread_context(self, thread_id: int) -> CommentThreadContext: + try: + thread = self.azure_devops_client.get_pull_request_thread(self.repo_slug, self.pr_num, thread_id, self.workspace_slug) + return thread.thread_context + except Exception as e: + get_logger().exception(f"Failed to set thread status, error: {e}") + @staticmethod def _parse_pr_url(pr_url: str) -> Tuple[str, str, int]: parsed_url = urlparse(pr_url) diff --git a/pr_agent/servers/azuredevops_server_webhook.py b/pr_agent/servers/azuredevops_server_webhook.py index 3a03250b..45533385 100644 --- a/pr_agent/servers/azuredevops_server_webhook.py +++ b/pr_agent/servers/azuredevops_server_webhook.py @@ -23,6 +23,7 @@ from pr_agent.agent.pr_agent import PRAgent, command2class from pr_agent.algo.utils import update_settings_from_args from pr_agent.config_loader import get_settings from pr_agent.git_providers import get_git_provider_with_context +from pr_agent.git_providers.azuredevops_provider import AzureDevopsProvider from pr_agent.git_providers.utils import apply_repo_settings from pr_agent.log import LoggingFormat, get_logger, setup_logger @@ -41,6 +42,7 @@ async def handle_request_comment(url: str, body: str, thread_id: int, comment_id with get_logger().contextualize(**log_context): agent = PRAgent() provider = get_git_provider_with_context(pr_url=url) + body = handle_line_comment(body, thread_id, provider) handled = await agent.handle_request(url, body, notify=lambda: provider.reply_to_thread(thread_id, "On it! ⏳", True)) # mark command comment as closed if handled: @@ -49,6 +51,29 @@ async def handle_request_comment(url: str, body: str, thread_id: int, comment_id except Exception as e: get_logger().exception(f"Failed to handle webhook", artifact={"url": url, "body": body}, error=str(e)) +def handle_line_comment(body: str, thread_id: int, provider: AzureDevopsProvider): + body = body.strip() + if not body.startswith('/ask '): + return body + thread_context = provider.get_thread_context(thread_id) + if not thread_context: + return body + + path = thread_context.file_path + if thread_context.left_file_end or thread_context.left_file_start: + start_line = thread_context.left_file_start.line + end_line = thread_context.left_file_end.line + side = "left" + elif thread_context.right_file_end or thread_context.right_file_start: + start_line = thread_context.right_file_start.line + end_line = thread_context.right_file_end.line + side = "right" + else: + get_logger().info("No line range found in thread context", artifact={"thread_context": thread_context}) + return body + + question = body[5:].lstrip() # remove 4 chars: '/ask ' + return f"/ask_line --line_start={start_line} --line_end={end_line} --side={side} --file_name={path} --comment_id={thread_id} {question}" # currently only basic auth is supported with azure webhooks # for this reason, https must be enabled to ensure the credentials are not sent in clear text