Skip to content

Commit 563b5eb

Browse files
committed
[server] handle listSuggestedRepositories
1 parent aca6a81 commit 563b5eb

File tree

3 files changed

+147
-128
lines changed

3 files changed

+147
-128
lines changed

components/server/src/api/scm-service-api.ts

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* See License.AGPL.txt in the project root for license information.
55
*/
66

7-
import { Code, ConnectError, HandlerContext, ServiceImpl } from "@connectrpc/connect";
7+
import { HandlerContext, ServiceImpl } from "@connectrpc/connect";
88
import { SCMService as ScmServiceInterface } from "@gitpod/public-api/lib/gitpod/v1/scm_connect";
99
import { inject, injectable } from "inversify";
1010
import { PublicAPIConverter } from "@gitpod/gitpod-protocol/lib/public-api-converter";
@@ -21,15 +21,21 @@ import {
2121
} from "@gitpod/public-api/lib/gitpod/v1/scm_pb";
2222
import { ctxUserId } from "../util/request-context";
2323
import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error";
24+
import { validate as uuidValidate } from "uuid";
25+
import { ProjectsService } from "../projects/projects-service";
26+
import { WorkspaceService } from "../workspace/workspace-service";
27+
import { PaginationResponse } from "@gitpod/public-api/lib/gitpod/v1/pagination_pb";
2428

2529
@injectable()
2630
export class ScmServiceAPI implements ServiceImpl<typeof ScmServiceInterface> {
2731
constructor(
2832
@inject(PublicAPIConverter) private readonly apiConverter: PublicAPIConverter,
2933
@inject(ScmService) private readonly scmService: ScmService,
34+
@inject(ProjectsService) private readonly projectService: ProjectsService,
35+
@inject(WorkspaceService) private readonly workspaceService: WorkspaceService,
3036
) {}
3137

32-
async getSCMToken(request: GetSCMTokenRequest, context: HandlerContext): Promise<GetSCMTokenResponse> {
38+
async getSCMToken(request: GetSCMTokenRequest, _: HandlerContext): Promise<GetSCMTokenResponse> {
3339
const userId = ctxUserId();
3440
try {
3541
const token = await this.scmService.getToken(userId, request);
@@ -39,10 +45,7 @@ export class ScmServiceAPI implements ServiceImpl<typeof ScmServiceInterface> {
3945
}
4046
}
4147

42-
async guessTokenScopes(
43-
request: GuessTokenScopesRequest,
44-
_context: HandlerContext,
45-
): Promise<GuessTokenScopesResponse> {
48+
async guessTokenScopes(request: GuessTokenScopesRequest, _: HandlerContext): Promise<GuessTokenScopesResponse> {
4649
const userId = ctxUserId();
4750
const { scopes, message } = await this.scmService.guessTokenScopes(userId, request);
4851
return new GuessTokenScopesResponse({
@@ -53,7 +56,7 @@ export class ScmServiceAPI implements ServiceImpl<typeof ScmServiceInterface> {
5356

5457
async searchRepositories(
5558
request: SearchRepositoriesRequest,
56-
_context: HandlerContext,
59+
_: HandlerContext,
5760
): Promise<SearchRepositoriesResponse> {
5861
const userId = ctxUserId();
5962
const repos = await this.scmService.searchRepositories(userId, {
@@ -67,8 +70,24 @@ export class ScmServiceAPI implements ServiceImpl<typeof ScmServiceInterface> {
6770

6871
async listSuggestedRepositories(
6972
request: ListSuggestedRepositoriesRequest,
70-
_context: HandlerContext,
73+
_: HandlerContext,
7174
): Promise<ListSuggestedRepositoriesResponse> {
72-
throw new ConnectError("unimplemented", Code.Unimplemented);
75+
const userId = ctxUserId();
76+
const { organizationId } = request;
77+
78+
if (!uuidValidate(organizationId)) {
79+
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "organizationId must be a valid UUID");
80+
}
81+
82+
const projectsPromise = this.projectService.getProjects(userId, organizationId);
83+
const workspacesPromise = this.workspaceService.getWorkspaces(userId, { organizationId });
84+
const repos = await this.scmService.listSuggestedRepositories(userId, { projectsPromise, workspacesPromise });
85+
return new ListSuggestedRepositoriesResponse({
86+
repositories: repos.map((r) => this.apiConverter.toSuggestedRepository(r)),
87+
pagination: new PaginationResponse({
88+
nextToken: "",
89+
total: repos.length,
90+
}),
91+
});
7392
}
7493
}

components/server/src/scm/scm-service.ts

Lines changed: 112 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { inject, injectable } from "inversify";
88
import { Authorizer } from "../authorization/authorizer";
99
import { Config } from "../config";
1010
import { TokenProvider } from "../user/token-provider";
11-
import { Project, SuggestedRepository, Token, User } from "@gitpod/gitpod-protocol";
11+
import { CommitContext, Project, SuggestedRepository, Token, User, WorkspaceInfo } from "@gitpod/gitpod-protocol";
1212
import { HostContextProvider } from "../auth/host-context-provider";
1313
import { RepoURL } from "../repohost";
1414
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
@@ -19,6 +19,8 @@ import { ApplicationError, ErrorCodes } from "@gitpod/gitpod-protocol/lib/messag
1919
import {
2020
SuggestedRepositoryWithSorting,
2121
sortSuggestedRepositories,
22+
suggestionFromProject,
23+
suggestionFromRecentWorkspace,
2224
suggestionFromUserRepo,
2325
} from "../workspace/suggested-repos-sorter";
2426

@@ -120,4 +122,113 @@ export class ScmService {
120122
}),
121123
);
122124
}
125+
126+
public async listSuggestedRepositories(
127+
userId: string,
128+
params: {
129+
projectsPromise: Promise<Project[]>;
130+
workspacesPromise: Promise<WorkspaceInfo[]>;
131+
},
132+
) {
133+
const user = await this.userService.findUserById(userId, userId);
134+
const logCtx = { userId: user.id };
135+
136+
const fetchProjects = async (): Promise<SuggestedRepositoryWithSorting[]> => {
137+
const projects = await params.projectsPromise;
138+
139+
const projectRepos = projects.map((project) => {
140+
return suggestionFromProject({
141+
url: project.cloneUrl.replace(/\.git$/, ""),
142+
projectId: project.id,
143+
projectName: project.name,
144+
});
145+
});
146+
147+
return projectRepos;
148+
};
149+
150+
// Load user repositories (from Git hosts directly)
151+
const fetchUserRepos = async (): Promise<SuggestedRepositoryWithSorting[]> => {
152+
const authProviders = await this.authProviderService.getAuthProviderDescriptions(user);
153+
154+
const providerRepos = await Promise.all(
155+
authProviders.map(async (p): Promise<SuggestedRepositoryWithSorting[]> => {
156+
try {
157+
const hostContext = this.hostContextProvider.get(p.host);
158+
const services = hostContext?.services;
159+
if (!services) {
160+
log.error(logCtx, "Unsupported repository host: " + p.host);
161+
return [];
162+
}
163+
const userRepos = await services.repositoryProvider.getUserRepos(user);
164+
165+
return userRepos.map((r) =>
166+
suggestionFromUserRepo({
167+
url: r.url.replace(/\.git$/, ""),
168+
repositoryName: r.name,
169+
}),
170+
);
171+
} catch (error) {
172+
log.debug(logCtx, "Could not get user repositories from host " + p.host, error);
173+
}
174+
175+
return [];
176+
}),
177+
);
178+
179+
return providerRepos.flat();
180+
};
181+
182+
const fetchRecentRepos = async (): Promise<SuggestedRepositoryWithSorting[]> => {
183+
const workspaces = await params.workspacesPromise;
184+
const recentRepos: SuggestedRepositoryWithSorting[] = [];
185+
186+
for (const ws of workspaces) {
187+
let repoUrl;
188+
let repoName;
189+
if (CommitContext.is(ws.workspace.context)) {
190+
repoUrl = ws.workspace.context?.repository?.cloneUrl?.replace(/\.git$/, "");
191+
repoName = ws.workspace.context?.repository?.name;
192+
}
193+
if (!repoUrl) {
194+
repoUrl = ws.workspace.contextURL;
195+
}
196+
if (repoUrl) {
197+
const lastUse = WorkspaceInfo.lastActiveISODate(ws);
198+
199+
recentRepos.push(
200+
suggestionFromRecentWorkspace(
201+
{
202+
url: repoUrl,
203+
projectId: ws.workspace.projectId,
204+
repositoryName: repoName || "",
205+
},
206+
lastUse,
207+
),
208+
);
209+
}
210+
}
211+
return recentRepos;
212+
};
213+
214+
const repoResults = await Promise.allSettled([
215+
fetchProjects().catch((e) => log.error(logCtx, "Could not fetch projects", e)),
216+
fetchUserRepos().catch((e) => log.error(logCtx, "Could not fetch user repositories", e)),
217+
fetchRecentRepos().catch((e) => log.error(logCtx, "Could not fetch recent repositories", e)),
218+
]);
219+
220+
const sortedRepos = sortSuggestedRepositories(
221+
repoResults.map((r) => (r.status === "fulfilled" ? r.value || [] : [])).flat(),
222+
);
223+
224+
// Convert to SuggestedRepository (drops sorting props)
225+
return sortedRepos.map(
226+
(repo): SuggestedRepository => ({
227+
url: repo.url,
228+
projectId: repo.projectId,
229+
projectName: repo.projectName,
230+
repositoryName: repo.repositoryName,
231+
}),
232+
);
233+
}
123234
}

components/server/src/workspace/gitpod-server-impl.ts

Lines changed: 7 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -161,13 +161,6 @@ import { SSHKeyService } from "../user/sshkey-service";
161161
import { StartWorkspaceOptions, WorkspaceService } from "./workspace-service";
162162
import { GitpodTokenService } from "../user/gitpod-token-service";
163163
import { EnvVarService } from "../user/env-var-service";
164-
import {
165-
SuggestedRepositoryWithSorting,
166-
sortSuggestedRepositories,
167-
suggestionFromProject,
168-
suggestionFromRecentWorkspace,
169-
suggestionFromUserRepo,
170-
} from "./suggested-repos-sorter";
171164
import { SubjectId } from "../auth/subject-id";
172165
import { runWithSubjectId } from "../util/request-context";
173166
import { ScmService } from "../scm/scm-service";
@@ -1366,118 +1359,14 @@ export class GitpodServerImpl implements GitpodServerWithTracing, Disposable {
13661359
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "organizationId must be a valid UUID");
13671360
}
13681361

1369-
const logCtx: LogContext = { userId: user.id };
1370-
1371-
const fetchProjects = async (): Promise<SuggestedRepositoryWithSorting[]> => {
1372-
const span = TraceContext.startSpan("getSuggestedRepositories.fetchProjects", ctx);
1373-
const projects = await this.projectsService.getProjects(user.id, organizationId);
1374-
1375-
const projectRepos = projects.map((project) => {
1376-
return suggestionFromProject({
1377-
url: project.cloneUrl.replace(/\.git$/, ""),
1378-
projectId: project.id,
1379-
projectName: project.name,
1380-
});
1381-
});
1382-
1383-
span.finish();
1384-
1385-
return projectRepos;
1386-
};
1387-
1388-
// Load user repositories (from Git hosts directly)
1389-
const fetchUserRepos = async (): Promise<SuggestedRepositoryWithSorting[]> => {
1390-
const span = TraceContext.startSpan("getSuggestedRepositories.fetchUserRepos", ctx);
1391-
const authProviders = await this.getAuthProviders(ctx);
1392-
1393-
const providerRepos = await Promise.all(
1394-
authProviders.map(async (p): Promise<SuggestedRepositoryWithSorting[]> => {
1395-
try {
1396-
span.setTag("host", p.host);
1397-
1398-
const hostContext = this.hostContextProvider.get(p.host);
1399-
const services = hostContext?.services;
1400-
if (!services) {
1401-
log.error(logCtx, "Unsupported repository host: " + p.host);
1402-
return [];
1403-
}
1404-
const userRepos = await services.repositoryProvider.getUserRepos(user);
1405-
1406-
return userRepos.map((r) =>
1407-
suggestionFromUserRepo({
1408-
url: r.url.replace(/\.git$/, ""),
1409-
repositoryName: r.name,
1410-
}),
1411-
);
1412-
} catch (error) {
1413-
log.debug(logCtx, "Could not get user repositories from host " + p.host, error);
1414-
}
1415-
1416-
return [];
1417-
}),
1418-
);
1419-
1420-
span.finish();
1421-
1422-
return providerRepos.flat();
1423-
};
1424-
1425-
const fetchRecentRepos = async (): Promise<SuggestedRepositoryWithSorting[]> => {
1426-
const span = TraceContext.startSpan("getSuggestedRepositories.fetchRecentRepos", ctx);
1427-
1428-
const workspaces = await this.getWorkspaces(ctx, { organizationId });
1429-
const recentRepos: SuggestedRepositoryWithSorting[] = [];
1430-
1431-
for (const ws of workspaces) {
1432-
let repoUrl;
1433-
let repoName;
1434-
if (CommitContext.is(ws.workspace.context)) {
1435-
repoUrl = ws.workspace.context?.repository?.cloneUrl?.replace(/\.git$/, "");
1436-
repoName = ws.workspace.context?.repository?.name;
1437-
}
1438-
if (!repoUrl) {
1439-
repoUrl = ws.workspace.contextURL;
1440-
}
1441-
if (repoUrl) {
1442-
const lastUse = WorkspaceInfo.lastActiveISODate(ws);
1443-
1444-
recentRepos.push(
1445-
suggestionFromRecentWorkspace(
1446-
{
1447-
url: repoUrl,
1448-
projectId: ws.workspace.projectId,
1449-
repositoryName: repoName || "",
1450-
},
1451-
lastUse,
1452-
),
1453-
);
1454-
}
1455-
}
1456-
1457-
span.finish();
1458-
1459-
return recentRepos;
1460-
};
1461-
1462-
const repoResults = await Promise.allSettled([
1463-
fetchProjects().catch((e) => log.error(logCtx, "Could not fetch projects", e)),
1464-
fetchUserRepos().catch((e) => log.error(logCtx, "Could not fetch user repositories", e)),
1465-
fetchRecentRepos().catch((e) => log.error(logCtx, "Could not fetch recent repositories", e)),
1466-
]);
1467-
1468-
const sortedRepos = sortSuggestedRepositories(
1469-
repoResults.map((r) => (r.status === "fulfilled" ? r.value || [] : [])).flat(),
1470-
);
1362+
if (!uuidValidate(organizationId)) {
1363+
throw new ApplicationError(ErrorCodes.BAD_REQUEST, "organizationId must be a valid UUID");
1364+
}
14711365

1472-
// Convert to SuggestedRepository (drops sorting props)
1473-
return sortedRepos.map(
1474-
(repo): SuggestedRepository => ({
1475-
url: repo.url,
1476-
projectId: repo.projectId,
1477-
projectName: repo.projectName,
1478-
repositoryName: repo.repositoryName,
1479-
}),
1480-
);
1366+
const projectsPromise = this.projectsService.getProjects(user.id, organizationId);
1367+
const workspacesPromise = this.workspaceService.getWorkspaces(user.id, { organizationId });
1368+
const repos = await this.scmService.listSuggestedRepositories(user.id, { projectsPromise, workspacesPromise });
1369+
return repos;
14811370
}
14821371

14831372
public async searchRepositories(

0 commit comments

Comments
 (0)