Wan Arif 3504ca7752
feat: add Azure DevOps integration support (#11243)
Co-authored-by: Graham Neubig <neubig@gmail.com>
2025-11-22 14:00:24 -05:00

179 lines
6.4 KiB
Python

"""Repository operations for Azure DevOps integration."""
from openhands.integrations.azure_devops.service.base import AzureDevOpsMixinBase
from openhands.integrations.service_types import ProviderType, Repository
from openhands.server.types import AppMode
class AzureDevOpsReposMixin(AzureDevOpsMixinBase):
"""Mixin for Azure DevOps repository operations."""
async def search_repositories(
self,
query: str,
per_page: int = 30,
sort: str = 'updated',
order: str = 'desc',
public: bool = False,
app_mode: AppMode = AppMode.OSS,
) -> list[Repository]:
"""Search for repositories in Azure DevOps."""
# Get all repositories across all projects in the organization
url = f'{self.base_url}/_apis/git/repositories?api-version=7.1'
response, _ = await self._make_request(url)
# Filter repositories by query if provided
repos = response.get('value', [])
if query:
repos = [
repo for repo in repos if query.lower() in repo.get('name', '').lower()
]
# Limit to per_page
repos = repos[:per_page]
return [
Repository(
id=str(repo.get('id')),
full_name=f'{self.organization}/{repo.get("project", {}).get("name", "")}/{repo.get("name")}',
git_provider=ProviderType.AZURE_DEVOPS,
is_public=False, # Azure DevOps repos are private by default
)
for repo in repos
]
async def get_repositories(self, sort: str, app_mode: AppMode) -> list[Repository]:
"""Get repositories for the authenticated user."""
MAX_REPOS = 1000
# Get all projects first
projects_url = f'{self.base_url}/_apis/projects?api-version=7.1'
projects_response, _ = await self._make_request(projects_url)
projects = projects_response.get('value', [])
all_repos = []
# For each project, get its repositories
for project in projects:
project_name = project.get('name')
project_enc = self._encode_url_component(project_name)
repos_url = (
f'{self.base_url}/{project_enc}/_apis/git/repositories?api-version=7.1'
)
repos_response, _ = await self._make_request(repos_url)
repos = repos_response.get('value', [])
for repo in repos:
all_repos.append(
{
'id': repo.get('id'),
'name': repo.get('name'),
'project_name': project_name,
'updated_date': repo.get('lastUpdateTime'),
}
)
if len(all_repos) >= MAX_REPOS:
break
if len(all_repos) >= MAX_REPOS:
break
# Sort repositories based on the sort parameter
if sort == 'updated':
all_repos.sort(key=lambda r: r.get('updated_date', ''), reverse=True)
elif sort == 'name':
all_repos.sort(key=lambda r: r.get('name', '').lower())
return [
Repository(
id=str(repo.get('id')),
full_name=f'{self.organization}/{repo.get("project_name")}/{repo.get("name")}',
git_provider=ProviderType.AZURE_DEVOPS,
is_public=False, # Azure DevOps repos are private by default
)
for repo in all_repos[:MAX_REPOS]
]
async def get_all_repositories(
self, sort: str, app_mode: AppMode
) -> list[Repository]:
"""Get repositories for the authenticated user (alias for get_repositories)."""
return await self.get_repositories(sort, app_mode)
def _parse_repository_response(
self, repo: dict, project_name: str, link_header: str | None = None
) -> Repository:
"""Parse an Azure DevOps API repository response into a Repository object.
Args:
repo: Repository data from Azure DevOps API
project_name: The project name the repository belongs to
link_header: Optional link header for pagination
Returns:
Repository object
"""
return Repository(
id=str(repo.get('id')),
full_name=f'{self.organization}/{project_name}/{repo.get("name")}',
git_provider=ProviderType.AZURE_DEVOPS,
is_public=False, # Azure DevOps repos are private by default
link_header=link_header,
)
async def get_paginated_repos(
self,
page: int,
per_page: int,
sort: str,
installation_id: str | None,
query: str | None = None,
) -> list[Repository]:
"""Get a page of repositories for the authenticated user."""
# Get all repos first, then paginate manually
# Azure DevOps doesn't have native pagination for repositories
all_repos = await self.get_repositories(sort, AppMode.SAAS)
# Calculate pagination
start_idx = (page - 1) * per_page
end_idx = start_idx + per_page
# Filter by query if provided
if query:
query_lower = query.lower()
all_repos = [
repo for repo in all_repos if query_lower in repo.full_name.lower()
]
return all_repos[start_idx:end_idx]
async def get_repository_details_from_repo_name(
self, repository: str
) -> Repository:
"""Gets all repository details from repository name.
Args:
repository: Repository name in format 'organization/project/repo'
Returns:
Repository object with details
"""
org, project, repo = self._parse_repository(repository)
# URL-encode components to handle spaces and special characters
org_enc = self._encode_url_component(org)
project_enc = self._encode_url_component(project)
repo_enc = self._encode_url_component(repo)
url = f'https://dev.azure.com/{org_enc}/{project_enc}/_apis/git/repositories/{repo_enc}?api-version=7.1'
response, _ = await self._make_request(url)
return Repository(
id=str(response.get('id')),
full_name=f'{org}/{project}/{repo}',
git_provider=ProviderType.AZURE_DEVOPS,
is_public=False, # Azure DevOps repos are private by default
)