fix: Enhance GitHub repository search to include user organizations (#10324)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
sp.wack
2025-08-19 19:56:15 +04:00
committed by GitHub
parent 0297b3da18
commit aa6b454772
4 changed files with 258 additions and 57 deletions

View File

@@ -321,6 +321,36 @@ class GitHubService(BaseGitService, GitService, InstallationsService):
installations = response.get('installations', [])
return [str(i['id']) for i in installations]
async def get_user_organizations(self) -> list[str]:
"""Get list of organization logins that the user is a member of."""
url = f'{self.BASE_URL}/user/orgs'
try:
response, _ = await self._make_request(url)
orgs = [org['login'] for org in response]
return orgs
except Exception as e:
logger.warning(f'Failed to get user organizations: {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(' ', '')
org_lower = org_name.lower().replace('-', '').replace('_', '').replace(' ', '')
# Exact match after normalization
if query_lower == org_lower:
return True
# Query is a substring of org name
if query_lower in org_lower:
return True
# Org name is a substring of query (less common but possible)
if org_lower in query_lower:
return True
return False
async def search_repositories(
self, query: str, per_page: int, sort: str, order: str, public: bool
) -> list[Repository]:
@@ -341,21 +371,68 @@ class GitHubService(BaseGitService, GitService, InstallationsService):
# Add is:public to the query to ensure we only search for public repositories
params['q'] = f'in:name {org}/{repo_name} is:public'
# Perhaps we should go through all orgs and the search for repos under every org
# Currently it will only search user repos, and org repos when '/' is in the name
# Handle private repository searches
if not public and '/' in query:
org, repo_query = query.split('/', 1)
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
user = await self.get_user()
params['q'] = f'in:name {query} user:{user.login}'
user_orgs = await self.get_user_organizations()
# Search in user repos and org repos separately
all_repos = []
# Search in user repositories
user_query = f'{query} user:{user.login}'
user_params = params.copy()
user_params['q'] = user_query
try:
user_response, _ = await self._make_request(url, user_params)
user_items = user_response.get('items', [])
all_repos.extend(user_items)
except Exception as e:
logger.warning(f'User search failed: {e}')
# Search for repos named "query" in each organization
for org in user_orgs:
org_query = f'{query} org:{org}'
org_params = params.copy()
org_params['q'] = org_query
try:
org_response, _ = await self._make_request(url, org_params)
org_items = org_response.get('items', [])
all_repos.extend(org_items)
except Exception as e:
logger.warning(f'Org {org} search failed: {e}')
# Also search for top repos from orgs that match the query name
for org in user_orgs:
if self._fuzzy_match_org_name(query, org):
org_repos_query = f'org:{org}'
org_repos_params = params.copy()
org_repos_params['q'] = org_repos_query
org_repos_params['sort'] = 'stars'
org_repos_params['per_page'] = 2 # Limit to first 2 repos
try:
org_repos_response, _ = await self._make_request(
url, org_repos_params
)
org_repo_items = org_repos_response.get('items', [])
all_repos.extend(org_repo_items)
except Exception as e:
logger.warning(f'Org repos search for {org} failed: {e}')
return [self._parse_repository(repo) for repo in all_repos]
# Default case (public search or slash query)
response, _ = await self._make_request(url, params)
repo_items = response.get('items', [])
repos = [self._parse_repository(repo) for repo in repo_items]
return repos
return [self._parse_repository(repo) for repo in repo_items]
async def execute_graphql_query(
self, query: str, variables: dict[str, Any]