Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,6 @@ import { useCopilotStore } from '~/components/shared/modules/copilot/store/copil
import LfxTag from '~/components/uikit/tag/tag.vue';
import { useCommunityStore } from '~/components/modules/project/components/community/store/community.store';
import { useBannerStore } from '~/components/shared/store/banner.store';
import { normalizeRepoName } from '~/components/shared/utils/helper';

const props = defineProps<{
project?: Project;
Expand All @@ -257,28 +256,17 @@ const { openCommunityFilterModal } = useCommunityStore();
const { hasLfxInsightsPermission } = storeToRefs(useAuthStore());

const repos = computed<ProjectRepository[]>(() =>
projectRepos.value
.filter((repo) => selectedRepoSlugs.value.includes(repo.slug))
.map((repo) => {
return {
...repo,
name: normalizeRepoName(repo),
};
}),
projectRepos.value.filter((repo) => selectedRepoSlugs.value.includes(repo.slug)),
);

const reposNoDuplicates = computed<ProjectRepository[]>(() => {
return repos.value.filter((repo, index, self) => index === self.findIndex((t) => t.name === repo.name));
});

const repoName = computed<string>(() => {
if (reposNoDuplicates.value.length === 0) {
if (repos.value.length === 0) {
return '';
}
if (reposNoDuplicates.value.length === 1) {
return reposNoDuplicates.value[0]!.name; //.split('/').at(-1) || '';
if (repos.value.length === 1) {
return repos.value[0]!.name; //.split('/').at(-1) || '';
}
return `${reposNoDuplicates.value.length} repositories`;
return `${repos.value.length} repositories`;
});

const archivedRepoLabel = computed<string>(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ import type { ProjectRepository } from '~~/types/project';
import LfxProjectRepositorySwitchItem from '~/components/modules/project/components/shared/header/repository-switch/repository-switch-item.vue';
import LfxArchivedTag from '~/components/shared/components/archived-tag.vue';
import type { ProjectLinkConfig } from '~/components/modules/project/config/links';
import { normalizeRepoName } from '~/components/shared/utils/helper';

const props = defineProps<{
link: ProjectLinkConfig;
Expand All @@ -114,17 +113,10 @@ const repositories = computed<RepositoryItem[]>(() =>
})),
);

const normalizedRepos = computed<RepositoryItem[]>(() =>
repositories.value.map((repo) => ({
...repo,
name: normalizeRepoName(repo),
})),
);

const result = computed<RepositoryItem[]>(() => {
const seen = new Set<string>();

return normalizedRepos.value
return repositories.value
.filter((repo) => {
if (seen.has(repo.name)) {
return false;
Expand All @@ -141,7 +133,7 @@ const { list, containerProps, wrapperProps } = useVirtualList(result, {

const handleReposChange = (repo: RepositoryItem) => {
let repos: string[] = [];
const slugs: string[] = normalizedRepos.value.filter((r) => r.name === repo.name).map((r) => r.slug);
const slugs: string[] = repositories.value.filter((r) => r.name === repo.name).map((r) => r.slug);
const isSelected = slugs.some((slug) => selectedRepoSlugs.value.includes(slug));
if (isSelected) {
repos = selectedRepoSlugs.value.filter((s) => !slugs.includes(s));
Expand Down
30 changes: 27 additions & 3 deletions frontend/app/components/modules/project/store/project.store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,9 +101,33 @@ export const useProjectStore = defineStore('project', () => {
});

// Selected repository URLs
const selectedReposValues = computed<string[]>(() =>
selectedRepositories.value.map((repo: ProjectRepository) => repo.url),
);
const selectedReposValues = computed<string[]>(() => {
const hasGerrit = project.value?.connectedPlatforms?.some((platform) =>
platform.toLowerCase().includes('gerrit'),
);
const repos = selectedRepositories.value.map((repo: ProjectRepository) => repo.url);

if (hasGerrit) {
// For Gerrit repos, add the query URL format alongside each repo URL
// Input: https://gerrit.<domain>/<namespace>/<project>/<repo>
// Output: https://gerrit.<domain>/<namespace>/q/project:<project>/<repo>
const gerritUrlPattern = /^(https:\/\/gerrit\.[^/]+\/[^/]+)\/(.+)$/;
const expandedRepos: string[] = [];

for (const repoUrl of repos) {
expandedRepos.push(repoUrl);
const match = repoUrl.match(gerritUrlPattern);
if (match) {
const [, baseWithNamespace, projectPath] = match;
expandedRepos.push(`${baseWithNamespace}/q/project:${projectPath}`);
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

selectedReposValues now expands Gerrit repos by appending a derived /q/project: URL, but the construction can generate malformed URLs:

  • If repoUrl is already in /q/project: form, this will produce /q/project:q/project:....
  • projectPath may contain / and should be URL-encoded (Gerrit query URLs typically expect project:foo%2Fbar as a single path segment).
    Consider guarding against already-query URLs and encoding the project path when generating the query URL.
Suggested change
const match = repoUrl.match(gerritUrlPattern);
if (match) {
const [, baseWithNamespace, projectPath] = match;
expandedRepos.push(`${baseWithNamespace}/q/project:${projectPath}`);
// Skip expansion if this is already a Gerrit query URL
if (repoUrl.includes('/q/project:')) {
continue;
}
const match = repoUrl.match(gerritUrlPattern);
if (match) {
const [, baseWithNamespace, projectPath] = match;
const encodedProjectPath = encodeURIComponent(projectPath);
expandedRepos.push(`${baseWithNamespace}/q/project:${encodedProjectPath}`);

Copilot uses AI. Check for mistakes.
}
}

return expandedRepos;
}

return repos;
});
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

selectedReposValues is used for both API repo filtering and for archive-state calculations (allArchived / hasSelectedArchivedRepos). Expanding the list with derived Gerrit query URLs changes the meaning of “selected repos” and will cause allArchived to become false when the archived list only contains the canonical repo URLs (the added /q/project: URLs won’t be present). Suggestion: keep selectedReposValues as the canonical selected repo URLs and introduce a separate computed (e.g. selectedReposValuesForApi) for the expanded Gerrit list, or adjust the archive checks to use the unexpanded URLs.

Copilot uses AI. Check for mistakes.

// Determine granularity options based on selected date range
const customRangeGranularity = computed<string[]>(() =>
Expand Down
24 changes: 0 additions & 24 deletions frontend/app/components/shared/utils/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// SPDX-License-Identifier: MIT

import { isArray } from 'lodash-es';
import type { ProjectRepository } from '~~/types/project';
export const isEmptyData = (value: Record<string, unknown>[] | null | undefined) => {
// check if the value is null or undefined or the length of the value is 0
if (value === null || value === undefined || value.length === 0 || !isArray(value)) {
Expand All @@ -24,26 +23,3 @@ export const isElementVisible = (element: HTMLElement) => {
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
return rect.top >= 0 && rect.bottom <= windowHeight;
};

export const normalizeRepoName = (repo: ProjectRepository): string => {
try {
const cleanUrl = repo.url.replace('q/', '').replace('project:', '');
const url = new URL(cleanUrl);
const pathParts = url.pathname.split('/').filter(Boolean);

// GitHub URLs: return last 2 segments (owner/repo)
if (url.hostname === 'github.com') {
return pathParts.slice(-2).join('/');
}

if (pathParts.length > 2) {
return pathParts.slice(-2).join('/');
}

// Fallback: return repo name or last path segment
return pathParts[pathParts.length - 1] || repo.name;
} catch {
// If URL parsing fails, return the repo name as fallback
return repo.name;
}
};
Loading