Bitbucket server, WIP

This commit is contained in:
Ori Kotek
2023-08-24 18:35:41 +03:00
parent 5079daa4ad
commit 355abfc39a
6 changed files with 174 additions and 79 deletions

View File

@ -9,6 +9,10 @@ FROM base as github_app
ADD pr_agent pr_agent
CMD ["python", "pr_agent/servers/github_app.py"]
FROM base as bitbucket_app
ADD pr_agent pr_agent
CMD ["python", "pr_agent/servers/bitbucket_app.py"]
FROM base as github_polling
ADD pr_agent pr_agent
CMD ["python", "pr_agent/servers/github_polling.py"]

View File

@ -12,14 +12,18 @@ from .git_provider import FilePatchInfo, GitProvider
class BitbucketProvider(GitProvider):
def __init__(self, pr_url: Optional[str] = None, incremental: Optional[bool] = False):
def __init__(
self, pr_url: Optional[str] = None, incremental: Optional[bool] = False
):
s = requests.Session()
try:
bearer = context.get("bitbucket_bearer_token", None)
s.headers['Authorization'] = f'Bearer {bearer}'
s.headers["Authorization"] = f"Bearer {bearer}"
except Exception:
s.headers['Authorization'] = f'Bearer {get_settings().get("BITBUCKET.BEARER_TOKEN", None)}'
s.headers['Content-Type'] = 'application/json'
s.headers[
"Authorization"
] = f'Bearer {get_settings().get("BITBUCKET.BEARER_TOKEN", None)}'
s.headers["Content-Type"] = "application/json"
self.headers = s.headers
self.bitbucket_client = Cloud(session=s)
self.workspace_slug = None
@ -31,37 +35,44 @@ class BitbucketProvider(GitProvider):
self.incremental = incremental
if pr_url:
self.set_pr(pr_url)
self.bitbucket_comment_api_url = self.pr._BitbucketBase__data['links']['comments']['href']
self.bitbucket_comment_api_url = self.pr._BitbucketBase__data["links"][
"comments"
]["href"]
def get_repo_settings(self):
try:
contents = self.repo_obj.get_contents(".pr_agent.toml", ref=self.pr.head.sha).decoded_content
contents = self.repo_obj.get_contents(
".pr_agent.toml", ref=self.pr.head.sha
).decoded_content
return contents
except Exception:
return ""
def publish_code_suggestions(self, code_suggestions: list) -> bool:
"""
Publishes code suggestions as comments on the PR.
"""
post_parameters_list = []
for suggestion in code_suggestions:
body = suggestion['body']
relevant_file = suggestion['relevant_file']
relevant_lines_start = suggestion['relevant_lines_start']
relevant_lines_end = suggestion['relevant_lines_end']
body = suggestion["body"]
relevant_file = suggestion["relevant_file"]
relevant_lines_start = suggestion["relevant_lines_start"]
relevant_lines_end = suggestion["relevant_lines_end"]
if not relevant_lines_start or relevant_lines_start == -1:
if get_settings().config.verbosity_level >= 2:
logging.exception(
f"Failed to publish code suggestion, relevant_lines_start is {relevant_lines_start}")
f"Failed to publish code suggestion, relevant_lines_start is {relevant_lines_start}"
)
continue
if relevant_lines_end < relevant_lines_start:
if get_settings().config.verbosity_level >= 2:
logging.exception(f"Failed to publish code suggestion, "
f"relevant_lines_end is {relevant_lines_end} and "
f"relevant_lines_start is {relevant_lines_start}")
logging.exception(
f"Failed to publish code suggestion, "
f"relevant_lines_end is {relevant_lines_end} and "
f"relevant_lines_start is {relevant_lines_start}"
)
continue
if relevant_lines_end > relevant_lines_start:
@ -80,8 +91,7 @@ class BitbucketProvider(GitProvider):
"side": "RIGHT",
}
post_parameters_list.append(post_parameters)
try:
self.publish_inline_comments(post_parameters_list)
return True
@ -91,7 +101,12 @@ class BitbucketProvider(GitProvider):
return False
def is_supported(self, capability: str) -> bool:
if capability in ['get_issue_comments', 'create_inline_comment', 'publish_inline_comments', 'get_labels']:
if capability in [
"get_issue_comments",
"create_inline_comment",
"publish_inline_comments",
"get_labels",
]:
return False
return True
@ -104,57 +119,65 @@ class BitbucketProvider(GitProvider):
def get_diff_files(self) -> list[FilePatchInfo]:
diffs = self.pr.diffstat()
diff_split = ['diff --git%s' % x for x in self.pr.diff().split('diff --git') if x.strip()]
diff_split = [
"diff --git%s" % x for x in self.pr.diff().split("diff --git") if x.strip()
]
diff_files = []
for index, diff in enumerate(diffs):
original_file_content_str = self._get_pr_file_content(diff.old.get_data('links'))
new_file_content_str = self._get_pr_file_content(diff.new.get_data('links'))
diff_files.append(FilePatchInfo(original_file_content_str, new_file_content_str,
diff_split[index], diff.new.path))
original_file_content_str = self._get_pr_file_content(
diff.old.get_data("links")
)
new_file_content_str = self._get_pr_file_content(diff.new.get_data("links"))
diff_files.append(
FilePatchInfo(
original_file_content_str,
new_file_content_str,
diff_split[index],
diff.new.path,
)
)
return diff_files
def publish_comment(self, pr_comment: str, is_temporary: bool = False):
comment = self.pr.comment(pr_comment)
if is_temporary:
self.temp_comments.append(comment['id'])
self.temp_comments.append(comment["id"])
def remove_initial_comment(self):
try:
for comment in self.temp_comments:
self.pr.delete(f'comments/{comment}')
self.pr.delete(f"comments/{comment}")
except Exception as e:
logging.exception(f"Failed to remove temp comments, error: {e}")
def publish_inline_comment(self, comment: str, from_line: int, to_line: int, file: str):
payload = json.dumps( {
"content": {
"raw": comment,
},
"inline": {
"to": from_line,
"path": file
},
})
def publish_inline_comment(
self, comment: str, from_line: int, to_line: int, file: str
):
payload = json.dumps(
{
"content": {
"raw": comment,
},
"inline": {"to": from_line, "path": file},
}
)
response = requests.request(
"POST",
self.bitbucket_comment_api_url,
data=payload,
headers=self.headers
"POST", self.bitbucket_comment_api_url, data=payload, headers=self.headers
)
return response
def publish_inline_comments(self, comments: list[dict]):
for comment in comments:
self.publish_inline_comment(comment['body'], comment['start_line'], comment['line'], comment['path'])
self.publish_inline_comment(
comment["body"], comment["start_line"], comment["line"], comment["path"]
)
def get_title(self):
return self.pr.title
def get_languages(self):
languages = {self._get_repo().get_data('language'): 0}
languages = {self._get_repo().get_data("language"): 0}
return languages
def get_pr_branch(self):
@ -167,7 +190,9 @@ class BitbucketProvider(GitProvider):
return 0
def get_issue_comments(self):
raise NotImplementedError("Bitbucket provider does not support issue comments yet")
raise NotImplementedError(
"Bitbucket provider does not support issue comments yet"
)
def add_eyes_reaction(self, issue_comment_id: int) -> Optional[int]:
return True
@ -178,14 +203,16 @@ class BitbucketProvider(GitProvider):
@staticmethod
def _parse_pr_url(pr_url: str) -> Tuple[str, int]:
parsed_url = urlparse(pr_url)
if 'bitbucket.org' not in parsed_url.netloc:
if "bitbucket.org" not in parsed_url.netloc:
raise ValueError("The provided URL is not a valid Bitbucket URL")
path_parts = parsed_url.path.strip('/').split('/')
if len(path_parts) < 4 or path_parts[2] != 'pull-requests':
raise ValueError("The provided URL does not appear to be a Bitbucket PR URL")
path_parts = parsed_url.path.strip("/").split("/")
if len(path_parts) < 4 or path_parts[2] != "pull-requests":
raise ValueError(
"The provided URL does not appear to be a Bitbucket PR URL"
)
workspace_slug = path_parts[0]
repo_slug = path_parts[1]
@ -198,7 +225,9 @@ class BitbucketProvider(GitProvider):
def _get_repo(self):
if self.repo is None:
self.repo = self.bitbucket_client.workspaces.get(self.workspace_slug).repositories.get(self.repo_slug)
self.repo = self.bitbucket_client.workspaces.get(
self.workspace_slug
).repositories.get(self.repo_slug)
return self.repo
def _get_pr(self):
@ -209,3 +238,16 @@ class BitbucketProvider(GitProvider):
def get_commit_messages(self):
return "" # not implemented yet
def publish_description(self, pr_title: str, pr_body: str):
pass
def create_inline_comment(
self, body: str, relevant_file: str, relevant_line_in_file: str
):
pass
def publish_labels(self, labels):
pass
def get_labels(self):
pass

View File

@ -0,0 +1,33 @@
{
"name": "CodiumAI PR-Agent",
"description": "CodiumAI PR-Agent",
"key": "app_key",
"vendor": {
"name": "CodiumAI",
"url": "https://codium.ai"
},
"authentication": {
"type": "jwt"
},
"baseUrl": "https://53e2-212-199-118-78.ngrok-free.app",
"lifecycle": {
"installed": "/installed",
"uninstalled": "/uninstalled"
},
"scopes": [
"account",
"repository",
"pullrequest"
],
"contexts": [
"account"
],
"modules": {
"webhooks": [
{
"event": "*",
"url": "/webhook"
}
]
}
}

View File

@ -1,22 +1,26 @@
import copy
import hashlib
import json
import logging
import os
import sys
import time
import jwt
import requests
import uvicorn
from fastapi import APIRouter, FastAPI, Request, Response
from starlette.background import BackgroundTasks
from starlette.middleware import Middleware
from starlette.responses import JSONResponse
from starlette_context import context
from starlette_context.middleware import RawContextMiddleware
from pr_agent.agent.pr_agent import PRAgent
from pr_agent.config_loader import get_settings
from pr_agent.config_loader import get_settings, global_settings
from pr_agent.secret_providers import get_secret_provider
logging.basicConfig(stream=sys.stdout, level=logging.INFO)
router = APIRouter()
secret_provider = get_secret_provider()
@ -51,29 +55,44 @@ async def get_bearer_token(shared_secret: str, client_key: str):
@router.get("/")
async def handle_manifest(request: Request, response: Response):
manifest = open("atlassian-connect.json", "rt").read()
try:
manifest = manifest.replace("app_key", get_settings().bitbucket.app_key)
except:
logging.error("Failed to replace api_key in Bitbucket manifest, trying to continue")
manifest_obj = json.loads(manifest)
return JSONResponse(manifest_obj)
@router.post("/webhook")
async def handle_github_webhooks(request: Request, response: Response):
try:
print(request.headers)
data = await request.json()
print(data)
owner = data["data"]["repository"]["owner"]["username"]
secrets = json.loads(secret_provider.get_secret(owner))
shared_secret = secrets["shared_secret"]
client_key = secrets["client_key"]
bearer_token = await get_bearer_token(shared_secret, client_key)
context['bitbucket_bearer_token'] = bearer_token
event = data["event"]
agent = PRAgent()
if event == "pullrequest:created":
pr_url = data["data"]["pullrequest"]["links"]["html"]["href"]
await agent.handle_request(pr_url, "review")
except Exception as e:
logging.error(f"Failed to handle webhook: {e}")
return JSONResponse({"error": "Unable to handle webhook"}, status_code=500)
async def handle_github_webhooks(background_tasks: BackgroundTasks, request: Request):
print(request.headers)
jwt_header = request.headers.get("authorization", None)
if jwt_header:
input_jwt = jwt_header.split(" ")[1]
data = await request.json()
print(data)
async def inner():
try:
owner = data["data"]["repository"]["owner"]["username"]
secrets = json.loads(secret_provider.get_secret(owner))
shared_secret = secrets["shared_secret"]
client_key = secrets["client_key"]
jwt.decode(input_jwt, shared_secret, audience=client_key, algorithms=["HS256"])
bearer_token = await get_bearer_token(shared_secret, client_key)
context['bitbucket_bearer_token'] = bearer_token
context["settings"] = copy.deepcopy(global_settings)
event = data["event"]
agent = PRAgent()
if event == "pullrequest:created":
pr_url = data["data"]["pullrequest"]["links"]["html"]["href"]
await agent.handle_request(pr_url, "review")
elif event == "pullrequest:comment_created":
pr_url = data["data"]["pullrequest"]["links"]["html"]["href"]
comment_body = data["data"]["comment"]["content"]["raw"]
await agent.handle_request(pr_url, comment_body)
except Exception as e:
logging.error(f"Failed to handle webhook: {e}")
background_tasks.add_task(inner)
return "OK"
@router.get("/webhook")
async def handle_github_webhooks(request: Request, response: Response):
@ -106,6 +125,7 @@ async def handle_uninstalled_webhooks(request: Request, response: Response):
def start():
get_settings().set("CONFIG.PUBLISH_OUTPUT_PROGRESS", False)
get_settings().set("CONFIG.GIT_PROVIDER", "bitbucket")
get_settings().set("PR_DESCRIPTION.PUBLISH_DESCRIPTION_AS_COMMENT", True)
middleware = [Middleware(RawContextMiddleware)]
app = FastAPI(middleware=middleware)
app.include_router(router)

View File

@ -167,4 +167,4 @@ def start():
if __name__ == '__main__':
start()
start()

View File

@ -153,12 +153,6 @@ class PRDescription:
# Initialization
pr_types = []
# Iterate over the dictionary items and append the key and value to 'markdown_text' in a markdown format
markdown_text = ""
for key, value in data.items():
markdown_text += f"## {key}\n\n"
markdown_text += f"{value}\n\n"
# If the 'PR Type' key is present in the dictionary, split its value by comma and assign it to 'pr_types'
if 'PR Type' in data:
if type(data['PR Type']) == list:
@ -194,6 +188,8 @@ class PRDescription:
if idx < len(data) - 1:
pr_body += "\n___\n"
markdown_text = f"## Title\n\n{title}\n\n___\n{pr_body}"
if get_settings().config.verbosity_level >= 2:
logging.info(f"title:\n{title}\n{pr_body}")