Skip to content

Commit 7f7fa96

Browse files
committed
[bitbucket] enable projects
1 parent 6285314 commit 7f7fa96

File tree

8 files changed

+160
-35
lines changed

8 files changed

+160
-35
lines changed

components/dashboard/src/projects/NewProject.tsx

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ export default function NewProject() {
141141
}, [selectedAccount]);
142142

143143
useEffect(() => {
144-
if (!selectedProviderHost || isBitbucket()) {
144+
if (!selectedProviderHost) {
145145
return;
146146
}
147147
(async () => {
@@ -161,12 +161,11 @@ export default function NewProject() {
161161
}, [project, sourceOfConfig]);
162162

163163
const isGitHub = () => selectedProviderHost === "github.com";
164-
const isBitbucket = () => selectedProviderHost === "bitbucket.org";
165164

166165
const updateReposInAccounts = async (installationId?: string) => {
167166
setLoaded(false);
168167
setReposInAccounts([]);
169-
if (!selectedProviderHost || isBitbucket()) {
168+
if (!selectedProviderHost) {
170169
return [];
171170
}
172171
try {
@@ -194,7 +193,7 @@ export default function NewProject() {
194193
}
195194

196195
const createProject = async (teamOrUser: Team | User, repo: ProviderRepository) => {
197-
if (!selectedProviderHost || isBitbucket()) {
196+
if (!selectedProviderHost) {
198197
return;
199198
}
200199
const repoSlug = repo.path || repo.name;
@@ -382,11 +381,11 @@ export default function NewProject() {
382381
setSelectedProviderHost(host);
383382
}
384383

385-
if (!loaded && !isBitbucket()) {
384+
if (!loaded) {
386385
return renderLoadingState();
387386
}
388387

389-
if (showGitProviders || isBitbucket()) {
388+
if (showGitProviders) {
390389
return (<GitProviders onHostSelected={onGitProviderSeleted} authProviders={authProviders} />);
391390
}
392391

@@ -437,18 +436,6 @@ export default function NewProject() {
437436
</>)
438437
};
439438

440-
const renderBitbucketWarning = () => {
441-
return (
442-
<div className="mt-16 flex space-x-2 py-6 px-6 w-96 justify-betweeen bg-gitpod-kumquat-light rounded-xl">
443-
<div className="pr-3 self-center w-6">
444-
<img src={exclamation} />
445-
</div>
446-
<div className="flex-1 flex flex-col">
447-
<p className="text-gitpod-red text-sm">Bitbucket support for projects is not available yet. Follow <a className="gp-link" href="https://github.com/gitpod-io/gitpod/issues/5980">#5980</a> for updates.</p>
448-
</div>
449-
</div>);
450-
}
451-
452439
const onNewWorkspace = async () => {
453440
const redirectToNewWorkspace = () => {
454441
// 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() {
473460
{selectedRepo && selectedTeamOrUser && (<div></div>)}
474461
</>
475462

476-
{isBitbucket() && renderBitbucketWarning()}
477-
478463
</div>);
479464
} else {
480465
const projectLink = User.is(selectedTeamOrUser) ? `/projects/${project.slug}` : `/t/${selectedTeamOrUser?.slug}/${project.slug}`;
@@ -534,8 +519,8 @@ function GitProviders(props: {
534519
});
535520
}
536521

537-
// for now we exclude bitbucket.org and GitHub Enterprise
538-
const filteredProviders = () => props.authProviders.filter(p => p.host === "github.com" || p.authProviderType === "GitLab");
522+
// for now we exclude GitHub Enterprise
523+
const filteredProviders = () => props.authProviders.filter(p => p.host === "github.com" || p.host === "bitbucket.org" || p.authProviderType === "GitLab");
539524

540525
return (
541526
<div className="mt-8 border rounded-t-xl border-gray-100 dark:border-gray-800 flex-col">
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/**
2+
* Copyright (c) 2020 Gitpod GmbH. All rights reserved.
3+
* Licensed under the Gitpod Enterprise Source Code License,
4+
* See License.enterprise.txt in the project root folder.
5+
*/
6+
7+
import { AuthProviderInfo, ProviderRepository, User } from "@gitpod/gitpod-protocol";
8+
import { inject, injectable } from "inversify";
9+
import { TokenProvider } from "../../../src/user/token-provider";
10+
import { Bitbucket } from "bitbucket";
11+
12+
@injectable()
13+
export class BitbucketAppSupport {
14+
15+
@inject(TokenProvider) protected readonly tokenProvider: TokenProvider;
16+
17+
async getProviderRepositoriesForUser(params: { user: User, provider: AuthProviderInfo }): Promise<ProviderRepository[]> {
18+
const token = await this.tokenProvider.getTokenForHost(params.user, params.provider.host);
19+
const oauthToken = token.value;
20+
21+
const api = new Bitbucket({
22+
baseUrl: `https://api.${params.provider.host}/2.0`,
23+
auth: {
24+
token: oauthToken
25+
}
26+
});
27+
28+
const result: ProviderRepository[] = [];
29+
const ownersRepos: ProviderRepository[] = [];
30+
31+
const identity = params.user.identities.find(i => i.authProviderId === params.provider.authProviderId);
32+
if (!identity) {
33+
return result;
34+
}
35+
const usersGitLabAccount = identity.authName;
36+
37+
const workspaces = (await api.workspaces.getWorkspaces({ pagelen: 100 })).data.values?.map(w => w.slug!) || [];
38+
39+
const reposPromise = Promise.all(workspaces.map(workspace => api.repositories.list({
40+
workspace,
41+
pagelen: 100,
42+
role: "admin"
43+
}).catch(e => {
44+
45+
})));
46+
47+
const reposInWorkspace = await reposPromise;
48+
for (const repos of reposInWorkspace) {
49+
if (repos) {
50+
for (const repo of (repos.data.values || [])) {
51+
const fullName = repo.full_name!;
52+
const cloneUrl = repo.links!.clone!.find((x: any) => x.name === "https")!.href!;
53+
const updatedAt = repo.updated_on!;
54+
const accountAvatarUrl = repo.links!.avatar?.href!;
55+
const account = fullName.split("/")[0];
56+
57+
(account === usersGitLabAccount ? ownersRepos : result).push({
58+
name: repo.name!,
59+
account,
60+
cloneUrl,
61+
updatedAt,
62+
accountAvatarUrl,
63+
})
64+
}
65+
}
66+
}
67+
68+
// put owner's repos first. the frontend will pick first account to continue with
69+
result.unshift(...ownersRepos);
70+
return result;
71+
}
72+
73+
}

components/server/ee/src/container-module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ import { GitHubAppSupport } from "./github/github-app-support";
5050
import { GitLabAppSupport } from "./gitlab/gitlab-app-support";
5151
import { Config } from "../../src/config";
5252
import { SnapshotService } from "./workspace/snapshot-service";
53+
import { BitbucketAppSupport } from "./bitbucket/bitbucket-app-support";
5354

5455
export const productionEEContainerModule = new ContainerModule((bind, unbind, isBound, rebind) => {
5556
rebind(Server).to(ServerEE).inSingletonScope();
@@ -68,6 +69,7 @@ export const productionEEContainerModule = new ContainerModule((bind, unbind, is
6869
bind(GitLabApp).toSelf().inSingletonScope();
6970
bind(GitLabAppSupport).toSelf().inSingletonScope();
7071
bind(BitbucketApp).toSelf().inSingletonScope();
72+
bind(BitbucketAppSupport).toSelf().inSingletonScope();
7173

7274
bind(LicenseEvaluator).toSelf().inSingletonScope();
7375
bind(LicenseKeySource).to(DBLicenseKeySource).inSingletonScope();

components/server/ee/src/prebuilds/bitbucket-service.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,17 @@ export class BitbucketService extends RepositoryService {
2727

2828
async canInstallAutomatedPrebuilds(user: User, cloneUrl: string): Promise<boolean> {
2929
const { host } = await this.bitbucketContextParser.parseURL(user, cloneUrl);
30-
return host === this.authProviderConfig.host;
30+
if (host !== this.authProviderConfig.host) {
31+
return false;
32+
}
33+
34+
// only admins may install webhooks on repositories
35+
const { owner, repoName } = await this.bitbucketContextParser.parseURL(user, cloneUrl);
36+
const api = await this.api.create(user);
37+
const response = await api.user.listPermissionsForRepos({
38+
q: `repository.full_name="${owner}/${repoName}"`
39+
})
40+
return !!response.data?.values && response.data.values[0]?.permission === "admin";
3141
}
3242

3343
async installAutomatedPrebuilds(user: User, cloneUrl: string): Promise<void> {

components/server/ee/src/workspace/gitpod-server-impl.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import { Config } from "../../../src/config";
4141
import { SnapshotService, WaitForSnapshotOptions } from "./snapshot-service";
4242
import { SafePromise } from "@gitpod/gitpod-protocol/lib/util/safe-promise";
4343
import { ClientMetadata } from "../../../src/websocket/websocket-connection-manager";
44+
import { BitbucketAppSupport } from "../bitbucket/bitbucket-app-support";
4445

4546
@injectable()
4647
export class GitpodServerEEImpl extends GitpodServerImpl {
@@ -68,6 +69,7 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
6869

6970
@inject(GitHubAppSupport) protected readonly githubAppSupport: GitHubAppSupport;
7071
@inject(GitLabAppSupport) protected readonly gitLabAppSupport: GitLabAppSupport;
72+
@inject(BitbucketAppSupport) protected readonly bitbucketAppSupport: BitbucketAppSupport;
7173

7274
@inject(Config) protected readonly config: Config;
7375

@@ -1429,6 +1431,8 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
14291431

14301432
if (providerHost === "github.com") {
14311433
repositories.push(...(await this.githubAppSupport.getProviderRepositoriesForUser({ user, ...params })));
1434+
} else if (providerHost === "bitbucket.org" && provider) {
1435+
repositories.push(...(await this.bitbucketAppSupport.getProviderRepositoriesForUser({ user, provider })));
14321436
} else if (provider?.authProviderType === "GitLab") {
14331437
repositories.push(...(await this.gitLabAppSupport.getProviderRepositoriesForUser({ user, provider })));
14341438
} else {

components/server/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
"@probot/get-private-key": "^1.1.1",
4545
"amqplib": "^0.8.0",
4646
"base-64": "^1.0.0",
47-
"bitbucket": "^2.4.2",
47+
"bitbucket": "^2.7.0",
4848
"body-parser": "^1.18.2",
4949
"cookie": "^0.4.1",
5050
"cookie-parser": "^1.4.5",

components/server/src/bitbucket/bitbucket-repository-provider.ts

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,18 +26,69 @@ export class BitbucketRepositoryProvider implements RepositoryProvider {
2626
return { host, owner, name, cloneUrl, description, avatarUrl, webUrl };
2727
}
2828

29-
async getBranch(user: User, owner: string, repo: string, branch: string): Promise<Branch> {
30-
// todo
31-
throw new Error("not implemented");
29+
async getBranch(user: User, owner: string, repo: string, branchName: string): Promise<Branch> {
30+
const api = await this.apiFactory.create(user);
31+
const response = await api.repositories.getBranch({
32+
workspace: owner,
33+
repo_slug: repo,
34+
name: branchName
35+
})
36+
37+
const branch = response.data;
38+
39+
return {
40+
htmlUrl: branch.links?.html?.href!,
41+
name: branch.name!,
42+
commit: {
43+
sha: branch.target?.hash!,
44+
author: branch.target?.author?.user?.display_name!,
45+
authorAvatarUrl: branch.target?.author?.user?.links?.avatar?.href,
46+
authorDate: branch.target?.date!,
47+
commitMessage: branch.target?.message || "missing commit message",
48+
}
49+
};
3250
}
3351

3452
async getBranches(user: User, owner: string, repo: string): Promise<Branch[]> {
35-
// todo
36-
return [];
53+
const branches: Branch[] = [];
54+
const api = await this.apiFactory.create(user);
55+
const response = await api.repositories.listBranches({
56+
workspace: owner,
57+
repo_slug: repo,
58+
sort: "target.date"
59+
})
60+
61+
for (const branch of response.data.values!) {
62+
branches.push({
63+
htmlUrl: branch.links?.html?.href!,
64+
name: branch.name!,
65+
commit: {
66+
sha: branch.target?.hash!,
67+
author: branch.target?.author?.user?.display_name!,
68+
authorAvatarUrl: branch.target?.author?.user?.links?.avatar?.href,
69+
authorDate: branch.target?.date!,
70+
commitMessage: branch.target?.message || "missing commit message",
71+
}
72+
});
73+
}
74+
75+
return branches;
3776
}
3877

3978
async getCommitInfo(user: User, owner: string, repo: string, ref: string): Promise<CommitInfo | undefined> {
40-
// todo
41-
return undefined;
79+
const api = await this.apiFactory.create(user);
80+
const response = await api.commits.get({
81+
workspace: owner,
82+
repo_slug: repo,
83+
commit: ref
84+
})
85+
const commit = response.data;
86+
return {
87+
sha: commit.hash!,
88+
author: commit.author?.user?.display_name!,
89+
authorDate: commit.date!,
90+
commitMessage: commit.message || "missing commit message",
91+
authorAvatarUrl: commit.author?.user?.links?.avatar?.href,
92+
};
4293
}
4394
}

yarn.lock

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5041,10 +5041,10 @@ [email protected]:
50415041
resolved "https://registry.yarnpkg.com/bintrees/-/bintrees-1.0.1.tgz#0e655c9b9c2435eaab68bf4027226d2b55a34524"
50425042
integrity sha1-DmVcm5wkNeqraL9AJyJtK1WjRSQ=
50435043

5044-
bitbucket@^2.4.2:
5045-
version "2.6.3"
5046-
resolved "https://registry.yarnpkg.com/bitbucket/-/bitbucket-2.6.3.tgz#e7aa030406720e24c19a40701506b1c366daf544"
5047-
integrity sha512-t23mlPsCchl+7TCGGHqI4Up++mnGd6smaKsNe/t+kGlkGfIzm+QmVdWvBboHl8Nyequ8Wm0Whi2lKq9qmfJmxA==
5044+
bitbucket@^2.7.0:
5045+
version "2.7.0"
5046+
resolved "https://registry.yarnpkg.com/bitbucket/-/bitbucket-2.7.0.tgz#fd11b19a42cc9b89f6a899ff669fd1575183a5b3"
5047+
integrity sha512-6fw3MzXeFp3TLmo6jF7IWFn9tFpFKpzCpDjKek9s5EY559Ff3snbu2hmS5ZKmR7D0XomPbIT0dBN1juoJ/gGyA==
50485048
dependencies:
50495049
before-after-hook "^2.1.0"
50505050
deepmerge "^4.2.2"

0 commit comments

Comments
 (0)