Skip to content

Commit b4d0ac4

Browse files
committed
clean up and add some tracing
1 parent a20d949 commit b4d0ac4

File tree

3 files changed

+229
-180
lines changed

3 files changed

+229
-180
lines changed

components/server/src/auth/auth-provider-service.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
import { injectable, inject } from "inversify";
8-
import { AuthProviderEntry as AuthProviderEntry, User } from "@gitpod/gitpod-protocol";
8+
import { AuthProviderEntry as AuthProviderEntry, AuthProviderInfo, User } from "@gitpod/gitpod-protocol";
99
import { AuthProviderParams } from "./auth-provider";
1010
import { AuthProviderEntryDB, TeamDB } from "@gitpod/gitpod-db/lib";
1111
import { Config } from "../config";
@@ -16,6 +16,7 @@ import { oauthUrls as bbsUrls } from "../bitbucket-server/bitbucket-server-urls"
1616
import { oauthUrls as bbUrls } from "../bitbucket/bitbucket-urls";
1717
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
1818
import fetch from "node-fetch";
19+
import { HostContextProvider } from "./host-context-provider";
1920

2021
@injectable()
2122
export class AuthProviderService {
@@ -28,6 +29,60 @@ export class AuthProviderService {
2829
@inject(Config)
2930
protected readonly config: Config;
3031

32+
@inject(HostContextProvider)
33+
private readonly hostContextProvider: HostContextProvider;
34+
35+
public async getAuthProvidersInfo(user?: User): Promise<AuthProviderInfo[]> {
36+
const { builtinAuthProvidersConfigured } = this.config;
37+
38+
const hostContexts = this.hostContextProvider.getAll();
39+
const authProviders = hostContexts.map((hc) => hc.authProvider.info);
40+
41+
const isBuiltIn = (info: AuthProviderInfo) => !info.ownerId;
42+
const isNotHidden = (info: AuthProviderInfo) => !info.hiddenOnDashboard;
43+
const isVerified = (info: AuthProviderInfo) => info.verified;
44+
const isNotOrgProvider = (info: AuthProviderInfo) => !info.organizationId;
45+
46+
// if no user session is available, compute public information only
47+
if (!user) {
48+
const toPublic = (info: AuthProviderInfo) =>
49+
<AuthProviderInfo>{
50+
authProviderId: info.authProviderId,
51+
authProviderType: info.authProviderType,
52+
disallowLogin: info.disallowLogin,
53+
host: info.host,
54+
icon: info.icon,
55+
description: info.description,
56+
};
57+
let result = authProviders.filter(isNotHidden).filter(isVerified).filter(isNotOrgProvider);
58+
if (builtinAuthProvidersConfigured) {
59+
result = result.filter(isBuiltIn);
60+
}
61+
return result.map(toPublic);
62+
}
63+
64+
// otherwise show all the details
65+
const result: AuthProviderInfo[] = [];
66+
for (const info of authProviders) {
67+
const identity = user.identities.find((i) => i.authProviderId === info.authProviderId);
68+
if (identity) {
69+
result.push({ ...info, isReadonly: identity.readonly });
70+
continue;
71+
}
72+
if (info.ownerId === user.id) {
73+
result.push(info);
74+
continue;
75+
}
76+
if (builtinAuthProvidersConfigured && !isBuiltIn(info)) {
77+
continue;
78+
}
79+
if (isNotHidden(info) && isVerified(info)) {
80+
result.push(info);
81+
}
82+
}
83+
return result;
84+
}
85+
3186
/**
3287
* Returns all auth providers.
3388
*/

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

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

7-
import { Project, User } from "@gitpod/gitpod-protocol";
7+
import { CommitContext, Project, SuggestedRepository, User, WorkspaceInfo } from "@gitpod/gitpod-protocol";
88
import { RepoURL } from "../repohost";
99
import { inject, injectable } from "inversify";
1010
import { HostContextProvider } from "../auth/host-context-provider";
1111
import { Config } from "../config";
12-
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
12+
import { LogContext, log } from "@gitpod/gitpod-protocol/lib/util/logging";
13+
import { ProjectsService } from "./projects-service";
14+
import { WorkspaceService } from "../workspace/workspace-service";
15+
import { AuthProviderService } from "../auth/auth-provider-service";
16+
import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing";
17+
18+
type SuggestedRepositoryWithSorting = SuggestedRepository & {
19+
priority: number;
20+
lastUse?: string;
21+
};
1322

1423
@injectable()
1524
export class ScmService {
1625
constructor(
1726
@inject(HostContextProvider) private readonly hostContextProvider: HostContextProvider,
1827
@inject(Config) private readonly config: Config,
28+
@inject(ProjectsService) private readonly projectsService: ProjectsService,
29+
@inject(WorkspaceService) private readonly workspaceService: WorkspaceService,
30+
@inject(AuthProviderService) private readonly authProviderService: AuthProviderService,
1931
) {}
2032

2133
async canInstallWebhook(currentUser: User, cloneURL: string) {
@@ -74,4 +86,156 @@ export class ScmService {
7486
}
7587
}
7688
}
89+
90+
public async getSuggestedRepositories(ctx: TraceContext, user: User, organizationId: string) {
91+
const logCtx: LogContext = { userId: user.id };
92+
const span = TraceContext.startSpan("SCMService.getSuggestedRepositories", ctx);
93+
94+
const repoResults = await Promise.allSettled([
95+
this.fetchProjects(ctx, user, organizationId).catch((e) =>
96+
log.error(logCtx, "Could not fetch projects", e),
97+
),
98+
this.fetchUserRepos(ctx, user).catch((e) => log.error(logCtx, "Could not fetch user repositories", e)),
99+
this.fetchRecentRepos(ctx, user, organizationId).catch((e) =>
100+
log.error(logCtx, "Could not fetch recent repositories", e),
101+
),
102+
]);
103+
104+
const sortedRepos = repoResults
105+
// filter out rejected promises
106+
.map((r) => (r.status === "fulfilled" ? r.value || [] : []))
107+
// flatten out results from all promises
108+
.flat()
109+
.sort((a, b) => {
110+
// priority first
111+
if (a.priority !== b.priority) {
112+
return a.priority < b.priority ? 1 : -1;
113+
}
114+
// Most recently used second
115+
if (b.lastUse || a.lastUse) {
116+
const la = a.lastUse || "";
117+
const lb = b.lastUse || "";
118+
return la < lb ? 1 : la === lb ? 0 : -1;
119+
}
120+
// Otherwise, alphasort
121+
const ua = a.url.toLowerCase();
122+
const ub = b.url.toLowerCase();
123+
return ua > ub ? 1 : ua === ub ? 0 : -1;
124+
});
125+
126+
const uniqueRepositories = new Map<string, SuggestedRepositoryWithSorting>();
127+
128+
for (const repo of sortedRepos) {
129+
const existingRepo = uniqueRepositories.get(repo.url);
130+
131+
uniqueRepositories.set(repo.url, {
132+
...(existingRepo || {}),
133+
...repo,
134+
});
135+
}
136+
137+
// Convert to return type
138+
const result = Array.from(uniqueRepositories.values()).map(
139+
(repo): SuggestedRepository => ({
140+
url: repo.url,
141+
projectId: repo.projectId,
142+
projectName: repo.projectName,
143+
}),
144+
);
145+
146+
span.finish();
147+
148+
return result;
149+
}
150+
151+
private async fetchProjects(
152+
ctx: TraceContext,
153+
user: User,
154+
organizationId: string,
155+
): Promise<SuggestedRepositoryWithSorting[]> {
156+
const span = TraceContext.startSpan("SCMService.fetchProjects", ctx);
157+
const projects = await this.projectsService.getProjects(user.id, organizationId);
158+
span.finish();
159+
160+
return projects.map((project) => ({
161+
url: project.cloneUrl.replace(/\.git$/, ""),
162+
projectId: project.id,
163+
projectName: project.name,
164+
priority: 1,
165+
}));
166+
}
167+
168+
// Load user repositories (from Git hosts directly)
169+
private async fetchUserRepos(ctx: TraceContext, user: User): Promise<SuggestedRepositoryWithSorting[]> {
170+
const span = TraceContext.startSpan("SCMService.fetchUserRepos", ctx);
171+
const logCtx: LogContext = { userId: user.id };
172+
173+
const authProviders = await this.authProviderService.getAuthProvidersInfo(user);
174+
175+
const providerRepos = await Promise.all(
176+
authProviders.map(async (p): Promise<SuggestedRepositoryWithSorting[]> => {
177+
try {
178+
span.setTag("host", p.host);
179+
180+
const hostContext = this.hostContextProvider.get(p.host);
181+
const services = hostContext?.services;
182+
if (!services) {
183+
log.error(logCtx, "Unsupported repository host: " + p.host);
184+
return [];
185+
}
186+
const userRepos = await services.repositoryProvider.getUserRepos(user);
187+
188+
return userRepos.map((r) => ({
189+
url: r.replace(/\.git$/, ""),
190+
priority: 5,
191+
}));
192+
} catch (error) {
193+
log.debug(logCtx, "Could not get user repositories from host " + p.host, error);
194+
}
195+
196+
return [];
197+
}),
198+
);
199+
200+
span.finish();
201+
202+
return providerRepos.flat();
203+
}
204+
205+
private async fetchRecentRepos(
206+
ctx: TraceContext,
207+
user: User,
208+
organizationId: string,
209+
): Promise<SuggestedRepositoryWithSorting[]> {
210+
const span = TraceContext.startSpan("SCMService.fetchRecentRepos", ctx);
211+
212+
// TODO: do we need to check permissions on each ws here like we do in gitpod server?
213+
const workspaces = await this.workspaceService.getWorkspaces(user.id, { organizationId });
214+
215+
const recentRepos: SuggestedRepositoryWithSorting[] = [];
216+
217+
for (const ws of workspaces) {
218+
let repoUrl;
219+
if (CommitContext.is(ws.workspace.context)) {
220+
repoUrl = ws.workspace.context?.repository?.cloneUrl?.replace(/\.git$/, "");
221+
}
222+
if (!repoUrl) {
223+
repoUrl = ws.workspace.contextURL;
224+
}
225+
if (repoUrl) {
226+
const lastUse = WorkspaceInfo.lastActiveISODate(ws);
227+
228+
recentRepos.push({
229+
url: repoUrl,
230+
projectId: ws.workspace.projectId,
231+
priority: 10,
232+
lastUse,
233+
});
234+
}
235+
}
236+
237+
span.finish();
238+
239+
return recentRepos;
240+
}
77241
}

0 commit comments

Comments
 (0)