Skip to content

Commit 2ee6854

Browse files
committed
[dashboard] deduplicate repositories
1 parent 98ea4f6 commit 2ee6854

File tree

3 files changed

+118
-34
lines changed

3 files changed

+118
-34
lines changed
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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+
});
24+
25+
test("it should not deduplicate project entries", () => {
26+
const suggestedRepos: SuggestedRepository[] = [
27+
repo("foo", "project-foo2"),
28+
repo("foo2"),
29+
repo("foo", "project-foo"),
30+
];
31+
const deduplicated = deduplicateAndFilterRepositories("foo", false, suggestedRepos);
32+
expect(deduplicated.length).toEqual(3);
33+
});
34+
35+
test("it should exclude project entries", () => {
36+
const suggestedRepos: SuggestedRepository[] = [
37+
repo("foo", "project-foo2"),
38+
repo("foo2"),
39+
repo("foo", "project-foo"),
40+
];
41+
const deduplicated = deduplicateAndFilterRepositories("foo", true, suggestedRepos);
42+
expect(deduplicated.length).toEqual(1);
43+
});
44+
45+
test("it should match entries in url as well as poject name", () => {
46+
const suggestedRepos: SuggestedRepository[] = [
47+
repo("somefOOtest"),
48+
repo("Footest"),
49+
repo("somefoO"),
50+
repo("bar", "somefOO"),
51+
repo("bar", "someFootest"),
52+
repo("bar", "FOOtest"),
53+
];
54+
var deduplicated = deduplicateAndFilterRepositories("foo", false, suggestedRepos);
55+
expect(deduplicated.length).toEqual(6);
56+
deduplicated = deduplicateAndFilterRepositories("foot", false, suggestedRepos);
57+
expect(deduplicated.length).toEqual(4);
58+
});
59+
60+
test("it keeps the order", () => {
61+
const suggestedRepos: SuggestedRepository[] = [
62+
repo("somefOOtest"),
63+
repo("Footest"),
64+
repo("somefoO"),
65+
repo("bar", "somefOO"),
66+
repo("bar", "someFootest"),
67+
repo("bar", "FOOtest"),
68+
];
69+
const deduplicated = deduplicateAndFilterRepositories("foot", false, suggestedRepos);
70+
expect(deduplicated[0].repositoryName).toEqual("somefOOtest");
71+
expect(deduplicated[1].repositoryName).toEqual("Footest");
72+
expect(deduplicated[2].projectName).toEqual("someFootest");
73+
expect(deduplicated[3].projectName).toEqual("FOOtest");
74+
});

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

Lines changed: 43 additions & 33 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;
36+
export function deduplicateAndFilterRepositories(
37+
searchString: string,
38+
excludeProjects = false,
39+
suggestedRepos: SuggestedRepository[],
40+
): SuggestedRepository[] {
5641
const normalizedSearchString = searchString.trim();
57-
58-
if (normalizedSearchString.length > 1) {
59-
results = suggestedRepos.filter((r) => {
60-
return `${r.url}${r.projectName || ""}`.toLowerCase().includes(normalizedSearchString);
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
7383
return results.length > 200 ? results.slice(0, 200) : results;
74-
};
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)