blob: 7846425b20034ab06adef7e70419e74529a8a530 [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
"""Bitbucket Cloud API operations."""
from __future__ import annotations
from typing import Any
from urllib.parse import urlparse
from magpie_bitbucket.client import BitbucketConfig, BitbucketError, get_json, get_text, quote_path, require
CLOUD_API_BASE = "https://api.bitbucket.org/2.0"
def _validated_next_url(next_url: object, seen_urls: set[str]) -> str:
"""Return a safe Bitbucket Cloud pagination URL or an empty string."""
if not isinstance(next_url, str):
return ""
parsed_next = urlparse(next_url)
parsed_base = urlparse(CLOUD_API_BASE)
if parsed_next.scheme != parsed_base.scheme or parsed_next.hostname != parsed_base.hostname:
msg = "Bitbucket Cloud pagination URL changed scheme or host"
raise BitbucketError(msg)
if next_url in seen_urls:
msg = "Bitbucket Cloud pagination returned a repeated URL"
raise BitbucketError(msg)
seen_urls.add(next_url)
return next_url
def get_repository(config: BitbucketConfig) -> dict[str, Any]:
"""Fetch repository metadata from Bitbucket Cloud."""
workspace = quote_path(require(config.workspace, "BITBUCKET_WORKSPACE"))
repo_slug = quote_path(require(config.repo_slug, "BITBUCKET_REPO_SLUG"))
url = f"{CLOUD_API_BASE}/repositories/{workspace}/{repo_slug}"
return get_json(url, config)
def list_open_pull_requests(config: BitbucketConfig) -> dict[str, Any]:
"""List all open pull requests from Bitbucket Cloud."""
workspace = quote_path(require(config.workspace, "BITBUCKET_WORKSPACE"))
repo_slug = quote_path(require(config.repo_slug, "BITBUCKET_REPO_SLUG"))
url = f"{CLOUD_API_BASE}/repositories/{workspace}/{repo_slug}/pullrequests?state=OPEN"
combined: dict[str, Any] = {
"values": [],
"paginated": True,
"pages": [],
}
seen_urls = {url}
while url:
page = get_json(url, config)
combined["pages"].append(page)
values = page.get("values")
if isinstance(values, list):
combined["values"].extend(item for item in values if isinstance(item, dict))
url = _validated_next_url(page.get("next"), seen_urls)
return combined
def get_pull_request(config: BitbucketConfig, pull_request_id: str) -> dict[str, Any]:
"""Fetch one pull request from Bitbucket Cloud."""
workspace = quote_path(require(config.workspace, "BITBUCKET_WORKSPACE"))
repo_slug = quote_path(require(config.repo_slug, "BITBUCKET_REPO_SLUG"))
pr_id = quote_path(pull_request_id)
url = f"{CLOUD_API_BASE}/repositories/{workspace}/{repo_slug}/pullrequests/{pr_id}"
return get_json(url, config)
def get_pull_request_commits(config: BitbucketConfig, pull_request_id: str) -> dict[str, Any]:
"""Fetch commits from a Bitbucket Cloud pull request."""
workspace = quote_path(require(config.workspace, "BITBUCKET_WORKSPACE"))
repo_slug = quote_path(require(config.repo_slug, "BITBUCKET_REPO_SLUG"))
pr_id = quote_path(pull_request_id)
url = f"{CLOUD_API_BASE}/repositories/{workspace}/{repo_slug}/pullrequests/{pr_id}/commits"
combined: dict[str, Any] = {
"pull_request_id": pull_request_id,
"values": [],
"paginated": True,
"pages": [],
}
seen_urls = {url}
while url:
page = get_json(url, config)
combined["pages"].append(page)
values = page.get("values")
if isinstance(values, list):
combined["values"].extend(item for item in values if isinstance(item, dict))
url = _validated_next_url(page.get("next"), seen_urls)
return combined
def get_pull_request_diff(config: BitbucketConfig, pull_request_id: str) -> dict[str, Any]:
"""Fetch the unified diff for a Bitbucket Cloud pull request."""
workspace = quote_path(require(config.workspace, "BITBUCKET_WORKSPACE"))
repo_slug = quote_path(require(config.repo_slug, "BITBUCKET_REPO_SLUG"))
pr_id = quote_path(pull_request_id)
url = f"{CLOUD_API_BASE}/repositories/{workspace}/{repo_slug}/pullrequests/{pr_id}/diff"
response = get_text(url, config, accept="text/x-diff")
return {
"pull_request_id": pull_request_id,
"body": response["body"],
"content_type": response["content_type"],
"url": response["url"],
}
def get_pull_request_reviews(config: BitbucketConfig, pull_request_id: str) -> dict[str, Any]:
"""Fetch review-state activity for a Bitbucket Cloud pull request."""
pull_request = get_pull_request(config, pull_request_id)
workspace = quote_path(require(config.workspace, "BITBUCKET_WORKSPACE"))
repo_slug = quote_path(require(config.repo_slug, "BITBUCKET_REPO_SLUG"))
pr_id = quote_path(pull_request_id)
url = f"{CLOUD_API_BASE}/repositories/{workspace}/{repo_slug}/pullrequests/{pr_id}/activity"
combined: dict[str, Any] = {
"pull_request_id": pull_request_id,
"pull_request": pull_request,
"values": [],
"paginated": True,
"pages": [],
}
seen_urls = {url}
while url:
page = get_json(url, config)
combined["pages"].append(page)
values = page.get("values")
if isinstance(values, list):
combined["values"].extend(item for item in values if isinstance(item, dict))
url = _validated_next_url(page.get("next"), seen_urls)
return combined
def get_pull_request_merge_checks(config: BitbucketConfig, pull_request_id: str) -> dict[str, Any]:
"""Fetch read-only merge-check context for a Bitbucket Cloud pull request."""
status = get_pull_request_status(config, pull_request_id)
reviews = get_pull_request_reviews(config, pull_request_id)
pull_request = status.get("pull_request")
if not isinstance(pull_request, dict):
pull_request = reviews.get("pull_request")
if not isinstance(pull_request, dict):
pull_request = {}
return {
"pull_request_id": pull_request_id,
"pull_request": pull_request,
"status": status,
"reviews": reviews,
}
def get_pull_request_status(config: BitbucketConfig, pull_request_id: str) -> dict[str, Any]:
"""Fetch build statuses for a Bitbucket Cloud pull request."""
workspace = quote_path(require(config.workspace, "BITBUCKET_WORKSPACE"))
repo_slug = quote_path(require(config.repo_slug, "BITBUCKET_REPO_SLUG"))
pr_id = quote_path(pull_request_id)
url = f"{CLOUD_API_BASE}/repositories/{workspace}/{repo_slug}/pullrequests/{pr_id}/statuses"
combined: dict[str, Any] = {
"pull_request_id": pull_request_id,
"values": [],
"paginated": True,
"pages": [],
}
seen_urls = {url}
while url:
page = get_json(url, config)
combined["pages"].append(page)
values = page.get("values")
if isinstance(values, list):
combined["values"].extend(item for item in values if isinstance(item, dict))
url = _validated_next_url(page.get("next"), seen_urls)
return combined
def get_pull_request_discussion(config: BitbucketConfig, pull_request_id: str) -> dict[str, Any]:
"""Fetch pull request comments from Bitbucket Cloud."""
workspace = quote_path(require(config.workspace, "BITBUCKET_WORKSPACE"))
repo_slug = quote_path(require(config.repo_slug, "BITBUCKET_REPO_SLUG"))
pr_id = quote_path(pull_request_id)
url = f"{CLOUD_API_BASE}/repositories/{workspace}/{repo_slug}/pullrequests/{pr_id}/comments"
combined: dict[str, Any] = {
"pull_request_id": pull_request_id,
"values": [],
"paginated": True,
"pages": [],
}
seen_urls = {url}
while url:
page = get_json(url, config)
combined["pages"].append(page)
values = page.get("values")
if isinstance(values, list):
combined["values"].extend(item for item in values if isinstance(item, dict))
url = _validated_next_url(page.get("next"), seen_urls)
return combined