diff --git a/components/dashboard/src/projects/NewProject.tsx b/components/dashboard/src/projects/NewProject.tsx
index feddf6cd8f2160..47cce7c49d5822 100644
--- a/components/dashboard/src/projects/NewProject.tsx
+++ b/components/dashboard/src/projects/NewProject.tsx
@@ -141,7 +141,7 @@ export default function NewProject() {
}, [selectedAccount]);
useEffect(() => {
- if (!selectedProviderHost || isBitbucket()) {
+ if (!selectedProviderHost) {
return;
}
(async () => {
@@ -161,12 +161,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 {
@@ -194,7 +193,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;
@@ -382,11 +381,11 @@ export default function NewProject() {
setSelectedProviderHost(host);
}
- if (!loaded && !isBitbucket()) {
+ if (!loaded) {
return renderLoadingState();
}
- if (showGitProviders || isBitbucket()) {
+ if (showGitProviders) {
return ();
}
@@ -437,18 +436,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)
@@ -473,8 +460,6 @@ export default function NewProject() {
{selectedRepo && selectedTeamOrUser && ()}
>
- {isBitbucket() && renderBitbucketWarning()}
-
);
} else {
const projectLink = User.is(selectedTeamOrUser) ? `/projects/${project.slug}` : `/t/${selectedTeamOrUser?.slug}/${project.slug}`;
@@ -534,8 +519,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..2b5a8bedb84eec
--- /dev/null
+++ b/components/server/ee/src/bitbucket/bitbucket-app-support.ts
@@ -0,0 +1,79 @@
+/**
+ * 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";
+import { URL } from "url";
+
+@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 usersBitbucketAccount = 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" // installation of webhooks is allowed for admins only
+ }).catch(e => {
+ console.error(e)
+ })));
+
+ const reposInWorkspace = await reposPromise;
+ for (const repos of reposInWorkspace) {
+ if (repos) {
+ for (const repo of (repos.data.values || [])) {
+ let cloneUrl = repo.links!.clone!.find((x: any) => x.name === "https")!.href!;
+ if (cloneUrl) {
+ const url = new URL(cloneUrl);
+ url.username = '';
+ cloneUrl = url.toString();
+ }
+ const fullName = repo.full_name!;
+ const updatedAt = repo.updated_on!;
+ const accountAvatarUrl = repo.links!.avatar?.href!;
+ const account = fullName.split("/")[0];
+
+ (account === usersBitbucketAccount ? 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..48af8c8c47d8ff 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;
+ }
+
+ // only admins may install webhooks on repositories
+ const { owner, repoName } = await this.bitbucketContextParser.parseURL(user, cloneUrl);
+ const api = await this.api.create(user);
+ const response = await api.user.listPermissionsForRepos({
+ q: `repository.full_name="${owner}/${repoName}"`
+ })
+ return !!response.data?.values && response.data.values[0]?.permission === "admin";
}
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 e03918659e9203..a93cdcae8d0b00 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-context-parser.ts b/components/server/src/bitbucket/bitbucket-context-parser.ts
index 876e6b8ebdb0e1..36d36205a157ae 100644
--- a/components/server/src/bitbucket/bitbucket-context-parser.ts
+++ b/components/server/src/bitbucket/bitbucket-context-parser.ts
@@ -250,8 +250,6 @@ export class BitbucketContextParser extends AbstractContextParser implements ICo
const result: Repository = {
cloneUrl: `https://${host}/${repo.full_name}.git`,
- // cloneUrl: repoQueryResult.links.html.href + ".git",
- // cloneUrl: repoQueryResult.links.clone.find((x: any) => x.name === "https").href,
host,
name,
owner,
diff --git a/components/server/src/bitbucket/bitbucket-repository-provider.ts b/components/server/src/bitbucket/bitbucket-repository-provider.ts
index e62da2084c0537..188396b5a2059f 100644
--- a/components/server/src/bitbucket/bitbucket-repository-provider.ts
+++ b/components/server/src/bitbucket/bitbucket-repository-provider.ts
@@ -6,6 +6,7 @@
import { Branch, CommitInfo, Repository, User } from "@gitpod/gitpod-protocol";
import { inject, injectable } from 'inversify';
+import { URL } from "url";
import { RepoURL } from '../repohost/repo-url';
import { RepositoryProvider } from '../repohost/repository-provider';
import { BitbucketApiFactory } from './bitbucket-api-factory';
@@ -18,7 +19,12 @@ export class BitbucketRepositoryProvider implements RepositoryProvider {
async getRepo(user: User, owner: string, name: string): Promise {
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!;
+ let cloneUrl = repo.links!.clone!.find((x: any) => x.name === "https")!.href!;
+ if (cloneUrl) {
+ const url = new URL(cloneUrl);
+ url.username = '';
+ cloneUrl = url.toString();
+ }
const host = RepoURL.parseRepoUrl(cloneUrl)!.host;
const description = repo.description;
const avatarUrl = repo.owner!.links!.avatar!.href;
@@ -26,18 +32,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"