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[]>([]);
|
const [authProviders, setAuthProviders] = useState<AuthProviderInfo[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
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 () => {
|
(async () => {
|
||||||
setAuthProviders(await getGitpodService().server.getAuthProviders());
|
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(() => {
|
useEffect(() => {
|
||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(location.search);
|
||||||
@ -385,7 +392,7 @@ export default function NewProject() {
|
|||||||
>
|
>
|
||||||
{toSimpleName(r.name)}
|
{toSimpleName(r.name)}
|
||||||
</div>
|
</div>
|
||||||
<p>Updated {moment(r.updatedAt).fromNow()}</p>
|
{r.updatedAt && <p>Updated {moment(r.updatedAt).fromNow()}</p>}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end">
|
<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">
|
<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 = () =>
|
const filteredProviders = () =>
|
||||||
props.authProviders.filter(
|
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 (
|
return (
|
||||||
|
|||||||
@ -307,7 +307,7 @@ export interface ProviderRepository {
|
|||||||
account: string;
|
account: string;
|
||||||
accountAvatarUrl: string;
|
accountAvatarUrl: string;
|
||||||
cloneUrl: string;
|
cloneUrl: string;
|
||||||
updatedAt: string;
|
updatedAt?: string;
|
||||||
installationId?: number;
|
installationId?: number;
|
||||||
installationUpdatedAt?: string;
|
installationUpdatedAt?: string;
|
||||||
|
|
||||||
|
|||||||
@ -1015,6 +1015,8 @@ export interface Repository {
|
|||||||
owner: string;
|
owner: string;
|
||||||
name: string;
|
name: string;
|
||||||
cloneUrl: string;
|
cloneUrl: string;
|
||||||
|
/* Optional kind to differentiate between repositories of orgs/groups/projects and personal repos. */
|
||||||
|
repoKind?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
avatarUrl?: string;
|
avatarUrl?: string;
|
||||||
webUrl?: string;
|
webUrl?: string;
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { HostContainerMapping } from "../../../src/auth/host-container-mapping";
|
|||||||
import { gitlabContainerModuleEE } from "../gitlab/container-module";
|
import { gitlabContainerModuleEE } from "../gitlab/container-module";
|
||||||
import { bitbucketContainerModuleEE } from "../bitbucket/container-module";
|
import { bitbucketContainerModuleEE } from "../bitbucket/container-module";
|
||||||
import { gitHubContainerModuleEE } from "../github/container-module";
|
import { gitHubContainerModuleEE } from "../github/container-module";
|
||||||
|
import { bitbucketServerContainerModuleEE } from "../bitbucket-server/container-module";
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class HostContainerMappingEE extends HostContainerMapping {
|
export class HostContainerMappingEE extends HostContainerMapping {
|
||||||
@ -20,9 +21,8 @@ export class HostContainerMappingEE extends HostContainerMapping {
|
|||||||
return (modules || []).concat([gitlabContainerModuleEE]);
|
return (modules || []).concat([gitlabContainerModuleEE]);
|
||||||
case "Bitbucket":
|
case "Bitbucket":
|
||||||
return (modules || []).concat([bitbucketContainerModuleEE]);
|
return (modules || []).concat([bitbucketContainerModuleEE]);
|
||||||
// case "BitbucketServer":
|
case "BitbucketServer":
|
||||||
// FIXME
|
return (modules || []).concat([bitbucketServerContainerModuleEE]);
|
||||||
// return (modules || []).concat([bitbucketContainerModuleEE]);
|
|
||||||
case "GitHub":
|
case "GitHub":
|
||||||
return (modules || []).concat([gitHubContainerModuleEE]);
|
return (modules || []).concat([gitHubContainerModuleEE]);
|
||||||
default:
|
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 { SnapshotService } from "./workspace/snapshot-service";
|
||||||
import { BitbucketAppSupport } from "./bitbucket/bitbucket-app-support";
|
import { BitbucketAppSupport } from "./bitbucket/bitbucket-app-support";
|
||||||
import { UserCounter } from "./user/user-counter";
|
import { UserCounter } from "./user/user-counter";
|
||||||
|
import { BitbucketServerApp } from "./prebuilds/bitbucket-server-app";
|
||||||
|
|
||||||
export const productionEEContainerModule = new ContainerModule((bind, unbind, isBound, rebind) => {
|
export const productionEEContainerModule = new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||||
rebind(Server).to(ServerEE).inSingletonScope();
|
rebind(Server).to(ServerEE).inSingletonScope();
|
||||||
@ -77,6 +78,7 @@ export const productionEEContainerModule = new ContainerModule((bind, unbind, is
|
|||||||
bind(BitbucketApp).toSelf().inSingletonScope();
|
bind(BitbucketApp).toSelf().inSingletonScope();
|
||||||
bind(BitbucketAppSupport).toSelf().inSingletonScope();
|
bind(BitbucketAppSupport).toSelf().inSingletonScope();
|
||||||
bind(GitHubEnterpriseApp).toSelf().inSingletonScope();
|
bind(GitHubEnterpriseApp).toSelf().inSingletonScope();
|
||||||
|
bind(BitbucketServerApp).toSelf().inSingletonScope();
|
||||||
|
|
||||||
bind(UserCounter).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;
|
@inject(BitbucketContextParser) protected bitbucketContextParser: BitbucketContextParser;
|
||||||
|
|
||||||
async canInstallAutomatedPrebuilds(user: User, cloneUrl: string): Promise<boolean> {
|
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) {
|
if (host !== this.authProviderConfig.host) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// only admins may install webhooks on repositories
|
// only admins may install webhooks on repositories
|
||||||
const { owner, repoName } = await this.bitbucketContextParser.parseURL(user, cloneUrl);
|
|
||||||
const api = await this.api.create(user);
|
const api = await this.api.create(user);
|
||||||
const response = await api.user.listPermissionsForRepos({
|
const response = await api.user.listPermissionsForRepos({
|
||||||
q: `repository.full_name="${owner}/${repoName}"`,
|
q: `repository.full_name="${owner}/${repoName}"`,
|
||||||
|
|||||||
@ -14,11 +14,13 @@ import { BitbucketApp } from "./prebuilds/bitbucket-app";
|
|||||||
import { GithubApp } from "./prebuilds/github-app";
|
import { GithubApp } from "./prebuilds/github-app";
|
||||||
import { SnapshotService } from "./workspace/snapshot-service";
|
import { SnapshotService } from "./workspace/snapshot-service";
|
||||||
import { GitHubEnterpriseApp } from "./prebuilds/github-enterprise-app";
|
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> {
|
export class ServerEE<C extends GitpodClient, S extends GitpodServer> extends Server<C, S> {
|
||||||
@inject(GithubApp) protected readonly githubApp: GithubApp;
|
@inject(GithubApp) protected readonly githubApp: GithubApp;
|
||||||
@inject(GitLabApp) protected readonly gitLabApp: GitLabApp;
|
@inject(GitLabApp) protected readonly gitLabApp: GitLabApp;
|
||||||
@inject(BitbucketApp) protected readonly bitbucketApp: BitbucketApp;
|
@inject(BitbucketApp) protected readonly bitbucketApp: BitbucketApp;
|
||||||
|
@inject(BitbucketServerApp) protected readonly bitbucketServerApp: BitbucketServerApp;
|
||||||
@inject(SnapshotService) protected readonly snapshotService: SnapshotService;
|
@inject(SnapshotService) protected readonly snapshotService: SnapshotService;
|
||||||
@inject(GitHubEnterpriseApp) protected readonly gitHubEnterpriseApp: GitHubEnterpriseApp;
|
@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);
|
log.info("Registered GitHub EnterpriseApp app at " + GitHubEnterpriseApp.path);
|
||||||
app.use(GitHubEnterpriseApp.path, this.gitHubEnterpriseApp.router);
|
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) {
|
} else if (providerHost === "bitbucket.org" && provider) {
|
||||||
repositories.push(...(await this.bitbucketAppSupport.getProviderRepositoriesForUser({ user, 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") {
|
} else if (provider?.authProviderType === "GitLab") {
|
||||||
repositories.push(...(await this.gitLabAppSupport.getProviderRepositoriesForUser({ user, provider })));
|
repositories.push(...(await this.gitLabAppSupport.getProviderRepositoriesForUser({ user, provider })));
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -27,7 +27,6 @@
|
|||||||
"/dist"
|
"/dist"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atlassian/bitbucket-server": "^0.0.6",
|
|
||||||
"@gitbeaker/node": "^25.6.0",
|
"@gitbeaker/node": "^25.6.0",
|
||||||
"@gitpod/content-service": "0.1.5",
|
"@gitpod/content-service": "0.1.5",
|
||||||
"@gitpod/gitpod-db": "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(AuthProviderParams) protected readonly config: AuthProviderParams;
|
||||||
@inject(BitbucketServerTokenHelper) protected readonly tokenHelper: BitbucketServerTokenHelper;
|
@inject(BitbucketServerTokenHelper) protected readonly tokenHelper: BitbucketServerTokenHelper;
|
||||||
|
|
||||||
public async runQuery<T>(user: User, urlPath: string): Promise<T> {
|
public async runQuery<T>(
|
||||||
const token = (await this.tokenHelper.getTokenWithScopes(user, [])).value;
|
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}`;
|
const fullUrl = `${this.baseUrl}${urlPath}`;
|
||||||
|
let result: string = "OK";
|
||||||
|
try {
|
||||||
const response = await fetch(fullUrl, {
|
const response = await fetch(fullUrl, {
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
method: "GET",
|
method,
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: `Bearer ${token}`,
|
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) {
|
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 {
|
protected get baseUrl(): string {
|
||||||
return `https://${this.config.host}/rest/api/1.0`;
|
return `https://${this.config.host}/rest/api/1.0`;
|
||||||
}
|
}
|
||||||
|
|
||||||
getRepository(
|
async getRepository(
|
||||||
user: User,
|
user: User,
|
||||||
params: { kind: "projects" | "users"; userOrProject: string; repositorySlug: string },
|
params: { repoKind: "projects" | "users"; owner: string; repositorySlug: string },
|
||||||
): Promise<BitbucketServer.Repository> {
|
): Promise<BitbucketServer.Repository> {
|
||||||
return this.runQuery<BitbucketServer.Repository>(
|
return this.runQuery<BitbucketServer.Repository>(
|
||||||
user,
|
user,
|
||||||
`/${params.kind}/${params.userOrProject}/repos/${params.repositorySlug}`,
|
`/${params.repoKind}/${params.owner}/repos/${params.repositorySlug}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getCommits(
|
async getCommits(
|
||||||
user: User,
|
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>> {
|
): 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>>(
|
return this.runQuery<BitbucketServer.Paginated<BitbucketServer.Commit>>(
|
||||||
user,
|
user,
|
||||||
`/${params.kind}/${params.userOrProject}/repos/${params.repositorySlug}/commits`,
|
`/${params.repoKind}/${params.owner}/repos/${params.repositorySlug}/commits${q}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getDefaultBranch(
|
async getDefaultBranch(
|
||||||
user: User,
|
user: User,
|
||||||
params: { kind: "projects" | "users"; userOrProject: string; repositorySlug: string },
|
params: { repoKind: "projects" | "users"; owner: string; repositorySlug: string },
|
||||||
): Promise<BitbucketServer.Branch> {
|
): 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>(
|
return this.runQuery<BitbucketServer.Branch>(
|
||||||
user,
|
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 namespace BitbucketServer {
|
||||||
|
export type RepoKind = "users" | "projects";
|
||||||
export interface Repository {
|
export interface Repository {
|
||||||
id: number;
|
id: number;
|
||||||
slug: string;
|
slug: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
description?: string;
|
||||||
public: boolean;
|
public: boolean;
|
||||||
links: {
|
links: {
|
||||||
clone: {
|
clone: {
|
||||||
href: string;
|
href: string;
|
||||||
name: string;
|
name: string;
|
||||||
}[];
|
}[];
|
||||||
|
self: {
|
||||||
|
href: string;
|
||||||
|
}[];
|
||||||
};
|
};
|
||||||
project: Project;
|
project: Project;
|
||||||
}
|
}
|
||||||
@ -100,6 +339,14 @@ export namespace BitbucketServer {
|
|||||||
isDefault: boolean;
|
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 {
|
export interface User {
|
||||||
name: string;
|
name: string;
|
||||||
emailAddress: string;
|
emailAddress: string;
|
||||||
@ -115,12 +362,17 @@ export namespace BitbucketServer {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
avatarUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Commit {
|
export interface Commit {
|
||||||
id: string;
|
id: string;
|
||||||
displayId: string;
|
displayId: string;
|
||||||
author: BitbucketServer.User;
|
author: BitbucketServer.User;
|
||||||
|
authorTimestamp: number;
|
||||||
|
commiter: BitbucketServer.User;
|
||||||
|
committerTimestamp: number;
|
||||||
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Paginated<T> {
|
export interface Paginated<T> {
|
||||||
@ -131,4 +383,45 @@ export namespace BitbucketServer {
|
|||||||
values?: T[];
|
values?: T[];
|
||||||
[k: string]: any;
|
[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 { AuthProviderInfo } from "@gitpod/gitpod-protocol";
|
||||||
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
|
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
|
||||||
import * as express from "express";
|
import * as express from "express";
|
||||||
import { injectable } from "inversify";
|
import { inject, injectable } from "inversify";
|
||||||
import fetch from "node-fetch";
|
|
||||||
import { AuthUserSetup } from "../auth/auth-provider";
|
import { AuthUserSetup } from "../auth/auth-provider";
|
||||||
import { GenericAuthProvider } from "../auth/generic-auth-provider";
|
import { GenericAuthProvider } from "../auth/generic-auth-provider";
|
||||||
import { BitbucketServerOAuthScopes } from "./bitbucket-server-oauth-scopes";
|
import { BitbucketServerOAuthScopes } from "./bitbucket-server-oauth-scopes";
|
||||||
import * as BitbucketServer from "@atlassian/bitbucket-server";
|
import { BitbucketServerApi } from "./bitbucket-server-api";
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class BitbucketServerAuthProvider extends GenericAuthProvider {
|
export class BitbucketServerAuthProvider extends GenericAuthProvider {
|
||||||
|
@inject(BitbucketServerApi) protected readonly api: BitbucketServerApi;
|
||||||
|
|
||||||
get info(): AuthProviderInfo {
|
get info(): AuthProviderInfo {
|
||||||
return {
|
return {
|
||||||
...this.defaultInfo(),
|
...this.defaultInfo(),
|
||||||
@ -54,41 +55,18 @@ export class BitbucketServerAuthProvider extends GenericAuthProvider {
|
|||||||
|
|
||||||
protected readAuthUserSetup = async (accessToken: string, _tokenResponse: object) => {
|
protected readAuthUserSetup = async (accessToken: string, _tokenResponse: object) => {
|
||||||
try {
|
try {
|
||||||
const fetchResult = await fetch(`https://${this.params.host}/plugins/servlet/applinks/whoami`, {
|
const username = await this.api.currentUsername(accessToken);
|
||||||
timeout: 10000,
|
const userProfile = await this.api.getUserProfile(accessToken, username);
|
||||||
headers: {
|
const avatarUrl = await this.api.getAvatarUrl(username);
|
||||||
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?
|
|
||||||
|
|
||||||
return <AuthUserSetup>{
|
return <AuthUserSetup>{
|
||||||
authUser: {
|
authUser: {
|
||||||
authId: `${user.id!}`,
|
// e.g. 105
|
||||||
authName: user.slug!,
|
authId: `${userProfile.id!}`,
|
||||||
primaryEmail: user.emailAddress!,
|
// HINT: userProfile.name is used to match permission in repo/webhook services
|
||||||
name: user.displayName!,
|
authName: userProfile.name,
|
||||||
// avatarUrl: user.links!.avatar!.href // TODO
|
primaryEmail: userProfile.emailAddress!,
|
||||||
|
name: userProfile.displayName!,
|
||||||
|
avatarUrl,
|
||||||
},
|
},
|
||||||
currentScopes: BitbucketServerOAuthScopes.ALL,
|
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);
|
const span = TraceContext.startSpan("BitbucketServerContextParser.handle", ctx);
|
||||||
|
|
||||||
try {
|
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,
|
user,
|
||||||
contextUrl,
|
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) {
|
} catch (e) {
|
||||||
span.addTags({ contextUrl }).log({ error: e });
|
span.addTags({ contextUrl }).log({ error: e });
|
||||||
log.error({ userId: user.id }, "Error parsing Bitbucket context", 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 url = new URL(contextUrl);
|
||||||
const pathname = url.pathname.replace(/^\//, "").replace(/\/$/, ""); // pathname without leading and trailing slash
|
const pathname = url.pathname.replace(/^\//, "").replace(/\/$/, ""); // pathname without leading and trailing slash
|
||||||
const segments = pathname.split("/");
|
const segments = pathname.split("/");
|
||||||
@ -54,14 +60,31 @@ export class BitbucketServerContextParser extends AbstractContextParser implemen
|
|||||||
segments.splice(0, lenghtOfRelativePath);
|
segments.splice(0, lenghtOfRelativePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
const resourceKind = segments[0];
|
let firstSegment = segments[0];
|
||||||
const owner: string = segments[1];
|
let owner: string = segments[1];
|
||||||
const repoName: string = segments[3];
|
let repoKind: "users" | "projects";
|
||||||
const moreSegmentsStart: number = 4;
|
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 endsWithRepoName = segments.length === moreSegmentsStart;
|
||||||
|
|
||||||
const searchParams = url.searchParams;
|
const searchParams = url.searchParams;
|
||||||
return {
|
return {
|
||||||
resourceKind,
|
repoKind,
|
||||||
host,
|
host,
|
||||||
owner,
|
owner,
|
||||||
repoName: this.parseRepoName(repoName, endsWithRepoName),
|
repoName: this.parseRepoName(repoName, endsWithRepoName),
|
||||||
@ -83,7 +106,7 @@ export class BitbucketServerContextParser extends AbstractContextParser implemen
|
|||||||
protected async handleNavigatorContext(
|
protected async handleNavigatorContext(
|
||||||
ctx: TraceContext,
|
ctx: TraceContext,
|
||||||
user: User,
|
user: User,
|
||||||
resourceKind: string,
|
repoKind: "projects" | "users",
|
||||||
host: string,
|
host: string,
|
||||||
owner: string,
|
owner: string,
|
||||||
repoName: string,
|
repoName: string,
|
||||||
@ -91,20 +114,17 @@ export class BitbucketServerContextParser extends AbstractContextParser implemen
|
|||||||
): Promise<NavigatorContext> {
|
): Promise<NavigatorContext> {
|
||||||
const span = TraceContext.startSpan("BitbucketServerContextParser.handleNavigatorContext", ctx);
|
const span = TraceContext.startSpan("BitbucketServerContextParser.handleNavigatorContext", ctx);
|
||||||
try {
|
try {
|
||||||
if (resourceKind !== "users" && resourceKind !== "projects") {
|
|
||||||
throw new Error("Only /users/ and /projects/ resources are supported.");
|
|
||||||
}
|
|
||||||
const repo = await this.api.getRepository(user, {
|
const repo = await this.api.getRepository(user, {
|
||||||
kind: resourceKind,
|
repoKind,
|
||||||
userOrProject: owner,
|
owner,
|
||||||
repositorySlug: repoName,
|
repositorySlug: repoName,
|
||||||
});
|
});
|
||||||
const defaultBranch = await this.api.getDefaultBranch(user, {
|
const defaultBranch = await this.api.getDefaultBranch(user, {
|
||||||
kind: resourceKind,
|
repoKind,
|
||||||
userOrProject: owner,
|
owner,
|
||||||
repositorySlug: repoName,
|
repositorySlug: repoName,
|
||||||
});
|
});
|
||||||
const repository = await this.toRepository(user, host, repo, defaultBranch);
|
const repository = this.toRepository(host, repo, repoKind, defaultBranch);
|
||||||
span.log({ "request.finished": "" });
|
span.log({ "request.finished": "" });
|
||||||
|
|
||||||
if (!repo) {
|
if (!repo) {
|
||||||
@ -124,10 +144,10 @@ export class BitbucketServerContextParser extends AbstractContextParser implemen
|
|||||||
|
|
||||||
if (!more.revision) {
|
if (!more.revision) {
|
||||||
const tipCommitOnDefaultBranch = await this.api.getCommits(user, {
|
const tipCommitOnDefaultBranch = await this.api.getCommits(user, {
|
||||||
kind: resourceKind,
|
repoKind,
|
||||||
userOrProject: owner,
|
owner,
|
||||||
repositorySlug: repoName,
|
repositorySlug: repoName,
|
||||||
q: { limit: 1 },
|
query: { limit: 1 },
|
||||||
});
|
});
|
||||||
const commits = tipCommitOnDefaultBranch?.values || [];
|
const commits = tipCommitOnDefaultBranch?.values || [];
|
||||||
if (commits.length === 0) {
|
if (commits.length === 0) {
|
||||||
@ -137,15 +157,19 @@ export class BitbucketServerContextParser extends AbstractContextParser implemen
|
|||||||
more.refType = undefined;
|
more.refType = undefined;
|
||||||
} else {
|
} else {
|
||||||
more.revision = commits[0].id;
|
more.revision = commits[0].id;
|
||||||
more.refType = "revision";
|
// more.refType = "revision";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return <NavigatorContext>{
|
||||||
...more,
|
isFile: false,
|
||||||
|
path: "",
|
||||||
title: `${owner}/${repoName} - ${more.ref || more.revision}${more.path ? ":" + more.path : ""}`,
|
title: `${owner}/${repoName} - ${more.ref || more.revision}${more.path ? ":" + more.path : ""}`,
|
||||||
|
ref: more.ref,
|
||||||
|
refType: more.refType,
|
||||||
|
revision: more.revision,
|
||||||
repository,
|
repository,
|
||||||
} as NavigatorContext;
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
span.log({ error: e });
|
span.log({ error: e });
|
||||||
log.error({ userId: user.id }, "Error parsing Bitbucket navigator request context", 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(
|
protected toRepository(
|
||||||
user: User,
|
|
||||||
host: string,
|
host: string,
|
||||||
repo: BitbucketServer.Repository,
|
repo: BitbucketServer.Repository,
|
||||||
|
repoKind: string,
|
||||||
defaultBranch: BitbucketServer.Branch,
|
defaultBranch: BitbucketServer.Branch,
|
||||||
): Promise<Repository> {
|
): Repository {
|
||||||
if (!repo) {
|
|
||||||
throw new Error("Unknown repository.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const owner = repo.project.owner ? repo.project.owner.slug : repo.project.key;
|
const owner = repo.project.owner ? repo.project.owner.slug : repo.project.key;
|
||||||
const name = repo.name;
|
const name = repo.name;
|
||||||
const cloneUrl = repo.links.clone.find((u) => u.name === "http")?.href!;
|
const cloneUrl = repo.links.clone.find((u) => u.name === "http")?.href!;
|
||||||
|
const webUrl = repo.links?.self[0]?.href?.replace(/\/browse$/, "");
|
||||||
|
|
||||||
const result: Repository = {
|
const result: Repository = {
|
||||||
|
webUrl,
|
||||||
cloneUrl,
|
cloneUrl,
|
||||||
host,
|
host,
|
||||||
name,
|
name,
|
||||||
owner,
|
owner,
|
||||||
|
repoKind,
|
||||||
private: !repo.public,
|
private: !repo.public,
|
||||||
defaultBranch: defaultBranch.displayId || DEFAULT_BRANCH,
|
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 { Commit, Repository, User } from "@gitpod/gitpod-protocol";
|
||||||
import { injectable } from "inversify";
|
import { inject, injectable } from "inversify";
|
||||||
import { FileProvider, MaybeContent } from "../repohost/file-provider";
|
import { FileProvider, MaybeContent } from "../repohost/file-provider";
|
||||||
|
import { BitbucketServerApi } from "./bitbucket-server-api";
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class BitbucketServerFileProvider implements FileProvider {
|
export class BitbucketServerFileProvider implements FileProvider {
|
||||||
|
@inject(BitbucketServerApi) protected api: BitbucketServerApi;
|
||||||
|
|
||||||
public async getGitpodFileContent(commit: Commit, user: User): Promise<MaybeContent> {
|
public async getGitpodFileContent(commit: Commit, user: User): Promise<MaybeContent> {
|
||||||
return undefined;
|
return this.getFileContent(commit, user, ".gitpod.yml");
|
||||||
// const yamlVersion1 = await Promise.all([
|
|
||||||
// this.getFileContent(commit, user, '.gitpod.yml'),
|
|
||||||
// this.getFileContent(commit, user, '.gitpod')
|
|
||||||
// ]);
|
|
||||||
// return yamlVersion1.filter(f => !!f)[0];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getLastChangeRevision(
|
public async getLastChangeRevision(
|
||||||
@ -25,29 +23,32 @@ export class BitbucketServerFileProvider implements FileProvider {
|
|||||||
user: User,
|
user: User,
|
||||||
path: string,
|
path: string,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
// try {
|
const { owner, name, repoKind } = repository;
|
||||||
// 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;
|
if (!repoKind) {
|
||||||
// return (fileMetaData as any).commit.hash;
|
throw new Error("Repo kind is missing.");
|
||||||
// } catch (err) {
|
}
|
||||||
// log.error({ userId: user.id }, err);
|
|
||||||
// throw new Error(`Could not fetch ${path} of repository ${repository.owner}/${repository.name}: ${err}`);
|
const result = await this.api.getCommits(user, {
|
||||||
// }
|
owner,
|
||||||
return "f00";
|
repoKind,
|
||||||
|
repositorySlug: name,
|
||||||
|
query: { limit: 1, path, shaOrRevision: revisionOrBranch },
|
||||||
|
});
|
||||||
|
return result.values![0].id;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getFileContent(commit: Commit, user: User, path: string) {
|
public async getFileContent(commit: Commit, user: User, path: string) {
|
||||||
|
if (!commit.revision || !commit.repository.webUrl) {
|
||||||
return undefined;
|
return undefined;
|
||||||
// if (!commit.revision) {
|
}
|
||||||
// return undefined;
|
const { owner, name, repoKind } = commit.repository;
|
||||||
// }
|
|
||||||
|
|
||||||
// try {
|
try {
|
||||||
// const api = await this.apiFactory.create(user);
|
const result = await this.api.fetchContent(user, `/${repoKind}/${owner}/repos/${name}/raw/${path}`);
|
||||||
// const contents = (await api.repositories.readSrc({ workspace: commit.repository.owner, repo_slug: commit.repository.name, commit: commit.revision, path })).data;
|
return result;
|
||||||
// return contents as string;
|
} catch (err) {
|
||||||
// } catch (err) {
|
console.error({ userId: user.id }, err);
|
||||||
// log.error({ userId: user.id }, err);
|
}
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,7 +14,10 @@ export namespace BitbucketServerOAuthScopes {
|
|||||||
/** Push over https, fork repo */
|
/** Push over https, fork repo */
|
||||||
export const REPOSITORY_WRITE = "REPO_WRITE";
|
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 = {
|
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 { 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 { RepositoryProvider } from "../repohost/repository-provider";
|
||||||
|
import { BitbucketServerApi } from "./bitbucket-server-api";
|
||||||
|
|
||||||
@injectable()
|
@injectable()
|
||||||
export class BitbucketServerRepositoryProvider implements RepositoryProvider {
|
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> {
|
async getRepo(user: User, owner: string, name: string): Promise<Repository> {
|
||||||
// const api = await this.apiFactory.create(user);
|
const repoKind = await this.getOwnerKind(user, owner);
|
||||||
// const repo = (await api.repositories.get({ workspace: owner, repo_slug: name })).data;
|
if (!repoKind) {
|
||||||
// let cloneUrl = repo.links!.clone!.find((x: any) => x.name === "https")!.href!;
|
throw new Error(`Could not find project "${owner}"`);
|
||||||
// if (cloneUrl) {
|
}
|
||||||
// const url = new URL(cloneUrl);
|
|
||||||
// url.username = '';
|
const repo = await this.api.getRepository(user, {
|
||||||
// cloneUrl = url.toString();
|
repoKind,
|
||||||
// }
|
owner,
|
||||||
// const host = RepoURL.parseRepoUrl(cloneUrl)!.host;
|
repositorySlug: name,
|
||||||
// const description = repo.description;
|
});
|
||||||
// const avatarUrl = repo.owner!.links!.avatar!.href;
|
const defaultBranch = await this.api.getDefaultBranch(user, {
|
||||||
// const webUrl = repo.links!.html!.href;
|
repoKind,
|
||||||
// const defaultBranch = repo.mainbranch?.name;
|
owner,
|
||||||
// return { host, owner, name, cloneUrl, description, avatarUrl, webUrl, defaultBranch };
|
repositorySlug: name,
|
||||||
throw new Error("getRepo unimplemented");
|
});
|
||||||
|
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> {
|
async getBranch(user: User, owner: string, repo: string, branchName: string): Promise<Branch> {
|
||||||
// const api = await this.apiFactory.create(user);
|
const repoKind = await this.getOwnerKind(user, owner);
|
||||||
// const response = await api.repositories.getBranch({
|
if (!repoKind) {
|
||||||
// workspace: owner,
|
throw new Error(`Could not find project "${owner}"`);
|
||||||
// repo_slug: repo,
|
}
|
||||||
// name: branchName
|
const branch = await this.api.getBranch(user, {
|
||||||
// })
|
repoKind,
|
||||||
|
owner,
|
||||||
|
repositorySlug: repo,
|
||||||
|
branchName,
|
||||||
|
});
|
||||||
|
const commit = branch.latestCommitMetadata;
|
||||||
|
|
||||||
// const branch = response.data;
|
return {
|
||||||
|
htmlUrl: branch.htmlUrl,
|
||||||
// return {
|
name: branch.displayId,
|
||||||
// htmlUrl: branch.links?.html?.href!,
|
commit: {
|
||||||
// name: branch.name!,
|
sha: commit.id,
|
||||||
// commit: {
|
author: commit.author.displayName,
|
||||||
// sha: branch.target?.hash!,
|
authorAvatarUrl: commit.author.avatarUrl,
|
||||||
// author: branch.target?.author?.user?.display_name!,
|
authorDate: new Date(commit.authorTimestamp).toISOString(),
|
||||||
// authorAvatarUrl: branch.target?.author?.user?.links?.avatar?.href,
|
commitMessage: commit.message || "missing commit message",
|
||||||
// authorDate: branch.target?.date!,
|
},
|
||||||
// commitMessage: branch.target?.message || "missing commit message",
|
};
|
||||||
// }
|
|
||||||
// };
|
|
||||||
throw new Error("getBranch unimplemented");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async getBranches(user: User, owner: string, repo: string): Promise<Branch[]> {
|
async getBranches(user: User, owner: string, repo: string): Promise<Branch[]> {
|
||||||
const branches: 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!) {
|
const repoKind = await this.getOwnerKind(user, owner);
|
||||||
// branches.push({
|
if (!repoKind) {
|
||||||
// htmlUrl: branch.links?.html?.href!,
|
throw new Error(`Could not find project "${owner}"`);
|
||||||
// name: branch.name!,
|
}
|
||||||
// commit: {
|
const branchesResult = await this.api.getBranches(user, {
|
||||||
// sha: branch.target?.hash!,
|
repoKind,
|
||||||
// author: branch.target?.author?.user?.display_name!,
|
owner,
|
||||||
// authorAvatarUrl: branch.target?.author?.user?.links?.avatar?.href,
|
repositorySlug: repo,
|
||||||
// authorDate: branch.target?.date!,
|
});
|
||||||
// commitMessage: branch.target?.message || "missing commit message",
|
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;
|
return branches;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getCommitInfo(user: User, owner: string, repo: string, ref: string): Promise<CommitInfo | undefined> {
|
async getCommitInfo(user: User, owner: string, repo: string, ref: string): Promise<CommitInfo | undefined> {
|
||||||
return undefined;
|
const repoKind = await this.getOwnerKind(user, owner);
|
||||||
// const api = await this.apiFactory.create(user);
|
if (!repoKind) {
|
||||||
// const response = await api.commits.get({
|
throw new Error(`Could not find project "${owner}"`);
|
||||||
// workspace: owner,
|
}
|
||||||
// repo_slug: repo,
|
|
||||||
// commit: ref
|
const commitsResult = await this.api.getCommits(user, {
|
||||||
// })
|
owner,
|
||||||
// const commit = response.data;
|
repoKind,
|
||||||
// return {
|
repositorySlug: repo,
|
||||||
// sha: commit.hash!,
|
query: { shaOrRevision: ref, limit: 1 },
|
||||||
// author: commit.author?.user?.display_name!,
|
});
|
||||||
// authorDate: commit.date!,
|
|
||||||
// commitMessage: commit.message || "missing commit message",
|
if (commitsResult.values && commitsResult.values[0]) {
|
||||||
// authorAvatarUrl: commit.author?.user?.links?.avatar?.href,
|
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[]> {
|
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[]> {
|
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 hostContext = parsedUrl?.host ? this.hostContextProvider.get(parsedUrl?.host) : undefined;
|
||||||
const authProvider = hostContext && hostContext.authProvider.info;
|
const authProvider = hostContext && hostContext.authProvider.info;
|
||||||
const type = authProvider && authProvider.authProviderType;
|
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;
|
const repositoryService = hostContext?.services?.repositoryService;
|
||||||
if (repositoryService) {
|
if (repositoryService) {
|
||||||
// Note: For GitLab, we expect .canInstallAutomatedPrebuilds() to always return true, because earlier
|
// Note: For GitLab, we expect .canInstallAutomatedPrebuilds() to always return true, because earlier
|
||||||
|
|||||||
@ -18,7 +18,10 @@ export namespace RepoURL {
|
|||||||
}
|
}
|
||||||
if (segments.length > 2) {
|
if (segments.length > 2) {
|
||||||
const endSegment = segments[segments.length - 1];
|
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 owner = ownerSegments.join("/");
|
||||||
const repo = endSegment.endsWith(".git") ? endSegment.slice(0, -4) : endSegment;
|
const repo = endSegment.endsWith(".git") ? endSegment.slice(0, -4) : endSegment;
|
||||||
return { host, owner, repo };
|
return { host, owner, repo };
|
||||||
|
|||||||
19
yarn.lock
19
yarn.lock
@ -2,18 +2,6 @@
|
|||||||
# yarn lockfile v1
|
# 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":
|
"@babel/code-frame@7.10.4":
|
||||||
version "7.10.4"
|
version "7.10.4"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.10.4.tgz#168da1a36e90da68ae8d49c0f1b48c7c6249213a"
|
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:
|
dependencies:
|
||||||
tweetnacl "^0.14.3"
|
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:
|
before-after-hook@^2.1.0, before-after-hook@^2.2.0:
|
||||||
version "2.2.2"
|
version "2.2.2"
|
||||||
resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-2.2.2.tgz#a6e8ca41028d90ee2c24222f201c90956091613e"
|
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:
|
dependencies:
|
||||||
lodash "^4.17.21"
|
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"
|
version "2.6.7"
|
||||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
|
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
|
||||||
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
|
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user