Skip to content

Commit f45d583

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

File tree

9 files changed

+173
-38
lines changed

9 files changed

+173
-38
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: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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+
import { URL } from "url";
12+
13+
@injectable()
14+
export class BitbucketAppSupport {
15+
16+
@inject(TokenProvider) protected readonly tokenProvider: TokenProvider;
17+
18+
async getProviderRepositoriesForUser(params: { user: User, provider: AuthProviderInfo }): Promise<ProviderRepository[]> {
19+
const token = await this.tokenProvider.getTokenForHost(params.user, params.provider.host);
20+
const oauthToken = token.value;
21+
22+
const api = new Bitbucket({
23+
baseUrl: `https://api.${params.provider.host}/2.0`,
24+
auth: {
25+
token: oauthToken
26+
}
27+
});
28+
29+
const result: ProviderRepository[] = [];
30+
const ownersRepos: ProviderRepository[] = [];
31+
32+
const identity = params.user.identities.find(i => i.authProviderId === params.provider.authProviderId);
33+
if (!identity) {
34+
return result;
35+
}
36+
const usersGitLabAccount = identity.authName;
37+
38+
const workspaces = (await api.workspaces.getWorkspaces({ pagelen: 100 })).data.values?.map(w => w.slug!) || [];
39+
40+
const reposPromise = Promise.all(workspaces.map(workspace => api.repositories.list({
41+
workspace,
42+
pagelen: 100,
43+
role: "admin"
44+
}).catch(e => {
45+
46+
})));
47+
48+
const reposInWorkspace = await reposPromise;
49+
for (const repos of reposInWorkspace) {
50+
if (repos) {
51+
for (const repo of (repos.data.values || [])) {
52+
let cloneUrl = repo.links!.clone!.find((x: any) => x.name === "https")!.href!;
53+
if (cloneUrl) {
54+
const url = new URL(cloneUrl);
55+
url.username = '';
56+
cloneUrl = url.toString();
57+
}
58+
const fullName = repo.full_name!;
59+
const updatedAt = repo.updated_on!;
60+
const accountAvatarUrl = repo.links!.avatar?.href!;
61+
const account = fullName.split("/")[0];
62+
63+
(account === usersGitLabAccount ? ownersRepos : result).push({
64+
name: repo.name!,
65+
account,
66+
cloneUrl,
67+
updatedAt,
68+
accountAvatarUrl,
69+
})
70+
}
71+
}
72+
}
73+
74+
// put owner's repos first. the frontend will pick first account to continue with
75+
result.unshift(...ownersRepos);
76+
return result;
77+
}
78+
79+
}

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-context-parser.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,8 +250,6 @@ export class BitbucketContextParser extends AbstractContextParser implements ICo
250250

251251
const result: Repository = {
252252
cloneUrl: `https://${host}/${repo.full_name}.git`,
253-
// cloneUrl: repoQueryResult.links.html.href + ".git",
254-
// cloneUrl: repoQueryResult.links.clone.find((x: any) => x.name === "https").href,
255253
host,
256254
name,
257255
owner,

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

Lines changed: 65 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
import { Branch, CommitInfo, Repository, User } from "@gitpod/gitpod-protocol";
88
import { inject, injectable } from 'inversify';
9+
import { URL } from "url";
910
import { RepoURL } from '../repohost/repo-url';
1011
import { RepositoryProvider } from '../repohost/repository-provider';
1112
import { BitbucketApiFactory } from './bitbucket-api-factory';
@@ -18,26 +19,82 @@ export class BitbucketRepositoryProvider implements RepositoryProvider {
1819
async getRepo(user: User, owner: string, name: string): Promise<Repository> {
1920
const api = await this.apiFactory.create(user);
2021
const repo = (await api.repositories.get({ workspace: owner, repo_slug: name })).data;
21-
const cloneUrl = repo.links!.clone!.find((x: any) => x.name === "https")!.href!;
22+
let cloneUrl = repo.links!.clone!.find((x: any) => x.name === "https")!.href!;
23+
if (cloneUrl) {
24+
const url = new URL(cloneUrl);
25+
url.username = '';
26+
cloneUrl = url.toString();
27+
}
2228
const host = RepoURL.parseRepoUrl(cloneUrl)!.host;
2329
const description = repo.description;
2430
const avatarUrl = repo.owner!.links!.avatar!.href;
2531
const webUrl = repo.links!.html!.href;
2632
return { host, owner, name, cloneUrl, description, avatarUrl, webUrl };
2733
}
2834

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

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

3984
async getCommitInfo(user: User, owner: string, repo: string, ref: string): Promise<CommitInfo | undefined> {
40-
// todo
41-
return undefined;
85+
const api = await this.apiFactory.create(user);
86+
const response = await api.commits.get({
87+
workspace: owner,
88+
repo_slug: repo,
89+
commit: ref
90+
})
91+
const commit = response.data;
92+
return {
93+
sha: commit.hash!,
94+
author: commit.author?.user?.display_name!,
95+
authorDate: commit.date!,
96+
commitMessage: commit.message || "missing commit message",
97+
authorAvatarUrl: commit.author?.user?.links?.avatar?.href,
98+
};
4299
}
43100
}

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)