From 709a825ac1f4748e3004a683cd7a60c20b8a34ae Mon Sep 17 00:00:00 2001 From: Alex Tugarev Date: Wed, 15 Dec 2021 13:27:17 +0000 Subject: [PATCH] [bitbucket] enable projects --- .../dashboard/src/projects/NewProject.tsx | 31 ++------ .../ee/src/bitbucket/bitbucket-app-support.ts | 73 +++++++++++++++++++ components/server/ee/src/container-module.ts | 2 + .../ee/src/prebuilds/bitbucket-service.ts | 12 ++- .../ee/src/workspace/gitpod-server-impl.ts | 4 + components/server/package.json | 2 +- .../bitbucket-repository-provider.ts | 65 +++++++++++++++-- yarn.lock | 8 +- 8 files changed, 161 insertions(+), 36 deletions(-) create mode 100644 components/server/ee/src/bitbucket/bitbucket-app-support.ts diff --git a/components/dashboard/src/projects/NewProject.tsx b/components/dashboard/src/projects/NewProject.tsx index 25deb4d39df51b..c393bc6df6ea1c 100644 --- a/components/dashboard/src/projects/NewProject.tsx +++ b/components/dashboard/src/projects/NewProject.tsx @@ -143,7 +143,7 @@ export default function NewProject() { }, [selectedAccount]); useEffect(() => { - if (!selectedProviderHost || isBitbucket()) { + if (!selectedProviderHost) { return; } (async () => { @@ -164,12 +164,11 @@ export default function NewProject() { }, [project, sourceOfConfig]); const isGitHub = () => selectedProviderHost === "github.com"; - const isBitbucket = () => selectedProviderHost === "bitbucket.org"; const updateReposInAccounts = async (installationId?: string) => { setLoaded(false); setReposInAccounts([]); - if (!selectedProviderHost || isBitbucket()) { + if (!selectedProviderHost) { return []; } try { @@ -226,7 +225,7 @@ export default function NewProject() { } const createProject = async (teamOrUser: Team | User, repo: ProviderRepository) => { - if (!selectedProviderHost || isBitbucket()) { + if (!selectedProviderHost) { return; } const repoSlug = repo.path || repo.name; @@ -300,7 +299,7 @@ export default function NewProject() { const showSearchInput = !!repoSearchFilter || filteredRepos.length > 0; const renderRepos = () => (<> - {!isBitbucket() &&

Select a Git repository on {selectedProviderHost}. ( setShowGitProviders(true)}>change)

} + {

Select a Git repository on {selectedProviderHost}. ( setShowGitProviders(true)}>change)

}
@@ -400,11 +399,11 @@ export default function NewProject() { setSelectedProviderHost(host); } - if (!loaded && !isBitbucket()) { + if (!loaded) { return renderLoadingState(); } - if (showGitProviders || isBitbucket()) { + if (showGitProviders) { return (); } @@ -455,18 +454,6 @@ export default function NewProject() { ) }; - const renderBitbucketWarning = () => { - return ( -
-
- -
-
-

Bitbucket support for projects is not available yet. Follow #5980 for updates.

-
-
); - } - const onNewWorkspace = async () => { const redirectToNewWorkspace = () => { // instead of `history.push` we want forcibly to redirect here in order to avoid a following redirect from `/` -> `/projects` (cf. App.tsx) @@ -491,8 +478,6 @@ export default function NewProject() { {selectedRepo && selectedTeamOrUser && (
)} - {isBitbucket() && renderBitbucketWarning()} -
); } else { const projectLink = User.is(selectedTeamOrUser) ? `/projects/${project.slug}` : `/t/${selectedTeamOrUser?.slug}/${project.slug}`; @@ -552,8 +537,8 @@ function GitProviders(props: { }); } - // for now we exclude bitbucket.org and GitHub Enterprise - const filteredProviders = () => props.authProviders.filter(p => p.host === "github.com" || p.authProviderType === "GitLab"); + // for now we exclude GitHub Enterprise + const filteredProviders = () => props.authProviders.filter(p => p.host === "github.com" || p.host === "bitbucket.org" || p.authProviderType === "GitLab"); return (
diff --git a/components/server/ee/src/bitbucket/bitbucket-app-support.ts b/components/server/ee/src/bitbucket/bitbucket-app-support.ts new file mode 100644 index 00000000000000..bc30678e0a2699 --- /dev/null +++ b/components/server/ee/src/bitbucket/bitbucket-app-support.ts @@ -0,0 +1,73 @@ +/** + * Copyright (c) 2020 Gitpod GmbH. All rights reserved. + * Licensed under the Gitpod Enterprise Source Code License, + * See License.enterprise.txt in the project root folder. + */ + +import { AuthProviderInfo, ProviderRepository, User } from "@gitpod/gitpod-protocol"; +import { inject, injectable } from "inversify"; +import { TokenProvider } from "../../../src/user/token-provider"; +import { Bitbucket } from "bitbucket"; + +@injectable() +export class BitbucketAppSupport { + + @inject(TokenProvider) protected readonly tokenProvider: TokenProvider; + + async getProviderRepositoriesForUser(params: { user: User, provider: AuthProviderInfo }): Promise { + const token = await this.tokenProvider.getTokenForHost(params.user, params.provider.host); + const oauthToken = token.value; + + const api = new Bitbucket({ + baseUrl: `https://api.${params.provider.host}/2.0`, + auth: { + token: oauthToken + } + }); + + const result: ProviderRepository[] = []; + const ownersRepos: ProviderRepository[] = []; + + const identity = params.user.identities.find(i => i.authProviderId === params.provider.authProviderId); + if (!identity) { + return result; + } + const usersGitLabAccount = identity.authName; + + const workspaces = (await api.workspaces.getWorkspaces({ pagelen: 100 })).data.values?.map(w => w.slug!) || []; + + const reposPromise = Promise.all(workspaces.map(workspace => api.repositories.list({ + workspace, + pagelen: 100, + role: "admin" + }).catch(e => { + + }))); + + const reposInWorkspace = await reposPromise; + for (const repos of reposInWorkspace) { + if (repos) { + for (const repo of (repos.data.values || [])) { + const fullName = repo.full_name!; + const cloneUrl = repo.links!.clone!.find((x: any) => x.name === "https")!.href!; + const updatedAt = repo.updated_on!; + const accountAvatarUrl = repo.links!.avatar?.href!; + const account = fullName.split("/")[0]; + + (account === usersGitLabAccount ? ownersRepos : result).push({ + name: repo.name!, + account, + cloneUrl, + updatedAt, + accountAvatarUrl, + }) + } + } + } + + // put owner's repos first. the frontend will pick first account to continue with + result.unshift(...ownersRepos); + return result; + } + +} \ No newline at end of file diff --git a/components/server/ee/src/container-module.ts b/components/server/ee/src/container-module.ts index 1f61fd66a0ce29..8c9c96d35a7da7 100644 --- a/components/server/ee/src/container-module.ts +++ b/components/server/ee/src/container-module.ts @@ -50,6 +50,7 @@ import { GitHubAppSupport } from "./github/github-app-support"; import { GitLabAppSupport } from "./gitlab/gitlab-app-support"; import { Config } from "../../src/config"; import { SnapshotService } from "./workspace/snapshot-service"; +import { BitbucketAppSupport } from "./bitbucket/bitbucket-app-support"; export const productionEEContainerModule = new ContainerModule((bind, unbind, isBound, rebind) => { rebind(Server).to(ServerEE).inSingletonScope(); @@ -68,6 +69,7 @@ export const productionEEContainerModule = new ContainerModule((bind, unbind, is bind(GitLabApp).toSelf().inSingletonScope(); bind(GitLabAppSupport).toSelf().inSingletonScope(); bind(BitbucketApp).toSelf().inSingletonScope(); + bind(BitbucketAppSupport).toSelf().inSingletonScope(); bind(LicenseEvaluator).toSelf().inSingletonScope(); bind(LicenseKeySource).to(DBLicenseKeySource).inSingletonScope(); diff --git a/components/server/ee/src/prebuilds/bitbucket-service.ts b/components/server/ee/src/prebuilds/bitbucket-service.ts index afa1328cbc2722..214c24565afca7 100644 --- a/components/server/ee/src/prebuilds/bitbucket-service.ts +++ b/components/server/ee/src/prebuilds/bitbucket-service.ts @@ -27,7 +27,17 @@ export class BitbucketService extends RepositoryService { async canInstallAutomatedPrebuilds(user: User, cloneUrl: string): Promise { const { host } = await this.bitbucketContextParser.parseURL(user, cloneUrl); - return host === this.authProviderConfig.host; + if (host !== this.authProviderConfig.host) { + return false; + } + // TODO: change to actaully check for admin permissions + + // const { owner, repoName } = await this.bitbucketContextParser.parseURL(user, cloneUrl); + // const api = await this.api.create(user); + // api.user.listPermissionsForRepos({ + // q: `repository.full_name="#{owner}/${repoName}"" AND` + // }) + return true; } async installAutomatedPrebuilds(user: User, cloneUrl: string): Promise { diff --git a/components/server/ee/src/workspace/gitpod-server-impl.ts b/components/server/ee/src/workspace/gitpod-server-impl.ts index f3e6a57babcb25..e56ebee99cb8ce 100644 --- a/components/server/ee/src/workspace/gitpod-server-impl.ts +++ b/components/server/ee/src/workspace/gitpod-server-impl.ts @@ -41,6 +41,7 @@ import { Config } from "../../../src/config"; import { SnapshotService, WaitForSnapshotOptions } from "./snapshot-service"; import { SafePromise } from "@gitpod/gitpod-protocol/lib/util/safe-promise"; import { ClientMetadata } from "../../../src/websocket/websocket-connection-manager"; +import { BitbucketAppSupport } from "../bitbucket/bitbucket-app-support"; @injectable() export class GitpodServerEEImpl extends GitpodServerImpl { @@ -68,6 +69,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl { @inject(GitHubAppSupport) protected readonly githubAppSupport: GitHubAppSupport; @inject(GitLabAppSupport) protected readonly gitLabAppSupport: GitLabAppSupport; + @inject(BitbucketAppSupport) protected readonly bitbucketAppSupport: BitbucketAppSupport; @inject(Config) protected readonly config: Config; @@ -1429,6 +1431,8 @@ export class GitpodServerEEImpl extends GitpodServerImpl { if (providerHost === "github.com") { repositories.push(...(await this.githubAppSupport.getProviderRepositoriesForUser({ user, ...params }))); + } else if (providerHost === "bitbucket.org" && provider) { + repositories.push(...(await this.bitbucketAppSupport.getProviderRepositoriesForUser({ user, provider }))); } else if (provider?.authProviderType === "GitLab") { repositories.push(...(await this.gitLabAppSupport.getProviderRepositoriesForUser({ user, provider }))); } else { diff --git a/components/server/package.json b/components/server/package.json index 784fabcffd2ac3..e824860dfac442 100644 --- a/components/server/package.json +++ b/components/server/package.json @@ -44,7 +44,7 @@ "@probot/get-private-key": "^1.1.1", "amqplib": "^0.8.0", "base-64": "^1.0.0", - "bitbucket": "^2.4.2", + "bitbucket": "^2.7.0", "body-parser": "^1.18.2", "cookie": "^0.4.1", "cookie-parser": "^1.4.5", diff --git a/components/server/src/bitbucket/bitbucket-repository-provider.ts b/components/server/src/bitbucket/bitbucket-repository-provider.ts index e62da2084c0537..7d55d23044cffe 100644 --- a/components/server/src/bitbucket/bitbucket-repository-provider.ts +++ b/components/server/src/bitbucket/bitbucket-repository-provider.ts @@ -26,18 +26,69 @@ export class BitbucketRepositoryProvider implements RepositoryProvider { return { host, owner, name, cloneUrl, description, avatarUrl, webUrl }; } - async getBranch(user: User, owner: string, repo: string, branch: string): Promise { - // todo - throw new Error("not implemented"); + async getBranch(user: User, owner: string, repo: string, branchName: string): Promise { + const api = await this.apiFactory.create(user); + const response = await api.repositories.getBranch({ + workspace: owner, + repo_slug: repo, + name: branchName + }) + + const branch = response.data; + + return { + htmlUrl: branch.links?.html?.href!, + name: branch.name!, + commit: { + sha: branch.target?.hash!, + author: branch.target?.author?.user?.display_name!, + authorAvatarUrl: branch.target?.author?.user?.links?.avatar?.href, + authorDate: branch.target?.date!, + commitMessage: branch.target?.message || "missing commit message", + } + }; } async getBranches(user: User, owner: string, repo: string): Promise { - // todo - return []; + const branches: Branch[] = []; + const api = await this.apiFactory.create(user); + const response = await api.repositories.listBranches({ + workspace: owner, + repo_slug: repo, + sort: "target.date" + }) + + for (const branch of response.data.values!) { + branches.push({ + htmlUrl: branch.links?.html?.href!, + name: branch.name!, + commit: { + sha: branch.target?.hash!, + author: branch.target?.author?.user?.display_name!, + authorAvatarUrl: branch.target?.author?.user?.links?.avatar?.href, + authorDate: branch.target?.date!, + commitMessage: branch.target?.message || "missing commit message", + } + }); + } + + return branches; } async getCommitInfo(user: User, owner: string, repo: string, ref: string): Promise { - // todo - return undefined; + const api = await this.apiFactory.create(user); + const response = await api.commits.get({ + workspace: owner, + repo_slug: repo, + commit: ref + }) + const commit = response.data; + return { + sha: commit.hash!, + author: commit.author?.user?.display_name!, + authorDate: commit.date!, + commitMessage: commit.message || "missing commit message", + authorAvatarUrl: commit.author?.user?.links?.avatar?.href, + }; } } diff --git a/yarn.lock b/yarn.lock index 8a08e47eb00c38..e0f4adc94df7d5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5041,10 +5041,10 @@ bintrees@1.0.1: resolved "https://registry.yarnpkg.com/bintrees/-/bintrees-1.0.1.tgz#0e655c9b9c2435eaab68bf4027226d2b55a34524" integrity sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ= -bitbucket@^2.4.2: - version "2.6.3" - resolved "https://registry.yarnpkg.com/bitbucket/-/bitbucket-2.6.3.tgz#e7aa030406720e24c19a40701506b1c366daf544" - integrity sha512-t23mlPsCchl+7TCGGHqI4Up++mnGd6smaKsNe/t+kGlkGfIzm+QmVdWvBboHl8Nyequ8Wm0Whi2lKq9qmfJmxA== +bitbucket@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/bitbucket/-/bitbucket-2.7.0.tgz#fd11b19a42cc9b89f6a899ff669fd1575183a5b3" + integrity sha512-6fw3MzXeFp3TLmo6jF7IWFn9tFpFKpzCpDjKek9s5EY559Ff3snbu2hmS5ZKmR7D0XomPbIT0dBN1juoJ/gGyA== dependencies: before-after-hook "^2.1.0" deepmerge "^4.2.2"