mirror of
https://github.com/gitpod-io/gitpod.git
synced 2025-12-08 17:36:30 +00:00
[bitbucket-server] support for projects and prebuilds
This commit is contained in:
parent
9526f87665
commit
76b51bc224
@ -43,19 +43,26 @@ export default function NewProject() {
|
||||
const [authProviders, setAuthProviders] = useState<AuthProviderInfo[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (user && selectedProviderHost === undefined) {
|
||||
if (user.identities.find((i) => i.authProviderId === "Public-GitLab")) {
|
||||
setSelectedProviderHost("gitlab.com");
|
||||
} else if (user.identities.find((i) => i.authProviderId === "Public-GitHub")) {
|
||||
setSelectedProviderHost("github.com");
|
||||
} else if (user.identities.find((i) => i.authProviderId === "Public-Bitbucket")) {
|
||||
setSelectedProviderHost("bitbucket.org");
|
||||
}
|
||||
(async () => {
|
||||
setAuthProviders(await getGitpodService().server.getAuthProviders());
|
||||
})();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (user && authProviders && selectedProviderHost === undefined) {
|
||||
for (let i = user.identities.length - 1; i >= 0; i--) {
|
||||
const candidate = user.identities[i];
|
||||
if (candidate) {
|
||||
const authProvider = authProviders.find((ap) => ap.authProviderId === candidate.authProviderId);
|
||||
const host = authProvider?.host;
|
||||
if (host) {
|
||||
setSelectedProviderHost(host);
|
||||
break;
|
||||
}
|
||||
}, [user]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [user, authProviders]);
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URLSearchParams(location.search);
|
||||
@ -385,7 +392,7 @@ export default function NewProject() {
|
||||
>
|
||||
{toSimpleName(r.name)}
|
||||
</div>
|
||||
<p>Updated {moment(r.updatedAt).fromNow()}</p>
|
||||
{r.updatedAt && <p>Updated {moment(r.updatedAt).fromNow()}</p>}
|
||||
</div>
|
||||
<div className="flex justify-end">
|
||||
<div className="h-full my-auto flex self-center opacity-0 group-hover:opacity-100 items-center mr-2 text-right">
|
||||
@ -653,7 +660,11 @@ function GitProviders(props: {
|
||||
|
||||
const filteredProviders = () =>
|
||||
props.authProviders.filter(
|
||||
(p) => p.authProviderType === "GitHub" || p.host === "bitbucket.org" || p.authProviderType === "GitLab",
|
||||
(p) =>
|
||||
p.authProviderType === "GitHub" ||
|
||||
p.host === "bitbucket.org" ||
|
||||
p.authProviderType === "GitLab" ||
|
||||
p.authProviderType === "BitbucketServer",
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@ -307,7 +307,7 @@ export interface ProviderRepository {
|
||||
account: string;
|
||||
accountAvatarUrl: string;
|
||||
cloneUrl: string;
|
||||
updatedAt: string;
|
||||
updatedAt?: string;
|
||||
installationId?: number;
|
||||
installationUpdatedAt?: string;
|
||||
|
||||
|
||||
@ -1015,6 +1015,8 @@ export interface Repository {
|
||||
owner: string;
|
||||
name: string;
|
||||
cloneUrl: string;
|
||||
/* Optional kind to differentiate between repositories of orgs/groups/projects and personal repos. */
|
||||
repoKind?: string;
|
||||
description?: string;
|
||||
avatarUrl?: string;
|
||||
webUrl?: string;
|
||||
|
||||
@ -9,6 +9,7 @@ import { HostContainerMapping } from "../../../src/auth/host-container-mapping";
|
||||
import { gitlabContainerModuleEE } from "../gitlab/container-module";
|
||||
import { bitbucketContainerModuleEE } from "../bitbucket/container-module";
|
||||
import { gitHubContainerModuleEE } from "../github/container-module";
|
||||
import { bitbucketServerContainerModuleEE } from "../bitbucket-server/container-module";
|
||||
|
||||
@injectable()
|
||||
export class HostContainerMappingEE extends HostContainerMapping {
|
||||
@ -20,9 +21,8 @@ export class HostContainerMappingEE extends HostContainerMapping {
|
||||
return (modules || []).concat([gitlabContainerModuleEE]);
|
||||
case "Bitbucket":
|
||||
return (modules || []).concat([bitbucketContainerModuleEE]);
|
||||
// case "BitbucketServer":
|
||||
// FIXME
|
||||
// return (modules || []).concat([bitbucketContainerModuleEE]);
|
||||
case "BitbucketServer":
|
||||
return (modules || []).concat([bitbucketServerContainerModuleEE]);
|
||||
case "GitHub":
|
||||
return (modules || []).concat([gitHubContainerModuleEE]);
|
||||
default:
|
||||
|
||||
@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 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 { ContainerModule } from "inversify";
|
||||
import { RepositoryService } from "../../../src/repohost/repo-service";
|
||||
import { BitbucketServerService } from "../prebuilds/bitbucket-server-service";
|
||||
|
||||
export const bitbucketServerContainerModuleEE = new ContainerModule((_bind, _unbind, _isBound, rebind) => {
|
||||
rebind(RepositoryService).to(BitbucketServerService).inSingletonScope();
|
||||
});
|
||||
@ -58,6 +58,7 @@ import { Config } from "../../src/config";
|
||||
import { SnapshotService } from "./workspace/snapshot-service";
|
||||
import { BitbucketAppSupport } from "./bitbucket/bitbucket-app-support";
|
||||
import { UserCounter } from "./user/user-counter";
|
||||
import { BitbucketServerApp } from "./prebuilds/bitbucket-server-app";
|
||||
|
||||
export const productionEEContainerModule = new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
rebind(Server).to(ServerEE).inSingletonScope();
|
||||
@ -77,6 +78,7 @@ export const productionEEContainerModule = new ContainerModule((bind, unbind, is
|
||||
bind(BitbucketApp).toSelf().inSingletonScope();
|
||||
bind(BitbucketAppSupport).toSelf().inSingletonScope();
|
||||
bind(GitHubEnterpriseApp).toSelf().inSingletonScope();
|
||||
bind(BitbucketServerApp).toSelf().inSingletonScope();
|
||||
|
||||
bind(UserCounter).toSelf().inSingletonScope();
|
||||
|
||||
|
||||
244
components/server/ee/src/prebuilds/bitbucket-server-app.ts
Normal file
244
components/server/ee/src/prebuilds/bitbucket-server-app.ts
Normal file
@ -0,0 +1,244 @@
|
||||
/**
|
||||
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
|
||||
* Licensed under the Gitpod Enterprise Source Code License,
|
||||
* See License.enterprise.txt in the project root folder.
|
||||
*/
|
||||
|
||||
import * as express from "express";
|
||||
import { postConstruct, injectable, inject } from "inversify";
|
||||
import { ProjectDB, TeamDB, UserDB } from "@gitpod/gitpod-db/lib";
|
||||
import { PrebuildManager } from "../prebuilds/prebuild-manager";
|
||||
import { TokenService } from "../../../src/user/token-service";
|
||||
import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing";
|
||||
import { CommitContext, CommitInfo, Project, StartPrebuildResult, User } from "@gitpod/gitpod-protocol";
|
||||
import { RepoURL } from "../../../src/repohost";
|
||||
import { HostContextProvider } from "../../../src/auth/host-context-provider";
|
||||
import { ContextParser } from "../../../src/workspace/context-parser-service";
|
||||
|
||||
@injectable()
|
||||
export class BitbucketServerApp {
|
||||
@inject(UserDB) protected readonly userDB: UserDB;
|
||||
@inject(PrebuildManager) protected readonly prebuildManager: PrebuildManager;
|
||||
@inject(TokenService) protected readonly tokenService: TokenService;
|
||||
@inject(ProjectDB) protected readonly projectDB: ProjectDB;
|
||||
@inject(TeamDB) protected readonly teamDB: TeamDB;
|
||||
@inject(ContextParser) protected readonly contextParser: ContextParser;
|
||||
@inject(HostContextProvider) protected readonly hostCtxProvider: HostContextProvider;
|
||||
|
||||
protected _router = express.Router();
|
||||
public static path = "/apps/bitbucketserver/";
|
||||
|
||||
@postConstruct()
|
||||
protected init() {
|
||||
this._router.post("/", async (req, res) => {
|
||||
try {
|
||||
const payload = req.body;
|
||||
if (PushEventPayload.is(req.body)) {
|
||||
const span = TraceContext.startSpan("BitbucketApp.handleEvent", {});
|
||||
let queryToken = req.query["token"] as string;
|
||||
if (typeof queryToken === "string") {
|
||||
queryToken = decodeURIComponent(queryToken);
|
||||
}
|
||||
const user = await this.findUser({ span }, queryToken);
|
||||
if (!user) {
|
||||
// If the webhook installer is no longer found in Gitpod's DB
|
||||
// we should send a UNAUTHORIZED signal.
|
||||
res.statusCode = 401;
|
||||
res.send();
|
||||
return;
|
||||
}
|
||||
await this.handlePushHook({ span }, user, payload);
|
||||
} else {
|
||||
console.warn(`Ignoring unsupported BBS event.`, { headers: req.headers });
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`Couldn't handle request.`, err, { headers: req.headers, reqBody: req.body });
|
||||
} finally {
|
||||
// we always respond with OK, when we received a valid event.
|
||||
res.sendStatus(200);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected async findUser(ctx: TraceContext, secretToken: string): Promise<User> {
|
||||
const span = TraceContext.startSpan("BitbucketApp.findUser", ctx);
|
||||
try {
|
||||
span.setTag("secret-token", secretToken);
|
||||
const [userid, tokenValue] = secretToken.split("|");
|
||||
const user = await this.userDB.findUserById(userid);
|
||||
if (!user) {
|
||||
throw new Error("No user found for " + secretToken + " found.");
|
||||
} else if (!!user.blocked) {
|
||||
throw new Error(`Blocked user ${user.id} tried to start prebuild.`);
|
||||
}
|
||||
const identity = user.identities.find((i) => i.authProviderId === TokenService.GITPOD_AUTH_PROVIDER_ID);
|
||||
if (!identity) {
|
||||
throw new Error(`User ${user.id} has no identity for '${TokenService.GITPOD_AUTH_PROVIDER_ID}'.`);
|
||||
}
|
||||
const tokens = await this.userDB.findTokensForIdentity(identity);
|
||||
const token = tokens.find((t) => t.token.value === tokenValue);
|
||||
if (!token) {
|
||||
throw new Error(`User ${user.id} has no token with given value.`);
|
||||
}
|
||||
return user;
|
||||
} finally {
|
||||
span.finish();
|
||||
}
|
||||
}
|
||||
|
||||
protected async handlePushHook(
|
||||
ctx: TraceContext,
|
||||
user: User,
|
||||
event: PushEventPayload,
|
||||
): Promise<StartPrebuildResult | undefined> {
|
||||
const span = TraceContext.startSpan("Bitbucket.handlePushHook", ctx);
|
||||
try {
|
||||
const contextUrl = this.createContextUrl(event);
|
||||
span.setTag("contextUrl", contextUrl);
|
||||
const context = await this.contextParser.handle({ span }, user, contextUrl);
|
||||
if (!CommitContext.is(context)) {
|
||||
throw new Error("CommitContext exprected.");
|
||||
}
|
||||
const cloneUrl = context.repository.cloneUrl;
|
||||
const commit = context.revision;
|
||||
const projectAndOwner = await this.findProjectAndOwner(cloneUrl, user);
|
||||
const config = await this.prebuildManager.fetchConfig({ span }, user, context);
|
||||
if (!this.prebuildManager.shouldPrebuild(config)) {
|
||||
console.log("Bitbucket push event: No config. No prebuild.");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
console.debug("Bitbucket Server push event: Starting prebuild.", { contextUrl });
|
||||
|
||||
const commitInfo = await this.getCommitInfo(user, cloneUrl, commit);
|
||||
|
||||
const ws = await this.prebuildManager.startPrebuild(
|
||||
{ span },
|
||||
{
|
||||
user: projectAndOwner.user,
|
||||
project: projectAndOwner?.project,
|
||||
context,
|
||||
commitInfo,
|
||||
},
|
||||
);
|
||||
return ws;
|
||||
} finally {
|
||||
span.finish();
|
||||
}
|
||||
}
|
||||
|
||||
private async getCommitInfo(user: User, repoURL: string, commitSHA: string) {
|
||||
const parsedRepo = RepoURL.parseRepoUrl(repoURL)!;
|
||||
const hostCtx = this.hostCtxProvider.get(parsedRepo.host);
|
||||
let commitInfo: CommitInfo | undefined;
|
||||
if (hostCtx?.services?.repositoryProvider) {
|
||||
commitInfo = await hostCtx?.services?.repositoryProvider.getCommitInfo(
|
||||
user,
|
||||
parsedRepo.owner,
|
||||
parsedRepo.repo,
|
||||
commitSHA,
|
||||
);
|
||||
}
|
||||
return commitInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the relevant user account and project to the provided webhook event information.
|
||||
*
|
||||
* First of all it tries to find the project for the given `cloneURL`, then it tries to
|
||||
* find the installer, which is also supposed to be a team member. As a fallback, it
|
||||
* looks for a team member which also has a bitbucket.org connection.
|
||||
*
|
||||
* @param cloneURL of the webhook event
|
||||
* @param webhookInstaller the user account known from the webhook installation
|
||||
* @returns a promise which resolves to a user account and an optional project.
|
||||
*/
|
||||
protected async findProjectAndOwner(
|
||||
cloneURL: string,
|
||||
webhookInstaller: User,
|
||||
): Promise<{ user: User; project?: Project }> {
|
||||
const project = await this.projectDB.findProjectByCloneUrl(cloneURL);
|
||||
if (project) {
|
||||
if (project.userId) {
|
||||
const user = await this.userDB.findUserById(project.userId);
|
||||
if (user) {
|
||||
return { user, project };
|
||||
}
|
||||
} else if (project.teamId) {
|
||||
const teamMembers = await this.teamDB.findMembersByTeam(project.teamId || "");
|
||||
if (teamMembers.some((t) => t.userId === webhookInstaller.id)) {
|
||||
return { user: webhookInstaller, project };
|
||||
}
|
||||
for (const teamMember of teamMembers) {
|
||||
const user = await this.userDB.findUserById(teamMember.userId);
|
||||
if (user && user.identities.some((i) => i.authProviderId === "Public-Bitbucket")) {
|
||||
return { user, project };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { user: webhookInstaller };
|
||||
}
|
||||
|
||||
protected createContextUrl(event: PushEventPayload): string {
|
||||
const projectBrowseUrl = event.repository.links.self[0].href;
|
||||
const branchName = event.changes[0].ref.displayId;
|
||||
const contextUrl = `${projectBrowseUrl}?at=${encodeURIComponent(branchName)}`;
|
||||
return contextUrl;
|
||||
}
|
||||
|
||||
get router(): express.Router {
|
||||
return this._router;
|
||||
}
|
||||
}
|
||||
|
||||
interface PushEventPayload {
|
||||
eventKey: "repo:refs_changed" | string;
|
||||
date: string;
|
||||
actor: {
|
||||
name: string;
|
||||
emailAddress: string;
|
||||
id: number;
|
||||
displayName: string;
|
||||
slug: string;
|
||||
type: "NORMAL" | string;
|
||||
};
|
||||
repository: {
|
||||
slug: string;
|
||||
id: number;
|
||||
name: string;
|
||||
project: {
|
||||
key: string;
|
||||
id: number;
|
||||
name: string;
|
||||
public: boolean;
|
||||
type: "NORMAL" | "PERSONAL";
|
||||
};
|
||||
links: {
|
||||
clone: {
|
||||
href: string;
|
||||
name: string;
|
||||
}[];
|
||||
self: {
|
||||
href: string;
|
||||
}[];
|
||||
};
|
||||
public: boolean;
|
||||
};
|
||||
changes: {
|
||||
ref: {
|
||||
id: string;
|
||||
displayId: string;
|
||||
type: "BRANCH" | string;
|
||||
};
|
||||
refId: string;
|
||||
fromHash: string;
|
||||
toHash: string;
|
||||
type: "UPDATE" | string;
|
||||
}[];
|
||||
}
|
||||
namespace PushEventPayload {
|
||||
export function is(payload: any): payload is PushEventPayload {
|
||||
return typeof payload === "object" && "eventKey" in payload && payload["eventKey"] === "repo:refs_changed";
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,157 @@
|
||||
/**
|
||||
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
|
||||
* Licensed under the GNU Affero General Public License (AGPL).
|
||||
* See License-AGPL.txt in the project root for license information.
|
||||
*/
|
||||
|
||||
import { User } from "@gitpod/gitpod-protocol";
|
||||
import { skipIfEnvVarNotSet } from "@gitpod/gitpod-protocol/lib/util/skip-if";
|
||||
import { Container, ContainerModule } from "inversify";
|
||||
import { retries, suite, test, timeout } from "mocha-typescript";
|
||||
import { AuthProviderParams } from "../../../src/auth/auth-provider";
|
||||
import { HostContextProvider } from "../../../src/auth/host-context-provider";
|
||||
import { BitbucketServerApi } from "../../../src/bitbucket-server/bitbucket-server-api";
|
||||
import { BitbucketServerContextParser } from "../../../src/bitbucket-server/bitbucket-server-context-parser";
|
||||
import { BitbucketServerTokenHelper } from "../../../src/bitbucket-server/bitbucket-server-token-handler";
|
||||
import { TokenProvider } from "../../../src/user/token-provider";
|
||||
import { BitbucketServerService } from "./bitbucket-server-service";
|
||||
import { expect } from "chai";
|
||||
import { Config } from "../../../src/config";
|
||||
import { TokenService } from "../../../src/user/token-service";
|
||||
import { GitpodHostUrl } from "@gitpod/gitpod-protocol/lib/util/gitpod-host-url";
|
||||
|
||||
@suite(timeout(10000), retries(1), skipIfEnvVarNotSet("GITPOD_TEST_TOKEN_BITBUCKET_SERVER"))
|
||||
class TestBitbucketServerService {
|
||||
protected service: BitbucketServerService;
|
||||
protected user: User;
|
||||
|
||||
static readonly AUTH_HOST_CONFIG: Partial<AuthProviderParams> = {
|
||||
id: "MyBitbucketServer",
|
||||
type: "BitbucketServer",
|
||||
verified: true,
|
||||
description: "",
|
||||
icon: "",
|
||||
host: "bitbucket.gitpod-self-hosted.com",
|
||||
oauth: {
|
||||
callBackUrl: "",
|
||||
clientId: "not-used",
|
||||
clientSecret: "",
|
||||
tokenUrl: "",
|
||||
scope: "",
|
||||
authorizationUrl: "",
|
||||
},
|
||||
};
|
||||
|
||||
public before() {
|
||||
const container = new Container();
|
||||
container.load(
|
||||
new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(BitbucketServerService).toSelf().inSingletonScope();
|
||||
bind(BitbucketServerContextParser).toSelf().inSingletonScope();
|
||||
bind(AuthProviderParams).toConstantValue(TestBitbucketServerService.AUTH_HOST_CONFIG);
|
||||
bind(BitbucketServerTokenHelper).toSelf().inSingletonScope();
|
||||
bind(TokenService).toConstantValue({
|
||||
createGitpodToken: async () => ({ token: { value: "foobar123-token" } }),
|
||||
} as any);
|
||||
bind(Config).toConstantValue({
|
||||
hostUrl: new GitpodHostUrl(),
|
||||
});
|
||||
bind(TokenProvider).toConstantValue(<TokenProvider>{
|
||||
getTokenForHost: async () => {
|
||||
return {
|
||||
value: process.env["GITPOD_TEST_TOKEN_BITBUCKET_SERVER"] || "undefined",
|
||||
scopes: [],
|
||||
};
|
||||
},
|
||||
getFreshPortAuthenticationToken: undefined as any,
|
||||
});
|
||||
bind(BitbucketServerApi).toSelf().inSingletonScope();
|
||||
bind(HostContextProvider).toConstantValue({
|
||||
get: (hostname: string) => {
|
||||
authProvider: {
|
||||
("BBS");
|
||||
}
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
this.service = container.get(BitbucketServerService);
|
||||
this.user = {
|
||||
creationDate: "",
|
||||
id: "user1",
|
||||
identities: [
|
||||
{
|
||||
authId: "user1",
|
||||
authName: "AlexTugarev",
|
||||
authProviderId: "MyBitbucketServer",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@test async test_canInstallAutomatedPrebuilds_unauthorized() {
|
||||
const result = await this.service.canInstallAutomatedPrebuilds(
|
||||
this.user,
|
||||
"https://bitbucket.gitpod-self-hosted.com/users/jldec/repos/test-repo",
|
||||
);
|
||||
expect(result).to.be.false;
|
||||
}
|
||||
|
||||
@test async test_canInstallAutomatedPrebuilds_in_project_ok() {
|
||||
const result = await this.service.canInstallAutomatedPrebuilds(
|
||||
this.user,
|
||||
"https://bitbucket.gitpod-self-hosted.com/projects/jldec/repos/jldec-repo-march-30",
|
||||
);
|
||||
expect(result).to.be.true;
|
||||
}
|
||||
|
||||
@test async test_canInstallAutomatedPrebuilds_ok() {
|
||||
const result = await this.service.canInstallAutomatedPrebuilds(
|
||||
this.user,
|
||||
"https://bitbucket.gitpod-self-hosted.com/projects/FOO/repos/repo123",
|
||||
);
|
||||
expect(result).to.be.true;
|
||||
}
|
||||
|
||||
@test async test_canInstallAutomatedPrebuilds_users_project_ok() {
|
||||
const result = await this.service.canInstallAutomatedPrebuilds(
|
||||
this.user,
|
||||
"https://bitbucket.gitpod-self-hosted.com/scm/~alextugarev/yolo.git",
|
||||
);
|
||||
expect(result).to.be.true;
|
||||
}
|
||||
|
||||
@test async test_installAutomatedPrebuilds_ok() {
|
||||
try {
|
||||
await this.service.installAutomatedPrebuilds(
|
||||
this.user,
|
||||
"https://bitbucket.gitpod-self-hosted.com/projects/FOO/repos/repo123",
|
||||
);
|
||||
} catch (error) {
|
||||
expect.fail(error);
|
||||
}
|
||||
}
|
||||
|
||||
@test async test_installAutomatedPrebuilds_unauthorized() {
|
||||
try {
|
||||
await this.service.installAutomatedPrebuilds(
|
||||
this.user,
|
||||
"https://bitbucket.gitpod-self-hosted.com/users/jldec/repos/test-repo",
|
||||
);
|
||||
expect.fail("should have failed");
|
||||
} catch (error) {}
|
||||
}
|
||||
|
||||
@test async test_installAutomatedPrebuilds_in_project_ok() {
|
||||
try {
|
||||
await this.service.installAutomatedPrebuilds(
|
||||
this.user,
|
||||
"https://bitbucket.gitpod-self-hosted.com/projects/jldec/repos/jldec-repo-march-30",
|
||||
);
|
||||
} catch (error) {
|
||||
expect.fail(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new TestBitbucketServerService();
|
||||
135
components/server/ee/src/prebuilds/bitbucket-server-service.ts
Normal file
135
components/server/ee/src/prebuilds/bitbucket-server-service.ts
Normal file
@ -0,0 +1,135 @@
|
||||
/**
|
||||
* 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 { RepositoryService } from "../../../src/repohost/repo-service";
|
||||
import { ProviderRepository, User } from "@gitpod/gitpod-protocol";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { BitbucketServerApi } from "../../../src/bitbucket-server/bitbucket-server-api";
|
||||
import { AuthProviderParams } from "../../../src/auth/auth-provider";
|
||||
import { BitbucketServerContextParser } from "../../../src/bitbucket-server/bitbucket-server-context-parser";
|
||||
import { Config } from "../../../src/config";
|
||||
import { TokenService } from "../../../src/user/token-service";
|
||||
import { BitbucketServerApp } from "./bitbucket-server-app";
|
||||
|
||||
@injectable()
|
||||
export class BitbucketServerService extends RepositoryService {
|
||||
static PREBUILD_TOKEN_SCOPE = "prebuilds";
|
||||
|
||||
@inject(BitbucketServerApi) protected api: BitbucketServerApi;
|
||||
@inject(Config) protected readonly config: Config;
|
||||
@inject(AuthProviderParams) protected authProviderConfig: AuthProviderParams;
|
||||
@inject(TokenService) protected tokenService: TokenService;
|
||||
@inject(BitbucketServerContextParser) protected contextParser: BitbucketServerContextParser;
|
||||
|
||||
async getRepositoriesForAutomatedPrebuilds(user: User): Promise<ProviderRepository[]> {
|
||||
const repos = await this.api.getRepos(user, { limit: 100, permission: "REPO_ADMIN" });
|
||||
return (repos.values || []).map((r) => {
|
||||
const cloneUrl = r.links.clone.find((u) => u.name === "http")?.href!;
|
||||
// const webUrl = r.links?.self[0]?.href?.replace("/browse", "");
|
||||
const accountAvatarUrl = this.api.getAvatarUrl(r.project.key);
|
||||
return <ProviderRepository>{
|
||||
name: r.name,
|
||||
cloneUrl,
|
||||
account: r.project.key,
|
||||
accountAvatarUrl,
|
||||
// updatedAt: TODO(at): this isn't provided directly
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async canInstallAutomatedPrebuilds(user: User, cloneUrl: string): Promise<boolean> {
|
||||
const { host, repoKind, owner, repoName } = await this.contextParser.parseURL(user, cloneUrl);
|
||||
if (host !== this.authProviderConfig.host) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const identity = user.identities.find((i) => i.authProviderId === this.authProviderConfig.id);
|
||||
if (!identity) {
|
||||
console.error(
|
||||
`Unexpected call of canInstallAutomatedPrebuilds. Not authorized with ${this.authProviderConfig.host}.`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.api.getWebhooks(user, { repoKind, repositorySlug: repoName, owner });
|
||||
// reading webhooks to check if admin scope is provided
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (repoKind === "users") {
|
||||
const ownProfile = await this.api.getUserProfile(user, identity.authName);
|
||||
if (owner === ownProfile.slug) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
let permission = await this.api.getPermission(user, { username: identity.authName, repoKind, owner, repoName });
|
||||
if (!permission && repoKind === "projects") {
|
||||
permission = await this.api.getPermission(user, { username: identity.authName, repoKind, owner });
|
||||
}
|
||||
|
||||
if (this.hasPermissionToCreateWebhooks(permission)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
console.debug(
|
||||
`User is not allowed to install webhooks.\n${JSON.stringify(identity)}\n${JSON.stringify(permission)}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
protected hasPermissionToCreateWebhooks(permission: string | undefined) {
|
||||
return permission && ["REPO_ADMIN", "PROJECT_ADMIN"].indexOf(permission) !== -1;
|
||||
}
|
||||
|
||||
async installAutomatedPrebuilds(user: User, cloneUrl: string): Promise<void> {
|
||||
const { owner, repoName, repoKind } = await this.contextParser.parseURL(user, cloneUrl);
|
||||
|
||||
const existing = await this.api.getWebhooks(user, {
|
||||
repoKind,
|
||||
repositorySlug: repoName,
|
||||
owner,
|
||||
});
|
||||
const hookUrl = this.getHookUrl();
|
||||
if (existing.values && existing.values.some((hook) => hook.url && hook.url.indexOf(hookUrl) !== -1)) {
|
||||
console.log(`BBS webhook already installed on ${cloneUrl}`);
|
||||
return;
|
||||
}
|
||||
const tokenEntry = await this.tokenService.createGitpodToken(
|
||||
user,
|
||||
BitbucketServerService.PREBUILD_TOKEN_SCOPE,
|
||||
cloneUrl,
|
||||
);
|
||||
try {
|
||||
await this.api.setWebhook(
|
||||
user,
|
||||
{ repoKind, repositorySlug: repoName, owner },
|
||||
{
|
||||
name: `Gitpod Prebuilds for ${this.config.hostUrl}.`,
|
||||
active: true,
|
||||
configuration: {
|
||||
secret: "foobar123-secret",
|
||||
},
|
||||
url: hookUrl + `?token=${encodeURIComponent(user.id + "|" + tokenEntry.token.value)}`,
|
||||
events: ["repo:refs_changed"],
|
||||
},
|
||||
);
|
||||
console.log("Installed Bitbucket Server Webhook for " + cloneUrl);
|
||||
} catch (error) {
|
||||
console.error(`Couldn't install Bitbucket Server Webhook for ${cloneUrl}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
protected getHookUrl() {
|
||||
return this.config.hostUrl
|
||||
.with({
|
||||
pathname: BitbucketServerApp.path,
|
||||
})
|
||||
.toString();
|
||||
}
|
||||
}
|
||||
@ -25,13 +25,12 @@ export class BitbucketService extends RepositoryService {
|
||||
@inject(BitbucketContextParser) protected bitbucketContextParser: BitbucketContextParser;
|
||||
|
||||
async canInstallAutomatedPrebuilds(user: User, cloneUrl: string): Promise<boolean> {
|
||||
const { host } = await this.bitbucketContextParser.parseURL(user, cloneUrl);
|
||||
const { host, owner, repoName } = await this.bitbucketContextParser.parseURL(user, cloneUrl);
|
||||
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}"`,
|
||||
|
||||
@ -14,11 +14,13 @@ import { BitbucketApp } from "./prebuilds/bitbucket-app";
|
||||
import { GithubApp } from "./prebuilds/github-app";
|
||||
import { SnapshotService } from "./workspace/snapshot-service";
|
||||
import { GitHubEnterpriseApp } from "./prebuilds/github-enterprise-app";
|
||||
import { BitbucketServerApp } from "./prebuilds/bitbucket-server-app";
|
||||
|
||||
export class ServerEE<C extends GitpodClient, S extends GitpodServer> extends Server<C, S> {
|
||||
@inject(GithubApp) protected readonly githubApp: GithubApp;
|
||||
@inject(GitLabApp) protected readonly gitLabApp: GitLabApp;
|
||||
@inject(BitbucketApp) protected readonly bitbucketApp: BitbucketApp;
|
||||
@inject(BitbucketServerApp) protected readonly bitbucketServerApp: BitbucketServerApp;
|
||||
@inject(SnapshotService) protected readonly snapshotService: SnapshotService;
|
||||
@inject(GitHubEnterpriseApp) protected readonly gitHubEnterpriseApp: GitHubEnterpriseApp;
|
||||
|
||||
@ -48,5 +50,8 @@ export class ServerEE<C extends GitpodClient, S extends GitpodServer> extends Se
|
||||
|
||||
log.info("Registered GitHub EnterpriseApp app at " + GitHubEnterpriseApp.path);
|
||||
app.use(GitHubEnterpriseApp.path, this.gitHubEnterpriseApp.router);
|
||||
|
||||
log.info("Registered Bitbucket Server app at " + BitbucketServerApp.path);
|
||||
app.use(BitbucketServerApp.path, this.bitbucketServerApp.router);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1785,6 +1785,13 @@ export class GitpodServerEEImpl extends GitpodServerImpl {
|
||||
}
|
||||
} else if (providerHost === "bitbucket.org" && provider) {
|
||||
repositories.push(...(await this.bitbucketAppSupport.getProviderRepositoriesForUser({ user, provider })));
|
||||
} else if (provider?.authProviderType === "BitbucketServer") {
|
||||
const hostContext = this.hostContextProvider.get(providerHost);
|
||||
if (hostContext?.services) {
|
||||
repositories.push(
|
||||
...(await hostContext.services.repositoryService.getRepositoriesForAutomatedPrebuilds(user)),
|
||||
);
|
||||
}
|
||||
} else if (provider?.authProviderType === "GitLab") {
|
||||
repositories.push(...(await this.gitLabAppSupport.getProviderRepositoriesForUser({ user, provider })));
|
||||
} else {
|
||||
|
||||
@ -27,7 +27,6 @@
|
||||
"/dist"
|
||||
],
|
||||
"dependencies": {
|
||||
"@atlassian/bitbucket-server": "^0.0.6",
|
||||
"@gitbeaker/node": "^25.6.0",
|
||||
"@gitpod/content-service": "0.1.5",
|
||||
"@gitpod/gitpod-db": "0.1.5",
|
||||
|
||||
@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
|
||||
* Licensed under the GNU Affero General Public License (AGPL).
|
||||
* See License-AGPL.txt in the project root for license information.
|
||||
*/
|
||||
|
||||
import { User } from "@gitpod/gitpod-protocol";
|
||||
import { skipIfEnvVarNotSet } from "@gitpod/gitpod-protocol/lib/util/skip-if";
|
||||
import { Container, ContainerModule } from "inversify";
|
||||
import { retries, suite, test, timeout } from "mocha-typescript";
|
||||
import { expect } from "chai";
|
||||
import { GitpodHostUrl } from "@gitpod/gitpod-protocol/lib/util/gitpod-host-url";
|
||||
import { AuthProviderParams } from "../auth/auth-provider";
|
||||
import { BitbucketServerContextParser } from "./bitbucket-server-context-parser";
|
||||
import { BitbucketServerTokenHelper } from "./bitbucket-server-token-handler";
|
||||
import { TokenService } from "../user/token-service";
|
||||
import { Config } from "../config";
|
||||
import { TokenProvider } from "../user/token-provider";
|
||||
import { BitbucketServerApi } from "./bitbucket-server-api";
|
||||
import { HostContextProvider } from "../auth/host-context-provider";
|
||||
|
||||
@suite(timeout(10000), retries(0), skipIfEnvVarNotSet("GITPOD_TEST_TOKEN_BITBUCKET_SERVER"))
|
||||
class TestBitbucketServerApi {
|
||||
protected api: BitbucketServerApi;
|
||||
protected user: User;
|
||||
|
||||
static readonly AUTH_HOST_CONFIG: Partial<AuthProviderParams> = {
|
||||
id: "MyBitbucketServer",
|
||||
type: "BitbucketServer",
|
||||
verified: true,
|
||||
host: "bitbucket.gitpod-self-hosted.com",
|
||||
oauth: {} as any,
|
||||
};
|
||||
|
||||
public before() {
|
||||
const container = new Container();
|
||||
container.load(
|
||||
new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(BitbucketServerApi).toSelf().inSingletonScope();
|
||||
bind(BitbucketServerContextParser).toSelf().inSingletonScope();
|
||||
bind(AuthProviderParams).toConstantValue(TestBitbucketServerApi.AUTH_HOST_CONFIG);
|
||||
bind(BitbucketServerTokenHelper).toSelf().inSingletonScope();
|
||||
bind(TokenService).toConstantValue({
|
||||
createGitpodToken: async () => ({ token: { value: "foobar123-token" } }),
|
||||
} as any);
|
||||
bind(Config).toConstantValue({
|
||||
hostUrl: new GitpodHostUrl(),
|
||||
});
|
||||
bind(TokenProvider).toConstantValue(<TokenProvider>{
|
||||
getTokenForHost: async () => {
|
||||
return {
|
||||
value: process.env["GITPOD_TEST_TOKEN_BITBUCKET_SERVER"] || "undefined",
|
||||
scopes: [],
|
||||
};
|
||||
},
|
||||
getFreshPortAuthenticationToken: undefined as any,
|
||||
});
|
||||
bind(HostContextProvider).toConstantValue({
|
||||
get: (hostname: string) => {
|
||||
authProvider: {
|
||||
("BBS");
|
||||
}
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
this.api = container.get(BitbucketServerApi);
|
||||
this.user = {
|
||||
creationDate: "",
|
||||
id: "user1",
|
||||
identities: [
|
||||
{
|
||||
authId: "user1",
|
||||
authName: "AlexTugarev",
|
||||
authProviderId: "MyBitbucketServer",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@test async test_currentUsername_ok() {
|
||||
const result = await this.api.currentUsername(process.env["GITPOD_TEST_TOKEN_BITBUCKET_SERVER"]!);
|
||||
expect(result).to.equal("AlexTugarev");
|
||||
}
|
||||
|
||||
@test async test_getUserProfile_ok() {
|
||||
const result = await this.api.getUserProfile(process.env["GITPOD_TEST_TOKEN_BITBUCKET_SERVER"]!, "AlexTugarev");
|
||||
expect(result).to.deep.include({
|
||||
id: 105, // Identity.authId
|
||||
name: "AlexTugarev", // Identity.authName
|
||||
slug: "alextugarev", // used in URLs
|
||||
displayName: "Alex Tugarev",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new TestBitbucketServerApi();
|
||||
@ -15,71 +15,310 @@ export class BitbucketServerApi {
|
||||
@inject(AuthProviderParams) protected readonly config: AuthProviderParams;
|
||||
@inject(BitbucketServerTokenHelper) protected readonly tokenHelper: BitbucketServerTokenHelper;
|
||||
|
||||
public async runQuery<T>(user: User, urlPath: string): Promise<T> {
|
||||
const token = (await this.tokenHelper.getTokenWithScopes(user, [])).value;
|
||||
public async runQuery<T>(
|
||||
userOrToken: User | string,
|
||||
urlPath: string,
|
||||
method: string = "GET",
|
||||
body?: string,
|
||||
): Promise<T> {
|
||||
const token =
|
||||
typeof userOrToken === "string"
|
||||
? userOrToken
|
||||
: (await this.tokenHelper.getTokenWithScopes(userOrToken, [])).value;
|
||||
const fullUrl = `${this.baseUrl}${urlPath}`;
|
||||
let result: string = "OK";
|
||||
try {
|
||||
const response = await fetch(fullUrl, {
|
||||
timeout: 10000,
|
||||
method: "GET",
|
||||
method,
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
let json: object | undefined;
|
||||
try {
|
||||
json = await response.json();
|
||||
} catch {
|
||||
// ignoring non-json responses and handling in general case bellow
|
||||
}
|
||||
if (BitbucketServer.ErrorResponse.is(json)) {
|
||||
throw Object.assign(new Error(`${response.status} / ${json.errors[0]?.message}`), {
|
||||
json,
|
||||
});
|
||||
}
|
||||
throw Object.assign(new Error(`${response.status} / ${response.statusText}`), { response });
|
||||
}
|
||||
return (await response.json()) as T;
|
||||
} catch (error) {
|
||||
result = "error " + error?.message;
|
||||
throw error;
|
||||
} finally {
|
||||
console.debug(`BitbucketServer GET ${fullUrl} - ${result}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async fetchContent(user: User, urlPath: string): Promise<string> {
|
||||
const token = (await this.tokenHelper.getTokenWithScopes(user, [])).value;
|
||||
const fullUrl = `${this.baseUrl}${urlPath}`;
|
||||
let result: string = "OK";
|
||||
try {
|
||||
const response = await fetch(fullUrl, {
|
||||
timeout: 10000,
|
||||
method: "GET",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw Error(`${response.status} / ${response.statusText}`);
|
||||
}
|
||||
return await response.text();
|
||||
} catch (error) {
|
||||
result = "error " + error?.message;
|
||||
throw error;
|
||||
} finally {
|
||||
console.debug(`BBS GET ${fullUrl} - ${result}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async currentUsername(accessToken: string): Promise<string> {
|
||||
const fullUrl = `https://${this.config.host}/plugins/servlet/applinks/whoami`;
|
||||
let result: string = "OK";
|
||||
try {
|
||||
const response = await fetch(fullUrl, {
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw Error(response.statusText);
|
||||
throw new Error(`${response.status} / ${response.statusText}`);
|
||||
}
|
||||
return response.text();
|
||||
} catch (error) {
|
||||
result = error?.message;
|
||||
console.error(`BBS GET ${fullUrl} - ${result}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
getAvatarUrl(username: string) {
|
||||
return `https://${this.config.host}/users/${username}/avatar.png`;
|
||||
}
|
||||
|
||||
async getUserProfile(userOrToken: User | string, username: string): Promise<BitbucketServer.User> {
|
||||
return this.runQuery<BitbucketServer.User>(userOrToken, `/users/${username}`);
|
||||
}
|
||||
|
||||
async getProject(userOrToken: User | string, projectSlug: string): Promise<BitbucketServer.Project> {
|
||||
return this.runQuery<BitbucketServer.Project>(userOrToken, `/projects/${projectSlug}`);
|
||||
}
|
||||
|
||||
async getPermission(
|
||||
user: User,
|
||||
params: { username: string; repoKind: BitbucketServer.RepoKind; owner: string; repoName?: string },
|
||||
): Promise<string | undefined> {
|
||||
const { username, repoKind, owner, repoName } = params;
|
||||
if (repoName) {
|
||||
const repoPermissions = await this.runQuery<BitbucketServer.Paginated<BitbucketServer.PermissionEntry>>(
|
||||
user,
|
||||
`/${repoKind}/${owner}/repos/${repoName}/permissions/users`,
|
||||
);
|
||||
const repoPermission = repoPermissions.values?.find((p) => p.user.name === username)?.permission;
|
||||
if (repoPermission) {
|
||||
return repoPermission;
|
||||
}
|
||||
}
|
||||
if (repoKind === "projects") {
|
||||
const projectPermissions = await this.runQuery<BitbucketServer.Paginated<BitbucketServer.PermissionEntry>>(
|
||||
user,
|
||||
`/${repoKind}/${owner}/permissions/users`,
|
||||
);
|
||||
const projectPermission = projectPermissions.values?.find((p) => p.user.name === username)?.permission;
|
||||
return projectPermission;
|
||||
}
|
||||
const result = await response.json();
|
||||
return result as T;
|
||||
}
|
||||
|
||||
protected get baseUrl(): string {
|
||||
return `https://${this.config.host}/rest/api/1.0`;
|
||||
}
|
||||
|
||||
getRepository(
|
||||
async getRepository(
|
||||
user: User,
|
||||
params: { kind: "projects" | "users"; userOrProject: string; repositorySlug: string },
|
||||
params: { repoKind: "projects" | "users"; owner: string; repositorySlug: string },
|
||||
): Promise<BitbucketServer.Repository> {
|
||||
return this.runQuery<BitbucketServer.Repository>(
|
||||
user,
|
||||
`/${params.kind}/${params.userOrProject}/repos/${params.repositorySlug}`,
|
||||
`/${params.repoKind}/${params.owner}/repos/${params.repositorySlug}`,
|
||||
);
|
||||
}
|
||||
|
||||
getCommits(
|
||||
async getCommits(
|
||||
user: User,
|
||||
params: { kind: "projects" | "users"; userOrProject: string; repositorySlug: string; q?: { limit: number } },
|
||||
params: {
|
||||
repoKind: "projects" | "users" | string;
|
||||
owner: string;
|
||||
repositorySlug: string;
|
||||
query?: { limit?: number; path?: string; shaOrRevision?: string };
|
||||
},
|
||||
): Promise<BitbucketServer.Paginated<BitbucketServer.Commit>> {
|
||||
let q = "";
|
||||
if (params.query) {
|
||||
const segments = [];
|
||||
if (params.query.limit) {
|
||||
segments.push(`limit=${params.query.limit}`);
|
||||
}
|
||||
if (params.query.path) {
|
||||
segments.push(`path=${params.query.path}`);
|
||||
}
|
||||
if (params.query.shaOrRevision) {
|
||||
segments.push(`until=${params.query.shaOrRevision}`);
|
||||
}
|
||||
if (segments.length > 0) {
|
||||
q = `?${segments.join("&")}`;
|
||||
}
|
||||
}
|
||||
return this.runQuery<BitbucketServer.Paginated<BitbucketServer.Commit>>(
|
||||
user,
|
||||
`/${params.kind}/${params.userOrProject}/repos/${params.repositorySlug}/commits`,
|
||||
`/${params.repoKind}/${params.owner}/repos/${params.repositorySlug}/commits${q}`,
|
||||
);
|
||||
}
|
||||
|
||||
getDefaultBranch(
|
||||
async getDefaultBranch(
|
||||
user: User,
|
||||
params: { kind: "projects" | "users"; userOrProject: string; repositorySlug: string },
|
||||
params: { repoKind: "projects" | "users"; owner: string; repositorySlug: string },
|
||||
): Promise<BitbucketServer.Branch> {
|
||||
//https://bitbucket.gitpod-self-hosted.com/rest/api/1.0/users/jldec/repos/test-repo/default-branch
|
||||
return this.runQuery<BitbucketServer.Branch>(
|
||||
user,
|
||||
`/${params.kind}/${params.userOrProject}/repos/${params.repositorySlug}/default-branch`,
|
||||
`/${params.repoKind}/${params.owner}/repos/${params.repositorySlug}/default-branch`,
|
||||
);
|
||||
}
|
||||
|
||||
getHtmlUrlForBranch(params: {
|
||||
repoKind: "projects" | "users";
|
||||
owner: string;
|
||||
repositorySlug: string;
|
||||
branchName: string;
|
||||
}): string {
|
||||
return `https://${this.config.host}/${params.repoKind}/${params.owner}/repos/${
|
||||
params.repositorySlug
|
||||
}/browse?at=${encodeURIComponent(`refs/heads/${params.branchName}`)}`;
|
||||
}
|
||||
|
||||
async getBranch(
|
||||
user: User,
|
||||
params: { repoKind: "projects" | "users"; owner: string; repositorySlug: string; branchName: string },
|
||||
): Promise<BitbucketServer.BranchWithMeta> {
|
||||
const result = await this.runQuery<BitbucketServer.Paginated<BitbucketServer.BranchWithMeta>>(
|
||||
user,
|
||||
`/${params.repoKind}/${params.owner}/repos/${params.repositorySlug}/branches?details=true&filterText=${params.branchName}&boostMatches=true`,
|
||||
);
|
||||
const first = result.values && result.values[0];
|
||||
if (first && first.displayId === params.branchName) {
|
||||
first.latestCommitMetadata =
|
||||
first.metadata["com.atlassian.bitbucket.server.bitbucket-branch:latest-commit-metadata"];
|
||||
first.htmlUrl = this.getHtmlUrlForBranch({
|
||||
repoKind: params.repoKind,
|
||||
owner: params.owner,
|
||||
repositorySlug: params.repositorySlug,
|
||||
branchName: first.displayId,
|
||||
});
|
||||
return first;
|
||||
}
|
||||
throw new Error(`Could not find branch "${params.branchName}."`);
|
||||
}
|
||||
|
||||
async getBranches(
|
||||
user: User,
|
||||
params: { repoKind: "projects" | "users"; owner: string; repositorySlug: string },
|
||||
): Promise<BitbucketServer.BranchWithMeta[]> {
|
||||
const result = await this.runQuery<BitbucketServer.Paginated<BitbucketServer.BranchWithMeta>>(
|
||||
user,
|
||||
`/${params.repoKind}/${params.owner}/repos/${params.repositorySlug}/branches?details=true&orderBy=MODIFICATION&limit=1000`,
|
||||
);
|
||||
const branches = result.values || [];
|
||||
for (const branch of branches) {
|
||||
branch.latestCommitMetadata =
|
||||
branch.metadata["com.atlassian.bitbucket.server.bitbucket-branch:latest-commit-metadata"];
|
||||
branch.htmlUrl = this.getHtmlUrlForBranch({
|
||||
repoKind: params.repoKind,
|
||||
owner: params.owner,
|
||||
repositorySlug: params.repositorySlug,
|
||||
branchName: branch.displayId,
|
||||
});
|
||||
}
|
||||
return branches;
|
||||
}
|
||||
|
||||
async getWebhooks(
|
||||
user: User,
|
||||
params: { repoKind: "projects" | "users"; owner: string; repositorySlug: string },
|
||||
): Promise<BitbucketServer.Paginated<BitbucketServer.Webhook>> {
|
||||
return this.runQuery<BitbucketServer.Paginated<BitbucketServer.Webhook>>(
|
||||
user,
|
||||
`/${params.repoKind}/${params.owner}/repos/${params.repositorySlug}/webhooks`,
|
||||
);
|
||||
}
|
||||
|
||||
setWebhook(
|
||||
user: User,
|
||||
params: { repoKind: "projects" | "users"; owner: string; repositorySlug: string },
|
||||
webhook: BitbucketServer.WebhookParams,
|
||||
) {
|
||||
const body = JSON.stringify(webhook);
|
||||
return this.runQuery<any>(
|
||||
user,
|
||||
`/${params.repoKind}/${params.owner}/repos/${params.repositorySlug}/webhooks`,
|
||||
"POST",
|
||||
body,
|
||||
);
|
||||
}
|
||||
|
||||
async getRepos(
|
||||
user: User,
|
||||
query: {
|
||||
permission?: "REPO_READ" | "REPO_WRITE" | "REPO_ADMIN";
|
||||
limit: number;
|
||||
},
|
||||
) {
|
||||
let q = "";
|
||||
if (query) {
|
||||
const segments = [];
|
||||
if (query.permission) {
|
||||
segments.push(`permission=${query.permission}`);
|
||||
}
|
||||
if (query.limit) {
|
||||
segments.push(`limit=${query.limit}`);
|
||||
}
|
||||
if (segments.length > 0) {
|
||||
q = `?${segments.join("&")}`;
|
||||
}
|
||||
}
|
||||
return this.runQuery<BitbucketServer.Paginated<BitbucketServer.Repository>>(user, `/repos${q}`);
|
||||
}
|
||||
}
|
||||
|
||||
export namespace BitbucketServer {
|
||||
export type RepoKind = "users" | "projects";
|
||||
export interface Repository {
|
||||
id: number;
|
||||
slug: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
public: boolean;
|
||||
links: {
|
||||
clone: {
|
||||
href: string;
|
||||
name: string;
|
||||
}[];
|
||||
self: {
|
||||
href: string;
|
||||
}[];
|
||||
};
|
||||
project: Project;
|
||||
}
|
||||
@ -100,6 +339,14 @@ export namespace BitbucketServer {
|
||||
isDefault: boolean;
|
||||
}
|
||||
|
||||
export interface BranchWithMeta extends Branch {
|
||||
latestCommitMetadata: Commit;
|
||||
htmlUrl: string;
|
||||
metadata: {
|
||||
"com.atlassian.bitbucket.server.bitbucket-branch:latest-commit-metadata": Commit;
|
||||
};
|
||||
}
|
||||
|
||||
export interface User {
|
||||
name: string;
|
||||
emailAddress: string;
|
||||
@ -115,12 +362,17 @@ export namespace BitbucketServer {
|
||||
},
|
||||
];
|
||||
};
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
export interface Commit {
|
||||
id: string;
|
||||
displayId: string;
|
||||
author: BitbucketServer.User;
|
||||
authorTimestamp: number;
|
||||
commiter: BitbucketServer.User;
|
||||
committerTimestamp: number;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface Paginated<T> {
|
||||
@ -131,4 +383,45 @@ export namespace BitbucketServer {
|
||||
values?: T[];
|
||||
[k: string]: any;
|
||||
}
|
||||
|
||||
export interface Webhook {
|
||||
id: number;
|
||||
name: "test-webhook";
|
||||
createdDate: number;
|
||||
updatedDate: number;
|
||||
events: any;
|
||||
configuration: any;
|
||||
url: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export interface PermissionEntry {
|
||||
user: User;
|
||||
permission: string;
|
||||
}
|
||||
|
||||
export interface WebhookParams {
|
||||
name: string;
|
||||
events: string[];
|
||||
// "events": [
|
||||
// "repo:refs_changed",
|
||||
// "repo:modified"
|
||||
// ],
|
||||
configuration: {
|
||||
secret: string;
|
||||
};
|
||||
url: string;
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export interface ErrorResponse {
|
||||
errors: {
|
||||
message: string;
|
||||
}[];
|
||||
}
|
||||
export namespace ErrorResponse {
|
||||
export function is(o: any): o is ErrorResponse {
|
||||
return typeof o === "object" && "errors" in o;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,15 +7,16 @@
|
||||
import { AuthProviderInfo } from "@gitpod/gitpod-protocol";
|
||||
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
|
||||
import * as express from "express";
|
||||
import { injectable } from "inversify";
|
||||
import fetch from "node-fetch";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { AuthUserSetup } from "../auth/auth-provider";
|
||||
import { GenericAuthProvider } from "../auth/generic-auth-provider";
|
||||
import { BitbucketServerOAuthScopes } from "./bitbucket-server-oauth-scopes";
|
||||
import * as BitbucketServer from "@atlassian/bitbucket-server";
|
||||
import { BitbucketServerApi } from "./bitbucket-server-api";
|
||||
|
||||
@injectable()
|
||||
export class BitbucketServerAuthProvider extends GenericAuthProvider {
|
||||
@inject(BitbucketServerApi) protected readonly api: BitbucketServerApi;
|
||||
|
||||
get info(): AuthProviderInfo {
|
||||
return {
|
||||
...this.defaultInfo(),
|
||||
@ -54,41 +55,18 @@ export class BitbucketServerAuthProvider extends GenericAuthProvider {
|
||||
|
||||
protected readAuthUserSetup = async (accessToken: string, _tokenResponse: object) => {
|
||||
try {
|
||||
const fetchResult = await fetch(`https://${this.params.host}/plugins/servlet/applinks/whoami`, {
|
||||
timeout: 10000,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
if (!fetchResult.ok) {
|
||||
throw new Error(fetchResult.statusText);
|
||||
}
|
||||
const username = await fetchResult.text();
|
||||
if (!username) {
|
||||
throw new Error("username missing");
|
||||
}
|
||||
|
||||
log.warn(`(${this.strategyName}) username ${username}`);
|
||||
|
||||
const options = {
|
||||
baseUrl: `https://${this.params.host}`,
|
||||
};
|
||||
const client = new BitbucketServer(options);
|
||||
|
||||
client.authenticate({ type: "token", token: accessToken });
|
||||
const result = await client.api.getUser({ userSlug: username });
|
||||
|
||||
const user = result.data;
|
||||
|
||||
// TODO: check if user.active === true?
|
||||
|
||||
const username = await this.api.currentUsername(accessToken);
|
||||
const userProfile = await this.api.getUserProfile(accessToken, username);
|
||||
const avatarUrl = await this.api.getAvatarUrl(username);
|
||||
return <AuthUserSetup>{
|
||||
authUser: {
|
||||
authId: `${user.id!}`,
|
||||
authName: user.slug!,
|
||||
primaryEmail: user.emailAddress!,
|
||||
name: user.displayName!,
|
||||
// avatarUrl: user.links!.avatar!.href // TODO
|
||||
// e.g. 105
|
||||
authId: `${userProfile.id!}`,
|
||||
// HINT: userProfile.name is used to match permission in repo/webhook services
|
||||
authName: userProfile.name,
|
||||
primaryEmail: userProfile.emailAddress!,
|
||||
name: userProfile.displayName!,
|
||||
avatarUrl,
|
||||
},
|
||||
currentScopes: BitbucketServerOAuthScopes.ALL,
|
||||
};
|
||||
|
||||
@ -0,0 +1,163 @@
|
||||
/**
|
||||
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
|
||||
* Licensed under the GNU Affero General Public License (AGPL).
|
||||
* See License-AGPL.txt in the project root for license information.
|
||||
*/
|
||||
|
||||
import { User } from "@gitpod/gitpod-protocol";
|
||||
import { skipIfEnvVarNotSet } from "@gitpod/gitpod-protocol/lib/util/skip-if";
|
||||
import { Container, ContainerModule } from "inversify";
|
||||
import { suite, test, timeout } from "mocha-typescript";
|
||||
import { expect } from "chai";
|
||||
import { GitpodHostUrl } from "@gitpod/gitpod-protocol/lib/util/gitpod-host-url";
|
||||
import { BitbucketServerFileProvider } from "./bitbucket-server-file-provider";
|
||||
import { AuthProviderParams } from "../auth/auth-provider";
|
||||
import { BitbucketServerContextParser } from "./bitbucket-server-context-parser";
|
||||
import { BitbucketServerTokenHelper } from "./bitbucket-server-token-handler";
|
||||
import { TokenService } from "../user/token-service";
|
||||
import { Config } from "../config";
|
||||
import { TokenProvider } from "../user/token-provider";
|
||||
import { BitbucketServerApi } from "./bitbucket-server-api";
|
||||
import { HostContextProvider } from "../auth/host-context-provider";
|
||||
|
||||
@suite(timeout(10000), skipIfEnvVarNotSet("GITPOD_TEST_TOKEN_BITBUCKET_SERVER"))
|
||||
class TestBitbucketServerContextParser {
|
||||
protected parser: BitbucketServerContextParser;
|
||||
protected user: User;
|
||||
|
||||
static readonly AUTH_HOST_CONFIG: Partial<AuthProviderParams> = {
|
||||
id: "MyBitbucketServer",
|
||||
type: "BitbucketServer",
|
||||
host: "bitbucket.gitpod-self-hosted.com",
|
||||
};
|
||||
|
||||
public before() {
|
||||
const container = new Container();
|
||||
container.load(
|
||||
new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(BitbucketServerFileProvider).toSelf().inSingletonScope();
|
||||
bind(BitbucketServerContextParser).toSelf().inSingletonScope();
|
||||
bind(AuthProviderParams).toConstantValue(TestBitbucketServerContextParser.AUTH_HOST_CONFIG);
|
||||
bind(BitbucketServerTokenHelper).toSelf().inSingletonScope();
|
||||
bind(TokenService).toConstantValue({
|
||||
createGitpodToken: async () => ({ token: { value: "foobar123-token" } }),
|
||||
} as any);
|
||||
bind(Config).toConstantValue({
|
||||
hostUrl: new GitpodHostUrl(),
|
||||
});
|
||||
bind(TokenProvider).toConstantValue(<TokenProvider>{
|
||||
getTokenForHost: async () => {
|
||||
return {
|
||||
value: process.env["GITPOD_TEST_TOKEN_BITBUCKET_SERVER"] || "undefined",
|
||||
scopes: [],
|
||||
};
|
||||
},
|
||||
getFreshPortAuthenticationToken: undefined as any,
|
||||
});
|
||||
bind(BitbucketServerApi).toSelf().inSingletonScope();
|
||||
bind(HostContextProvider).toConstantValue({
|
||||
get: (hostname: string) => {
|
||||
authProvider: {
|
||||
("BBS");
|
||||
}
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
this.parser = container.get(BitbucketServerContextParser);
|
||||
this.user = {
|
||||
creationDate: "",
|
||||
id: "user1",
|
||||
identities: [
|
||||
{
|
||||
authId: "user1",
|
||||
authName: "AlexTugarev",
|
||||
authProviderId: "MyBitbucketServer",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@test async test_tree_context_01() {
|
||||
const result = await this.parser.handle(
|
||||
{},
|
||||
this.user,
|
||||
"https://bitbucket.gitpod-self-hosted.com/projects/FOO/repos/repo123",
|
||||
);
|
||||
|
||||
expect(result).to.deep.include({
|
||||
ref: "master",
|
||||
refType: "branch",
|
||||
revision: "535924584468074ec5dcbe935f4e68fbc3f0cb2d",
|
||||
path: "",
|
||||
isFile: false,
|
||||
repository: {
|
||||
host: "bitbucket.gitpod-self-hosted.com",
|
||||
owner: "FOO",
|
||||
name: "repo123",
|
||||
cloneUrl: "https://bitbucket.gitpod-self-hosted.com/scm/foo/repo123.git",
|
||||
webUrl: "https://bitbucket.gitpod-self-hosted.com/projects/FOO/repos/repo123",
|
||||
defaultBranch: "master",
|
||||
private: true,
|
||||
repoKind: "projects",
|
||||
},
|
||||
title: "FOO/repo123 - master",
|
||||
});
|
||||
}
|
||||
|
||||
@test async test_tree_context_02() {
|
||||
const result = await this.parser.handle(
|
||||
{},
|
||||
this.user,
|
||||
"https://bitbucket.gitpod-self-hosted.com/scm/foo/repo123.git",
|
||||
);
|
||||
|
||||
expect(result).to.deep.include({
|
||||
ref: "master",
|
||||
refType: "branch",
|
||||
revision: "535924584468074ec5dcbe935f4e68fbc3f0cb2d",
|
||||
path: "",
|
||||
isFile: false,
|
||||
repository: {
|
||||
host: "bitbucket.gitpod-self-hosted.com",
|
||||
owner: "FOO",
|
||||
name: "repo123",
|
||||
cloneUrl: "https://bitbucket.gitpod-self-hosted.com/scm/foo/repo123.git",
|
||||
webUrl: "https://bitbucket.gitpod-self-hosted.com/projects/FOO/repos/repo123",
|
||||
defaultBranch: "master",
|
||||
private: true,
|
||||
repoKind: "projects",
|
||||
},
|
||||
title: "foo/repo123 - master",
|
||||
});
|
||||
}
|
||||
|
||||
@test async test_tree_context_03() {
|
||||
const result = await this.parser.handle(
|
||||
{},
|
||||
this.user,
|
||||
"https://bitbucket.gitpod-self-hosted.com/scm/~alextugarev/tada.git",
|
||||
);
|
||||
|
||||
expect(result).to.deep.include({
|
||||
ref: "main",
|
||||
refType: "branch",
|
||||
revision: "a15d7d15adee54d0afdbe88148c8e587e8fb609d",
|
||||
path: "",
|
||||
isFile: false,
|
||||
repository: {
|
||||
host: "bitbucket.gitpod-self-hosted.com",
|
||||
owner: "alextugarev",
|
||||
name: "tada",
|
||||
cloneUrl: "https://bitbucket.gitpod-self-hosted.com/scm/~alextugarev/tada.git",
|
||||
webUrl: "https://bitbucket.gitpod-self-hosted.com/users/alextugarev/repos/tada",
|
||||
defaultBranch: "main",
|
||||
private: true,
|
||||
repoKind: "users",
|
||||
},
|
||||
title: "alextugarev/tada - main",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new TestBitbucketServerContextParser();
|
||||
@ -25,12 +25,18 @@ export class BitbucketServerContextParser extends AbstractContextParser implemen
|
||||
const span = TraceContext.startSpan("BitbucketServerContextParser.handle", ctx);
|
||||
|
||||
try {
|
||||
const { resourceKind, host, owner, repoName /*moreSegments, searchParams*/ } = await this.parseURL(
|
||||
const more: Partial<NavigatorContext> = {};
|
||||
const { repoKind, host, owner, repoName, /*moreSegments*/ searchParams } = await this.parseURL(
|
||||
user,
|
||||
contextUrl,
|
||||
);
|
||||
|
||||
return await this.handleNavigatorContext(ctx, user, resourceKind, host, owner, repoName);
|
||||
if (searchParams.has("at")) {
|
||||
more.ref = decodeURIComponent(searchParams.get("at")!);
|
||||
more.refType = "branch";
|
||||
}
|
||||
|
||||
return await this.handleNavigatorContext(ctx, user, repoKind, host, owner, repoName, more);
|
||||
} catch (e) {
|
||||
span.addTags({ contextUrl }).log({ error: e });
|
||||
log.error({ userId: user.id }, "Error parsing Bitbucket context", e);
|
||||
@ -40,7 +46,7 @@ export class BitbucketServerContextParser extends AbstractContextParser implemen
|
||||
}
|
||||
}
|
||||
|
||||
public async parseURL(user: User, contextUrl: string): Promise<{ resourceKind: string } & URLParts> {
|
||||
public async parseURL(user: User, contextUrl: string): Promise<{ repoKind: "projects" | "users" } & URLParts> {
|
||||
const url = new URL(contextUrl);
|
||||
const pathname = url.pathname.replace(/^\//, "").replace(/\/$/, ""); // pathname without leading and trailing slash
|
||||
const segments = pathname.split("/");
|
||||
@ -54,14 +60,31 @@ export class BitbucketServerContextParser extends AbstractContextParser implemen
|
||||
segments.splice(0, lenghtOfRelativePath);
|
||||
}
|
||||
|
||||
const resourceKind = segments[0];
|
||||
const owner: string = segments[1];
|
||||
const repoName: string = segments[3];
|
||||
const moreSegmentsStart: number = 4;
|
||||
let firstSegment = segments[0];
|
||||
let owner: string = segments[1];
|
||||
let repoKind: "users" | "projects";
|
||||
let repoName;
|
||||
let moreSegmentsStart;
|
||||
if (firstSegment === "scm") {
|
||||
repoKind = "projects";
|
||||
if (owner && owner.startsWith("~")) {
|
||||
repoKind = "users";
|
||||
owner = owner.substring(1);
|
||||
}
|
||||
repoName = segments[2];
|
||||
moreSegmentsStart = 3;
|
||||
} else if (firstSegment === "projects" || firstSegment === "users") {
|
||||
repoKind = firstSegment;
|
||||
repoName = segments[3];
|
||||
moreSegmentsStart = 4;
|
||||
} else {
|
||||
throw new Error("Unexpected repo kind: " + firstSegment);
|
||||
}
|
||||
const endsWithRepoName = segments.length === moreSegmentsStart;
|
||||
|
||||
const searchParams = url.searchParams;
|
||||
return {
|
||||
resourceKind,
|
||||
repoKind,
|
||||
host,
|
||||
owner,
|
||||
repoName: this.parseRepoName(repoName, endsWithRepoName),
|
||||
@ -83,7 +106,7 @@ export class BitbucketServerContextParser extends AbstractContextParser implemen
|
||||
protected async handleNavigatorContext(
|
||||
ctx: TraceContext,
|
||||
user: User,
|
||||
resourceKind: string,
|
||||
repoKind: "projects" | "users",
|
||||
host: string,
|
||||
owner: string,
|
||||
repoName: string,
|
||||
@ -91,20 +114,17 @@ export class BitbucketServerContextParser extends AbstractContextParser implemen
|
||||
): Promise<NavigatorContext> {
|
||||
const span = TraceContext.startSpan("BitbucketServerContextParser.handleNavigatorContext", ctx);
|
||||
try {
|
||||
if (resourceKind !== "users" && resourceKind !== "projects") {
|
||||
throw new Error("Only /users/ and /projects/ resources are supported.");
|
||||
}
|
||||
const repo = await this.api.getRepository(user, {
|
||||
kind: resourceKind,
|
||||
userOrProject: owner,
|
||||
repoKind,
|
||||
owner,
|
||||
repositorySlug: repoName,
|
||||
});
|
||||
const defaultBranch = await this.api.getDefaultBranch(user, {
|
||||
kind: resourceKind,
|
||||
userOrProject: owner,
|
||||
repoKind,
|
||||
owner,
|
||||
repositorySlug: repoName,
|
||||
});
|
||||
const repository = await this.toRepository(user, host, repo, defaultBranch);
|
||||
const repository = this.toRepository(host, repo, repoKind, defaultBranch);
|
||||
span.log({ "request.finished": "" });
|
||||
|
||||
if (!repo) {
|
||||
@ -124,10 +144,10 @@ export class BitbucketServerContextParser extends AbstractContextParser implemen
|
||||
|
||||
if (!more.revision) {
|
||||
const tipCommitOnDefaultBranch = await this.api.getCommits(user, {
|
||||
kind: resourceKind,
|
||||
userOrProject: owner,
|
||||
repoKind,
|
||||
owner,
|
||||
repositorySlug: repoName,
|
||||
q: { limit: 1 },
|
||||
query: { limit: 1 },
|
||||
});
|
||||
const commits = tipCommitOnDefaultBranch?.values || [];
|
||||
if (commits.length === 0) {
|
||||
@ -137,15 +157,19 @@ export class BitbucketServerContextParser extends AbstractContextParser implemen
|
||||
more.refType = undefined;
|
||||
} else {
|
||||
more.revision = commits[0].id;
|
||||
more.refType = "revision";
|
||||
// more.refType = "revision";
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...more,
|
||||
return <NavigatorContext>{
|
||||
isFile: false,
|
||||
path: "",
|
||||
title: `${owner}/${repoName} - ${more.ref || more.revision}${more.path ? ":" + more.path : ""}`,
|
||||
ref: more.ref,
|
||||
refType: more.refType,
|
||||
revision: more.revision,
|
||||
repository,
|
||||
} as NavigatorContext;
|
||||
};
|
||||
} catch (e) {
|
||||
span.log({ error: e });
|
||||
log.error({ userId: user.id }, "Error parsing Bitbucket navigator request context", e);
|
||||
@ -155,25 +179,24 @@ export class BitbucketServerContextParser extends AbstractContextParser implemen
|
||||
}
|
||||
}
|
||||
|
||||
protected async toRepository(
|
||||
user: User,
|
||||
protected toRepository(
|
||||
host: string,
|
||||
repo: BitbucketServer.Repository,
|
||||
repoKind: string,
|
||||
defaultBranch: BitbucketServer.Branch,
|
||||
): Promise<Repository> {
|
||||
if (!repo) {
|
||||
throw new Error("Unknown repository.");
|
||||
}
|
||||
|
||||
): Repository {
|
||||
const owner = repo.project.owner ? repo.project.owner.slug : repo.project.key;
|
||||
const name = repo.name;
|
||||
const cloneUrl = repo.links.clone.find((u) => u.name === "http")?.href!;
|
||||
const webUrl = repo.links?.self[0]?.href?.replace(/\/browse$/, "");
|
||||
|
||||
const result: Repository = {
|
||||
webUrl,
|
||||
cloneUrl,
|
||||
host,
|
||||
name,
|
||||
owner,
|
||||
repoKind,
|
||||
private: !repo.public,
|
||||
defaultBranch: defaultBranch.displayId || DEFAULT_BRANCH,
|
||||
};
|
||||
|
||||
@ -0,0 +1,129 @@
|
||||
/**
|
||||
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
|
||||
* Licensed under the GNU Affero General Public License (AGPL).
|
||||
* See License-AGPL.txt in the project root for license information.
|
||||
*/
|
||||
|
||||
import { Repository, User } from "@gitpod/gitpod-protocol";
|
||||
import { skipIfEnvVarNotSet } from "@gitpod/gitpod-protocol/lib/util/skip-if";
|
||||
import { Container, ContainerModule } from "inversify";
|
||||
import { retries, suite, test, timeout } from "mocha-typescript";
|
||||
import { expect } from "chai";
|
||||
import { GitpodHostUrl } from "@gitpod/gitpod-protocol/lib/util/gitpod-host-url";
|
||||
import { BitbucketServerFileProvider } from "./bitbucket-server-file-provider";
|
||||
import { AuthProviderParams } from "../auth/auth-provider";
|
||||
import { BitbucketServerContextParser } from "./bitbucket-server-context-parser";
|
||||
import { BitbucketServerTokenHelper } from "./bitbucket-server-token-handler";
|
||||
import { TokenService } from "../user/token-service";
|
||||
import { Config } from "../config";
|
||||
import { TokenProvider } from "../user/token-provider";
|
||||
import { BitbucketServerApi } from "./bitbucket-server-api";
|
||||
import { HostContextProvider } from "../auth/host-context-provider";
|
||||
|
||||
@suite(timeout(10000), retries(1), skipIfEnvVarNotSet("GITPOD_TEST_TOKEN_BITBUCKET_SERVER"))
|
||||
class TestBitbucketServerFileProvider {
|
||||
protected service: BitbucketServerFileProvider;
|
||||
protected user: User;
|
||||
|
||||
static readonly AUTH_HOST_CONFIG: Partial<AuthProviderParams> = {
|
||||
id: "MyBitbucketServer",
|
||||
type: "BitbucketServer",
|
||||
verified: true,
|
||||
description: "",
|
||||
icon: "",
|
||||
host: "bitbucket.gitpod-self-hosted.com",
|
||||
oauth: {
|
||||
callBackUrl: "",
|
||||
clientId: "not-used",
|
||||
clientSecret: "",
|
||||
tokenUrl: "",
|
||||
scope: "",
|
||||
authorizationUrl: "",
|
||||
},
|
||||
};
|
||||
|
||||
public before() {
|
||||
const container = new Container();
|
||||
container.load(
|
||||
new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(BitbucketServerFileProvider).toSelf().inSingletonScope();
|
||||
bind(BitbucketServerContextParser).toSelf().inSingletonScope();
|
||||
bind(AuthProviderParams).toConstantValue(TestBitbucketServerFileProvider.AUTH_HOST_CONFIG);
|
||||
bind(BitbucketServerTokenHelper).toSelf().inSingletonScope();
|
||||
bind(TokenService).toConstantValue({
|
||||
createGitpodToken: async () => ({ token: { value: "foobar123-token" } }),
|
||||
} as any);
|
||||
bind(Config).toConstantValue({
|
||||
hostUrl: new GitpodHostUrl(),
|
||||
});
|
||||
bind(TokenProvider).toConstantValue(<TokenProvider>{
|
||||
getTokenForHost: async () => {
|
||||
return {
|
||||
value: process.env["GITPOD_TEST_TOKEN_BITBUCKET_SERVER"] || "undefined",
|
||||
scopes: [],
|
||||
};
|
||||
},
|
||||
getFreshPortAuthenticationToken: undefined as any,
|
||||
});
|
||||
bind(BitbucketServerApi).toSelf().inSingletonScope();
|
||||
bind(HostContextProvider).toConstantValue({
|
||||
get: (hostname: string) => {
|
||||
authProvider: {
|
||||
("BBS");
|
||||
}
|
||||
},
|
||||
});
|
||||
}),
|
||||
);
|
||||
this.service = container.get(BitbucketServerFileProvider);
|
||||
this.user = {
|
||||
creationDate: "",
|
||||
id: "user1",
|
||||
identities: [
|
||||
{
|
||||
authId: "user1",
|
||||
authName: "AlexTugarev",
|
||||
authProviderId: "MyBitbucketServer",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@test async test_getGitpodFileContent_ok() {
|
||||
const result = await this.service.getGitpodFileContent(
|
||||
{
|
||||
revision: "master",
|
||||
repository: <Repository>{
|
||||
cloneUrl: "https://bitbucket.gitpod-self-hosted.com/projects/FOO/repos/repo123",
|
||||
webUrl: "https://bitbucket.gitpod-self-hosted.com/projects/FOO/repos/repo123",
|
||||
name: "repo123",
|
||||
repoKind: "projects",
|
||||
owner: "FOO",
|
||||
},
|
||||
} as any,
|
||||
this.user,
|
||||
);
|
||||
expect(result).not.to.be.empty;
|
||||
expect(result).to.contain("tasks:");
|
||||
}
|
||||
|
||||
@test async test_getLastChangeRevision_ok() {
|
||||
const result = await this.service.getLastChangeRevision(
|
||||
{
|
||||
owner: "FOO",
|
||||
name: "repo123",
|
||||
repoKind: "projects",
|
||||
revision: "foo",
|
||||
host: "bitbucket.gitpod-self-hosted.com",
|
||||
cloneUrl: "https://bitbucket.gitpod-self-hosted.com/projects/FOO/repos/repo123",
|
||||
webUrl: "https://bitbucket.gitpod-self-hosted.com/projects/FOO/repos/repo123",
|
||||
} as Repository,
|
||||
"foo",
|
||||
this.user,
|
||||
"folder/sub/test.txt",
|
||||
);
|
||||
expect(result).to.equal("1384b6842d73b8705feaf45f3e8aa41f00529042");
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new TestBitbucketServerFileProvider();
|
||||
@ -5,18 +5,16 @@
|
||||
*/
|
||||
|
||||
import { Commit, Repository, User } from "@gitpod/gitpod-protocol";
|
||||
import { injectable } from "inversify";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { FileProvider, MaybeContent } from "../repohost/file-provider";
|
||||
import { BitbucketServerApi } from "./bitbucket-server-api";
|
||||
|
||||
@injectable()
|
||||
export class BitbucketServerFileProvider implements FileProvider {
|
||||
@inject(BitbucketServerApi) protected api: BitbucketServerApi;
|
||||
|
||||
public async getGitpodFileContent(commit: Commit, user: User): Promise<MaybeContent> {
|
||||
return undefined;
|
||||
// const yamlVersion1 = await Promise.all([
|
||||
// this.getFileContent(commit, user, '.gitpod.yml'),
|
||||
// this.getFileContent(commit, user, '.gitpod')
|
||||
// ]);
|
||||
// return yamlVersion1.filter(f => !!f)[0];
|
||||
return this.getFileContent(commit, user, ".gitpod.yml");
|
||||
}
|
||||
|
||||
public async getLastChangeRevision(
|
||||
@ -25,29 +23,32 @@ export class BitbucketServerFileProvider implements FileProvider {
|
||||
user: User,
|
||||
path: string,
|
||||
): Promise<string> {
|
||||
// try {
|
||||
// const api = await this.apiFactory.create(user);
|
||||
// const fileMetaData = (await api.repositories.readSrc({ workspace: repository.owner, repo_slug: repository.name, commit: revisionOrBranch, path, format: "meta" })).data;
|
||||
// return (fileMetaData as any).commit.hash;
|
||||
// } catch (err) {
|
||||
// log.error({ userId: user.id }, err);
|
||||
// throw new Error(`Could not fetch ${path} of repository ${repository.owner}/${repository.name}: ${err}`);
|
||||
// }
|
||||
return "f00";
|
||||
const { owner, name, repoKind } = repository;
|
||||
|
||||
if (!repoKind) {
|
||||
throw new Error("Repo kind is missing.");
|
||||
}
|
||||
|
||||
const result = await this.api.getCommits(user, {
|
||||
owner,
|
||||
repoKind,
|
||||
repositorySlug: name,
|
||||
query: { limit: 1, path, shaOrRevision: revisionOrBranch },
|
||||
});
|
||||
return result.values![0].id;
|
||||
}
|
||||
|
||||
public async getFileContent(commit: Commit, user: User, path: string) {
|
||||
if (!commit.revision || !commit.repository.webUrl) {
|
||||
return undefined;
|
||||
// if (!commit.revision) {
|
||||
// return undefined;
|
||||
// }
|
||||
}
|
||||
const { owner, name, repoKind } = commit.repository;
|
||||
|
||||
// try {
|
||||
// const api = await this.apiFactory.create(user);
|
||||
// const contents = (await api.repositories.readSrc({ workspace: commit.repository.owner, repo_slug: commit.repository.name, commit: commit.revision, path })).data;
|
||||
// return contents as string;
|
||||
// } catch (err) {
|
||||
// log.error({ userId: user.id }, err);
|
||||
// }
|
||||
try {
|
||||
const result = await this.api.fetchContent(user, `/${repoKind}/${owner}/repos/${name}/raw/${path}`);
|
||||
return result;
|
||||
} catch (err) {
|
||||
console.error({ userId: user.id }, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -14,7 +14,10 @@ export namespace BitbucketServerOAuthScopes {
|
||||
/** Push over https, fork repo */
|
||||
export const REPOSITORY_WRITE = "REPO_WRITE";
|
||||
|
||||
export const ALL = [PUBLIC_REPOS, REPOSITORY_READ, REPOSITORY_WRITE];
|
||||
export const REPO_ADMIN = "REPO_ADMIN";
|
||||
export const PROJECT_ADMIN = "PROJECT_ADMIN";
|
||||
|
||||
export const ALL = [PUBLIC_REPOS, REPOSITORY_READ, REPOSITORY_WRITE, REPO_ADMIN, PROJECT_ADMIN];
|
||||
|
||||
export const Requirements = {
|
||||
/**
|
||||
|
||||
@ -0,0 +1,128 @@
|
||||
/**
|
||||
* Copyright (c) 2022 Gitpod GmbH. All rights reserved.
|
||||
* Licensed under the GNU Affero General Public License (AGPL).
|
||||
* See License-AGPL.txt in the project root for license information.
|
||||
*/
|
||||
|
||||
import { User } from "@gitpod/gitpod-protocol";
|
||||
import { skipIfEnvVarNotSet } from "@gitpod/gitpod-protocol/lib/util/skip-if";
|
||||
import { Container, ContainerModule } from "inversify";
|
||||
import { retries, suite, test, timeout } from "mocha-typescript";
|
||||
import { expect } from "chai";
|
||||
import { GitpodHostUrl } from "@gitpod/gitpod-protocol/lib/util/gitpod-host-url";
|
||||
import { AuthProviderParams } from "../auth/auth-provider";
|
||||
import { BitbucketServerContextParser } from "./bitbucket-server-context-parser";
|
||||
import { BitbucketServerTokenHelper } from "./bitbucket-server-token-handler";
|
||||
import { TokenService } from "../user/token-service";
|
||||
import { Config } from "../config";
|
||||
import { TokenProvider } from "../user/token-provider";
|
||||
import { BitbucketServerApi } from "./bitbucket-server-api";
|
||||
import { HostContextProvider } from "../auth/host-context-provider";
|
||||
import { BitbucketServerRepositoryProvider } from "./bitbucket-server-repository-provider";
|
||||
|
||||
@suite(timeout(10000), retries(0), skipIfEnvVarNotSet("GITPOD_TEST_TOKEN_BITBUCKET_SERVER"))
|
||||
class TestBitbucketServerRepositoryProvider {
|
||||
protected service: BitbucketServerRepositoryProvider;
|
||||
protected user: User;
|
||||
|
||||
static readonly AUTH_HOST_CONFIG: Partial<AuthProviderParams> = {
|
||||
id: "MyBitbucketServer",
|
||||
type: "BitbucketServer",
|
||||
verified: true,
|
||||
description: "",
|
||||
icon: "",
|
||||
host: "bitbucket.gitpod-self-hosted.com",
|
||||
oauth: {
|
||||
callBackUrl: "",
|
||||
clientId: "not-used",
|
||||
clientSecret: "",
|
||||
tokenUrl: "",
|
||||
scope: "",
|
||||
authorizationUrl: "",
|
||||
},
|
||||
};
|
||||
|
||||
public before() {
|
||||
const container = new Container();
|
||||
container.load(
|
||||
new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(BitbucketServerRepositoryProvider).toSelf().inSingletonScope();
|
||||
bind(BitbucketServerContextParser).toSelf().inSingletonScope();
|
||||
bind(AuthProviderParams).toConstantValue(TestBitbucketServerRepositoryProvider.AUTH_HOST_CONFIG);
|
||||
bind(BitbucketServerTokenHelper).toSelf().inSingletonScope();
|
||||
bind(TokenService).toConstantValue({
|
||||
createGitpodToken: async () => ({ token: { value: "foobar123-token" } }),
|
||||
} as any);
|
||||
bind(Config).toConstantValue({
|
||||
hostUrl: new GitpodHostUrl(),
|
||||
});
|
||||
bind(TokenProvider).toConstantValue(<TokenProvider>{
|
||||
getTokenForHost: async () => {
|
||||
return {
|
||||
value: process.env["GITPOD_TEST_TOKEN_BITBUCKET_SERVER"] || "undefined",
|
||||
scopes: [],
|
||||
};
|
||||
},
|
||||
getFreshPortAuthenticationToken: undefined as any,
|
||||
});
|
||||
bind(BitbucketServerApi).toSelf().inSingletonScope();
|
||||
bind(HostContextProvider).toConstantValue({});
|
||||
}),
|
||||
);
|
||||
this.service = container.get(BitbucketServerRepositoryProvider);
|
||||
this.user = {
|
||||
creationDate: "",
|
||||
id: "user1",
|
||||
identities: [
|
||||
{
|
||||
authId: "user1",
|
||||
authName: "AlexTugarev",
|
||||
authProviderId: "MyBitbucketServer",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@test async test_getRepo_ok() {
|
||||
const result = await this.service.getRepo(this.user, "JLDEC", "jldec-repo-march-30");
|
||||
expect(result).to.deep.include({
|
||||
webUrl: "https://bitbucket.gitpod-self-hosted.com/projects/JLDEC/repos/jldec-repo-march-30",
|
||||
cloneUrl: "https://bitbucket.gitpod-self-hosted.com/scm/jldec/jldec-repo-march-30.git",
|
||||
});
|
||||
}
|
||||
|
||||
@test async test_getBranch_ok() {
|
||||
const result = await this.service.getBranch(this.user, "JLDEC", "jldec-repo-march-30", "main");
|
||||
expect(result).to.deep.include({
|
||||
name: "main",
|
||||
});
|
||||
}
|
||||
|
||||
@test async test_getBranches_ok() {
|
||||
const result = await this.service.getBranches(this.user, "JLDEC", "jldec-repo-march-30");
|
||||
expect(result.length).to.be.gte(1);
|
||||
expect(result[0]).to.deep.include({
|
||||
name: "main",
|
||||
});
|
||||
}
|
||||
|
||||
@test async test_getBranches_ok_2() {
|
||||
try {
|
||||
await this.service.getBranches(this.user, "mil", "gitpod-large-image");
|
||||
expect.fail("this should not happen while 'mil/gitpod-large-image' has NO default branch configured.");
|
||||
} catch (error) {
|
||||
expect(error.message).to.include(
|
||||
"refs/heads/master is set as the default branch, but this branch does not exist",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@test async test_getCommitInfo_ok() {
|
||||
const result = await this.service.getCommitInfo(this.user, "JLDEC", "jldec-repo-march-30", "test");
|
||||
expect(result).to.deep.include({
|
||||
author: "Alex Tugarev",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new TestBitbucketServerRepositoryProvider();
|
||||
@ -5,95 +5,142 @@
|
||||
*/
|
||||
|
||||
import { Branch, CommitInfo, Repository, User } from "@gitpod/gitpod-protocol";
|
||||
import { injectable } from "inversify";
|
||||
import { inject, injectable } from "inversify";
|
||||
import { RepoURL } from "../repohost";
|
||||
import { RepositoryProvider } from "../repohost/repository-provider";
|
||||
import { BitbucketServerApi } from "./bitbucket-server-api";
|
||||
|
||||
@injectable()
|
||||
export class BitbucketServerRepositoryProvider implements RepositoryProvider {
|
||||
@inject(BitbucketServerApi) protected api: BitbucketServerApi;
|
||||
|
||||
protected async getOwnerKind(user: User, owner: string): Promise<"users" | "projects" | undefined> {
|
||||
try {
|
||||
await this.api.getProject(user, owner);
|
||||
return "projects";
|
||||
} catch (error) {
|
||||
// ignore
|
||||
}
|
||||
try {
|
||||
await this.api.getUserProfile(user, owner);
|
||||
return "users";
|
||||
} catch (error) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
// 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;
|
||||
// const defaultBranch = repo.mainbranch?.name;
|
||||
// return { host, owner, name, cloneUrl, description, avatarUrl, webUrl, defaultBranch };
|
||||
throw new Error("getRepo unimplemented");
|
||||
const repoKind = await this.getOwnerKind(user, owner);
|
||||
if (!repoKind) {
|
||||
throw new Error(`Could not find project "${owner}"`);
|
||||
}
|
||||
|
||||
const repo = await this.api.getRepository(user, {
|
||||
repoKind,
|
||||
owner,
|
||||
repositorySlug: name,
|
||||
});
|
||||
const defaultBranch = await this.api.getDefaultBranch(user, {
|
||||
repoKind,
|
||||
owner,
|
||||
repositorySlug: name,
|
||||
});
|
||||
const cloneUrl = repo.links.clone.find((u) => u.name === "http")?.href!;
|
||||
const webUrl = repo.links?.self[0]?.href?.replace(/\/browse$/, "");
|
||||
const host = RepoURL.parseRepoUrl(cloneUrl)!.host;
|
||||
const avatarUrl = this.api.getAvatarUrl(owner);
|
||||
return {
|
||||
host,
|
||||
owner,
|
||||
name,
|
||||
cloneUrl,
|
||||
description: repo.description,
|
||||
avatarUrl,
|
||||
webUrl,
|
||||
defaultBranch: defaultBranch.displayId,
|
||||
};
|
||||
}
|
||||
|
||||
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 repoKind = await this.getOwnerKind(user, owner);
|
||||
if (!repoKind) {
|
||||
throw new Error(`Could not find project "${owner}"`);
|
||||
}
|
||||
const branch = await this.api.getBranch(user, {
|
||||
repoKind,
|
||||
owner,
|
||||
repositorySlug: repo,
|
||||
branchName,
|
||||
});
|
||||
const commit = branch.latestCommitMetadata;
|
||||
|
||||
// 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",
|
||||
// }
|
||||
// };
|
||||
throw new Error("getBranch unimplemented");
|
||||
return {
|
||||
htmlUrl: branch.htmlUrl,
|
||||
name: branch.displayId,
|
||||
commit: {
|
||||
sha: commit.id,
|
||||
author: commit.author.displayName,
|
||||
authorAvatarUrl: commit.author.avatarUrl,
|
||||
authorDate: new Date(commit.authorTimestamp).toISOString(),
|
||||
commitMessage: commit.message || "missing commit message",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async getBranches(user: User, owner: string, repo: string): Promise<Branch[]> {
|
||||
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",
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
const repoKind = await this.getOwnerKind(user, owner);
|
||||
if (!repoKind) {
|
||||
throw new Error(`Could not find project "${owner}"`);
|
||||
}
|
||||
const branchesResult = await this.api.getBranches(user, {
|
||||
repoKind,
|
||||
owner,
|
||||
repositorySlug: repo,
|
||||
});
|
||||
for (const entry of branchesResult) {
|
||||
const commit = entry.latestCommitMetadata;
|
||||
|
||||
branches.push({
|
||||
htmlUrl: entry.htmlUrl,
|
||||
name: entry.displayId,
|
||||
commit: {
|
||||
sha: commit.id,
|
||||
author: commit.author.displayName,
|
||||
authorAvatarUrl: commit.author.avatarUrl,
|
||||
authorDate: new Date(commit.authorTimestamp).toISOString(),
|
||||
commitMessage: commit.message || "missing commit message",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return branches;
|
||||
}
|
||||
|
||||
async getCommitInfo(user: User, owner: string, repo: string, ref: string): Promise<CommitInfo | undefined> {
|
||||
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,
|
||||
// };
|
||||
const repoKind = await this.getOwnerKind(user, owner);
|
||||
if (!repoKind) {
|
||||
throw new Error(`Could not find project "${owner}"`);
|
||||
}
|
||||
|
||||
const commitsResult = await this.api.getCommits(user, {
|
||||
owner,
|
||||
repoKind,
|
||||
repositorySlug: repo,
|
||||
query: { shaOrRevision: ref, limit: 1 },
|
||||
});
|
||||
|
||||
if (commitsResult.values && commitsResult.values[0]) {
|
||||
const commit = commitsResult.values[0];
|
||||
return {
|
||||
sha: commit.id,
|
||||
author: commit.author.displayName,
|
||||
authorDate: new Date(commit.authorTimestamp).toISOString(),
|
||||
commitMessage: commit.message || "missing commit message",
|
||||
authorAvatarUrl: commit.author.avatarUrl,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async getUserRepos(user: User): Promise<string[]> {
|
||||
@ -107,6 +154,19 @@ export class BitbucketServerRepositoryProvider implements RepositoryProvider {
|
||||
}
|
||||
|
||||
async getCommitHistory(user: User, owner: string, repo: string, ref: string, maxDepth: number): Promise<string[]> {
|
||||
return [];
|
||||
const repoKind = await this.getOwnerKind(user, owner);
|
||||
if (!repoKind) {
|
||||
throw new Error(`Could not find project "${owner}"`);
|
||||
}
|
||||
|
||||
const commitsResult = await this.api.getCommits(user, {
|
||||
owner,
|
||||
repoKind,
|
||||
repositorySlug: repo,
|
||||
query: { shaOrRevision: ref, limit: 1000 },
|
||||
});
|
||||
|
||||
const commits = commitsResult.values || [];
|
||||
return commits.map((c) => c.id);
|
||||
}
|
||||
}
|
||||
|
||||
@ -150,7 +150,12 @@ export class ProjectsService {
|
||||
const hostContext = parsedUrl?.host ? this.hostContextProvider.get(parsedUrl?.host) : undefined;
|
||||
const authProvider = hostContext && hostContext.authProvider.info;
|
||||
const type = authProvider && authProvider.authProviderType;
|
||||
if (type === "GitLab" || type === "Bitbucket" || AuthProviderInfo.isGitHubEnterprise(authProvider)) {
|
||||
if (
|
||||
type === "GitLab" ||
|
||||
type === "Bitbucket" ||
|
||||
type === "BitbucketServer" ||
|
||||
AuthProviderInfo.isGitHubEnterprise(authProvider)
|
||||
) {
|
||||
const repositoryService = hostContext?.services?.repositoryService;
|
||||
if (repositoryService) {
|
||||
// Note: For GitLab, we expect .canInstallAutomatedPrebuilds() to always return true, because earlier
|
||||
|
||||
@ -18,7 +18,10 @@ export namespace RepoURL {
|
||||
}
|
||||
if (segments.length > 2) {
|
||||
const endSegment = segments[segments.length - 1];
|
||||
const ownerSegments = segments.slice(0, segments.length - 1);
|
||||
let ownerSegments = segments.slice(0, segments.length - 1);
|
||||
if (ownerSegments[0] === "scm") {
|
||||
ownerSegments = ownerSegments.slice(1);
|
||||
}
|
||||
const owner = ownerSegments.join("/");
|
||||
const repo = endSegment.endsWith(".git") ? endSegment.slice(0, -4) : endSegment;
|
||||
return { host, owner, repo };
|
||||
|
||||
19
yarn.lock
19
yarn.lock
@ -2,18 +2,6 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@atlassian/bitbucket-server@^0.0.6":
|
||||
version "0.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@atlassian/bitbucket-server/-/bitbucket-server-0.0.6.tgz#7a0678264083c851e50a66216e66021efe1638cf"
|
||||
integrity sha512-92EpKlSPw0ZZXiS4Qt+1DKuDxSIntL2j8Q0CWc8o/nUNOJAW/D9szIgcef5VPTbVslHeb2C3gBSMNnETdykdmQ==
|
||||
dependencies:
|
||||
before-after-hook "^1.1.0"
|
||||
btoa-lite "^1.0.0"
|
||||
debug "^3.1.0"
|
||||
is-plain-object "^2.0.4"
|
||||
node-fetch "^2.1.2"
|
||||
url-template "^2.0.8"
|
||||
|
||||
"@babel/code-frame@7.10.4":
|
||||
version "7.10.4"
|
||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a"
|
||||
@ -4600,11 +4588,6 @@ bcrypt-pbkdf@^1.0.0:
|
||||
dependencies:
|
||||
tweetnacl "^0.14.3"
|
||||
|
||||
before-after-hook@^1.1.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-1.4.0.tgz#2b6bf23dca4f32e628fd2747c10a37c74a4b484d"
|
||||
integrity sha512-l5r9ir56nda3qu14nAXIlyq1MmUSs0meCIaFAh8HwkFwP1F8eToOuS3ah2VAHHcY04jaYD7FpJC5JTXHYRbkzg==
|
||||
|
||||
before-after-hook@^2.1.0, before-after-hook@^2.2.0:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.2.tgz#a6e8ca41028d90ee2c24222f201c90956091613e"
|
||||
@ -12139,7 +12122,7 @@ node-emoji@^1.11.0:
|
||||
dependencies:
|
||||
lodash "^4.17.21"
|
||||
|
||||
node-fetch@^2.1.2, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.5, node-fetch@^2.6.7:
|
||||
node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.5, node-fetch@^2.6.7:
|
||||
version "2.6.7"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
|
||||
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user