Skip to content

Commit a4ebb5b

Browse files
[Feat]: Add GitLab support for repo list in Cloud Openhands (#7633)
Co-authored-by: openhands <[email protected]>
1 parent cc1aada commit a4ebb5b

File tree

11 files changed

+81
-153
lines changed

11 files changed

+81
-153
lines changed

frontend/src/api/git.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,12 @@ export const retrieveGitHubAppRepositories = async (
5454
* Given a PAT, retrieves the repositories of the user
5555
* @returns A list of repositories
5656
*/
57-
export const retrieveUserGitRepositories = async (page = 1, per_page = 30) => {
57+
export const retrieveUserGitRepositories = async () => {
5858
const response = await openHands.get<GitRepository[]>(
5959
"/api/user/repositories",
6060
{
6161
params: {
6262
sort: "pushed",
63-
page,
64-
per_page,
6563
},
6664
},
6765
);

frontend/src/api/open-hands.ts

-5
Original file line numberDiff line numberDiff line change
@@ -323,11 +323,6 @@ class OpenHands {
323323
return user;
324324
}
325325

326-
static async getGitHubUserInstallationIds(): Promise<number[]> {
327-
const response = await openHands.get<number[]>("/api/user/installations");
328-
return response.data;
329-
}
330-
331326
static async searchGitRepositories(
332327
query: string,
333328
per_page = 5,

frontend/src/components/features/git/git-repositories-suggestion-box.tsx

+2-8
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { useNavigate } from "react-router";
44
import { I18nKey } from "#/i18n/declaration";
55
import { SuggestionBox } from "#/components/features/suggestions/suggestion-box";
66
import { GitRepositorySelector } from "./git-repo-selector";
7-
import { useAppRepositories } from "#/hooks/query/use-app-repositories";
87
import { useSearchRepositories } from "#/hooks/query/use-search-repositories";
98
import { useUserRepositories } from "#/hooks/query/use-user-repositories";
109
import { sanitizeQuery } from "#/utils/sanitize-query";
@@ -31,20 +30,15 @@ export function GitRepositoriesSuggestionBox({
3130
const debouncedSearchQuery = useDebounce(searchQuery, 300);
3231

3332
// TODO: Use `useQueries` to fetch all repositories in parallel
34-
const { data: appRepositories, isLoading: isAppReposLoading } =
35-
useAppRepositories();
3633
const { data: userRepositories, isLoading: isUserReposLoading } =
3734
useUserRepositories();
3835
const { data: searchedRepos, isLoading: isSearchReposLoading } =
3936
useSearchRepositories(sanitizeQuery(debouncedSearchQuery));
4037

41-
const isLoading =
42-
isAppReposLoading || isUserReposLoading || isSearchReposLoading;
38+
const isLoading = isUserReposLoading || isSearchReposLoading;
4339

4440
const repositories =
45-
userRepositories?.pages.flatMap((page) => page.data) ||
46-
appRepositories?.pages.flatMap((page) => page.data) ||
47-
[];
41+
userRepositories?.pages.flatMap((page) => page.data) || [];
4842

4943
const handleConnectToGitHub = () => {
5044
if (gitHubAuthUrl) {

frontend/src/hooks/query/use-app-installations.ts

-20
This file was deleted.

frontend/src/hooks/query/use-app-repositories.ts

-67
This file was deleted.

frontend/src/hooks/query/use-user-repositories.ts

+2-5
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,17 @@
11
import { useInfiniteQuery } from "@tanstack/react-query";
22
import React from "react";
33
import { retrieveUserGitRepositories } from "#/api/git";
4-
import { useConfig } from "./use-config";
54
import { useAuth } from "#/context/auth-context";
65

76
export const useUserRepositories = () => {
87
const { providerTokensSet, providersAreSet } = useAuth();
9-
const { data: config } = useConfig();
108

119
const repos = useInfiniteQuery({
1210
queryKey: ["repositories", providerTokensSet],
13-
queryFn: async ({ pageParam }) =>
14-
retrieveUserGitRepositories(pageParam, 100),
11+
queryFn: async () => retrieveUserGitRepositories(),
1512
initialPageParam: 1,
1613
getNextPageParam: (lastPage) => lastPage.nextPage,
17-
enabled: providersAreSet && config?.APP_MODE === "oss",
14+
enabled: providersAreSet,
1815
staleTime: 1000 * 60 * 5, // 5 minutes
1916
gcTime: 1000 * 60 * 15, // 15 minutes
2017
});

openhands/integrations/github/github_service.py

+64-23
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
UnknownException,
1717
User,
1818
)
19+
from openhands.server.types import AppMode
1920
from openhands.utils.import_utils import get_impl
2021

2122

@@ -99,40 +100,80 @@ async def get_user(self) -> User:
99100
email=response.get('email'),
100101
)
101102

102-
async def get_repositories(
103-
self, sort: str, installation_id: int | None
104-
) -> list[Repository]:
105-
MAX_REPOS = 1000
106-
PER_PAGE = 100 # Maximum allowed by GitHub API
107-
all_repos: list[dict] = []
103+
104+
async def _fetch_paginated_repos(
105+
self, url: str, params: dict, max_repos: int, extract_key: str | None = None
106+
) -> list[dict]:
107+
"""
108+
Fetch repositories with pagination support.
109+
110+
Args:
111+
url: The API endpoint URL
112+
params: Query parameters for the request
113+
max_repos: Maximum number of repositories to fetch
114+
extract_key: If provided, extract repositories from this key in the response
115+
116+
Returns:
117+
List of repository dictionaries
118+
"""
119+
repos: list[dict] = []
108120
page = 1
109121

110-
while len(all_repos) < MAX_REPOS:
111-
params = {'page': str(page), 'per_page': str(PER_PAGE)}
112-
if installation_id:
113-
url = (
114-
f'{self.BASE_URL}/user/installations/{installation_id}/repositories'
115-
)
116-
response, headers = await self._fetch_data(url, params)
117-
response = response.get('repositories', [])
118-
else:
119-
url = f'{self.BASE_URL}/user/repos'
120-
params['sort'] = sort
121-
response, headers = await self._fetch_data(url, params)
122-
123-
if not response: # No more repositories
122+
while len(repos) < max_repos:
123+
page_params = {**params, 'page': str(page)}
124+
response, headers = await self._fetch_data(url, page_params)
125+
126+
# Extract repositories from response
127+
page_repos = response.get(extract_key, []) if extract_key else response
128+
129+
if not page_repos: # No more repositories
124130
break
125131

126-
all_repos.extend(response)
132+
repos.extend(page_repos)
127133
page += 1
128134

129135
# Check if we've reached the last page
130136
link_header = headers.get('Link', '')
131137
if 'rel="next"' not in link_header:
132138
break
133139

134-
# Trim to MAX_REPOS if needed and convert to Repository objects
135-
all_repos = all_repos[:MAX_REPOS]
140+
return repos[:max_repos] # Trim to max_repos if needed
141+
142+
async def get_repositories(self, sort: str, app_mode: AppMode) -> list[Repository]:
143+
MAX_REPOS = 1000
144+
PER_PAGE = 100 # Maximum allowed by GitHub API
145+
all_repos: list[dict] = []
146+
147+
if app_mode == AppMode.SAAS:
148+
# Get all installation IDs and fetch repos for each one
149+
installation_ids = await self.get_installation_ids()
150+
151+
# Iterate through each installation ID
152+
for installation_id in installation_ids:
153+
params = {'per_page': str(PER_PAGE)}
154+
url = (
155+
f'{self.BASE_URL}/user/installations/{installation_id}/repositories'
156+
)
157+
158+
# Fetch repositories for this installation
159+
installation_repos = await self._fetch_paginated_repos(
160+
url, params, MAX_REPOS - len(all_repos), extract_key='repositories'
161+
)
162+
163+
all_repos.extend(installation_repos)
164+
165+
# If we've already reached MAX_REPOS, no need to check other installations
166+
if len(all_repos) >= MAX_REPOS:
167+
break
168+
else:
169+
# Original behavior for non-SaaS mode
170+
params = {'per_page': str(PER_PAGE), 'sort': sort}
171+
url = f'{self.BASE_URL}/user/repos'
172+
173+
# Fetch user repositories
174+
all_repos = await self._fetch_paginated_repos(url, params, MAX_REPOS)
175+
176+
# Convert to Repository objects
136177
return [
137178
Repository(
138179
id=repo.get('id'),

openhands/integrations/gitlab/gitlab_service.py

+3-6
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
from typing import Any
3+
from urllib.parse import quote_plus
34

45
import httpx
56
from pydantic import SecretStr
@@ -12,6 +13,7 @@
1213
UnknownException,
1314
User,
1415
)
16+
from openhands.server.types import AppMode
1517
from openhands.utils.import_utils import get_impl
1618

1719

@@ -119,12 +121,7 @@ async def search_repositories(
119121

120122
return repos
121123

122-
async def get_repositories(
123-
self, sort: str, installation_id: int | None
124-
) -> list[Repository]:
125-
if installation_id:
126-
return [] # Not implementing installation_token case yet
127-
124+
async def get_repositories(self, sort: str, app_mode: AppMode) -> list[Repository]:
128125
MAX_REPOS = 1000
129126
PER_PAGE = 100 # Maximum allowed by GitLab API
130127
all_repos: list[dict] = []

openhands/integrations/provider.py

+3-6
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
Repository,
2727
User,
2828
)
29+
from openhands.server.types import AppMode
2930

3031

3132
class ProviderToken(BaseModel):
@@ -187,11 +188,7 @@ async def _get_latest_provider_token(
187188
service = self._get_service(provider)
188189
return await service.get_latest_token()
189190

190-
async def get_repositories(
191-
self,
192-
sort: str,
193-
installation_id: int | None,
194-
) -> list[Repository]:
191+
async def get_repositories(self, sort: str, app_mode: AppMode) -> list[Repository]:
195192
"""
196193
Get repositories from a selected providers with pagination support
197194
"""
@@ -200,7 +197,7 @@ async def get_repositories(
200197
for provider in self.provider_tokens:
201198
try:
202199
service = self._get_service(provider)
203-
service_repos = await service.get_repositories(sort, installation_id)
200+
service_repos = await service.get_repositories(sort, app_mode)
204201
all_repos.extend(service_repos)
205202
except Exception:
206203
continue

openhands/integrations/service_types.py

+3-5
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33

44
from pydantic import BaseModel, SecretStr
55

6+
from openhands.server.types import AppMode
7+
68

79
class ProviderType(Enum):
810
GITHUB = 'github'
@@ -86,10 +88,6 @@ async def search_repositories(
8688
"""Search for repositories"""
8789
...
8890

89-
async def get_repositories(
90-
self,
91-
sort: str,
92-
installation_id: int | None,
93-
) -> list[Repository]:
91+
async def get_repositories(self, sort: str, app_mode: AppMode) -> list[Repository]:
9492
"""Get repositories for the authenticated user"""
9593
...

openhands/server/routes/git.py

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from fastapi import APIRouter, Depends, status
22
from fastapi.responses import JSONResponse
33
from pydantic import SecretStr
4-
4+
from openhands.server.shared import server_config
55
from openhands.integrations.github.github_service import GithubServiceImpl
66
from openhands.integrations.provider import (
77
PROVIDER_TOKEN_TYPE,
@@ -16,14 +16,14 @@
1616
User,
1717
)
1818
from openhands.server.auth import get_access_token, get_provider_tokens
19+
from openhands.server.types import AppMode
1920

2021
app = APIRouter(prefix='/api/user')
2122

2223

2324
@app.get('/repositories', response_model=list[Repository])
2425
async def get_user_repositories(
2526
sort: str = 'pushed',
26-
installation_id: int | None = None,
2727
provider_tokens: PROVIDER_TOKEN_TYPE | None = Depends(get_provider_tokens),
2828
access_token: SecretStr | None = Depends(get_access_token),
2929
):
@@ -33,9 +33,7 @@ async def get_user_repositories(
3333
)
3434

3535
try:
36-
repos: list[Repository] = await client.get_repositories(
37-
sort, installation_id
38-
)
36+
repos: list[Repository] = await client.get_repositories(sort, server_config.app_mode)
3937
return repos
4038

4139
except AuthenticationError as e:

0 commit comments

Comments
 (0)