mirror of
https://github.com/OpenHands/OpenHands.git
synced 2025-12-26 05:48:36 +08:00
[Feat]: Add GitLab support for repo list in Cloud Openhands (#7633)
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
parent
cc1aadaba5
commit
a4ebb5bf85
@ -54,14 +54,12 @@ export const retrieveGitHubAppRepositories = async (
|
||||
* Given a PAT, retrieves the repositories of the user
|
||||
* @returns A list of repositories
|
||||
*/
|
||||
export const retrieveUserGitRepositories = async (page = 1, per_page = 30) => {
|
||||
export const retrieveUserGitRepositories = async () => {
|
||||
const response = await openHands.get<GitRepository[]>(
|
||||
"/api/user/repositories",
|
||||
{
|
||||
params: {
|
||||
sort: "pushed",
|
||||
page,
|
||||
per_page,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
@ -323,11 +323,6 @@ class OpenHands {
|
||||
return user;
|
||||
}
|
||||
|
||||
static async getGitHubUserInstallationIds(): Promise<number[]> {
|
||||
const response = await openHands.get<number[]>("/api/user/installations");
|
||||
return response.data;
|
||||
}
|
||||
|
||||
static async searchGitRepositories(
|
||||
query: string,
|
||||
per_page = 5,
|
||||
|
||||
@ -4,7 +4,6 @@ import { useNavigate } from "react-router";
|
||||
import { I18nKey } from "#/i18n/declaration";
|
||||
import { SuggestionBox } from "#/components/features/suggestions/suggestion-box";
|
||||
import { GitRepositorySelector } from "./git-repo-selector";
|
||||
import { useAppRepositories } from "#/hooks/query/use-app-repositories";
|
||||
import { useSearchRepositories } from "#/hooks/query/use-search-repositories";
|
||||
import { useUserRepositories } from "#/hooks/query/use-user-repositories";
|
||||
import { sanitizeQuery } from "#/utils/sanitize-query";
|
||||
@ -31,20 +30,15 @@ export function GitRepositoriesSuggestionBox({
|
||||
const debouncedSearchQuery = useDebounce(searchQuery, 300);
|
||||
|
||||
// TODO: Use `useQueries` to fetch all repositories in parallel
|
||||
const { data: appRepositories, isLoading: isAppReposLoading } =
|
||||
useAppRepositories();
|
||||
const { data: userRepositories, isLoading: isUserReposLoading } =
|
||||
useUserRepositories();
|
||||
const { data: searchedRepos, isLoading: isSearchReposLoading } =
|
||||
useSearchRepositories(sanitizeQuery(debouncedSearchQuery));
|
||||
|
||||
const isLoading =
|
||||
isAppReposLoading || isUserReposLoading || isSearchReposLoading;
|
||||
const isLoading = isUserReposLoading || isSearchReposLoading;
|
||||
|
||||
const repositories =
|
||||
userRepositories?.pages.flatMap((page) => page.data) ||
|
||||
appRepositories?.pages.flatMap((page) => page.data) ||
|
||||
[];
|
||||
userRepositories?.pages.flatMap((page) => page.data) || [];
|
||||
|
||||
const handleConnectToGitHub = () => {
|
||||
if (gitHubAuthUrl) {
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useConfig } from "./use-config";
|
||||
import OpenHands from "#/api/open-hands";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
|
||||
export const useAppInstallations = () => {
|
||||
const { data: config } = useConfig();
|
||||
const { providersAreSet } = useAuth();
|
||||
|
||||
return useQuery({
|
||||
queryKey: ["installations", providersAreSet, config?.GITHUB_CLIENT_ID],
|
||||
queryFn: OpenHands.getGitHubUserInstallationIds,
|
||||
enabled:
|
||||
providersAreSet &&
|
||||
!!config?.GITHUB_CLIENT_ID &&
|
||||
config?.APP_MODE === "saas",
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
};
|
||||
@ -1,67 +0,0 @@
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import { retrieveGitHubAppRepositories } from "#/api/git";
|
||||
import { useAppInstallations } from "./use-app-installations";
|
||||
import { useConfig } from "./use-config";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
|
||||
export const useAppRepositories = () => {
|
||||
const { providersAreSet } = useAuth();
|
||||
const { data: config } = useConfig();
|
||||
const { data: installations } = useAppInstallations();
|
||||
|
||||
const repos = useInfiniteQuery({
|
||||
queryKey: ["repositories", providersAreSet, installations],
|
||||
queryFn: async ({
|
||||
pageParam,
|
||||
}: {
|
||||
pageParam: { installationIndex: number | null; repoPage: number | null };
|
||||
}) => {
|
||||
const { repoPage, installationIndex } = pageParam;
|
||||
|
||||
if (!installations) {
|
||||
throw new Error("Missing installation list");
|
||||
}
|
||||
|
||||
return retrieveGitHubAppRepositories(
|
||||
installationIndex || 0,
|
||||
installations,
|
||||
repoPage || 1,
|
||||
30,
|
||||
);
|
||||
},
|
||||
initialPageParam: { installationIndex: 0, repoPage: 1 },
|
||||
getNextPageParam: (lastPage) => {
|
||||
if (lastPage.nextPage) {
|
||||
return {
|
||||
installationIndex: lastPage.installationIndex,
|
||||
repoPage: lastPage.nextPage,
|
||||
};
|
||||
}
|
||||
|
||||
if (lastPage.installationIndex !== null) {
|
||||
return { installationIndex: lastPage.installationIndex, repoPage: 1 };
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
enabled:
|
||||
providersAreSet &&
|
||||
Array.isArray(installations) &&
|
||||
installations.length > 0 &&
|
||||
config?.APP_MODE === "saas",
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
|
||||
// TODO: Once we create our custom dropdown component, we should fetch data onEndReached
|
||||
// (nextui autocomplete doesn't support onEndReached nor is it compatible for extending)
|
||||
const { isSuccess, isFetchingNextPage, hasNextPage, fetchNextPage } = repos;
|
||||
React.useEffect(() => {
|
||||
if (!isFetchingNextPage && isSuccess && hasNextPage) {
|
||||
fetchNextPage();
|
||||
}
|
||||
}, [isFetchingNextPage, isSuccess, hasNextPage, fetchNextPage]);
|
||||
|
||||
return repos;
|
||||
};
|
||||
@ -1,20 +1,17 @@
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import React from "react";
|
||||
import { retrieveUserGitRepositories } from "#/api/git";
|
||||
import { useConfig } from "./use-config";
|
||||
import { useAuth } from "#/context/auth-context";
|
||||
|
||||
export const useUserRepositories = () => {
|
||||
const { providerTokensSet, providersAreSet } = useAuth();
|
||||
const { data: config } = useConfig();
|
||||
|
||||
const repos = useInfiniteQuery({
|
||||
queryKey: ["repositories", providerTokensSet],
|
||||
queryFn: async ({ pageParam }) =>
|
||||
retrieveUserGitRepositories(pageParam, 100),
|
||||
queryFn: async () => retrieveUserGitRepositories(),
|
||||
initialPageParam: 1,
|
||||
getNextPageParam: (lastPage) => lastPage.nextPage,
|
||||
enabled: providersAreSet && config?.APP_MODE === "oss",
|
||||
enabled: providersAreSet,
|
||||
staleTime: 1000 * 60 * 5, // 5 minutes
|
||||
gcTime: 1000 * 60 * 15, // 15 minutes
|
||||
});
|
||||
|
||||
@ -16,6 +16,7 @@ from openhands.integrations.service_types import (
|
||||
UnknownException,
|
||||
User,
|
||||
)
|
||||
from openhands.server.types import AppMode
|
||||
from openhands.utils.import_utils import get_impl
|
||||
|
||||
|
||||
@ -99,31 +100,36 @@ class GitHubService(GitService):
|
||||
email=response.get('email'),
|
||||
)
|
||||
|
||||
async def get_repositories(
|
||||
self, sort: str, installation_id: int | None
|
||||
) -> list[Repository]:
|
||||
MAX_REPOS = 1000
|
||||
PER_PAGE = 100 # Maximum allowed by GitHub API
|
||||
all_repos: list[dict] = []
|
||||
|
||||
async def _fetch_paginated_repos(
|
||||
self, url: str, params: dict, max_repos: int, extract_key: str | None = None
|
||||
) -> list[dict]:
|
||||
"""
|
||||
Fetch repositories with pagination support.
|
||||
|
||||
Args:
|
||||
url: The API endpoint URL
|
||||
params: Query parameters for the request
|
||||
max_repos: Maximum number of repositories to fetch
|
||||
extract_key: If provided, extract repositories from this key in the response
|
||||
|
||||
Returns:
|
||||
List of repository dictionaries
|
||||
"""
|
||||
repos: list[dict] = []
|
||||
page = 1
|
||||
|
||||
while len(all_repos) < MAX_REPOS:
|
||||
params = {'page': str(page), 'per_page': str(PER_PAGE)}
|
||||
if installation_id:
|
||||
url = (
|
||||
f'{self.BASE_URL}/user/installations/{installation_id}/repositories'
|
||||
)
|
||||
response, headers = await self._fetch_data(url, params)
|
||||
response = response.get('repositories', [])
|
||||
else:
|
||||
url = f'{self.BASE_URL}/user/repos'
|
||||
params['sort'] = sort
|
||||
response, headers = await self._fetch_data(url, params)
|
||||
while len(repos) < max_repos:
|
||||
page_params = {**params, 'page': str(page)}
|
||||
response, headers = await self._fetch_data(url, page_params)
|
||||
|
||||
if not response: # No more repositories
|
||||
# Extract repositories from response
|
||||
page_repos = response.get(extract_key, []) if extract_key else response
|
||||
|
||||
if not page_repos: # No more repositories
|
||||
break
|
||||
|
||||
all_repos.extend(response)
|
||||
repos.extend(page_repos)
|
||||
page += 1
|
||||
|
||||
# Check if we've reached the last page
|
||||
@ -131,8 +137,43 @@ class GitHubService(GitService):
|
||||
if 'rel="next"' not in link_header:
|
||||
break
|
||||
|
||||
# Trim to MAX_REPOS if needed and convert to Repository objects
|
||||
all_repos = all_repos[:MAX_REPOS]
|
||||
return repos[:max_repos] # Trim to max_repos if needed
|
||||
|
||||
async def get_repositories(self, sort: str, app_mode: AppMode) -> list[Repository]:
|
||||
MAX_REPOS = 1000
|
||||
PER_PAGE = 100 # Maximum allowed by GitHub API
|
||||
all_repos: list[dict] = []
|
||||
|
||||
if app_mode == AppMode.SAAS:
|
||||
# Get all installation IDs and fetch repos for each one
|
||||
installation_ids = await self.get_installation_ids()
|
||||
|
||||
# Iterate through each installation ID
|
||||
for installation_id in installation_ids:
|
||||
params = {'per_page': str(PER_PAGE)}
|
||||
url = (
|
||||
f'{self.BASE_URL}/user/installations/{installation_id}/repositories'
|
||||
)
|
||||
|
||||
# Fetch repositories for this installation
|
||||
installation_repos = await self._fetch_paginated_repos(
|
||||
url, params, MAX_REPOS - len(all_repos), extract_key='repositories'
|
||||
)
|
||||
|
||||
all_repos.extend(installation_repos)
|
||||
|
||||
# If we've already reached MAX_REPOS, no need to check other installations
|
||||
if len(all_repos) >= MAX_REPOS:
|
||||
break
|
||||
else:
|
||||
# Original behavior for non-SaaS mode
|
||||
params = {'per_page': str(PER_PAGE), 'sort': sort}
|
||||
url = f'{self.BASE_URL}/user/repos'
|
||||
|
||||
# Fetch user repositories
|
||||
all_repos = await self._fetch_paginated_repos(url, params, MAX_REPOS)
|
||||
|
||||
# Convert to Repository objects
|
||||
return [
|
||||
Repository(
|
||||
id=repo.get('id'),
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import os
|
||||
from typing import Any
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
import httpx
|
||||
from pydantic import SecretStr
|
||||
@ -12,6 +13,7 @@ from openhands.integrations.service_types import (
|
||||
UnknownException,
|
||||
User,
|
||||
)
|
||||
from openhands.server.types import AppMode
|
||||
from openhands.utils.import_utils import get_impl
|
||||
|
||||
|
||||
@ -119,12 +121,7 @@ class GitLabService(GitService):
|
||||
|
||||
return repos
|
||||
|
||||
async def get_repositories(
|
||||
self, sort: str, installation_id: int | None
|
||||
) -> list[Repository]:
|
||||
if installation_id:
|
||||
return [] # Not implementing installation_token case yet
|
||||
|
||||
async def get_repositories(self, sort: str, app_mode: AppMode) -> list[Repository]:
|
||||
MAX_REPOS = 1000
|
||||
PER_PAGE = 100 # Maximum allowed by GitLab API
|
||||
all_repos: list[dict] = []
|
||||
|
||||
@ -26,6 +26,7 @@ from openhands.integrations.service_types import (
|
||||
Repository,
|
||||
User,
|
||||
)
|
||||
from openhands.server.types import AppMode
|
||||
|
||||
|
||||
class ProviderToken(BaseModel):
|
||||
@ -187,11 +188,7 @@ class ProviderHandler:
|
||||
service = self._get_service(provider)
|
||||
return await service.get_latest_token()
|
||||
|
||||
async def get_repositories(
|
||||
self,
|
||||
sort: str,
|
||||
installation_id: int | None,
|
||||
) -> list[Repository]:
|
||||
async def get_repositories(self, sort: str, app_mode: AppMode) -> list[Repository]:
|
||||
"""
|
||||
Get repositories from a selected providers with pagination support
|
||||
"""
|
||||
@ -200,7 +197,7 @@ class ProviderHandler:
|
||||
for provider in self.provider_tokens:
|
||||
try:
|
||||
service = self._get_service(provider)
|
||||
service_repos = await service.get_repositories(sort, installation_id)
|
||||
service_repos = await service.get_repositories(sort, app_mode)
|
||||
all_repos.extend(service_repos)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
@ -3,6 +3,8 @@ from typing import Protocol
|
||||
|
||||
from pydantic import BaseModel, SecretStr
|
||||
|
||||
from openhands.server.types import AppMode
|
||||
|
||||
|
||||
class ProviderType(Enum):
|
||||
GITHUB = 'github'
|
||||
@ -86,10 +88,6 @@ class GitService(Protocol):
|
||||
"""Search for repositories"""
|
||||
...
|
||||
|
||||
async def get_repositories(
|
||||
self,
|
||||
sort: str,
|
||||
installation_id: int | None,
|
||||
) -> list[Repository]:
|
||||
async def get_repositories(self, sort: str, app_mode: AppMode) -> list[Repository]:
|
||||
"""Get repositories for the authenticated user"""
|
||||
...
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
from fastapi import APIRouter, Depends, status
|
||||
from fastapi.responses import JSONResponse
|
||||
from pydantic import SecretStr
|
||||
|
||||
from openhands.server.shared import server_config
|
||||
from openhands.integrations.github.github_service import GithubServiceImpl
|
||||
from openhands.integrations.provider import (
|
||||
PROVIDER_TOKEN_TYPE,
|
||||
@ -16,6 +16,7 @@ from openhands.integrations.service_types import (
|
||||
User,
|
||||
)
|
||||
from openhands.server.auth import get_access_token, get_provider_tokens
|
||||
from openhands.server.types import AppMode
|
||||
|
||||
app = APIRouter(prefix='/api/user')
|
||||
|
||||
@ -23,7 +24,6 @@ app = APIRouter(prefix='/api/user')
|
||||
@app.get('/repositories', response_model=list[Repository])
|
||||
async def get_user_repositories(
|
||||
sort: str = 'pushed',
|
||||
installation_id: int | None = None,
|
||||
provider_tokens: PROVIDER_TOKEN_TYPE | None = Depends(get_provider_tokens),
|
||||
access_token: SecretStr | None = Depends(get_access_token),
|
||||
):
|
||||
@ -33,9 +33,7 @@ async def get_user_repositories(
|
||||
)
|
||||
|
||||
try:
|
||||
repos: list[Repository] = await client.get_repositories(
|
||||
sort, installation_id
|
||||
)
|
||||
repos: list[Repository] = await client.get_repositories(sort, server_config.app_mode)
|
||||
return repos
|
||||
|
||||
except AuthenticationError as e:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user