mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-01-09 14:57:59 -05:00
fix(backend): repository search is not working in the production environment (#11386)
This commit is contained in:
@@ -13,7 +13,13 @@ class BitBucketReposMixin(BitBucketMixinBase):
|
||||
"""
|
||||
|
||||
async def search_repositories(
|
||||
self, query: str, per_page: int, sort: str, order: str, public: bool
|
||||
self,
|
||||
query: str,
|
||||
per_page: int,
|
||||
sort: str,
|
||||
order: str,
|
||||
public: bool,
|
||||
app_mode: AppMode,
|
||||
) -> list[Repository]:
|
||||
"""Search for repositories."""
|
||||
repositories = []
|
||||
|
||||
@@ -161,6 +161,29 @@ class GitHubReposMixin(GitHubMixinBase):
|
||||
logger.warning(f'Failed to get user organizations: {e}')
|
||||
return []
|
||||
|
||||
async def get_organizations_from_installations(self) -> list[str]:
|
||||
"""Get list of organization logins from GitHub App installations.
|
||||
|
||||
This method provides a more reliable way to get organizations that the
|
||||
GitHub App has access to, regardless of user membership context.
|
||||
"""
|
||||
try:
|
||||
# Get installations with account details
|
||||
url = f'{self.BASE_URL}/user/installations'
|
||||
response, _ = await self._make_request(url)
|
||||
installations = response.get('installations', [])
|
||||
|
||||
orgs = []
|
||||
for installation in installations:
|
||||
account = installation.get('account', {})
|
||||
if account.get('type') == 'Organization':
|
||||
orgs.append(account.get('login'))
|
||||
|
||||
return orgs
|
||||
except Exception as e:
|
||||
logger.warning(f'Failed to get organizations from installations: {e}')
|
||||
return []
|
||||
|
||||
def _fuzzy_match_org_name(self, query: str, org_name: str) -> bool:
|
||||
"""Check if query fuzzy matches organization name."""
|
||||
query_lower = query.lower().replace('-', '').replace('_', '').replace(' ', '')
|
||||
@@ -181,7 +204,13 @@ class GitHubReposMixin(GitHubMixinBase):
|
||||
return False
|
||||
|
||||
async def search_repositories(
|
||||
self, query: str, per_page: int, sort: str, order: str, public: bool
|
||||
self,
|
||||
query: str,
|
||||
per_page: int,
|
||||
sort: str,
|
||||
order: str,
|
||||
public: bool,
|
||||
app_mode: AppMode,
|
||||
) -> list[Repository]:
|
||||
url = f'{self.BASE_URL}/search/repositories'
|
||||
params = {
|
||||
@@ -206,9 +235,12 @@ class GitHubReposMixin(GitHubMixinBase):
|
||||
query_with_user = f'org:{org} in:name {repo_query}'
|
||||
params['q'] = query_with_user
|
||||
elif not public:
|
||||
# Expand search scope to include user's repositories and organizations they're a member of
|
||||
# Expand search scope to include user's repositories and organizations the app has access to
|
||||
user = await self.get_user()
|
||||
user_orgs = await self.get_user_organizations()
|
||||
if app_mode == AppMode.SAAS:
|
||||
user_orgs = await self.get_organizations_from_installations()
|
||||
else:
|
||||
user_orgs = await self.get_user_organizations()
|
||||
|
||||
# Search in user repos and org repos separately
|
||||
all_repos = []
|
||||
|
||||
@@ -75,6 +75,7 @@ class GitLabReposMixin(GitLabMixinBase):
|
||||
sort: str = 'updated',
|
||||
order: str = 'desc',
|
||||
public: bool = False,
|
||||
app_mode: AppMode = AppMode.OSS,
|
||||
) -> list[Repository]:
|
||||
if public:
|
||||
# When public=True, query is a GitLab URL that we need to parse
|
||||
|
||||
@@ -294,12 +294,13 @@ class ProviderHandler:
|
||||
per_page: int,
|
||||
sort: str,
|
||||
order: str,
|
||||
app_mode: AppMode,
|
||||
) -> list[Repository]:
|
||||
if selected_provider:
|
||||
service = self.get_service(selected_provider)
|
||||
public = self._is_repository_url(query, selected_provider)
|
||||
user_repos = await service.search_repositories(
|
||||
query, per_page, sort, order, public
|
||||
query, per_page, sort, order, public, app_mode
|
||||
)
|
||||
return self._deduplicate_repositories(user_repos)
|
||||
|
||||
@@ -309,7 +310,7 @@ class ProviderHandler:
|
||||
service = self.get_service(provider)
|
||||
public = self._is_repository_url(query, provider)
|
||||
service_repos = await service.search_repositories(
|
||||
query, per_page, sort, order, public
|
||||
query, per_page, sort, order, public, app_mode
|
||||
)
|
||||
all_repos.extend(service_repos)
|
||||
except Exception as e:
|
||||
|
||||
@@ -458,7 +458,13 @@ class GitService(Protocol):
|
||||
...
|
||||
|
||||
async def search_repositories(
|
||||
self, query: str, per_page: int, sort: str, order: str, public: bool
|
||||
self,
|
||||
query: str,
|
||||
per_page: int,
|
||||
sort: str,
|
||||
order: str,
|
||||
public: bool,
|
||||
app_mode: AppMode,
|
||||
) -> list[Repository]:
|
||||
"""Search for public repositories"""
|
||||
...
|
||||
|
||||
@@ -148,7 +148,7 @@ async def search_repositories(
|
||||
)
|
||||
try:
|
||||
repos: list[Repository] = await client.search_repositories(
|
||||
selected_provider, query, per_page, sort, order
|
||||
selected_provider, query, per_page, sort, order, server_config.app_mode
|
||||
)
|
||||
return repos
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from pydantic import SecretStr
|
||||
from openhands.integrations.bitbucket.bitbucket_service import BitBucketService
|
||||
from openhands.integrations.service_types import OwnerType, Repository
|
||||
from openhands.integrations.service_types import ProviderType as ServiceProviderType
|
||||
from openhands.server.types import AppMode
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -37,7 +38,12 @@ async def test_search_repositories_url_parsing_standard_url(bitbucket_service):
|
||||
) as mock_get_repo:
|
||||
url = 'https://bitbucket.org/workspace/repo'
|
||||
repositories = await bitbucket_service.search_repositories(
|
||||
query=url, per_page=10, sort='updated', order='desc', public=True
|
||||
query=url,
|
||||
per_page=10,
|
||||
sort='updated',
|
||||
order='desc',
|
||||
public=True,
|
||||
app_mode=AppMode.OSS,
|
||||
)
|
||||
|
||||
# Verify the correct workspace/repo combination was extracted and passed
|
||||
@@ -70,7 +76,12 @@ async def test_search_repositories_url_parsing_with_extra_path_segments(
|
||||
# Test complex URL with query params, fragments, and extra paths
|
||||
url = 'https://bitbucket.org/my-workspace/my-repo/src/feature-branch/src/main.py?at=feature-branch&fileviewer=file-view-default#lines-25'
|
||||
repositories = await bitbucket_service.search_repositories(
|
||||
query=url, per_page=10, sort='updated', order='desc', public=True
|
||||
query=url,
|
||||
per_page=10,
|
||||
sort='updated',
|
||||
order='desc',
|
||||
public=True,
|
||||
app_mode=AppMode.OSS,
|
||||
)
|
||||
|
||||
# Verify the correct workspace/repo combination was extracted from complex URL
|
||||
@@ -87,7 +98,12 @@ async def test_search_repositories_url_parsing_invalid_url(bitbucket_service):
|
||||
) as mock_get_repo:
|
||||
url = 'not-a-valid-url'
|
||||
repositories = await bitbucket_service.search_repositories(
|
||||
query=url, per_page=10, sort='updated', order='desc', public=True
|
||||
query=url,
|
||||
per_page=10,
|
||||
sort='updated',
|
||||
order='desc',
|
||||
public=True,
|
||||
app_mode=AppMode.OSS,
|
||||
)
|
||||
|
||||
# Should return empty list for invalid URL and not call API
|
||||
@@ -105,7 +121,12 @@ async def test_search_repositories_url_parsing_insufficient_path_segments(
|
||||
) as mock_get_repo:
|
||||
url = 'https://bitbucket.org/workspace'
|
||||
repositories = await bitbucket_service.search_repositories(
|
||||
query=url, per_page=10, sort='updated', order='desc', public=True
|
||||
query=url,
|
||||
per_page=10,
|
||||
sort='updated',
|
||||
order='desc',
|
||||
public=True,
|
||||
app_mode=AppMode.OSS,
|
||||
)
|
||||
|
||||
# Should return empty list for insufficient path segments and not call API
|
||||
|
||||
@@ -277,7 +277,7 @@ async def test_github_search_repositories_with_organizations():
|
||||
patch.object(service, 'get_user', return_value=mock_user),
|
||||
patch.object(
|
||||
service,
|
||||
'get_user_organizations',
|
||||
'get_organizations_from_installations',
|
||||
return_value=['All-Hands-AI', 'example-org'],
|
||||
),
|
||||
patch.object(
|
||||
@@ -285,7 +285,12 @@ async def test_github_search_repositories_with_organizations():
|
||||
) as mock_request,
|
||||
):
|
||||
repositories = await service.search_repositories(
|
||||
query='openhands', per_page=10, sort='stars', order='desc', public=False
|
||||
query='openhands',
|
||||
per_page=10,
|
||||
sort='stars',
|
||||
order='desc',
|
||||
public=False,
|
||||
app_mode=AppMode.SAAS,
|
||||
)
|
||||
|
||||
# Verify that separate requests were made for user and each organization
|
||||
|
||||
Reference in New Issue
Block a user