diff --git a/components/dashboard/src/projects/ConfigureProject.tsx b/components/dashboard/src/projects/ConfigureProject.tsx index 55101778070eeb..02fa8920d80cf7 100644 --- a/components/dashboard/src/projects/ConfigureProject.tsx +++ b/components/dashboard/src/projects/ConfigureProject.tsx @@ -63,6 +63,7 @@ export default function () { const location = useLocation(); const team = getCurrentTeam(location, teams); const routeMatch = useRouteMatch<{ teamSlug: string, projectSlug: string }>("/(t/)?:teamSlug/:projectSlug/configure"); + const projectSlug = routeMatch?.params.projectSlug; const [project, setProject] = useState(); const [gitpodYml, setGitpodYml] = useState(''); const [dockerfile, setDockerfile] = useState(''); @@ -93,7 +94,11 @@ export default function () { const projects = (!!team ? await getGitpodService().server.getTeamProjects(team.id) : await getGitpodService().server.getUserProjects()); - const project = projects.find(p => p.name === routeMatch?.params.projectSlug); + + const project = projectSlug && projects.find( + p => p.slug ? p.slug === projectSlug : + p.name === projectSlug); + if (!project) { setIsDetecting(false); setEditorMessage(); diff --git a/components/dashboard/src/projects/NewProject.tsx b/components/dashboard/src/projects/NewProject.tsx index c19bbdfa651fa6..7cd2ca9acf97a6 100644 --- a/components/dashboard/src/projects/NewProject.tsx +++ b/components/dashboard/src/projects/NewProject.tsx @@ -162,7 +162,7 @@ export default function NewProject() { if (!provider) { return; } - const repo = reposInAccounts.find(r => r.account === selectedAccount && r.name === selectedRepo); + const repo = reposInAccounts.find(r => r.account === selectedAccount && r.path === selectedRepo); if (!repo) { console.error("No repo selected!") return; @@ -179,7 +179,7 @@ export default function NewProject() { appInstallationId: String(repo.installationId), }); - history.push(`/${User.is(teamOrUser) ? 'projects' : 't/'+teamOrUser.slug}/${repo.name}/configure`); + history.push(`/${User.is(teamOrUser) ? 'projects' : 't/'+teamOrUser.slug}/${repo.path}/configure`); } catch (error) { const message = (error && error?.message) || "Failed to create new project." window.alert(message); @@ -269,7 +269,7 @@ export default function NewProject() {
{!r.inUse ? ( - + ) : (

already taken

)} diff --git a/components/dashboard/src/projects/Prebuild.tsx b/components/dashboard/src/projects/Prebuild.tsx index 78b8d90a33cdad..a50e19a561c68f 100644 --- a/components/dashboard/src/projects/Prebuild.tsx +++ b/components/dashboard/src/projects/Prebuild.tsx @@ -24,7 +24,7 @@ export default function () { const team = getCurrentTeam(location, teams); const match = useRouteMatch<{ team: string, project: string, prebuildId: string }>("/(t/)?:team/:project/:prebuildId"); - const projectName = match?.params?.project; + const projectSlug = match?.params?.project; const prebuildId = match?.params?.prebuildId; const [ prebuild, setPrebuild ] = useState(); @@ -33,16 +33,20 @@ export default function () { const [ isCancellingPrebuild, setIsCancellingPrebuild ] = useState(false); useEffect(() => { - if (!teams || !projectName || !prebuildId) { + if (!teams || !projectSlug || !prebuildId) { return; } (async () => { const projects = (!!team ? await getGitpodService().server.getTeamProjects(team.id) : await getGitpodService().server.getUserProjects()); - const project = projects.find(p => p.name === projectName); + + const project = projectSlug && projects.find( + p => p.slug ? p.slug === projectSlug : + p.name === projectSlug); + if (!project) { - console.error(new Error(`Project not found! (teamId: ${team?.id}, projectName: ${projectName})`)); + console.error(new Error(`Project not found! (teamId: ${team?.id}, projectName: ${projectSlug})`)); return; } const prebuilds = await getGitpodService().server.findPrebuilds({ @@ -88,7 +92,7 @@ export default function () { setIsRerunningPrebuild(true); await getGitpodService().server.triggerPrebuild(prebuild.info.projectId, prebuild.info.branch); // TODO: Open a Prebuilds page that's specific to `prebuild.info.branch`? - history.push(`/${!!team ? 't/'+team.slug : 'projects'}/${projectName}/prebuilds`); + history.push(`/${!!team ? 't/'+team.slug : 'projects'}/${projectSlug}/prebuilds`); } catch (error) { console.error('Could not rerun prebuild', error); } finally { diff --git a/components/dashboard/src/projects/Prebuilds.tsx b/components/dashboard/src/projects/Prebuilds.tsx index d522cfcaf7e0e3..f67e236f8d9fd3 100644 --- a/components/dashboard/src/projects/Prebuilds.tsx +++ b/components/dashboard/src/projects/Prebuilds.tsx @@ -30,7 +30,7 @@ export default function () { const team = getCurrentTeam(location, teams); const match = useRouteMatch<{ team: string, resource: string }>("/(t/)?:team/:resource"); - const projectName = match?.params?.resource; + const projectSlug = match?.params?.resource; const [project, setProject] = useState(); @@ -70,7 +70,10 @@ export default function () { ? await getGitpodService().server.getTeamProjects(team.id) : await getGitpodService().server.getUserProjects()); - const newProject = projectName && projects.find(p => p.name === projectName); + const newProject = projectSlug && projects.find( + p => p.slug ? p.slug === projectSlug : + p.name === projectSlug); + if (newProject) { setProject(newProject); } @@ -180,7 +183,7 @@ export default function () { {prebuilds.filter(filter).sort(prebuildSorter).map((p, index) => - +
{prebuildStatusIcon(p)}
{prebuildStatusLabel(p)} diff --git a/components/dashboard/src/workspaces/Workspaces.tsx b/components/dashboard/src/workspaces/Workspaces.tsx index 84db7608b9f1a9..482b3b519767eb 100644 --- a/components/dashboard/src/workspaces/Workspaces.tsx +++ b/components/dashboard/src/workspaces/Workspaces.tsx @@ -34,7 +34,7 @@ export default function () { const { teams } = useContext(TeamsContext); const team = getCurrentTeam(location, teams); const match = useRouteMatch<{ team: string, resource: string }>("/(t/)?:team/:resource"); - const projectName = match?.params?.resource !== 'workspaces' ? match?.params?.resource : undefined; + const projectSlug = match?.params?.resource !== 'workspaces' ? match?.params?.resource : undefined; const [projects, setProjects] = useState([]); const [activeWorkspaces, setActiveWorkspaces] = useState([]); const [inactiveWorkspaces, setInactiveWorkspaces] = useState([]); @@ -59,7 +59,7 @@ export default function () { useEffect(() => { // only show example repos on the global user context - if (!team && !projectName) { + if (!team && !projectSlug) { getGitpodService().server.getFeaturedRepositories().then(setRepos); } (async () => { @@ -68,8 +68,8 @@ export default function () { : await getGitpodService().server.getUserProjects()); let project: Project | undefined = undefined; - if (projectName) { - project = projects.find(p => p.name === projectName); + if (projectSlug) { + project = projects.find(p => p.slug ? p.slug === projectSlug : p.name === projectSlug); if (project) { setProjects([project]); } @@ -95,7 +95,7 @@ export default function () { const hideStartWSModal = () => setIsTemplateModelOpen(false); const getRecentSuggestions: () => WsStartEntry[] = () => { - if (projectName || team) { + if (projectSlug || team) { return projects.map(p => { const remoteUrl = toRemoteURL(p.cloneUrl); return { diff --git a/components/gitpod-db/src/project-db.ts b/components/gitpod-db/src/project-db.ts index 4aed843c05dd51..ab64e65184e4ef 100644 --- a/components/gitpod-db/src/project-db.ts +++ b/components/gitpod-db/src/project-db.ts @@ -11,7 +11,6 @@ export interface ProjectDB { findProjectById(projectId: string): Promise; findProjectByCloneUrl(cloneUrl: string): Promise; findProjectsByCloneUrls(cloneUrls: string[]): Promise; - findProjectByTeamAndName(teamId: string, projectName: string): Promise; findTeamProjects(teamId: string): Promise; findUserProjects(userId: string): Promise; storeProject(project: Project): Promise; diff --git a/components/gitpod-db/src/typeorm/project-db-impl.ts b/components/gitpod-db/src/typeorm/project-db-impl.ts index 052acafccc500d..b5c12110158b8e 100644 --- a/components/gitpod-db/src/typeorm/project-db-impl.ts +++ b/components/gitpod-db/src/typeorm/project-db-impl.ts @@ -45,11 +45,6 @@ export class ProjectDBImpl implements ProjectDB { return result; } - public async findProjectByTeamAndName(teamId: string, projectName: string): Promise { - const projects = await this.findTeamProjects(teamId); - return projects.find(p => p.name === projectName); - } - public async findTeamProjects(teamId: string): Promise { const repo = await this.getRepo(); return repo.find({ teamId, markedDeleted: false }); diff --git a/components/server/ee/src/workspace/workspace-factory.ts b/components/server/ee/src/workspace/workspace-factory.ts index 762b8fb0fd4960..1fc1bef7a2455d 100644 --- a/components/server/ee/src/workspace/workspace-factory.ts +++ b/components/server/ee/src/workspace/workspace-factory.ts @@ -16,7 +16,7 @@ import { ResponseError } from 'vscode-jsonrpc'; import { ErrorCodes } from '@gitpod/gitpod-protocol/lib/messaging/error'; import { generateWorkspaceID } from '@gitpod/gitpod-protocol/lib/util/generate-workspace-id'; import { HostContextProvider } from '../../../src/auth/host-context-provider'; -import { parseRepoUrl } from '../../../src/repohost'; +import { RepoURL } from '../../../src/repohost'; @injectable() export class WorkspaceFactoryEE extends WorkspaceFactory { @@ -153,7 +153,7 @@ export class WorkspaceFactoryEE extends WorkspaceFactory { protected async storePrebuildInfo(ctx: TraceContext, project: Project, pws: PrebuiltWorkspace, ws: Workspace, user: User) { const span = TraceContext.startSpan("storePrebuildInfo", ctx); const { userId, teamId, name: projectName, id: projectId } = project; - const parsedUrl = parseRepoUrl(project.cloneUrl); + const parsedUrl = RepoURL.parseRepoUrl(project.cloneUrl); if (!parsedUrl) { return; } diff --git a/components/server/src/bitbucket/bitbucket-repository-provider.ts b/components/server/src/bitbucket/bitbucket-repository-provider.ts index 9d95984167fdc4..e62da2084c0537 100644 --- a/components/server/src/bitbucket/bitbucket-repository-provider.ts +++ b/components/server/src/bitbucket/bitbucket-repository-provider.ts @@ -6,7 +6,7 @@ import { Branch, CommitInfo, Repository, User } from "@gitpod/gitpod-protocol"; import { inject, injectable } from 'inversify'; -import { parseRepoUrl } from '../repohost/repo-url'; +import { RepoURL } from '../repohost/repo-url'; import { RepositoryProvider } from '../repohost/repository-provider'; import { BitbucketApiFactory } from './bitbucket-api-factory'; @@ -19,7 +19,7 @@ export class BitbucketRepositoryProvider implements RepositoryProvider { const api = await this.apiFactory.create(user); const repo = (await api.repositories.get({ workspace: owner, repo_slug: name })).data; const cloneUrl = repo.links!.clone!.find((x: any) => x.name === "https")!.href!; - const host = parseRepoUrl(cloneUrl)!.host; + const host = RepoURL.parseRepoUrl(cloneUrl)!.host; const description = repo.description; const avatarUrl = repo.owner!.links!.avatar!.href; const webUrl = repo.links!.html!.href; diff --git a/components/server/src/github/github-repository-provider.ts b/components/server/src/github/github-repository-provider.ts index 044dd3a442a055..6795fcd79f28bb 100644 --- a/components/server/src/github/github-repository-provider.ts +++ b/components/server/src/github/github-repository-provider.ts @@ -9,7 +9,7 @@ import { injectable, inject } from 'inversify'; import { User, Repository } from "@gitpod/gitpod-protocol" import { GitHubGraphQlEndpoint, GitHubRestApi } from "./api"; import { RepositoryProvider } from '../repohost/repository-provider'; -import { parseRepoUrl } from '../repohost/repo-url'; +import { RepoURL } from '../repohost/repo-url'; import { Branch, CommitInfo } from '@gitpod/gitpod-protocol/src/protocol'; @injectable() @@ -20,7 +20,7 @@ export class GithubRepositoryProvider implements RepositoryProvider { async getRepo(user: User, owner: string, repo: string): Promise { const repository = await this.github.getRepository(user, { owner, repo }); const cloneUrl = repository.clone_url; - const host = parseRepoUrl(cloneUrl)!.host; + const host = RepoURL.parseRepoUrl(cloneUrl)!.host; const description = repository.description; const avatarUrl = repository.owner.avatar_url; const webUrl = repository.html_url; diff --git a/components/server/src/gitlab/gitlab-repository-provider.ts b/components/server/src/gitlab/gitlab-repository-provider.ts index 0ecbb256021c20..2c4d93a16d03f6 100644 --- a/components/server/src/gitlab/gitlab-repository-provider.ts +++ b/components/server/src/gitlab/gitlab-repository-provider.ts @@ -9,7 +9,7 @@ import { injectable, inject } from 'inversify'; import { User, Repository, Branch, CommitInfo } from "@gitpod/gitpod-protocol" import { GitLabApi, GitLab } from "./api"; import { RepositoryProvider } from '../repohost/repository-provider'; -import { parseRepoUrl } from '../repohost/repo-url'; +import { RepoURL } from '../repohost/repo-url'; import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; @@ -26,7 +26,7 @@ export class GitlabRepositoryProvider implements RepositoryProvider { } const cloneUrl = response.http_url_to_repo; const description = response.default_branch; - const host = parseRepoUrl(cloneUrl)!.host; + const host = RepoURL.parseRepoUrl(cloneUrl)!.host; const avatarUrl = response.owner?.avatar_url || undefined; const webUrl = response.web_url; const defaultBranch = response.default_branch diff --git a/components/server/src/projects/projects-service.ts b/components/server/src/projects/projects-service.ts index a63aa4e5eac68f..9f0ad9a3419f1b 100644 --- a/components/server/src/projects/projects-service.ts +++ b/components/server/src/projects/projects-service.ts @@ -9,7 +9,7 @@ import { DBWithTracing, ProjectDB, TeamDB, TracedWorkspaceDB, UserDB, WorkspaceD import { Branch, CommitContext, PrebuildWithStatus, CreateProjectParams, FindPrebuildsParams, Project, ProjectConfig, User, WorkspaceConfig } from "@gitpod/gitpod-protocol"; import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing"; import { HostContextProvider } from "../auth/host-context-provider"; -import { FileProvider, parseRepoUrl } from "../repohost"; +import { FileProvider, RepoURL } from "../repohost"; import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; import { ContextParser } from "../workspace/context-parser-service"; import { ConfigInferrer } from "./config-inferrer"; @@ -46,13 +46,13 @@ export class ProjectsService { } protected getRepositoryProvider(project: Project) { - const parsedUrl = parseRepoUrl(project.cloneUrl); + const parsedUrl = RepoURL.parseRepoUrl(project.cloneUrl); const repositoryProvider = parsedUrl && this.hostContextProvider.get(parsedUrl.host)?.services?.repositoryProvider; return repositoryProvider; } async getBranchDetails(user: User, project: Project, branchName?: string): Promise { - const parsedUrl = parseRepoUrl(project.cloneUrl); + const parsedUrl = RepoURL.parseRepoUrl(project.cloneUrl); if (!parsedUrl) { return []; } @@ -109,7 +109,7 @@ export class ProjectsService { protected async onDidCreateProject(project: Project) { let { userId, teamId, cloneUrl } = project; - const parsedUrl = parseRepoUrl(project.cloneUrl); + const parsedUrl = RepoURL.parseRepoUrl(project.cloneUrl); if ("gitlab.com" === parsedUrl?.host) { const repositoryService = this.hostContextProvider.get(parsedUrl?.host)?.services?.repositoryService; if (repositoryService) { @@ -140,7 +140,7 @@ export class ProjectsService { if (!project) { return []; } - const parsedUrl = parseRepoUrl(project.cloneUrl); + const parsedUrl = RepoURL.parseRepoUrl(project.cloneUrl); if (!parsedUrl) { return []; } diff --git a/components/server/src/repohost/repo-url.spec.ts b/components/server/src/repohost/repo-url.spec.ts new file mode 100644 index 00000000000000..54ed440603eee0 --- /dev/null +++ b/components/server/src/repohost/repo-url.spec.ts @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2021 Gitpod GmbH. All rights reserved. + * Licensed under the GNU Affero General Public License (AGPL). + * See License-AGPL.txt in the project root for license information. + */ + +import * as chai from 'chai'; +import { suite, test } from 'mocha-typescript'; +import { RepoURL } from './repo-url'; + +const expect = chai.expect; + +@suite +export class RepoUrlTest { + + @test public parseRepoUrl() { + const testUrl = RepoURL.parseRepoUrl("https://gitlab.com/hello-group/my-cool-project.git") + expect(testUrl).to.deep.equal({ + host: 'gitlab.com', + owner: 'hello-group', + repo: 'my-cool-project' + }); + } + + @test public parseSubgroupOneLevel() { + const testUrl = RepoURL.parseRepoUrl("https://gitlab.com/hello-group/my-subgroup/my-cool-project.git") + expect(testUrl).to.deep.equal({ + host: 'gitlab.com', + owner: 'hello-group/my-subgroup', + repo: 'my-cool-project' + }); + } + + @test public parseSubgroupTwoLevels() { + const testUrl = RepoURL.parseRepoUrl("https://gitlab.com/hello-group/my-subgroup/my-sub-subgroup/my-cool-project.git") + expect(testUrl).to.deep.equal({ + host: 'gitlab.com', + owner: 'hello-group/my-subgroup/my-sub-subgroup', + repo: 'my-cool-project' + }); + } + + @test public parseSubgroupThreeLevels() { + const testUrl = RepoURL.parseRepoUrl( + "https://gitlab.com/hello-group/my-subgroup/my-sub-subgroup/my-sub-sub-subgroup/my-cool-project.git") + expect(testUrl).to.deep.equal({ + host: 'gitlab.com', + owner: 'hello-group/my-subgroup/my-sub-subgroup/my-sub-sub-subgroup', + repo: 'my-cool-project' + }); + } + + @test public parseSubgroupFourLevels() { + const testUrl = RepoURL.parseRepoUrl( + "https://gitlab.com/hello-group/my-subgroup/my-sub-subgroup/my-sub-sub-subgroup/my-sub-sub-sub-subgroup/my-cool-project.git") + expect(testUrl).to.deep.equal({ + host: 'gitlab.com', + owner: 'hello-group/my-subgroup/my-sub-subgroup/my-sub-sub-subgroup/my-sub-sub-sub-subgroup', + repo: 'my-cool-project' + }); + } + +} + +module.exports = new RepoUrlTest() \ No newline at end of file diff --git a/components/server/src/repohost/repo-url.ts b/components/server/src/repohost/repo-url.ts index 93f9edcdac4ba2..a5205fd58620cf 100644 --- a/components/server/src/repohost/repo-url.ts +++ b/components/server/src/repohost/repo-url.ts @@ -6,16 +6,24 @@ import * as url from 'url'; - -export function parseRepoUrl(repoUrl: string): { host: string, owner: string, repo: string} | undefined { - const u = url.parse(repoUrl); - const host = u.hostname || ''; - const path = u.pathname || ''; - const segments = path.split('/').filter(s => !!s); // e.g. [ 'gitpod-io', 'gitpod.git' ] - if (segments.length === 2) { - const owner = segments[0]; - const repo = segments[1].endsWith('.git') ? segments[1].slice(0, -4) : segments[1]; - return { host, owner, repo }; +export namespace RepoURL { + export function parseRepoUrl(repoUrl: string): { host: string, owner: string, repo: string} | undefined { + const u = url.parse(repoUrl); + const host = u.hostname || ''; + const path = u.pathname || ''; + const segments = path.split('/').filter(s => !!s); // e.g. [ 'gitpod-io', 'gitpod.git' ] + if (segments.length === 2) { + const owner = segments[0]; + const repo = segments[1].endsWith('.git') ? segments[1].slice(0, -4) : segments[1]; + return { host, owner, repo }; + } + if (segments.length > 2) { + const endSegment = segments[segments.length - 1]; + const ownerSegments = segments.slice(0, segments.length-1); + const owner = ownerSegments.join("/"); + const repo = endSegment.endsWith('.git') ? endSegment.slice(0, -4) : endSegment; + return { host, owner, repo }; + } + return undefined; } - return undefined; -} \ No newline at end of file +} diff --git a/components/server/src/workspace/gitpod-server-impl.ts b/components/server/src/workspace/gitpod-server-impl.ts index 6819f1f7fd8795..4fc5dbcfc4a7d0 100644 --- a/components/server/src/workspace/gitpod-server-impl.ts +++ b/components/server/src/workspace/gitpod-server-impl.ts @@ -34,7 +34,7 @@ import { HostContextProvider } from '../auth/host-context-provider'; import { GuardedResource, ResourceAccessGuard, ResourceAccessOp } from '../auth/resource-access'; import { Config } from '../config'; import { NotFoundError, UnauthorizedError } from '../errors'; -import { parseRepoUrl } from '../repohost/repo-url'; +import { RepoURL } from '../repohost/repo-url'; import { TermsProvider } from '../terms/terms-provider'; import { TheiaPluginService } from '../theia-plugin/theia-plugin-service'; import { AuthorizationService } from '../user/authorization-service'; @@ -970,7 +970,7 @@ export class GitpodServerImpl repo.url != undefined) .map(async whitelistedRepo => { - const repoUrl = parseRepoUrl(whitelistedRepo.url!); + const repoUrl = RepoURL.parseRepoUrl(whitelistedRepo.url!); if (!repoUrl) return undefined; const { host, owner, repo } = repoUrl;