Skip to content

Commit

Permalink
[bitbucket] enable projects
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexTugarev committed Dec 17, 2021
1 parent 6285314 commit 4d7bc93
Show file tree
Hide file tree
Showing 9 changed files with 173 additions and 38 deletions.
29 changes: 7 additions & 22 deletions components/dashboard/src/projects/NewProject.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ export default function NewProject() {
}, [selectedAccount]);

useEffect(() => {
if (!selectedProviderHost || isBitbucket()) {
if (!selectedProviderHost) {
return;
}
(async () => {
Expand All @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -382,11 +381,11 @@ export default function NewProject() {
setSelectedProviderHost(host);
}

if (!loaded && !isBitbucket()) {
if (!loaded) {
return renderLoadingState();
}

if (showGitProviders || isBitbucket()) {
if (showGitProviders) {
return (<GitProviders onHostSelected={onGitProviderSeleted} authProviders={authProviders} />);
}

Expand Down Expand Up @@ -437,18 +436,6 @@ export default function NewProject() {
</>)
};

const renderBitbucketWarning = () => {
return (
<div className="mt-16 flex space-x-2 py-6 px-6 w-96 justify-betweeen bg-gitpod-kumquat-light rounded-xl">
<div className="pr-3 self-center w-6">
<img src={exclamation} />
</div>
<div className="flex-1 flex flex-col">
<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>
</div>
</div>);
}

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)
Expand All @@ -473,8 +460,6 @@ export default function NewProject() {
{selectedRepo && selectedTeamOrUser && (<div></div>)}
</>

{isBitbucket() && renderBitbucketWarning()}

</div>);
} else {
const projectLink = User.is(selectedTeamOrUser) ? `/projects/${project.slug}` : `/t/${selectedTeamOrUser?.slug}/${project.slug}`;
Expand Down Expand Up @@ -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 (
<div className="mt-8 border rounded-t-xl border-gray-100 dark:border-gray-800 flex-col">
Expand Down
79 changes: 79 additions & 0 deletions components/server/ee/src/bitbucket/bitbucket-app-support.ts
Original file line number Diff line number Diff line change
@@ -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<ProviderRepository[]> {
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;
}

}
2 changes: 2 additions & 0 deletions components/server/ee/src/container-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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();
Expand Down
12 changes: 11 additions & 1 deletion components/server/ee/src/prebuilds/bitbucket-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,17 @@ export class BitbucketService extends RepositoryService {

async canInstallAutomatedPrebuilds(user: User, cloneUrl: string): Promise<boolean> {
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<void> {
Expand Down
4 changes: 4 additions & 0 deletions components/server/ee/src/workspace/gitpod-server-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion components/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 0 additions & 2 deletions components/server/src/bitbucket/bitbucket-context-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
73 changes: 65 additions & 8 deletions components/server/src/bitbucket/bitbucket-repository-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -18,26 +19,82 @@ export class BitbucketRepositoryProvider implements RepositoryProvider {
async getRepo(user: User, owner: string, name: string): Promise<Repository> {
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;
const webUrl = repo.links!.html!.href;
return { host, owner, name, cloneUrl, description, avatarUrl, webUrl };
}

async getBranch(user: User, owner: string, repo: string, branch: string): Promise<Branch> {
// todo
throw new Error("not implemented");
async getBranch(user: User, owner: string, repo: string, branchName: string): Promise<Branch> {
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<Branch[]> {
// 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<CommitInfo | undefined> {
// 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,
};
}
}
8 changes: 4 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5041,10 +5041,10 @@ [email protected]:
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"
Expand Down

0 comments on commit 4d7bc93

Please sign in to comment.