Skip to content

Commit ec47e6c

Browse files
committed
wip
1 parent 1cfbf1a commit ec47e6c

8 files changed

+306
-178
lines changed

components/server/src/bitbucket-server/bitbucket-api-factory.ts

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,10 @@
55
*/
66

77
import { User, Token } from "@gitpod/gitpod-protocol";
8-
import { APIClient, Bitbucket } from "bitbucket";
98
import { inject, injectable } from "inversify";
109
import { AuthProviderParams } from "../auth/auth-provider";
1110
import { BitbucketTokenHelper } from "../bitbucket/bitbucket-token-handler";
11+
import * as BitbucketServer from "@atlassian/bitbucket-server";
1212

1313
@injectable()
1414
export class BitbucketServerApiFactory {
@@ -20,35 +20,40 @@ export class BitbucketServerApiFactory {
2020
* Returns a Bitbucket API client for the given user.
2121
* @param user The user the API client should be created for.
2222
*/
23-
public async create(user: User): Promise<APIClient> {
23+
public async create(user: User): Promise<BitbucketServer> {
2424
const token = await this.tokenHelper.getTokenWithScopes(user, []);
2525
return this.createBitbucket(this.baseUrl, token);
2626
}
2727

28-
protected createBitbucket(baseUrl: string, token: Token): APIClient {
29-
return new Bitbucket({
28+
protected createBitbucket(baseUrl: string, token: Token): BitbucketServer {
29+
const options = {
3030
baseUrl,
31-
auth: {
32-
token: token.value
33-
}
34-
});
31+
};
32+
const client = new BitbucketServer(options);
33+
client.authenticate({
34+
type: "token",
35+
token: token.value
36+
})
37+
return client;
3538
}
3639

3740
protected get baseUrl(): string {
38-
return `https://api.${this.config.host}/2.0`;
41+
return `https://${this.config.host}`;
3942
}
4043
}
4144

4245
@injectable()
4346
export class BasicAuthBitbucketServerApiFactory extends BitbucketServerApiFactory {
44-
protected createBitbucket(baseUrl: string, token: Token): APIClient {
45-
46-
return new Bitbucket({
47+
protected createBitbucket(baseUrl: string, token: Token): BitbucketServer {
48+
const options = {
4749
baseUrl,
48-
auth: {
49-
username: token.username!,
50-
password: token.value
51-
}
52-
});
50+
};
51+
const client = new BitbucketServer(options);
52+
client.authenticate({
53+
type: "basic",
54+
username: token.username || "nobody",
55+
password: token.value
56+
})
57+
return client;
5358
}
5459
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
3+
* Licensed under the GNU Affero General Public License (AGPL).
4+
* See License-AGPL.txt in the project root for license information.
5+
*/
6+
7+
8+
declare namespace BitbucketServer {
9+
10+
export namespace Schema {
11+
export interface Repository {
12+
id: number;
13+
slug: string;
14+
name: string;
15+
public: boolean;
16+
links: {
17+
clone: {
18+
href: string;
19+
name: string;
20+
}[]
21+
}
22+
project: Project;
23+
}
24+
25+
export interface Project {
26+
key: string;
27+
id: number;
28+
name: string;
29+
public: boolean;
30+
}
31+
}
32+
}
33+
34+
35+
// {
36+
// "slug": "test123",
37+
// "id": 3,
38+
// "name": "test123",
39+
// "hierarchyId": "670d2b6499d312c9deb8",
40+
// "scmId": "git",
41+
// "state": "AVAILABLE",
42+
// "statusMessage": "Available",
43+
// "forkable": true,
44+
// "project": {
45+
// "key": "JLDEC",
46+
// "id": 2,
47+
// "name": "jldec-project",
48+
// "public": false,
49+
// "type": "NORMAL",
50+
// "links": {
51+
// "self": ...
52+
// }
53+
// },
54+
// "public": false,
55+
// "links": {
56+
// "clone": [
57+
// ...,
58+
// {
59+
// "href": "https://bitbucket.gitpod-self-hosted.com/scm/jldec/test123.git",
60+
// "name": "http"
61+
// }
62+
// ],
63+
// "self": ...
64+
// }
65+
// }
66+
67+
68+
69+
// {
70+
// "name": "roboquat",
71+
// "emailAddress": "[email protected]",
72+
// "id": 102,
73+
// "displayName": "Robot Kumquat",
74+
// "active": true,
75+
// "slug": "roboquat",
76+
// "type": "NORMAL",
77+
// "links": {
78+
// "self": [
79+
// {
80+
// "href": "https://bitbucket.gitpod-self-hosted.com/users/roboquat"
81+
// }
82+
// ]
83+
// }
84+
// }

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright (c) 2021 Gitpod GmbH. All rights reserved.
2+
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
33
* Licensed under the GNU Affero General Public License (AGPL).
44
* See License-AGPL.txt in the project root for license information.
55
*/
@@ -12,7 +12,7 @@ import fetch from "node-fetch";
1212
import { AuthUserSetup } from "../auth/auth-provider";
1313
import { GenericAuthProvider } from "../auth/generic-auth-provider";
1414
import { BitbucketServerOAuthScopes } from "./bitbucket-server-oauth-scopes";
15-
import BitbucketServer = require("@atlassian/bitbucket-server")
15+
import * as BitbucketServer from "@atlassian/bitbucket-server";
1616

1717
@injectable()
1818
export class BitbucketServerAuthProvider extends GenericAuthProvider {
@@ -94,7 +94,7 @@ export class BitbucketServerAuthProvider extends GenericAuthProvider {
9494
return <AuthUserSetup>{
9595
authUser: {
9696
authId: `${user.id!}`,
97-
authName: user.name!,
97+
authName: user.slug!,
9898
primaryEmail: user.emailAddress!,
9999
name: user.displayName!,
100100
// avatarUrl: user.links!.avatar!.href // TODO

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

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

77
import { ContainerModule } from "inversify";
88
import { AuthProvider } from "../auth/auth-provider";
9+
import { BitbucketApiFactory } from "../bitbucket/bitbucket-api-factory";
910
import { BitbucketFileProvider } from "../bitbucket/bitbucket-file-provider";
1011
import { BitbucketLanguagesProvider } from "../bitbucket/bitbucket-language-provider";
1112
import { BitbucketRepositoryProvider } from "../bitbucket/bitbucket-repository-provider";
@@ -30,6 +31,7 @@ export const bitbucketServerContainerModule = new ContainerModule((bind, _unbind
3031
bind(BitbucketServerAuthProvider).toSelf().inSingletonScope();
3132
bind(AuthProvider).to(BitbucketServerAuthProvider).inSingletonScope();
3233
bind(BitbucketTokenHelper).toSelf().inSingletonScope();
34+
bind(BitbucketApiFactory).toSelf().inSingletonScope();
3335
// bind(BitbucketTokenValidator).toSelf().inSingletonScope(); // TODO
3436
// bind(IGitTokenValidator).toService(BitbucketTokenValidator);
3537
});

components/server/src/bitbucket-server/bitbucket-server-context-parser.ts

Lines changed: 89 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@
77
import { NavigatorContext, Repository, User, WorkspaceContext } from "@gitpod/gitpod-protocol";
88
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
99
import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing";
10-
import { Schema } from "bitbucket";
10+
import { Schema } from "@atlassian/bitbucket-server";
1111
import { inject, injectable } from "inversify";
1212
import { BitbucketTokenHelper } from "../bitbucket/bitbucket-token-handler";
1313
import { NotFoundError } from "../errors";
14-
import { AbstractContextParser, IContextParser } from "../workspace/context-parser";
14+
import { AbstractContextParser, IContextParser, URLParts } from "../workspace/context-parser";
1515
import { BitbucketServerApiFactory } from "./bitbucket-api-factory";
16+
import { URL } from "url";
1617

1718
const DEFAULT_BRANCH = "master";
1819

@@ -43,38 +44,68 @@ export class BitbucketServerContextParser extends AbstractContextParser implemen
4344
}
4445
}
4546

46-
public async fetchCommitHistory(ctx: TraceContext, user: User, contextUrl: string, commit: string, maxDepth: number): Promise<string[] | undefined> {
47-
return undefined;
48-
}
47+
public async parseURL(user: User, contextUrl: string): Promise<URLParts> {
48+
const url = new URL(contextUrl);
49+
const pathname = url.pathname.replace(/^\//, "").replace(/\/$/, ""); // pathname without leading and trailing slash
50+
const segments = pathname.split('/');
4951

50-
protected async isValidCommitHash(user: User, owner: string, repoName: string, potentialCommitHash: string) {
51-
if (potentialCommitHash.length !== 40) {
52-
return false;
52+
const host = this.host; // as per contract, cf. `canHandle(user, contextURL)`
53+
54+
const lenghtOfRelativePath = host.split("/").length - 1; // e.g. "123.123.123.123/gitlab" => length of 1
55+
if (lenghtOfRelativePath > 0) {
56+
// remove segments from the path to be consider further, which belong to the relative location of the host
57+
// cf. https://github.com/gitpod-io/gitpod/issues/2637
58+
segments.splice(0, lenghtOfRelativePath);
5359
}
54-
try {
55-
const api = await this.api(user);
56-
const result = (await api.repositories.getCommit({ workspace: owner, repo_slug: repoName, commit: potentialCommitHash }));
57-
return result.data.hash === potentialCommitHash;
58-
} catch {
59-
return false;
60+
61+
var owner: string = segments[1];
62+
var repoName: string = segments[3];
63+
var moreSegmentsStart: number = 4;
64+
const endsWithRepoName = segments.length === moreSegmentsStart;
65+
const searchParams = url.searchParams;
66+
return {
67+
host,
68+
owner,
69+
repoName: this.parseRepoName(repoName, endsWithRepoName),
70+
moreSegments: endsWithRepoName ? [] : segments.slice(moreSegmentsStart),
71+
searchParams
6072
}
6173
}
6274

63-
protected async isTag(user: User, owner: string, repoName: string, potentialTag: string) {
64-
try {
65-
const api = await this.api(user);
66-
const result = (await api.repositories.getTag({ workspace: owner, repo_slug: repoName, name: potentialTag }));
67-
return result.data.name === potentialTag;
68-
} catch {
69-
return false;
70-
}
75+
public async fetchCommitHistory(ctx: TraceContext, user: User, contextUrl: string, commit: string, maxDepth: number): Promise<string[] | undefined> {
76+
return undefined;
7177
}
7278

73-
protected async handleNavigatorContext(ctx: TraceContext, user: User, host: string, owner: string, repoName: string, more: Partial<NavigatorContext> = {}, givenRepo?: Schema.Repository): Promise<NavigatorContext> {
79+
// protected async isValidCommitHash(user: User, owner: string, repoName: string, potentialCommitHash: string) {
80+
// if (potentialCommitHash.length !== 40) {
81+
// return false;
82+
// }
83+
// try {
84+
// const api = await this.api(user);
85+
// const result = (await api.repositories.getCommit({ workspace: owner, repo_slug: repoName, commit: potentialCommitHash }));
86+
// return result.data.hash === potentialCommitHash;
87+
// } catch {
88+
// return false;
89+
// }
90+
// }
91+
92+
// protected async isTag(user: User, owner: string, repoName: string, potentialTag: string) {
93+
// try {
94+
// const api = await this.api(user);
95+
// const result = (await api.repositories.getTag({ workspace: owner, repo_slug: repoName, name: potentialTag }));
96+
// return result.data.name === potentialTag;
97+
// } catch {
98+
// return false;
99+
// }
100+
// }
101+
102+
protected async handleNavigatorContext(ctx: TraceContext, user: User, host: string, owner: string, repoName: string, more: Partial<NavigatorContext> = {}): Promise<NavigatorContext> {
74103
const span = TraceContext.startSpan("BitbucketServerContextParser.handleNavigatorContext", ctx);
75104
try {
76105
const api = await this.api(user);
77-
const repo = givenRepo || (await api.repositories.get({ workspace: owner, repo_slug: repoName })).data;
106+
107+
const repo = (await api.repos.getRepository({projectKey: owner, repositorySlug: repoName})).data;
108+
78109
const repository = await this.toRepository(user, host, repo);
79110
span.log({ "request.finished": "" });
80111

@@ -88,23 +119,28 @@ export class BitbucketServerContextParser extends AbstractContextParser implemen
88119
more.refType = more.refType || "branch";
89120

90121
if (!more.revision) {
91-
const commits = (await api.repositories.listCommitsAt({ workspace: owner, repo_slug: repoName, revision: more.ref!, pagelen: 1 })).data;
92-
more.revision = commits.values && commits.values.length > 0 ? commits.values[0].hash : "";
93-
if ((!commits.values || commits.values.length === 0) && more.ref === repository.defaultBranch) {
94-
// empty repo
95-
more.ref = undefined;
96-
more.revision = "";
97-
more.refType = undefined;
98-
}
122+
const tipCommitOnDefaultBranch = await api.repos.getCommits({projectKey: owner, repositorySlug: repoName, q: { limit: 1 }});
123+
more.revision = tipCommitOnDefaultBranch.data.values[0]?.id || "";
99124
}
100125

101-
if (!more.path) {
102-
more.isFile = false;
103-
more.path = "";
104-
} else if (more.isFile === undefined) {
105-
const fileMeta = (await api.repositories.readSrc({ workspace: owner, repo_slug: repoName, format: "meta", commit: more.revision!, path: more.path!, pagelen: 1 })).data;
106-
more.isFile = (fileMeta as any).type === "commit_file";
107-
}
126+
// if (!more.revision) {
127+
// const commits = (await api.repositories.listCommitsAt({ workspace: owner, repo_slug: repoName, revision: more.ref!, pagelen: 1 })).data;
128+
// more.revision = commits.values && commits.values.length > 0 ? commits.values[0].hash : "";
129+
// if ((!commits.values || commits.values.length === 0) && more.ref === repository.defaultBranch) {
130+
// // empty repo
131+
// more.ref = undefined;
132+
// more.revision = "";
133+
// more.refType = undefined;
134+
// }
135+
// }
136+
137+
// if (!more.path) {
138+
// more.isFile = false;
139+
// more.path = "";
140+
// } else if (more.isFile === undefined) {
141+
// const fileMeta = (await api.repositories.readSrc({ workspace: owner, repo_slug: repoName, format: "meta", commit: more.revision!, path: more.path!, pagelen: 1 })).data;
142+
// more.isFile = (fileMeta as any).type === "commit_file";
143+
// }
108144

109145
return {
110146
...more,
@@ -125,27 +161,26 @@ export class BitbucketServerContextParser extends AbstractContextParser implemen
125161
if (!repo) {
126162
throw new Error('Unknown repository.');
127163
}
128-
// full_name: string
129-
// The concatenation of the repository owner's username and the slugified name, e.g. "evzijst/interruptingcow". This is the same string used in Bitbucket URLs.
130-
const fullName = repo.full_name!.split("/");
131-
const owner = fullName[0];
132-
const name = fullName[1];
164+
165+
const owner = repo.project.key;
166+
const name = repo.name;
167+
const cloneUrl = repo.links.clone.find(u => u.name === "http")?.href!;
133168

134169
const result: Repository = {
135-
cloneUrl: `https://${host}/${repo.full_name}.git`,
170+
cloneUrl,
136171
host,
137172
name,
138173
owner,
139-
private: !!repo.isPrivate,
140-
defaultBranch: repo.mainbranch ? repo.mainbranch.name : DEFAULT_BRANCH,
141-
}
142-
if (!!repo.parent && !!repo.parent.full_name) {
143-
const api = await this.api(user);
144-
const parentRepo = (await api.repositories.get({ workspace: repo.parent!.full_name!.split("/")[0], repo_slug: repo.parent!.full_name!.split("/")[1] })).data;
145-
result.fork = {
146-
parent: await this.toRepository(user, host, parentRepo)
147-
};
174+
private: !repo.public,
175+
defaultBranch: DEFAULT_BRANCH,
148176
}
177+
// if (!!repo.parent && !!repo.parent.full_name) {
178+
// const api = await this.api(user);
179+
// const parentRepo = (await api.repositories.get({ workspace: repo.parent!.full_name!.split("/")[0], repo_slug: repo.parent!.full_name!.split("/")[1] })).data;
180+
// result.fork = {
181+
// parent: await this.toRepository(user, host, parentRepo)
182+
// };
183+
// }
149184

150185
return result;
151186
}

0 commit comments

Comments
 (0)