Skip to content

Commit 59fb058

Browse files
authored
[dashboard] deduplicate repositories (#19013)
1 parent 98ea4f6 commit 59fb058

File tree

3 files changed

+123
-36
lines changed

3 files changed

+123
-36
lines changed
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* Copyright (c) 2023 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License.AGPL.txt in the project root for license information.
5+
*/
6+
7+
import { SuggestedRepository } from "@gitpod/gitpod-protocol";
8+
import { deduplicateAndFilterRepositories } from "./unified-repositories-search-query";
9+
10+
function repo(name: string, project?: string): SuggestedRepository {
11+
return {
12+
url: `http://github.com/efu3he4rf/${name}`,
13+
repositoryName: name,
14+
projectName: project,
15+
projectId: project,
16+
};
17+
}
18+
19+
test("it should deduplicate non-project entries", () => {
20+
const suggestedRepos: SuggestedRepository[] = [repo("foo"), repo("foo2"), repo("foo", "project-foo")];
21+
const deduplicated = deduplicateAndFilterRepositories("foo", false, suggestedRepos);
22+
expect(deduplicated.length).toEqual(2);
23+
expect(deduplicated[1].projectName).toEqual("project-foo");
24+
});
25+
26+
test("it should not deduplicate project entries", () => {
27+
const suggestedRepos: SuggestedRepository[] = [
28+
repo("foo", "project-foo2"),
29+
repo("foo2"),
30+
repo("foo", "project-foo"),
31+
];
32+
const deduplicated = deduplicateAndFilterRepositories("foo", false, suggestedRepos);
33+
expect(deduplicated.length).toEqual(3);
34+
});
35+
36+
test("it should exclude project entries", () => {
37+
const suggestedRepos: SuggestedRepository[] = [
38+
repo("foo", "project-foo2"),
39+
repo("foo2"),
40+
repo("foo", "project-foo"),
41+
];
42+
const deduplicated = deduplicateAndFilterRepositories("foo", true, suggestedRepos);
43+
expect(deduplicated.length).toEqual(1);
44+
});
45+
46+
test("it should match entries in url as well as poject name", () => {
47+
const suggestedRepos: SuggestedRepository[] = [
48+
repo("somefOOtest"),
49+
repo("Footest"),
50+
repo("somefoO"),
51+
repo("bar", "somefOO"),
52+
repo("bar", "someFootest"),
53+
repo("bar", "FOOtest"),
54+
];
55+
var deduplicated = deduplicateAndFilterRepositories("foo", false, suggestedRepos);
56+
expect(deduplicated.length).toEqual(6);
57+
deduplicated = deduplicateAndFilterRepositories("foot", false, suggestedRepos);
58+
expect(deduplicated.length).toEqual(4);
59+
deduplicated = deduplicateAndFilterRepositories("FOOT", false, suggestedRepos);
60+
expect(deduplicated.length).toEqual(4);
61+
});
62+
63+
test("it keeps the order", () => {
64+
const suggestedRepos: SuggestedRepository[] = [
65+
repo("somefOOtest"),
66+
repo("Footest"),
67+
repo("somefoO"),
68+
repo("bar", "somefOO"),
69+
repo("bar", "someFootest"),
70+
repo("bar", "FOOtest"),
71+
];
72+
const deduplicated = deduplicateAndFilterRepositories("foot", false, suggestedRepos);
73+
expect(deduplicated[0].repositoryName).toEqual("somefOOtest");
74+
expect(deduplicated[1].repositoryName).toEqual("Footest");
75+
expect(deduplicated[2].projectName).toEqual("someFootest");
76+
expect(deduplicated[3].projectName).toEqual("FOOtest");
77+
});

components/dashboard/src/data/git-providers/unified-repositories-search-query.ts

Lines changed: 45 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -20,26 +20,8 @@ export const useUnifiedRepositorySearch = ({ searchString, excludeProjects = fal
2020
const searchQuery = useSearchRepositories({ searchString });
2121

2222
const filteredRepos = useMemo(() => {
23-
const repoMap = new Map<string, SuggestedRepository>();
24-
// combine & flatten suggestions and search results, then merge them into a map
2523
const flattenedRepos = [suggestedQuery.data || [], searchQuery.data || []].flat();
26-
27-
for (const repo of flattenedRepos) {
28-
const key = excludeProjects ? repo.url : `${repo.url}:${repo.projectId || ""}`;
29-
30-
const newEntry = {
31-
...(repoMap.get(key) || {}),
32-
...repo,
33-
};
34-
if (excludeProjects) {
35-
// TODO: would be great if we can always include repositoryName on SuggestedRepository entities, then we could remove this
36-
newEntry.repositoryName = newEntry.repositoryName || newEntry.projectName;
37-
newEntry.projectName = undefined;
38-
}
39-
repoMap.set(key, newEntry);
40-
}
41-
42-
return filterRepos(searchString, Array.from(repoMap.values()));
24+
return deduplicateAndFilterRepositories(searchString, excludeProjects, flattenedRepos);
4325
}, [excludeProjects, searchQuery.data, searchString, suggestedQuery.data]);
4426

4527
return {
@@ -51,24 +33,52 @@ export const useUnifiedRepositorySearch = ({ searchString, excludeProjects = fal
5133
};
5234
};
5335

54-
export const filterRepos = (searchString: string, suggestedRepos: SuggestedRepository[]) => {
55-
let results = suggestedRepos;
56-
const normalizedSearchString = searchString.trim();
57-
58-
if (normalizedSearchString.length > 1) {
59-
results = suggestedRepos.filter((r) => {
60-
return `${r.url}${r.projectName || ""}`.toLowerCase().includes(normalizedSearchString);
36+
export function deduplicateAndFilterRepositories(
37+
searchString: string,
38+
excludeProjects = false,
39+
suggestedRepos: SuggestedRepository[],
40+
): SuggestedRepository[] {
41+
const normalizedSearchString = searchString.trim().toLowerCase();
42+
const collected = new Set<string>();
43+
const results: SuggestedRepository[] = [];
44+
const reposWithProject = new Set<string>();
45+
if (!excludeProjects) {
46+
suggestedRepos.forEach((r) => {
47+
if (r.projectId) {
48+
reposWithProject.add(r.url);
49+
}
6150
});
62-
63-
if (results.length === 0) {
64-
try {
65-
// If the normalizedSearchString is a URL, and it's not present in the proposed results, "artificially" add it here.
66-
new URL(normalizedSearchString);
67-
results.push({ url: normalizedSearchString });
68-
} catch {}
51+
}
52+
for (const repo of suggestedRepos) {
53+
// filter out project entries if excludeProjects is true
54+
if (repo.projectId && excludeProjects) {
55+
continue;
56+
}
57+
// filter out project-less entries if an entry with a project exists
58+
if (!repo.projectId && reposWithProject.has(repo.url)) {
59+
continue;
60+
}
61+
// filter out entries that don't match the search string
62+
if (!`${repo.url}${repo.projectName || ""}`.toLowerCase().includes(normalizedSearchString)) {
63+
continue;
64+
}
65+
// filter out duplicates
66+
const key = `${repo.url}:${repo.projectId || "no-project"}`;
67+
if (collected.has(key)) {
68+
continue;
6969
}
70+
collected.add(key);
71+
results.push(repo);
72+
}
73+
74+
if (results.length === 0) {
75+
try {
76+
// If the normalizedSearchString is a URL, and it's not present in the proposed results, "artificially" add it here.
77+
new URL(normalizedSearchString);
78+
results.push({ url: normalizedSearchString });
79+
} catch {}
7080
}
7181

7282
// Limit what we show to 200 results
73-
return results.length > 200 ? results.slice(0, 200) : results;
74-
};
83+
return results.slice(0, 200);
84+
}

components/server/src/bitbucket-server/bitbucket-server-repository-provider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ export class BitbucketServerRepositoryProvider implements RepositoryProvider {
210210

211211
public async searchRepos(user: User, searchString: string): Promise<RepositoryInfo[]> {
212212
// Only load 1 page of 10 results for our searchString
213-
const results = await this.api.getRepos(user, { maxPages: 1, limit: 10, searchString });
213+
const results = await this.api.getRepos(user, { maxPages: 1, limit: 30, searchString });
214214

215215
const repos: RepositoryInfo[] = [];
216216
results.forEach((r) => {

0 commit comments

Comments
 (0)