From fb49a9ee829ac9cb21b306eb0e5db074a382b197 Mon Sep 17 00:00:00 2001 From: Anbraten Date: Wed, 9 Feb 2022 08:02:34 +0000 Subject: [PATCH 01/16] first attempt for Gitea --- components/dashboard/src/images/gitea.svg | 3 + components/dashboard/src/provider-utils.tsx | 3 + .../ee/src/auth/host-container-mapping.ts | 3 + components/server/ee/src/container-module.ts | 4 + components/server/ee/src/server.ts | 2 + .../server/src/auth/auth-provider-service.ts | 3 +- .../server/src/auth/host-container-mapping.ts | 3 + components/server/src/gitea/api.ts | 738 ++++++++++++++++++ components/server/src/gitea/file-provider.ts | 57 ++ .../server/src/gitea/gitea-auth-provider.ts | 142 ++++ .../src/gitea/gitea-container-module.ts | 38 + .../src/gitea/gitea-context-parser.spec.ts | 590 ++++++++++++++ .../server/src/gitea/gitea-context-parser.ts | 480 ++++++++++++ .../src/gitea/gitea-repository-provider.ts | 131 ++++ .../server/src/gitea/gitea-token-helper.ts | 51 ++ .../server/src/gitea/gitea-token-validator.ts | 89 +++ components/server/src/gitea/gitea-urls.ts | 13 + .../server/src/gitea/languages-provider.ts | 23 + components/server/src/gitea/scopes.ts | 27 + .../server/src/projects/projects-service.ts | 1 + 20 files changed, 2400 insertions(+), 1 deletion(-) create mode 100644 components/dashboard/src/images/gitea.svg create mode 100644 components/server/src/gitea/api.ts create mode 100644 components/server/src/gitea/file-provider.ts create mode 100644 components/server/src/gitea/gitea-auth-provider.ts create mode 100644 components/server/src/gitea/gitea-container-module.ts create mode 100644 components/server/src/gitea/gitea-context-parser.spec.ts create mode 100644 components/server/src/gitea/gitea-context-parser.ts create mode 100644 components/server/src/gitea/gitea-repository-provider.ts create mode 100644 components/server/src/gitea/gitea-token-helper.ts create mode 100644 components/server/src/gitea/gitea-token-validator.ts create mode 100644 components/server/src/gitea/gitea-urls.ts create mode 100644 components/server/src/gitea/languages-provider.ts create mode 100644 components/server/src/gitea/scopes.ts diff --git a/components/dashboard/src/images/gitea.svg b/components/dashboard/src/images/gitea.svg new file mode 100644 index 00000000000000..69ae54daf26ccc --- /dev/null +++ b/components/dashboard/src/images/gitea.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/components/dashboard/src/provider-utils.tsx b/components/dashboard/src/provider-utils.tsx index 227a63a7cc88af..344df8aa118d67 100644 --- a/components/dashboard/src/provider-utils.tsx +++ b/components/dashboard/src/provider-utils.tsx @@ -7,6 +7,7 @@ import bitbucket from './images/bitbucket.svg'; import github from './images/github.svg'; import gitlab from './images/gitlab.svg'; +import gitea from './images/gitea.svg'; import { gitpodHostUrl } from "./service/service"; function iconForAuthProvider(type: string) { @@ -17,6 +18,8 @@ function iconForAuthProvider(type: string) { return ; case "Bitbucket": return ; + case "Gitea": + return ; default: return <>; } diff --git a/components/server/ee/src/auth/host-container-mapping.ts b/components/server/ee/src/auth/host-container-mapping.ts index ae06375629b401..ec081218cb3773 100644 --- a/components/server/ee/src/auth/host-container-mapping.ts +++ b/components/server/ee/src/auth/host-container-mapping.ts @@ -9,6 +9,7 @@ import { HostContainerMapping } from "../../../src/auth/host-container-mapping"; import { gitlabContainerModuleEE } from "../gitlab/container-module"; import { bitbucketContainerModuleEE } from "../bitbucket/container-module"; import { gitHubContainerModuleEE } from "../github/container-module"; +import { giteaContainerModuleEE } from "../gitea/container-module"; @injectable() export class HostContainerMappingEE extends HostContainerMapping { @@ -23,6 +24,8 @@ export class HostContainerMappingEE extends HostContainerMapping { return (modules || []).concat([bitbucketContainerModuleEE]); case "GitHub": return (modules || []).concat([gitHubContainerModuleEE]); + case "Gitea": + return (modules || []).concat([giteaContainerModuleEE]); default: return modules; } diff --git a/components/server/ee/src/container-module.ts b/components/server/ee/src/container-module.ts index c232119d063e16..294d6f8071ee79 100644 --- a/components/server/ee/src/container-module.ts +++ b/components/server/ee/src/container-module.ts @@ -49,6 +49,8 @@ import { GitLabAppSupport } from "./gitlab/gitlab-app-support"; import { Config } from "../../src/config"; import { SnapshotService } from "./workspace/snapshot-service"; import { BitbucketAppSupport } from "./bitbucket/bitbucket-app-support"; +import { GiteaAppSupport } from "./gitea/gitea-app-support"; +import { GiteaApp } from "./prebuilds/gitea-app"; export const productionEEContainerModule = new ContainerModule((bind, unbind, isBound, rebind) => { rebind(Server).to(ServerEE).inSingletonScope(); @@ -68,6 +70,8 @@ export const productionEEContainerModule = new ContainerModule((bind, unbind, is bind(GitLabAppSupport).toSelf().inSingletonScope(); bind(BitbucketApp).toSelf().inSingletonScope(); bind(BitbucketAppSupport).toSelf().inSingletonScope(); + bind(GiteaApp).toSelf().inSingletonScope(); + bind(GiteaAppSupport).toSelf().inSingletonScope(); bind(LicenseEvaluator).toSelf().inSingletonScope(); bind(LicenseKeySource).to(DBLicenseKeySource).inSingletonScope(); diff --git a/components/server/ee/src/server.ts b/components/server/ee/src/server.ts index ee54b423f6eab6..e700e5c210d2fb 100644 --- a/components/server/ee/src/server.ts +++ b/components/server/ee/src/server.ts @@ -12,12 +12,14 @@ import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; import { GitLabApp } from './prebuilds/gitlab-app'; import { BitbucketApp } from './prebuilds/bitbucket-app'; import { GithubApp } from './prebuilds/github-app'; +import { GiteaApp } from './prebuilds/gitea-app'; import { SnapshotService } from './workspace/snapshot-service'; export class ServerEE extends Server { @inject(GithubApp) protected readonly githubApp: GithubApp; @inject(GitLabApp) protected readonly gitLabApp: GitLabApp; @inject(BitbucketApp) protected readonly bitbucketApp: BitbucketApp; + @inject(GiteaApp) protected readonly giteaApp: GiteaApp; @inject(SnapshotService) protected readonly snapshotService: SnapshotService; public async init(app: express.Application) { diff --git a/components/server/src/auth/auth-provider-service.ts b/components/server/src/auth/auth-provider-service.ts index ca2825bc3d5cdf..6181ff7a114ac2 100644 --- a/components/server/src/auth/auth-provider-service.ts +++ b/components/server/src/auth/auth-provider-service.ts @@ -12,6 +12,7 @@ import { Config } from "../config"; import { v4 as uuidv4 } from 'uuid'; import { oauthUrls as githubUrls } from "../github/github-urls"; import { oauthUrls as gitlabUrls } from "../gitlab/gitlab-urls"; +import { oauthUrls as giteaUrls } from "../gitea/gitea-urls"; import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; @injectable() @@ -100,7 +101,7 @@ export class AuthProviderService { } protected initializeNewProvider(newEntry: AuthProviderEntry.NewEntry): AuthProviderEntry { const { host, type, clientId, clientSecret } = newEntry; - const urls = type === "GitHub" ? githubUrls(host) : (type === "GitLab" ? gitlabUrls(host) : undefined); + const urls = type === "GitHub" ? githubUrls(host) : (type === "GitLab" ? gitlabUrls(host) : (type === "Gitea" ? giteaUrls(host) : undefined)); if (!urls) { throw new Error("Unexpected service type."); } diff --git a/components/server/src/auth/host-container-mapping.ts b/components/server/src/auth/host-container-mapping.ts index 3db11e1d051d3e..a1ccb9fd5b8393 100644 --- a/components/server/src/auth/host-container-mapping.ts +++ b/components/server/src/auth/host-container-mapping.ts @@ -9,6 +9,7 @@ import { githubContainerModule } from "../github/github-container-module"; import { gitlabContainerModule } from "../gitlab/gitlab-container-module"; import { genericAuthContainerModule } from "./oauth-container-module"; import { bitbucketContainerModule } from "../bitbucket/bitbucket-container-module"; +import { giteaContainerModule } from "../gitea/gitea-container-module"; @injectable() export class HostContainerMapping { @@ -23,6 +24,8 @@ export class HostContainerMapping { return [genericAuthContainerModule]; case "Bitbucket": return [bitbucketContainerModule]; + case "Gitea": + return [giteaContainerModule]; default: return undefined; } diff --git a/components/server/src/gitea/api.ts b/components/server/src/gitea/api.ts new file mode 100644 index 00000000000000..00ba07b2ccd434 --- /dev/null +++ b/components/server/src/gitea/api.ts @@ -0,0 +1,738 @@ +/** + * Copyright (c) 2020 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 fetch from 'node-fetch'; +import { Octokit, RestEndpointMethodTypes } from "@octokit/rest" +import { OctokitResponse } from "@octokit/types" +import { OctokitOptions } from "@octokit/core/dist-types/types" + +import { Branch, CommitInfo, User } from "@gitpod/gitpod-protocol" +import { GarbageCollectedCache } from "@gitpod/gitpod-protocol/lib/util/garbage-collected-cache"; +import { injectable, inject } from 'inversify'; +import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; +import { GiteaScope } from './scopes'; +import { AuthProviderParams } from '../auth/auth-provider'; +import { GiteaTokenHelper } from './gitea-token-helper'; +import { Deferred } from '@gitpod/gitpod-protocol/lib/util/deferred'; + +import { URL } from 'url'; + +export class GiteaApiError extends Error { + constructor(public readonly response: OctokitResponse) { + super(`Gitea API Error. Status: ${response.status}`); + this.name = 'GiteaApiError'; + } +} +export namespace GiteaApiError { + export function is(error: Error | null): error is GiteaApiError { + return !!error && error.name === 'GiteaApiError'; + } +} + +@injectable() +export class GiteaGraphQlEndpoint { + + @inject(AuthProviderParams) readonly config: AuthProviderParams; + @inject(GiteaTokenHelper) protected readonly tokenHelper: GiteaTokenHelper; + + public async getFileContents(user: User, org: string, name: string, commitish: string, path: string): Promise { + const githubToken = await this.tokenHelper.getTokenWithScopes(user, [/* TODO: check if private_repo has to be required */]); + const token = githubToken.value; + const { host } = this.config; + const urlString = host === 'github.com' ? + `https://raw.githubusercontent.com/${org}/${name}/${commitish}/${path}` : + `https://${host}/${org}/${name}/raw/${commitish}/${path}`; + const response = await fetch(urlString, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + } + }); + if (!response.ok) { + return undefined; + } + return response.text(); + } + + /** + * +----+------------------------------------------+--------------------------------+ + * | | Enterprise | Gitea | + * +----+------------------------------------------+--------------------------------+ + * | v3 | https://[YOUR_HOST]/api/v3 | https://api.github.com | + * | v4 | https://[YOUR_HOST]/api/graphql | https://api.github.com/graphql | + * +----+------------------------------------------+--------------------------------+ + */ + get baseURLv4() { + return (this.config.host === 'github.com') ? 'https://api.github.com/graphql' : `https://${this.config.host}/api/graphql`; + } + + public async runQuery(user: User, query: string, variables?: object): Promise> { + const githubToken = await this.tokenHelper.getTokenWithScopes(user, [/* TODO: check if private_repo has to be required */]); + const token = githubToken.value; + const request = { + query: query.trim(), + variables + }; + return this.runQueryWithToken(token, request); + } + + async runQueryWithToken(token: string, request: object): Promise> { + const response = await fetch(this.baseURLv4, { + method: 'POST', + body: JSON.stringify(request), + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + } + }); + if (!response.ok) { + throw Error(response.statusText); + } + const result: QueryResult = await response.json(); + if (!result.data && result.errors) { + const error = new Error(JSON.stringify({ + request, + result + })); + (error as any).result = result; + throw error; + } + return result; + + } +} + +export interface QueryResult { + data: D + errors?: QueryError[]; +} + +export interface QueryError { + message: string + locations: QueryLocation +} + +export interface QueryLocation { + line: number + column: number +} + +@injectable() +export class GiteaRestApi { + + @inject(AuthProviderParams) readonly config: AuthProviderParams; + @inject(GiteaTokenHelper) protected readonly tokenHelper: GiteaTokenHelper; + protected async create(userOrToken: User | string) { + let token: string | undefined; + if (typeof userOrToken === 'string') { + token = userOrToken; + } else { + const githubToken = await this.tokenHelper.getTokenWithScopes(userOrToken, GiteaScope.Requirements.DEFAULT); + token = githubToken.value; + } + const api = new Octokit(this.getGiteaOptions(token)); + return api; + } + + protected get userAgent() { + return new URL(this.config.oauth!.callBackUrl).hostname; + } + + /** + * +----+------------------------------------------+--------------------------------+ + * | | Enterprise | Gitea | + * +----+------------------------------------------+--------------------------------+ + * | v3 | https://[YOUR_HOST]/api/v3 | https://api.github.com | + * | v4 | https://[YOUR_HOST]/api/graphql | https://api.github.com/graphql | + * +----+------------------------------------------+--------------------------------+ + */ + get baseURL() { + return (this.config.host === 'github.com') ? 'https://api.github.com' : `https://${this.config.host}/api/v3`; + } + + protected getGiteaOptions(auth: string): OctokitOptions { + return { + auth, + request: { + timeout: 5000 + }, + baseUrl: this.baseURL, + userAgent: this.userAgent + }; + } + + public async run(userOrToken: User | string, operation: (api: Octokit) => Promise>): Promise> { + const before = new Date().getTime(); + const userApi = await this.create(userOrToken); + + try { + const response = (await operation(userApi)); + const statusCode = response.status; + if (statusCode !== 200) { + throw new GiteaApiError(response); + } + return response; + } catch (error) { + if (error.status) { + throw new GiteaApiError(error); + } + throw error; + } finally { + log.debug(`Gitea request took ${new Date().getTime() - before} ms`); + } + } + + protected readonly cachedResponses = new GarbageCollectedCache>(120, 150); + public async runWithCache(key: string, user: User, operation: (api: Octokit) => Promise>): Promise> { + const result = new Deferred>(); + const before = new Date().getTime(); + const cacheKey = `${this.config.host}-${key}`; + const cachedResponse = this.cachedResponses.get(cacheKey); + const api = await this.create(user); + + // using hooks in Octokits lifecycle for caching results + // cf. https://github.com/octokit/rest.js/blob/master/docs/src/pages/api/06_hooks.md + api.hook.wrap("request", async (request, options) => { + + // send etag on each request if there is something cached for the given key + if (cachedResponse) { + if (cachedResponse.headers.etag) { + options.headers['If-None-Match'] = cachedResponse.headers.etag; + } + if (cachedResponse.headers["last-modified"]) { + options.headers['If-Modified-Since'] = cachedResponse.headers["last-modified"]; + } + } + + try { + const response = await request(options); + + // on successful responses (HTTP 2xx) we fill the cache + this.cachedResponses.delete(cacheKey); + if (response.headers.etag || response.headers["last-modified"]) { + this.cachedResponses.set(cacheKey, response); + } + result.resolve(response); + return response; + } catch (error) { + + // resolve with cached resource if GH tells us that it's not modified (HTTP 304) + if (error.status === 304 && cachedResponse) { + result.resolve(cachedResponse); + return cachedResponse; + } + this.cachedResponses.delete(cacheKey); + throw error; + } + }); + + try { + await operation(api); + } catch (e) { + result.reject(e); + } finally { + log.debug(`Gitea request took ${new Date().getTime() - before} ms`); + } + return result.promise; + } + + public async getRepository(user: User, params: RestEndpointMethodTypes["repos"]["get"]["parameters"]): Promise { + const key = `getRepository:${params.owner}/${params.owner}:${user.id}`; + const response = await this.runWithCache(key, user, (api) => api.repos.get(params)); + return response.data; + } + + public async getBranch(user: User, params: RestEndpointMethodTypes["repos"]["getBranch"]["parameters"]): Promise { + const key = `getBranch:${params.owner}/${params.owner}/${params.branch}:${user.id}`; + const getBranchResponse = (await this.runWithCache(key, user, (api) => api.repos.getBranch(params))) as RestEndpointMethodTypes["repos"]["getBranch"]["response"]; + const { commit: { sha }, name, _links: { html } } = getBranchResponse.data; + + const commit = await this.getCommit(user, { ...params, ref: sha }); + + return { + name, + commit, + htmlUrl: html + }; + } + + public async getBranches(user: User, params: RestEndpointMethodTypes["repos"]["listBranches"]["parameters"]): Promise { + const key = `getBranches:${params.owner}/${params.owner}:${user.id}`; + const listBranchesResponse = (await this.runWithCache(key, user, (api) => api.repos.listBranches(params))) as RestEndpointMethodTypes["repos"]["listBranches"]["response"]; + + const result: Branch[] = []; + + for (const branch of listBranchesResponse.data) { + const { commit: { sha } } = branch; + const commit = await this.getCommit(user, { ...params, ref: sha }); + + const key = `getBranch:${params.owner}/${params.owner}/${params.branch}:${user.id}`; + const getBranchResponse = (await this.runWithCache(key, user, (api) => api.repos.listBranches(params))) as RestEndpointMethodTypes["repos"]["getBranch"]["response"]; + const htmlUrl = getBranchResponse.data._links.html; + + result.push({ + name: branch.name, + commit, + htmlUrl + }); + } + + return result; + } + + public async getCommit(user: User, params: RestEndpointMethodTypes["repos"]["getCommit"]["parameters"]): Promise { + const key = `getCommit:${params.owner}/${params.owner}/${params.ref}:${user.id}`; + const getCommitResponse = (await this.runWithCache(key, user, (api) => api.repos.getCommit(params))) as RestEndpointMethodTypes["repos"]["getCommit"]["response"]; + const { sha, commit, author } = getCommitResponse.data; + return { + sha, + author: commit.author?.name || "nobody", + authorAvatarUrl: author?.avatar_url, + authorDate: commit.author?.date, + commitMessage: commit.message, + } + } + +} + +export interface GiteaResult extends OctokitResponse { } +export namespace GiteaResult { + export function actualScopes(result: OctokitResponse): string[] { + return (result.headers['x-oauth-scopes'] || "").split(",").map((s: any) => s.trim()); + } + export function mayReadOrgs(result: OctokitResponse): boolean { + return actualScopes(result).some(scope => scope === "read:org" || scope === "user"); + } + export function mayWritePrivate(result: OctokitResponse): boolean { + return actualScopes(result).some(scope => scope === "repo"); + } + export function mayWritePublic(result: OctokitResponse): boolean { + return actualScopes(result).some(scope => scope === "repo" || scope === "public_repo"); + } +} + +// Git +export interface CommitUser { + date: string + name: string + email: string +} + +export interface CommitVerification { + verified: boolean // ??? + reason: "unsigned" // ??? + signature: null // ??? + payload: null // ??? +} + +export interface Commit extends CommitRef { + author: CommitUser + committer: CommitUser + message: string + tree: TreeRef + parents: TreeRef[] + verification: CommitVerification +} + +export interface CommitRef { + sha: string + url: string +} + +export interface Tree extends TreeRef { + tree: TreeNode[] + truncated: boolean +} + +export interface TreeRef { + sha: string + url: string +} + +export interface TreeNode { + path: string + mode: number //"100644", + type: string //"blob", + sha: string //"5f2f16bfff90e6620509c0cf442e7a3586dad8fb", + size: number // 5 ??? + url: string //"https://api.github.com/repos/somefox/test/git/blobs/5f2f16bfff90e6620509c0cf442e7a3586dad8fb" +} + +export interface BlobTreeNode extends TreeNode { + type: "blob" +} + +export interface Blob { + content: string, // always base64 encoded! (https://developer.github.com/v3/git/blobs/#get-a-blob) + encoding: "base64", + url: string, + sha: string, + size: number // bytes? +} + +export interface BranchRef { + name: string + commit: CommitRef + protected: boolean + protection_url?: string +} + +export interface CommitDetails { + url: string + sha: string + node_id: string + html_url: string + comments_url: string + commit: Commit + author: UserRef + committer: UserRef + parents: CommitRef[] +} +export type CommitResponse = CommitDetails[]; + +// Gitea +export type UserEmails = UserEmail[]; +export interface UserEmail { + email: string + verified: boolean + primary: boolean + visibility: "public" | "private" +} + +export interface UserRef { + login: string + id: number + avatar_url: string + gravatar_id: string + url: string + html_url: string + followers_url: string + following_url: string + gists_url: string + starred_url: string + subscriptions_url: string + organizations_url: string + repos_url: string + events_url: string + received_events_url: string + type: "User" | "Organization" + site_admin: boolean +} + + +export interface License { + key: "mit" + name: string + spdx_id: string + url: string + html_url: string +} + +export interface Repository { + id: number + owner: UserRef + name: string + full_name: string + description: string + private: boolean + fork: boolean + url: string + html_url: string + archive_url: string + assignees_url: string + blobs_url: string + branches_url: string + clone_url: string + collaborators_url: string + comments_url: string + commits_url: string + compare_url: string + contents_url: string + contributors_url: string + deployments_url: string + downloads_url: string + events_url: string + forks_url: string + git_commits_url: string + git_refs_url: string + git_tags_url: string + git_url: string + hooks_url: string + issue_comment_url: string + issue_events_url: string + issues_url: string + keys_url: string + labels_url: string + languages_url: string + merges_url: string + milestones_url: string + mirror_url: string + notifications_url: string + pulls_url: string + releases_url: string + ssh_url: string + stargazers_url: string + statuses_url: string + subscribers_url: string + subscription_url: string + svn_url: string + tags_url: string + teams_url: string + trees_url: string + homepage: string + language: null + forks_count: number + stargazers_count: number + watchers_count: number + size: number + default_branch: string + open_issues_count: number + topics: string[] + has_issues: boolean + has_wiki: boolean + has_pages: boolean + has_downloads: boolean + archived: boolean + pushed_at: string + created_at: string + updated_at: string + permissions?: { // No permissions means "no permissions" + admin: boolean + push: boolean + pull: boolean + }, + allow_rebase_merge: boolean + allow_squash_merge: boolean + allow_merge_commit: boolean + subscribers_count: number + network_count: number + license: License + organization: UserRef + parent: Repository + source: Repository +} + +export interface CommitRefInUserRepo { + label: string + ref: string + sha: string + user: UserRef + repo: Repository +} + +export interface PullRequest { + id: number + url: string + html_url: string + diff_url: string + patch_url: string + issue_url: string + commits_url: string + review_comments_url: string + review_comment_url: string + comments_url: string + statuses_url: string + number: number + state: "open" + title: string + body: string + assignee: UserRef + labels: Label[] + milestone: Milestone + locked: boolean + active_lock_reason?: "too heated" | "off-topic" | "resolved" | "spam" // The reason for locking the issue or pull request conversation. Lock will fail if you don't use one of these reasons: ... + created_at: string + updated_at: string + closed_at: string + merged_at: string + head: CommitRefInUserRepo + base: CommitRefInUserRepo + "_links": { + self: Link + html: Link + issue: Link + comments: Link + review_comments: Link + review_comment: Link + commits: Link + statuses: Link + } + user: UserRef + merge_commit_sha: string + merged: boolean + mergeable: boolean + merged_by: UserRef + comments: number + commits: number + additions: number + deletions: number + changed_files: number + maintainer_can_modify: boolean +} + +export interface Link { href: string } + +export interface Label { + id: number + url: string + name: string + description: string + color: string // f29513 + default: boolean +} + +export interface Milestone { + // ??? Not relevant yet +} + +export interface Issue { + id: number + url: string + number: number + title: string + user: UserRef + labels: Label[] + state: "open" | "closed" + html_url: string + pull_request?: { + url: string + html_url: string + diff_url: string + patch_url: string + } + repository_url: string + labels_url: string + comments_url: string + events_url: string + body: string + assignee: null | UserRef + assignees: UserRef[] + milestone: null | Milestone + locked: boolean + active_lock_reason?: "too heated" | "off-topic" | "resolved" | "spam" // The reason for locking the issue or pull request conversation. Lock will fail if you don't use one of these reasons: ... + created_at: string + updated_at: string + closed_at: null | string + closed_by: null | UserRef +} + + +export namespace Issue { + export function isPullRequest(issue: Issue): boolean { + return 'pull_request' in issue; + } +} + +// Contents +export type ContentType = 'file' | 'dir' | 'symlink' | 'submodule'; +export interface ContentMetadata { + type: ContentType + size: number + name: string + path: string + sha: string + url: string + git_url: string + html_url: string + download_url: string | null + _links: { + self: string + git: string + html: string + } +} +export namespace ContentMetadata { + export function is(content: any): content is ContentMetadata { + return 'type' in content + && 'size' in content + && 'name' in content + && 'path' in content + && 'sha' in content; + } +} + +export interface FileMetadata extends ContentMetadata { + type: 'file' +} +export namespace FileMetadata { + export function is(content: any): content is FileMetadata { + return ContentMetadata.is(content) + && content.type === 'file'; + } +} + +export interface DirectoyMetadata extends ContentMetadata { + type: 'dir' +} +export namespace DirectoyMetadata { + export function is(content: any): content is DirectoyMetadata { + return ContentMetadata.is(content) + && content.type === 'dir'; + } +} + +export interface SymlinkMetadata extends ContentMetadata { + type: 'symlink' +} +export namespace SymlinkMetadata { + export function is(content: any): content is SymlinkMetadata { + return ContentMetadata.is(content) + && content.type === 'symlink'; + } +} + +export interface SubmoduleMetadata extends ContentMetadata { + type: 'submodule' +} +export namespace SubmoduleMetadata { + export function is(content: any): content is SubmoduleMetadata { + return ContentMetadata.is(content) + && content.type === 'submodule'; + } +} + +export interface FileContent extends FileMetadata { + encoding: 'base64' + content: string +} +export namespace FileContent { + export function is(content: any): content is FileContent { + return FileMetadata.is(content) + && 'encoding' in content + && 'content' in content; + } +} + +export type DirectoryContent = ContentMetadata[]; +export namespace DirectoryContent { + export function is(content: any): content is DirectoryContent { + return Array.isArray(content); + } +} + +export interface SymlinkContent extends SymlinkMetadata { + target: string +} +export namespace SymlinkContent { + export function is(content: any): content is SymlinkContent { + return SymlinkMetadata.is(content) + && 'target' in content; + } +} + +export interface SubmoduleContent extends SubmoduleMetadata { + submodule_git_url: string +} +export namespace SubmoduleContent { + export function is(content: any): content is SubmoduleContent { + return SubmoduleMetadata.is(content) + && 'submodule_git_url' in content; + } +} + +export type Content = ContentMetadata; +export type Contents = ContentMetadata | DirectoryContent; diff --git a/components/server/src/gitea/file-provider.ts b/components/server/src/gitea/file-provider.ts new file mode 100644 index 00000000000000..993dbe89facb84 --- /dev/null +++ b/components/server/src/gitea/file-provider.ts @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2020 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 { injectable, inject } from 'inversify'; + +import { FileProvider, MaybeContent } from "../repohost/file-provider"; +import { Commit, User, Repository } from "@gitpod/gitpod-protocol" +import { GitHubGraphQlEndpoint, GitHubRestApi } from "./api"; +import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; + +@injectable() +export class GiteaFileProvider implements FileProvider { + + @inject(GitHubGraphQlEndpoint) protected readonly githubGraphQlApi: GitHubGraphQlEndpoint; + @inject(GitHubRestApi) protected readonly githubApi: GitHubRestApi; + + public async getGitpodFileContent(commit: Commit, user: User): Promise { + 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(repository: Repository, revisionOrBranch: string, user: User, path: string): Promise { + const commits = (await this.githubApi.run(user, (gh) => gh.repos.listCommits({ + owner: repository.owner, + repo: repository.name, + sha: revisionOrBranch, + // per_page: 1, // we need just the last one right? + path + }))).data; + + const lastCommit = commits && commits[0]; + if (!lastCommit) { + throw new Error(`File ${path} does not exist in repository ${repository.owner}/${repository.name}`); + } + + return lastCommit.sha; + } + + public async getFileContent(commit: Commit, user: User, path: string) { + if (!commit.revision) { + return undefined; + } + + try { + const contents = await this.githubGraphQlApi.getFileContents(user, commit.repository.owner, commit.repository.name, commit.revision, path); + return contents; + } catch (err) { + log.error(err); + } + } +} diff --git a/components/server/src/gitea/gitea-auth-provider.ts b/components/server/src/gitea/gitea-auth-provider.ts new file mode 100644 index 00000000000000..0e79c71622990c --- /dev/null +++ b/components/server/src/gitea/gitea-auth-provider.ts @@ -0,0 +1,142 @@ +/** + * Copyright (c) 2020 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 { injectable } from 'inversify'; +import * as express from "express" +import { AuthProviderInfo } from '@gitpod/gitpod-protocol'; +import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; +import { GiteaScope } from "./scopes"; +import { AuthUserSetup } from "../auth/auth-provider"; +import { Octokit } from "@octokit/rest" +import { GiteaApiError } from "./api"; +import { GenericAuthProvider } from "../auth/generic-auth-provider"; +import { oauthUrls } from "./gitea-urls"; + +@injectable() +export class GiteaAuthProvider extends GenericAuthProvider { + + get info(): AuthProviderInfo { + return { + ...this.defaultInfo(), + scopes: GiteaScope.All, + requirements: { + default: GiteaScope.Requirements.DEFAULT, + publicRepo: GiteaScope.Requirements.PUBLIC_REPO, + privateRepo: GiteaScope.Requirements.PRIVATE_REPO, + }, + } + } + + /** + * Augmented OAuthConfig for Gitea + */ + protected get oauthConfig() { + const oauth = this.params.oauth!; + const defaultUrls = oauthUrls(this.params.host); + const scopeSeparator = ","; + return { + ...oauth!, + authorizationUrl: oauth.authorizationUrl || defaultUrls.authorizationUrl, + tokenUrl: oauth.tokenUrl || defaultUrls.tokenUrl, + scope: GiteaScope.All.join(scopeSeparator), + scopeSeparator + }; + } + + authorize(req: express.Request, res: express.Response, next: express.NextFunction, scope?: string[]): void { + super.authorize(req, res, next, scope ? scope : GiteaScope.Requirements.DEFAULT); + } + + /** + * +----+------------------------------------------+--------------------------------+ + * | | Enterprise | Gitea | + * +----+------------------------------------------+--------------------------------+ + * | v3 | https://[YOUR_HOST]/api/v3 | https://api.github.com | + * | v4 | https://[YOUR_HOST]/api/graphql | https://api.github.com/graphql | + * +----+------------------------------------------+--------------------------------+ + */ + protected get baseURL() { + return (this.params.host === 'github.com') ? 'https://api.github.com' : `https://${this.params.host}/api/v3`; + } + + protected readAuthUserSetup = async (accessToken: string, _tokenResponse: object) => { + const api = new Octokit({ + auth: accessToken, + request: { + timeout: 5000, + }, + userAgent: this.USER_AGENT, + baseUrl: this.baseURL + }); + const fetchCurrentUser = async () => { + const response = await api.users.getAuthenticated(); + if (response.status !== 200) { + throw new GiteaApiError(response); + } + return response; + } + const fetchUserEmails = async () => { + const response = await api.users.listEmailsForAuthenticated({}); + if (response.status !== 200) { + throw new GiteaApiError(response); + } + return response.data; + } + const currentUserPromise = this.retry(() => fetchCurrentUser()); + const userEmailsPromise = this.retry(() => fetchUserEmails()); + + try { + const [ { data: { id, login, avatar_url, name }, headers }, userEmails ] = await Promise.all([ currentUserPromise, userEmailsPromise ]); + + // https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/ + // e.g. X-OAuth-Scopes: repo, user + const currentScopes = this.normalizeScopes((headers as any)["x-oauth-scopes"] + .split(this.oauthConfig.scopeSeparator!) + .map((s: string) => s.trim()) + ); + + const filterPrimaryEmail = (emails: typeof userEmails) => { + if (this.config.blockNewUsers) { + // if there is any verified email with a domain that is in the blockNewUsersPassList then use this email as primary email + const emailDomainInPasslist = (mail: string) => this.config.blockNewUsers.passlist.some(e => mail.endsWith(`@${e}`)); + const result = emails.filter(e => e.verified).filter(e => emailDomainInPasslist(e.email)) + if (result.length > 0) { + return result[0].email; + } + } + // otherwise use Gitea's primary email as Gitpod's primary email + return emails.filter(e => e.primary)[0].email; + }; + + return { + authUser: { + authId: String(id), + authName: login, + avatarUrl: avatar_url, + name, + primaryEmail: filterPrimaryEmail(userEmails) + }, + currentScopes + } + + } catch (error) { + log.error(`(${this.strategyName}) Reading current user info failed`, error, { accessToken, error }); + throw error; + } + } + + protected normalizeScopes(scopes: string[]) { + const set = new Set(scopes); + if (set.has('repo')) { + set.add('public_repo'); + } + if (set.has('user')) { + set.add('user:email'); + } + return Array.from(set).sort(); + } + +} diff --git a/components/server/src/gitea/gitea-container-module.ts b/components/server/src/gitea/gitea-container-module.ts new file mode 100644 index 00000000000000..cf1c58d469846a --- /dev/null +++ b/components/server/src/gitea/gitea-container-module.ts @@ -0,0 +1,38 @@ +/** + * Copyright (c) 2020 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 { ContainerModule } from "inversify"; +import { AuthProvider } from "../auth/auth-provider"; +import { FileProvider, LanguagesProvider, RepositoryProvider, RepositoryHost } from "../repohost"; +import { IContextParser } from "../workspace/context-parser"; +import { GiteaGraphQlEndpoint, GiteaRestApi } from "./api"; +import { GiteaFileProvider } from "./file-provider"; +import { GiteaAuthProvider } from "./gitea-auth-provider"; +import { GiteaContextParser } from "./gitea-context-parser"; +import { GiteaRepositoryProvider } from "./gitea-repository-provider"; +import { GiteaTokenHelper } from "./gitea-token-helper"; +import { GiteaLanguagesProvider } from "./languages-provider"; +import { IGitTokenValidator } from "../workspace/git-token-validator"; +import { GiteaTokenValidator } from "./gitea-token-validator"; + +export const giteaContainerModule = new ContainerModule((bind, _unbind, _isBound, _rebind) => { + bind(RepositoryHost).toSelf().inSingletonScope(); + bind(GiteaRestApi).toSelf().inSingletonScope(); + bind(GiteaGraphQlEndpoint).toSelf().inSingletonScope(); + bind(GiteaFileProvider).toSelf().inSingletonScope(); + bind(FileProvider).toService(GiteaFileProvider); + bind(GiteaAuthProvider).toSelf().inSingletonScope(); + bind(AuthProvider).toService(GiteaAuthProvider); + bind(GiteaLanguagesProvider).toSelf().inSingletonScope(); + bind(LanguagesProvider).toService(GiteaLanguagesProvider); + bind(GiteaRepositoryProvider).toSelf().inSingletonScope(); + bind(RepositoryProvider).toService(GiteaRepositoryProvider); + bind(GiteaContextParser).toSelf().inSingletonScope(); + bind(IContextParser).toService(GiteaContextParser); + bind(GiteaTokenHelper).toSelf().inSingletonScope(); + bind(GiteaTokenValidator).toSelf().inSingletonScope(); + bind(IGitTokenValidator).toService(GiteaTokenValidator); +}); diff --git a/components/server/src/gitea/gitea-context-parser.spec.ts b/components/server/src/gitea/gitea-context-parser.spec.ts new file mode 100644 index 00000000000000..dd7b9f39af09f2 --- /dev/null +++ b/components/server/src/gitea/gitea-context-parser.spec.ts @@ -0,0 +1,590 @@ +/** + * Copyright (c) 2020 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. + */ + +// Use asyncIterators with es2015 +if (typeof (Symbol as any).asyncIterator === 'undefined') { + (Symbol as any).asyncIterator = Symbol.asyncIterator || Symbol('asyncIterator'); +} +import "reflect-metadata"; + +import { suite, test, timeout, retries } from "mocha-typescript"; +import * as chai from 'chai'; +const expect = chai.expect; + +import { BranchRef, GiteaGraphQlEndpoint } from './api'; +import { NotFoundError } from '../errors'; +import { GiteaContextParser } from './gitea-context-parser'; +import { User } from "@gitpod/gitpod-protocol"; +import { ContainerModule, Container } from "inversify"; +import { Config } from "../config"; +import { DevData } from "../dev/dev-data"; +import { AuthProviderParams } from "../auth/auth-provider"; +import { TokenProvider } from "../user/token-provider"; +import { GiteaTokenHelper } from "./gitea-token-helper"; +import { HostContextProvider } from "../auth/host-context-provider"; +import { skipIfEnvVarNotSet } from "@gitpod/gitpod-protocol/lib/util/skip-if"; + +@suite(timeout(10000), retries(2), skipIfEnvVarNotSet("GITPOD_TEST_TOKEN_GITEA")) +class TestGiteaContextParser { + + protected parser: GiteaContextParser; + protected user: User; + + public before() { + const container = new Container(); + container.load(new ContainerModule((bind, unbind, isBound, rebind) => { + bind(Config).toConstantValue({ + // meant to appease DI, but Config is never actually used here + }); + bind(GiteaContextParser).toSelf().inSingletonScope(); + bind(GiteaGraphQlEndpoint).toSelf().inSingletonScope(); + bind(AuthProviderParams).toConstantValue(TestGiteaContextParser.AUTH_HOST_CONFIG); + bind(GiteaTokenHelper).toSelf().inSingletonScope(); + bind(TokenProvider).toConstantValue({ + getTokenForHost: async (user: User, host: string) => { + return DevData.createGiteaTestToken(); + } + }); + bind(HostContextProvider).toConstantValue(DevData.createDummyHostContextProvider()); + })); + this.parser = container.get(GiteaContextParser); + this.user = DevData.createTestUser(); + } + + static readonly AUTH_HOST_CONFIG: Partial = { + id: "Public-Gitea", + type: "Gitea", + verified: true, + description: "", + icon: "", + host: "github.com", + oauth: "not-used" as any + } + + static readonly BRANCH_TEST = { + name: "test", + commit: { + sha: "testsha", + url: "testurl" + }, + protected: false, + protection_url: "" + }; + + static readonly BRANCH_ISSUE_974 = { + name: "ak/lmcbout-issue_974", + commit: { + sha: "sha974", + url: "url974" + }, + protected: false, + protection_url: "" + }; + + static readonly BLO_BLA_ERROR_DATA = { + host: "github.com", + lastUpdate: undefined, + owner: 'blo', + repoName: 'bla', + userIsOwner: false, + userScopes: ["user:email", "public_repo", "repo"], + }; + + protected getTestBranches(): BranchRef[] { + return [TestGiteaContextParser.BRANCH_TEST, TestGiteaContextParser.BRANCH_ISSUE_974]; + } + + protected get bloBlaErrorData() { + return TestGiteaContextParser.BLO_BLA_ERROR_DATA; + } + + @test public async testErrorContext_01() { + try { + await this.parser.handle({}, this.user, 'https://github.com/blo/bla'); + } catch (e) { + expect(NotFoundError.is(e)); + expect(e.data).to.deep.equal(this.bloBlaErrorData); + } + } + + @test public async testErrorContext_02() { + try { + await this.parser.handle({}, this.user, 'https://github.com/blo/bla/pull/42'); + } catch (e) { + expect(NotFoundError.is(e)); + expect(e.data).to.deep.equal(this.bloBlaErrorData); + } + } + + @test public async testErrorContext_03() { + try { + await this.parser.handle({}, this.user, 'https://github.com/blo/bla/issues/42'); + } catch (e) { + expect(NotFoundError.is(e)); + expect(e.data).to.deep.equal(this.bloBlaErrorData); + } + } + + @test public async testErrorContext_04() { + try { + await this.parser.handle({}, this.user, 'https://github.com/blo/bla/tree/my/branch/path/foo.ts'); + } catch (e) { + expect(NotFoundError.is(e)); + expect(e.data).to.deep.equal(this.bloBlaErrorData); + } + } + + @test public async testTreeContext_01() { + const result = await this.parser.handle({}, this.user, 'https://github.com/eclipse-theia/theia'); + expect(result).to.deep.include({ + "ref": "master", + "refType": "branch", + "path": "", + "isFile": false, + "repository": { + "host": "github.com", + "owner": "eclipse-theia", + "name": "theia", + "cloneUrl": "https://github.com/eclipse-theia/theia.git", + "private": false + }, + "title": "eclipse-theia/theia - master" + }) + } + + @test public async testTreeContext_02() { + const result = await this.parser.handle({}, this.user, 'https://github.com/eclipse-theia/theia/tree/master'); + expect(result).to.deep.include({ + "ref": "master", + "refType": "branch", + "path": "", + "isFile": false, + "repository": { + "host": "github.com", + "owner": "eclipse-theia", + "name": "theia", + "cloneUrl": "https://github.com/eclipse-theia/theia.git", + "private": false + }, + "title": "eclipse-theia/theia - master" + }) + } + + @test public async testTreeContext_03() { + const result = await this.parser.handle({}, this.user, 'https://github.com/eclipse-theia/theia/tree/master/LICENSE'); + expect(result).to.deep.include({ + "ref": "master", + "refType": "branch", + "path": "LICENSE", + "isFile": true, + "repository": { + "host": "github.com", + "owner": "eclipse-theia", + "name": "theia", + "cloneUrl": "https://github.com/eclipse-theia/theia.git", + "private": false + }, + "title": "eclipse-theia/theia - master" + }) + } + + @test public async testTreeContext_04() { + const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/blob/nametest/src/src/server.ts'); + expect(result).to.deep.include({ + "ref": "nametest/src", + "refType": "branch", + "path": "src/server.ts", + "isFile": true, + "repository": { + "host": "github.com", + "owner": "gitpod-io", + "name": "gitpod-test-repo", + "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", + "private": false + }, + "title": "gitpod-io/gitpod-test-repo - nametest/src" + }) + } + + @test public async testTreeContext_05() { + const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/tree/499efbbcb50e7e6e5e2883053f72a34cd5396be3/folder1/folder2'); + expect(result).to.deep.include( + { + "title": "gitpod-io/gitpod-test-repo - 499efbbc:folder1/folder2", + "repository": { + "host": "github.com", + "owner": "gitpod-io", + "name": "gitpod-test-repo", + "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", + "private": false + }, + "revision": "499efbbcb50e7e6e5e2883053f72a34cd5396be3", + "isFile": false, + "path": "folder1/folder2" + } + ) + } + + @test public async testTreeContext_06() { + const result = await this.parser.handle({}, this.user, 'https://github.com/Snailclimb/JavaGuide/blob/940982ebffa5f376b6baddeaf9ed41c91217a6b6/数据结构与算法/常见安全算法(MD5、SHA1、Base64等等)总结.md'); + expect(result).to.deep.include( + { + "title": "Snailclimb/JavaGuide - 940982eb:数据结构与算法/常见安全算法(MD5、SHA1、Base64等等)总结.md", + "repository": { + "host": "github.com", + "owner": "Snailclimb", + "name": "JavaGuide", + "cloneUrl": "https://github.com/Snailclimb/JavaGuide.git", + "private": false + }, + "revision": "940982ebffa5f376b6baddeaf9ed41c91217a6b6", + "isFile": true, + "path": "数据结构与算法/常见安全算法(MD5、SHA1、Base64等等)总结.md" + } + ) + } + + @test public async testTreeContext_07() { + const result = await this.parser.handle({}, this.user, 'https://github.com/eclipse-theia/theia#license'); + expect(result).to.deep.include({ + "ref": "master", + "refType": "branch", + "path": "", + "isFile": false, + "repository": { + "host": "github.com", + "owner": "eclipse-theia", + "name": "theia", + "cloneUrl": "https://github.com/eclipse-theia/theia.git", + "private": false + }, + "title": "eclipse-theia/theia - master" + }) + } + + @test public async testTreeContext_tag_01() { + const result = await this.parser.handle({}, this.user, 'https://github.com/eclipse-theia/theia/tree/v0.1.0'); + expect(result).to.deep.include( + { + "title": "eclipse-theia/theia - v0.1.0", + "repository": { + "host": "github.com", + "owner": "eclipse-theia", + "name": "theia", + "cloneUrl": "https://github.com/eclipse-theia/theia.git", + "private": false + }, + "revision": "f29626847a14ca50dd78483aebaf4b4fe26bcb73", + "isFile": false, + "ref": "v0.1.0", + "refType": "tag" + } + ) + } + + @test public async testReleasesContext_tag_01() { + const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod/releases/tag/v0.9.0'); + expect(result).to.deep.include( + { + "ref": "v0.9.0", + "refType": "tag", + "isFile": false, + "path": "", + "title": "gitpod-io/gitpod - v0.9.0", + "revision": "25ece59c495d525614f28971d41d5708a31bf1e3", + "repository": { + "cloneUrl": "https://github.com/gitpod-io/gitpod.git", + "host": "github.com", + "name": "gitpod", + "owner": "gitpod-io", + "private": false + } + } + ) + } + + @test public async testCommitsContext_01() { + const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/commits/4test'); + expect(result).to.deep.include({ + "ref": "4test", + "refType": "branch", + "path": "", + "isFile": false, + "repository": { + "host": "github.com", + "owner": "gitpod-io", + "name": "gitpod-test-repo", + "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", + "private": false + }, + "title": "gitpod-io/gitpod-test-repo - 4test" + }) + } + + @test public async testCommitContext_01() { + const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/commit/409ac2de49a53d679989d438735f78204f441634'); + expect(result).to.deep.include({ + "ref": "", + "refType": "revision", + "path": "", + "revision": "409ac2de49a53d679989d438735f78204f441634", + "isFile": false, + "repository": { + "host": "github.com", + "owner": "gitpod-io", + "name": "gitpod-test-repo", + "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", + "private": false + }, + "title": "gitpod-io/gitpod-test-repo - Test 3" + }) + } + + @test public async testCommitContext_02_notExistingCommit() { + try { + await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/commit/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); + // ensure that an error has been thrown + chai.assert.fail(); + } catch (e) { + expect(e.message).contains("Couldn't find commit"); + } + } + + @test public async testCommitContext_02_invalidSha() { + try { + await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/commit/invalid'); + // ensure that an error has been thrown + chai.assert.fail(); + } catch (e) { + expect(e.message).contains("Invalid commit ID"); + } + } + + @test public async testPullRequestContext_01() { + const result = await this.parser.handle({}, this.user, 'https://github.com/TypeFox/theia/pull/1'); + expect(result).to.deep.include( + { + "title": "Merge master", + "repository": { + "host": "github.com", + "owner": "eclipse-theia", + "name": "theia", + "cloneUrl": "https://github.com/eclipse-theia/theia.git", + "private": false + }, + "ref": "master", + "refType": "branch", + "nr": 1, + "base": { + "repository": { + "host": "github.com", + "owner": "TypeFox", + "name": "theia", + "cloneUrl": "https://github.com/TypeFox/theia.git", + "private": false, + "fork": { + "parent": { + "cloneUrl": "https://github.com/eclipse-theia/theia.git", + "host": "github.com", + "name": "theia", + "owner": "eclipse-theia", + "private": false + } + } + }, + "ref": "master", + "refType": "branch", + } + } + ) + } + + @test public async testPullRequestthroughIssueContext_04() { + const result = await this.parser.handle({}, this.user, 'https://github.com/TypeFox/theia/issues/1'); + expect(result).to.deep.include( + { + "title": "Merge master", + "repository": { + "host": "github.com", + "owner": "eclipse-theia", + "name": "theia", + "cloneUrl": "https://github.com/eclipse-theia/theia.git", + "private": false + }, + "ref": "master", + "refType": "branch", + "nr": 1, + "base": { + "repository": { + "host": "github.com", + "owner": "TypeFox", + "name": "theia", + "cloneUrl": "https://github.com/TypeFox/theia.git", + "private": false, + "fork": { + "parent": { + "cloneUrl": "https://github.com/eclipse-theia/theia.git", + "host": "github.com", + "name": "theia", + "owner": "eclipse-theia", + "private": false + } + } + }, + "ref": "master", + "refType": "branch", + } + } + ) + } + + @test public async testIssueContext_01() { + const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/issues/42'); + expect(result).to.deep.include( + { + "title": "Test issue web-extension", + "repository": { + "host": "github.com", + "owner": "gitpod-io", + "name": "gitpod-test-repo", + "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", + "private": false + }, + "owner": "gitpod-io", + "nr": 42, + "ref": "1test", + "refType": "branch", + "localBranch": "somefox/test-issue-web-extension-42" + } + ) + } + + @test public async testIssuePageContext() { + const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/issues'); + expect(result).to.deep.include( + { + "title": "gitpod-io/gitpod-test-repo - 1test", + "repository": { + "host": "github.com", + "owner": "gitpod-io", + "name": "gitpod-test-repo", + "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", + "private": false + }, + "ref": "1test", + "refType": "branch", + } + ) + } + + + @test public async testIssueThroughPullRequestContext() { + const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/pull/42'); + expect(result).to.deep.include( + { + "title": "Test issue web-extension", + "repository": { + "host": "github.com", + "owner": "gitpod-io", + "name": "gitpod-test-repo", + "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", + "private": false + }, + "owner": "gitpod-io", + "nr": 42, + "ref": "1test", + "refType": "branch", + "localBranch": "somefox/test-issue-web-extension-42" + } + ) + } + + @test public async testBlobContext_01() { + const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/blob/aba298d5084a817cdde3dd1f26692bc2a216e2b9/test-comment-01.md'); + expect(result).to.deep.include( + { + "title": "gitpod-io/gitpod-test-repo - aba298d5:test-comment-01.md", + "repository": { + "host": "github.com", + "owner": "gitpod-io", + "name": "gitpod-test-repo", + "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", + "private": false + }, + "revision": "aba298d5084a817cdde3dd1f26692bc2a216e2b9", + "isFile": true, + "path": "test-comment-01.md" + } + ) + } + + @test public async testBlobContext_02() { + const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/blob/499efbbcb50e7e6e5e2883053f72a34cd5396be3/folder1/folder2/content2'); + expect(result).to.deep.include( + { + "title": "gitpod-io/gitpod-test-repo - 499efbbc:folder1/folder2/content2", + "repository": { + "host": "github.com", + "owner": "gitpod-io", + "name": "gitpod-test-repo", + "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", + "private": false + }, + "revision": "499efbbcb50e7e6e5e2883053f72a34cd5396be3", + "isFile": true, + "path": "folder1/folder2/content2" + } + ) + } + + @test public async testBlobContextShort_01() { + const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/blob/499efbbc/folder1/folder2/content2'); + expect(result).to.deep.include( + { + "title": "gitpod-io/gitpod-test-repo - 499efbbc:folder1/folder2/content2", + "repository": { + "host": "github.com", + "owner": "gitpod-io", + "name": "gitpod-test-repo", + "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", + "private": false + }, + "revision": "499efbbcb50e7e6e5e2883053f72a34cd5396be3", + "isFile": true, + "path": "folder1/folder2/content2" + } + ) + } + + @test public async testBlobContextShort_02() { + const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/blob/499ef/folder1/folder2/content2'); + expect(result).to.deep.include( + { + "title": "gitpod-io/gitpod-test-repo - 499efbbc:folder1/folder2/content2", + "repository": { + "host": "github.com", + "owner": "gitpod-io", + "name": "gitpod-test-repo", + "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", + "private": false + }, + "revision": "499efbbcb50e7e6e5e2883053f72a34cd5396be3", + "isFile": true, + "path": "folder1/folder2/content2" + } + ) + } + + @test public async testFetchCommitHistory() { + const result = await this.parser.fetchCommitHistory({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo', '409ac2de49a53d679989d438735f78204f441634', 100); + expect(result).to.deep.equal([ + '506e5aed317f28023994ecf8ca6ed91430e9c1a4', + 'f5b041513bfab914b5fbf7ae55788d9835004d76', + ]) + } + +} +module.exports = new TestGiteaContextParser() // Only to circumvent no usage warning :-/ \ No newline at end of file diff --git a/components/server/src/gitea/gitea-context-parser.ts b/components/server/src/gitea/gitea-context-parser.ts new file mode 100644 index 00000000000000..3c1a53988202ac --- /dev/null +++ b/components/server/src/gitea/gitea-context-parser.ts @@ -0,0 +1,480 @@ +/** + * Copyright (c) 2020 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 { injectable, inject } from 'inversify'; + +import { Repository, PullRequestContext, NavigatorContext, IssueContext, User, CommitContext, RefType } from '@gitpod/gitpod-protocol'; +import { GiteaGraphQlEndpoint } from './api'; +import { NotFoundError, UnauthorizedError } from '../errors'; +import { log, LogContext, LogPayload } from '@gitpod/gitpod-protocol/lib/util/logging'; +import { IContextParser, IssueContexts, AbstractContextParser } from '../workspace/context-parser'; +import { GiteaScope } from './scopes'; +import { GiteaTokenHelper } from './gitea-token-helper'; +import { TraceContext } from '@gitpod/gitpod-protocol/lib/util/tracing'; + +@injectable() +export class GiteaContextParser extends AbstractContextParser implements IContextParser { + + @inject(GiteaGraphQlEndpoint) protected readonly githubQueryApi: GiteaGraphQlEndpoint; + @inject(GiteaTokenHelper) protected readonly tokenHelper: GiteaTokenHelper; + + protected get authProviderId() { + return this.config.id; + } + + public async handle(ctx: TraceContext, user: User, contextUrl: string): Promise { + const span = TraceContext.startSpan("GiteaContextParser.handle", ctx); + + try { + const { host, owner, repoName, moreSegments } = await this.parseURL(user, contextUrl); + if (moreSegments.length > 0) { + switch (moreSegments[0]) { + case 'pull': { + return await this.handlePullRequestContext({span}, user, host, owner, repoName, parseInt(moreSegments[1], 10)); + } + case 'tree': + case 'blob': + case 'commits': { + return await this.handleTreeContext({span}, user, host, owner, repoName, moreSegments.slice(1)); + } + case 'releases': { + if (moreSegments.length > 1 && moreSegments[1] === "tag") { + return await this.handleTreeContext({ span }, user, host, owner, repoName, moreSegments.slice(2)); + } + break; + } + case 'issues': { + const issueNr = parseInt(moreSegments[1], 10); + if (isNaN(issueNr)) + break; + return await this.handleIssueContext({span}, user, host, owner, repoName, issueNr); + } + case 'commit': { + return await this.handleCommitContext({span}, user, host, owner, repoName, moreSegments[1]); + } + } + } + return await this.handleDefaultContext({span}, user, host, owner, repoName); + } catch (error) { + if (error && error.code === 401) { + const token = await this.tokenHelper.getCurrentToken(user); + if (token) { + const scopes = token.scopes; + // most likely the token needs to be updated after revoking by user. + throw UnauthorizedError.create(this.config.host, scopes, "http-unauthorized"); + } + // todo@alex: this is very unlikely. is coercing it into a valid case helpful? + // here, GH API responded with a 401 code, and we are missing a token. OTOH, a missing token would not lead to a request. + throw UnauthorizedError.create(this.config.host, GiteaScope.Requirements.PUBLIC_REPO, "missing-identity"); + } + throw error; + } finally { + span.finish(); + } + } + + protected async handleDefaultContext(ctx: TraceContext, user: User, host: string, owner: string, repoName: string): Promise { + const span = TraceContext.startSpan("GiteaContextParser.handleDefaultContext", ctx); + + try { + const result: any = await this.githubQueryApi.runQuery(user, ` + query { + repository(name: "${repoName}", owner: "${owner}") { + ${this.repoProperties()} + defaultBranchRef { + name, + target { + oid + } + }, + } + } + `); + span.log({"request.finished": ""}); + + if (result.data.repository === null) { + throw await NotFoundError.create(await this.tokenHelper.getCurrentToken(user), user, this.config.host, owner, repoName); + } + const defaultBranch = result.data.repository.defaultBranchRef; + const ref = defaultBranch && defaultBranch.name || undefined; + const refType = ref ? "branch" : undefined; + return { + isFile: false, + path: '', + title: `${owner}/${repoName} ${defaultBranch ? '- ' + defaultBranch.name : ''}`, + ref, + refType, + revision: defaultBranch && defaultBranch.target.oid || '', + repository: this.toRepository(host, result.data.repository) + } + } catch (e) { + span.log({error: e}); + throw e; + } finally { + span.finish(); + } + } + + protected async handleTreeContext(ctx: TraceContext, user: User, host: string, owner: string, repoName: string, segments: string[]): Promise { + const span = TraceContext.startSpan("handleTreeContext", ctx); + + try { + if (segments.length === 0) { + return this.handleDefaultContext({span}, user, host, owner, repoName); + } + + for (let i = 1; i <= segments.length; i++) { + const branchNameOrCommitHash = decodeURIComponent(segments.slice(0, i).join('/')); + const couldBeHash = i === 1; + const path = decodeURIComponent(segments.slice(i).join('/')); + // Sanitize path expression to prevent GraphQL injections (e.g. escape any `"` or `\n`). + const pathExpression = JSON.stringify(`${branchNameOrCommitHash}:${path}`); + const result: any = await this.githubQueryApi.runQuery(user, ` + query { + repository(name: "${repoName}", owner: "${owner}") { + ${this.repoProperties()} + path: object(expression: ${pathExpression}) { + ... on Blob { + oid + } + } + commit: object(expression: "${branchNameOrCommitHash}") { + oid + } + ref(qualifiedName: "${branchNameOrCommitHash}") { + name + prefix + target { + oid + } + } + } + } + `); + span.log({"request.finished": ""}); + + const repo = result.data.repository; + if (repo === null) { + throw await NotFoundError.create(await this.tokenHelper.getCurrentToken(user), user, this.config.host, owner, repoName); + } + + const isFile = !!(repo.path && repo.path.oid); + const repository = this.toRepository(host, repo); + if (repo.ref !== null) { + return { + ref: repo.ref.name, + refType: this.toRefType({ userId: user.id }, { host, owner, repoName }, repo.ref.prefix), + isFile, + path, + title: `${owner}/${repoName} - ${repo.ref.name}`, + revision: repo.ref.target.oid, + repository + }; + } + if (couldBeHash && repo.commit !== null) { + const revision = repo.commit.oid as string; + const shortRevision = revision.substr(0, 8); + return { + isFile, + path, + title: `${owner}/${repoName} - ${shortRevision}:${path}`, + revision, + repository + }; + } + } + throw new Error(`Couldn't find branch and path for ${segments.join('/')} in repo ${owner}/${repoName}.`); + } catch (e) { + span.log({error: e}); + throw e; + } finally { + span.finish(); + } + } + + protected toRefType(logCtx: LogContext, logPayload: LogPayload, refPrefix: string): RefType { + switch (refPrefix) { + case 'refs/tags/': { + return 'tag'; + } + case 'refs/heads/': { + return 'branch'; + } + default: { + log.warn(logCtx, "Unexpected refPrefix: " + refPrefix, logPayload); + return 'branch'; + } + } + } + + protected async handleCommitContext(ctx: TraceContext, user: User, host: string, owner: string, repoName: string, sha: string): Promise { + const span = TraceContext.startSpan("handleCommitContext", ctx); + + if (sha.length != 40) { + throw new Error(`Invalid commit ID ${sha}.`); + } + + try { + const result: any = await this.githubQueryApi.runQuery(user, ` + query { + repository(name: "${repoName}", owner: "${owner}") { + object(oid: "${sha}") { + oid, + ... on Commit { + messageHeadline + } + } + ${this.repoProperties()} + defaultBranchRef { + name, + target { + oid + } + }, + } + } + `); + span.log({"request.finished": ""}); + + if (result.data.repository === null) { + throw await NotFoundError.create(await this.tokenHelper.getCurrentToken(user), user, this.config.host, owner, repoName); + } + + const commit = result.data.repository.object; + if (commit === null || commit.message === null) { + throw new Error(`Couldn't find commit ${sha} in repository ${owner}/${repoName}.`); + } + + return { + path: '', + ref: '', + refType: 'revision', + isFile: false, + title: `${owner}/${repoName} - ${commit.messageHeadline}`, + owner, + revision: sha, + repository: this.toRepository(host, result.data.repository), + }; + } catch (e) { + span.log({"error": e}); + throw e; + } finally { + span.finish(); + } + } + + protected async handlePullRequestContext(ctx: TraceContext, user: User, host: string, owner: string, repoName: string, pullRequestNr: number, tryIssueContext: boolean = true): Promise { + const span = TraceContext.startSpan("handlePullRequestContext", ctx); + + try { + const result: any = await this.githubQueryApi.runQuery(user, ` + query { + repository(name: "${repoName}", owner: "${owner}") { + pullRequest(number: ${pullRequestNr}) { + title + headRef { + name + repository { + ${this.repoProperties()} + } + target { + oid + } + } + baseRef { + name + repository { + ${this.repoProperties()} + } + target { + oid + } + } + } + } + } + `); + span.log({"request.finished": ""}); + + if (result.data.repository === null) { + throw await NotFoundError.create(await this.tokenHelper.getCurrentToken(user), user, this.config.host, owner, repoName); + } + const pr = result.data.repository.pullRequest; + if (pr === null) { + log.info(`PR ${owner}/${repoName}/pull/${pullRequestNr} not found. Trying issue context.`); + if (tryIssueContext) { + return this.handleIssueContext({span}, user, host, owner, repoName, pullRequestNr, false); + } else { + throw new Error(`Could not find issue or pull request #${pullRequestNr} in repository ${owner}/${repoName}.`) + } + } + if (pr.headRef === null) { + throw new Error(`Could not open pull request ${owner}/${repoName}#${pullRequestNr}. Source branch may have been removed.`); + } + return { + title: pr.title, + repository: this.toRepository(host, pr.headRef.repository), + ref: pr.headRef.name, + refType: "branch", + revision: pr.headRef.target.oid, + nr: pullRequestNr, + base: { + repository: this.toRepository(host, pr.baseRef.repository), + ref: pr.baseRef.name, + refType: "branch", + } + }; + } catch (e) { + span.log({"error": e}); + throw e; + } finally { + span.finish(); + } + } + + protected async handleIssueContext(ctx: TraceContext, user: User, host: string, owner: string, repoName: string, issueNr: number, tryPullrequestContext: boolean = true): Promise { + const span = TraceContext.startSpan("handleIssueContext", ctx); + + try { + const result: any = await this.githubQueryApi.runQuery(user, ` + query { + repository(name: "${repoName}", owner: "${owner}") { + issue(number: ${issueNr}) { + title + } + ${this.repoProperties()} + defaultBranchRef { + name, + target { + oid + } + }, + } + } + `); + span.log({"request.finished": ""}); + + if (result.data.repository === null) { + throw await NotFoundError.create(await this.tokenHelper.getCurrentToken(user), user, this.config.host, owner, repoName); + } + const issue = result.data.repository.issue; + if (issue === null) { + if (tryPullrequestContext) { + log.info(`Issue ${owner}/${repoName}/issues/${issueNr} not found. Trying issue context.`); + return this.handlePullRequestContext({span}, user, host, owner, repoName, issueNr, false); + } else { + throw new Error(`Couldn't find issue or pull request #${issueNr} in repository ${owner}/${repoName}.`) + } + } + const branchRef = result.data.repository.defaultBranchRef; + const ref = branchRef && branchRef.name || undefined; + const refType = ref ? "branch" : undefined; + + + return { + title: result.data.repository.issue.title, + owner, + nr: issueNr, + localBranch: IssueContexts.toBranchName(user, result.data.repository.issue.title as string || '', issueNr), + ref, + refType, + revision: branchRef && branchRef.target.oid || '', + repository: this.toRepository(host, result.data.repository) + }; + } catch (e) { + span.log({error: e}); + throw e; + } finally { + span.finish(); + } + } + + protected toRepository(host: string, repoQueryResult: any): Repository { + if (repoQueryResult === null) { + throw new Error('Unknown repository.'); + } + const result: Repository = { + cloneUrl: repoQueryResult.url + '.git', + host, + name: repoQueryResult.name, + owner: repoQueryResult.owner.login, + private: !!repoQueryResult.isPrivate + } + if (repoQueryResult.parent !== null) { + result.fork = { + parent: this.toRepository(host, repoQueryResult.parent) + }; + } + + return result; + } + + protected repoProperties(parents: number = 10): string { + return ` + name, + owner { + login + } + url, + isPrivate, + ${parents > 0 ? `parent { + ${this.repoProperties(parents - 1)} + }` : ''} + `; + } + + public async fetchCommitHistory(ctx: TraceContext, user: User, contextUrl: string, sha: string, maxDepth: number): Promise { + const span = TraceContext.startSpan("GiteaContextParser.fetchCommitHistory", ctx); + + try { + if (sha.length != 40) { + throw new Error(`Invalid commit ID ${sha}.`); + } + + // TODO(janx): To get more results than Gitea API's max page size (seems to be 100), pagination should be handled. + // These additional history properties may be helfpul: + // totalCount, + // pageInfo { + // haxNextPage, + // }, + const { owner, repoName } = await this.parseURL(user, contextUrl); + const result: any = await this.githubQueryApi.runQuery(user, ` + query { + repository(name: "${repoName}", owner: "${owner}") { + object(oid: "${sha}") { + ... on Commit { + history(first: ${maxDepth}) { + edges { + node { + oid + } + } + } + } + } + } + } + `); + span.log({"request.finished": ""}); + + if (result.data.repository === null) { + throw await NotFoundError.create(await this.tokenHelper.getCurrentToken(user), user, this.config.host, owner, repoName); + } + + const commit = result.data.repository.object; + if (commit === null) { + throw new Error(`Couldn't find commit ${sha} in repository ${owner}/${repoName}.`); + } + + return commit.history.edges.slice(1).map((e: any) => e.node.oid) || []; + } catch (e) { + span.log({"error": e}); + throw e; + } finally { + span.finish(); + } + } +} diff --git a/components/server/src/gitea/gitea-repository-provider.ts b/components/server/src/gitea/gitea-repository-provider.ts new file mode 100644 index 00000000000000..edf1fa48f6ce09 --- /dev/null +++ b/components/server/src/gitea/gitea-repository-provider.ts @@ -0,0 +1,131 @@ +/** + * Copyright (c) 2020 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 { injectable, inject } from 'inversify'; + +import { User, Repository } from "@gitpod/gitpod-protocol" +import { GiteaGraphQlEndpoint, GiteaRestApi } from "./api"; +import { RepositoryProvider } from '../repohost/repository-provider'; +import { RepoURL } from '../repohost/repo-url'; +import { Branch, CommitInfo } from '@gitpod/gitpod-protocol/src/protocol'; + +@injectable() +export class GithubRepositoryProvider implements RepositoryProvider { + @inject(GiteaRestApi) protected readonly github: GiteaRestApi; + @inject(GiteaGraphQlEndpoint) protected readonly githubQueryApi: GiteaGraphQlEndpoint; + + async getRepo(user: User, owner: string, repo: string): Promise { + const repository = await this.github.getRepository(user, { owner, repo }); + const cloneUrl = repository.clone_url; + const host = RepoURL.parseRepoUrl(cloneUrl)!.host; + const description = repository.description; + const avatarUrl = repository.owner.avatar_url; + const webUrl = repository.html_url; + const defaultBranch = repository.default_branch; + return { host, owner, name: repo, cloneUrl, description, avatarUrl, webUrl, defaultBranch }; + } + + async getBranch(user: User, owner: string, repo: string, branch: string): Promise { + const result = await this.github.getBranch(user, { repo, owner, branch }); + return result; + } + + async getBranches(user: User, owner: string, repo: string): Promise { + const branches: Branch[] = []; + let endCursor: string | undefined; + let hasNextPage: boolean = true; + + while (hasNextPage) { + const result: any = await this.githubQueryApi.runQuery(user, ` + query { + repository(name: "${repo}", owner: "${owner}") { + refs(refPrefix: "refs/heads/", orderBy: {field: TAG_COMMIT_DATE, direction: ASC}, first: 100 ${endCursor ? `, after: "${endCursor}"` : ""}) { + nodes { + name + target { + ... on Commit { + oid + history(first: 1) { + nodes { + messageHeadline + committedDate + oid + authoredDate + tree { + id + } + treeUrl + author { + avatarUrl + name + date + } + } + } + } + } + } + pageInfo { + endCursor + hasNextPage + hasPreviousPage + startCursor + } + totalCount + } + } + } + `); + + endCursor = result.data.repository?.refs?.pageInfo?.endCursor; + hasNextPage = result.data.repository?.refs?.pageInfo?.hasNextPage; + + const nodes = result.data.repository?.refs?.nodes; + for (const node of (nodes || [])) { + + branches.push({ + name: node.name, + commit: { + sha: node.target.oid, + commitMessage: node.target.history.nodes[0].messageHeadline, + author: node.target.history.nodes[0].author.name, + authorAvatarUrl: node.target.history.nodes[0].author.avatarUrl, + authorDate: node.target.history.nodes[0].author.date, + }, + htmlUrl: node.target.history.nodes[0].treeUrl.replace(node.target.oid, node.name) + }); + } + } + return branches; + } + + async getCommitInfo(user: User, owner: string, repo: string, ref: string): Promise { + const commit = await this.github.getCommit(user, { repo, owner, ref }); + return commit; + } + + async getUserRepos(user: User): Promise { + // Hint: Use this to get richer results: + // node { + // nameWithOwner + // shortDescriptionHTML(limit: 120) + // url + // } + const result: any = await this.githubQueryApi.runQuery(user, ` + query { + viewer { + repositoriesContributedTo(includeUserRepositories: true, first: 100) { + edges { + node { + url + } + } + } + } + }`); + return (result.data.viewer?.repositoriesContributedTo?.edges || []).map((edge: any) => edge.node.url) + } +} diff --git a/components/server/src/gitea/gitea-token-helper.ts b/components/server/src/gitea/gitea-token-helper.ts new file mode 100644 index 00000000000000..30753c54aa3d68 --- /dev/null +++ b/components/server/src/gitea/gitea-token-helper.ts @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2020 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 { injectable, inject } from "inversify"; +import { AuthProviderParams } from "../auth/auth-provider"; +import { User, Token } from "@gitpod/gitpod-protocol"; +import { UnauthorizedError } from "../errors"; +import { GiteaScope } from "./scopes"; +import { TokenProvider } from "../user/token-provider"; + +@injectable() +export class GiteaTokenHelper { + @inject(AuthProviderParams) readonly config: AuthProviderParams; + @inject(TokenProvider) protected readonly tokenProvider: TokenProvider; + + async getCurrentToken(user: User) { + try { + return await this.getTokenWithScopes(user, [/* any scopes */]); + } catch { + // no token + } + } + + async getTokenWithScopes(user: User, requiredScopes: string[]) { + const { host } = this.config; + try { + const token = await this.tokenProvider.getTokenForHost(user, host); + if (this.containsScopes(token, requiredScopes)) { + return token; + } + } catch { + // no token + } + if (requiredScopes.length === 0) { + requiredScopes = GiteaScope.Requirements.DEFAULT + } + throw UnauthorizedError.create(host, requiredScopes, "missing-identity"); + } + protected containsScopes(token: Token, wantedScopes: string[] | undefined): boolean { + const wantedSet = new Set(wantedScopes); + const currentScopes = [...token.scopes]; + if (currentScopes.some(s => s === GiteaScope.PRIVATE)) { + currentScopes.push(GiteaScope.PUBLIC); // normalize private_repo, which includes public_repo + } + currentScopes.forEach(s => wantedSet.delete(s)); + return wantedSet.size === 0; + } +} diff --git a/components/server/src/gitea/gitea-token-validator.ts b/components/server/src/gitea/gitea-token-validator.ts new file mode 100644 index 00000000000000..3c557f6db051b9 --- /dev/null +++ b/components/server/src/gitea/gitea-token-validator.ts @@ -0,0 +1,89 @@ +/** + * Copyright (c) 2021 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 { inject, injectable } from "inversify"; +import { CheckWriteAccessResult, IGitTokenValidator, IGitTokenValidatorParams } from "../workspace/git-token-validator"; +import { GiteaApiError, GiteaGraphQlEndpoint, GiteaRestApi, GiteaResult } from "./api"; +import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; + +@injectable() +export class GiteaTokenValidator implements IGitTokenValidator { + @inject(GiteaRestApi) githubRestApi: GiteaRestApi; + @inject(GiteaGraphQlEndpoint) githubGraphQLEndpoint: GiteaGraphQlEndpoint; + + async checkWriteAccess(params: IGitTokenValidatorParams): Promise { + + const { token, repoFullName } = params; + + const parsedRepoName = this.parseGiteaRepoName(repoFullName); + if (!parsedRepoName) { + throw new Error(`Could not parse repo name: ${repoFullName}`); + } + let repo; + try { + repo = await this.githubRestApi.run(token, api => api.repos.get(parsedRepoName)); + } catch (error) { + if (GiteaApiError.is(error) && error.response?.status === 404) { + return { found: false }; + } + log.error('Error getting repo information from Gitea', error, { repoFullName, parsedRepoName }) + return { found: false, error }; + } + + const mayWritePrivate = GiteaResult.mayWritePrivate(repo); + const mayWritePublic = GiteaResult.mayWritePublic(repo); + + const isPrivateRepo = repo.data.private; + let writeAccessToRepo = repo.data.permissions?.push; + const inOrg = repo.data.owner?.type === "Organization"; + + if (inOrg) { + // if this repository belongs to an organization and Gitpod is not authorized, + // we're not allowed to list repositories using this a token issues for + // Gitpod's OAuth App. + + const request = { + query: ` + query { + organization(login: "${parsedRepoName.owner}") { + repositories(first: 1) { + totalCount + } + } + } + `.trim() + }; + try { + await this.githubGraphQLEndpoint.runQueryWithToken(token, request) + } catch (error) { + const errors = error.result?.errors; + if (errors && errors[0] && (errors[0] as any)["type"] === "FORBIDDEN") { + writeAccessToRepo = false; + } else { + log.error('Error getting organization information from Gitea', error, { org: parsedRepoName.owner }) + throw error; + } + } + } + return { + found: true, + isPrivateRepo, + writeAccessToRepo, + mayWritePrivate, + mayWritePublic + } + } + + protected parseGiteaRepoName(repoFullName: string) { + const parts = repoFullName.split("/"); + if (parts.length === 2) { + return { + owner: parts[0], + repo: parts[1] + } + } + } +} \ No newline at end of file diff --git a/components/server/src/gitea/gitea-urls.ts b/components/server/src/gitea/gitea-urls.ts new file mode 100644 index 00000000000000..00b61a65814570 --- /dev/null +++ b/components/server/src/gitea/gitea-urls.ts @@ -0,0 +1,13 @@ +/** + * Copyright (c) 2020 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. + */ + + +export function oauthUrls(host: string) { + return { + authorizationUrl: `https://${host}/login/oauth/authorize`, + tokenUrl: `https://${host}/login/oauth/access_token`, + } +} diff --git a/components/server/src/gitea/languages-provider.ts b/components/server/src/gitea/languages-provider.ts new file mode 100644 index 00000000000000..aa11863850a867 --- /dev/null +++ b/components/server/src/gitea/languages-provider.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2020 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 { injectable, inject } from 'inversify'; + +import { User, Repository } from "@gitpod/gitpod-protocol" +import { GiteaRestApi } from "./api"; +import { LanguagesProvider } from '../repohost/languages-provider'; + +@injectable() +export class GiteaLanguagesProvider implements LanguagesProvider { + + @inject(GiteaRestApi) protected readonly gitea: GiteaRestApi; + + async getLanguages(repository: Repository, user: User): Promise { + const languages = await this.gitea.run(user, (gitea) => gitea.repos.listLanguages({ owner: repository.owner, repo: repository.name })); + + return languages.data; + } +} diff --git a/components/server/src/gitea/scopes.ts b/components/server/src/gitea/scopes.ts new file mode 100644 index 00000000000000..4199510caba76d --- /dev/null +++ b/components/server/src/gitea/scopes.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2020 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. + */ + + +export namespace GiteaScope { + export const EMAIL = "user:email"; + export const READ_USER = "read:user"; + export const PUBLIC = "public_repo"; + export const PRIVATE = "repo"; + export const ORGS = "read:org"; + export const WORKFLOW = "workflow"; + + export const All = [EMAIL, READ_USER, PUBLIC, PRIVATE, ORGS, WORKFLOW]; + export const Requirements = { + /** + * Minimal required permission. + * Gitea's API is not restricted any further. + */ + DEFAULT: [EMAIL], + + PUBLIC_REPO: [PUBLIC], + PRIVATE_REPO: [PRIVATE], + } +} diff --git a/components/server/src/projects/projects-service.ts b/components/server/src/projects/projects-service.ts index b0d7bf93ea7dfe..446ab94ee019d1 100644 --- a/components/server/src/projects/projects-service.ts +++ b/components/server/src/projects/projects-service.ts @@ -137,6 +137,7 @@ export class ProjectsService { const parsedUrl = RepoURL.parseRepoUrl(project.cloneUrl); const hostContext = parsedUrl?.host ? this.hostContextProvider.get(parsedUrl?.host) : undefined; const type = hostContext && hostContext.authProvider.info.authProviderType; + // TODO: handle gitea if (type === "GitLab" || type === "Bitbucket") { const repositoryService = hostContext?.services?.repositoryService; if (repositoryService) { From 1900190edcac39dcf32b58f8e4955628e3102260 Mon Sep 17 00:00:00 2001 From: Anbraten Date: Wed, 9 Feb 2022 16:45:26 +0000 Subject: [PATCH 02/16] adjust gitea api --- components/server/package.json | 1 + components/server/src/gitea/api.ts | 127 +++-------- components/server/src/gitea/file-provider.ts | 9 +- .../server/src/gitea/gitea-auth-provider.ts | 10 +- .../src/gitea/gitea-container-module.ts | 3 +- .../src/gitea/gitea-repository-provider.ts | 32 +-- .../server/src/gitea/gitea-token-validator.ts | 3 +- yarn.lock | 204 +++++++++++++++++- 8 files changed, 242 insertions(+), 147 deletions(-) diff --git a/components/server/package.json b/components/server/package.json index f01943bf88d8b2..f3e8ac76cfb177 100644 --- a/components/server/package.json +++ b/components/server/package.json @@ -56,6 +56,7 @@ "express-mysql-session": "^2.1.0", "express-session": "^1.15.6", "fs-extra": "^10.0.0", + "gitea-js": "1.0.1", "google-protobuf": "^3.18.0-rc.2", "heapdump": "^0.3.15", "inversify": "^5.0.1", diff --git a/components/server/src/gitea/api.ts b/components/server/src/gitea/api.ts index 00ba07b2ccd434..b1d29504f8515a 100644 --- a/components/server/src/gitea/api.ts +++ b/components/server/src/gitea/api.ts @@ -4,10 +4,7 @@ * See License-AGPL.txt in the project root for license information. */ -import fetch from 'node-fetch'; -import { Octokit, RestEndpointMethodTypes } from "@octokit/rest" -import { OctokitResponse } from "@octokit/types" -import { OctokitOptions } from "@octokit/core/dist-types/types" +import { Api } from "gitea-js" import { Branch, CommitInfo, User } from "@gitpod/gitpod-protocol" import { GarbageCollectedCache } from "@gitpod/gitpod-protocol/lib/util/garbage-collected-cache"; @@ -32,80 +29,6 @@ export namespace GiteaApiError { } } -@injectable() -export class GiteaGraphQlEndpoint { - - @inject(AuthProviderParams) readonly config: AuthProviderParams; - @inject(GiteaTokenHelper) protected readonly tokenHelper: GiteaTokenHelper; - - public async getFileContents(user: User, org: string, name: string, commitish: string, path: string): Promise { - const githubToken = await this.tokenHelper.getTokenWithScopes(user, [/* TODO: check if private_repo has to be required */]); - const token = githubToken.value; - const { host } = this.config; - const urlString = host === 'github.com' ? - `https://raw.githubusercontent.com/${org}/${name}/${commitish}/${path}` : - `https://${host}/${org}/${name}/raw/${commitish}/${path}`; - const response = await fetch(urlString, { - method: 'GET', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - } - }); - if (!response.ok) { - return undefined; - } - return response.text(); - } - - /** - * +----+------------------------------------------+--------------------------------+ - * | | Enterprise | Gitea | - * +----+------------------------------------------+--------------------------------+ - * | v3 | https://[YOUR_HOST]/api/v3 | https://api.github.com | - * | v4 | https://[YOUR_HOST]/api/graphql | https://api.github.com/graphql | - * +----+------------------------------------------+--------------------------------+ - */ - get baseURLv4() { - return (this.config.host === 'github.com') ? 'https://api.github.com/graphql' : `https://${this.config.host}/api/graphql`; - } - - public async runQuery(user: User, query: string, variables?: object): Promise> { - const githubToken = await this.tokenHelper.getTokenWithScopes(user, [/* TODO: check if private_repo has to be required */]); - const token = githubToken.value; - const request = { - query: query.trim(), - variables - }; - return this.runQueryWithToken(token, request); - } - - async runQueryWithToken(token: string, request: object): Promise> { - const response = await fetch(this.baseURLv4, { - method: 'POST', - body: JSON.stringify(request), - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${token}` - } - }); - if (!response.ok) { - throw Error(response.statusText); - } - const result: QueryResult = await response.json(); - if (!result.data && result.errors) { - const error = new Error(JSON.stringify({ - request, - result - })); - (error as any).result = result; - throw error; - } - return result; - - } -} - export interface QueryResult { data: D errors?: QueryError[]; @@ -134,7 +57,7 @@ export class GiteaRestApi { const githubToken = await this.tokenHelper.getTokenWithScopes(userOrToken, GiteaScope.Requirements.DEFAULT); token = githubToken.value; } - const api = new Octokit(this.getGiteaOptions(token)); + const api = new Api(this.getGiteaOptions(token)); return api; } @@ -143,15 +66,14 @@ export class GiteaRestApi { } /** - * +----+------------------------------------------+--------------------------------+ - * | | Enterprise | Gitea | - * +----+------------------------------------------+--------------------------------+ - * | v3 | https://[YOUR_HOST]/api/v3 | https://api.github.com | - * | v4 | https://[YOUR_HOST]/api/graphql | https://api.github.com/graphql | - * +----+------------------------------------------+--------------------------------+ + * +----+-------------------------------------+ + * | | Gitea | + * +----+-------------------------------------+ + * | v1 | https://[YOUR_HOST]/api/v1 | + * +----+-------------------------------------+ */ get baseURL() { - return (this.config.host === 'github.com') ? 'https://api.github.com' : `https://${this.config.host}/api/v3`; + return `https://${this.config.host}/api/v1`; } protected getGiteaOptions(auth: string): OctokitOptions { @@ -297,24 +219,27 @@ export class GiteaRestApi { } } -} - -export interface GiteaResult extends OctokitResponse { } -export namespace GiteaResult { - export function actualScopes(result: OctokitResponse): string[] { - return (result.headers['x-oauth-scopes'] || "").split(",").map((s: any) => s.trim()); - } - export function mayReadOrgs(result: OctokitResponse): boolean { - return actualScopes(result).some(scope => scope === "read:org" || scope === "user"); - } - export function mayWritePrivate(result: OctokitResponse): boolean { - return actualScopes(result).some(scope => scope === "repo"); - } - export function mayWritePublic(result: OctokitResponse): boolean { - return actualScopes(result).some(scope => scope === "repo" || scope === "public_repo"); + public async getFileContents(user, repositoryOwner, repositoryName, revision, path): Promise { + return []; } } +// export interface GiteaResult extends OctokitResponse { } +// export namespace GiteaResult { +// export function actualScopes(result: OctokitResponse): string[] { +// return (result.headers['x-oauth-scopes'] || "").split(",").map((s: any) => s.trim()); +// } +// export function mayReadOrgs(result: OctokitResponse): boolean { +// return actualScopes(result).some(scope => scope === "read:org" || scope === "user"); +// } +// export function mayWritePrivate(result: OctokitResponse): boolean { +// return actualScopes(result).some(scope => scope === "repo"); +// } +// export function mayWritePublic(result: OctokitResponse): boolean { +// return actualScopes(result).some(scope => scope === "repo" || scope === "public_repo"); +// } +// } + // Git export interface CommitUser { date: string diff --git a/components/server/src/gitea/file-provider.ts b/components/server/src/gitea/file-provider.ts index 993dbe89facb84..3bc67196fe4fd6 100644 --- a/components/server/src/gitea/file-provider.ts +++ b/components/server/src/gitea/file-provider.ts @@ -8,14 +8,13 @@ import { injectable, inject } from 'inversify'; import { FileProvider, MaybeContent } from "../repohost/file-provider"; import { Commit, User, Repository } from "@gitpod/gitpod-protocol" -import { GitHubGraphQlEndpoint, GitHubRestApi } from "./api"; +import { GiteaRestApi } from "./api"; import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; @injectable() export class GiteaFileProvider implements FileProvider { - @inject(GitHubGraphQlEndpoint) protected readonly githubGraphQlApi: GitHubGraphQlEndpoint; - @inject(GitHubRestApi) protected readonly githubApi: GitHubRestApi; + @inject(GiteaRestApi) protected readonly giteaApi: GiteaRestApi; public async getGitpodFileContent(commit: Commit, user: User): Promise { const yamlVersion1 = await Promise.all([ @@ -26,7 +25,7 @@ export class GiteaFileProvider implements FileProvider { } public async getLastChangeRevision(repository: Repository, revisionOrBranch: string, user: User, path: string): Promise { - const commits = (await this.githubApi.run(user, (gh) => gh.repos.listCommits({ + const commits = (await this.giteaApi.run(user, (gh) => gh.repos.listCommits({ owner: repository.owner, repo: repository.name, sha: revisionOrBranch, @@ -48,7 +47,7 @@ export class GiteaFileProvider implements FileProvider { } try { - const contents = await this.githubGraphQlApi.getFileContents(user, commit.repository.owner, commit.repository.name, commit.revision, path); + const contents = await this.giteaApi.getFileContents(user, commit.repository.owner, commit.repository.name, commit.revision, path); return contents; } catch (err) { log.error(err); diff --git a/components/server/src/gitea/gitea-auth-provider.ts b/components/server/src/gitea/gitea-auth-provider.ts index 0e79c71622990c..a8f82873726e77 100644 --- a/components/server/src/gitea/gitea-auth-provider.ts +++ b/components/server/src/gitea/gitea-auth-provider.ts @@ -50,16 +50,8 @@ export class GiteaAuthProvider extends GenericAuthProvider { super.authorize(req, res, next, scope ? scope : GiteaScope.Requirements.DEFAULT); } - /** - * +----+------------------------------------------+--------------------------------+ - * | | Enterprise | Gitea | - * +----+------------------------------------------+--------------------------------+ - * | v3 | https://[YOUR_HOST]/api/v3 | https://api.github.com | - * | v4 | https://[YOUR_HOST]/api/graphql | https://api.github.com/graphql | - * +----+------------------------------------------+--------------------------------+ - */ protected get baseURL() { - return (this.params.host === 'github.com') ? 'https://api.github.com' : `https://${this.params.host}/api/v3`; + return `https://${this.params.host}/api/v1`; } protected readAuthUserSetup = async (accessToken: string, _tokenResponse: object) => { diff --git a/components/server/src/gitea/gitea-container-module.ts b/components/server/src/gitea/gitea-container-module.ts index cf1c58d469846a..51eb97b3d9f055 100644 --- a/components/server/src/gitea/gitea-container-module.ts +++ b/components/server/src/gitea/gitea-container-module.ts @@ -8,7 +8,7 @@ import { ContainerModule } from "inversify"; import { AuthProvider } from "../auth/auth-provider"; import { FileProvider, LanguagesProvider, RepositoryProvider, RepositoryHost } from "../repohost"; import { IContextParser } from "../workspace/context-parser"; -import { GiteaGraphQlEndpoint, GiteaRestApi } from "./api"; +import { GiteaRestApi } from "./api"; import { GiteaFileProvider } from "./file-provider"; import { GiteaAuthProvider } from "./gitea-auth-provider"; import { GiteaContextParser } from "./gitea-context-parser"; @@ -21,7 +21,6 @@ import { GiteaTokenValidator } from "./gitea-token-validator"; export const giteaContainerModule = new ContainerModule((bind, _unbind, _isBound, _rebind) => { bind(RepositoryHost).toSelf().inSingletonScope(); bind(GiteaRestApi).toSelf().inSingletonScope(); - bind(GiteaGraphQlEndpoint).toSelf().inSingletonScope(); bind(GiteaFileProvider).toSelf().inSingletonScope(); bind(FileProvider).toService(GiteaFileProvider); bind(GiteaAuthProvider).toSelf().inSingletonScope(); diff --git a/components/server/src/gitea/gitea-repository-provider.ts b/components/server/src/gitea/gitea-repository-provider.ts index edf1fa48f6ce09..2de5889917c962 100644 --- a/components/server/src/gitea/gitea-repository-provider.ts +++ b/components/server/src/gitea/gitea-repository-provider.ts @@ -7,18 +7,17 @@ import { injectable, inject } from 'inversify'; import { User, Repository } from "@gitpod/gitpod-protocol" -import { GiteaGraphQlEndpoint, GiteaRestApi } from "./api"; +import { GiteaRestApi } from "./api"; import { RepositoryProvider } from '../repohost/repository-provider'; import { RepoURL } from '../repohost/repo-url'; import { Branch, CommitInfo } from '@gitpod/gitpod-protocol/src/protocol'; @injectable() export class GithubRepositoryProvider implements RepositoryProvider { - @inject(GiteaRestApi) protected readonly github: GiteaRestApi; - @inject(GiteaGraphQlEndpoint) protected readonly githubQueryApi: GiteaGraphQlEndpoint; + @inject(GiteaRestApi) protected readonly gitea: GiteaRestApi; async getRepo(user: User, owner: string, repo: string): Promise { - const repository = await this.github.getRepository(user, { owner, repo }); + const repository = await this.gitea.getRepository(user, { owner, repo }); const cloneUrl = repository.clone_url; const host = RepoURL.parseRepoUrl(cloneUrl)!.host; const description = repository.description; @@ -29,7 +28,7 @@ export class GithubRepositoryProvider implements RepositoryProvider { } async getBranch(user: User, owner: string, repo: string, branch: string): Promise { - const result = await this.github.getBranch(user, { repo, owner, branch }); + const result = await this.gitea.getBranch(user, { repo, owner, branch }); return result; } @@ -39,7 +38,7 @@ export class GithubRepositoryProvider implements RepositoryProvider { let hasNextPage: boolean = true; while (hasNextPage) { - const result: any = await this.githubQueryApi.runQuery(user, ` + const result: any = await this.gitea.runQuery(user, ` query { repository(name: "${repo}", owner: "${owner}") { refs(refPrefix: "refs/heads/", orderBy: {field: TAG_COMMIT_DATE, direction: ASC}, first: 100 ${endCursor ? `, after: "${endCursor}"` : ""}) { @@ -103,29 +102,12 @@ export class GithubRepositoryProvider implements RepositoryProvider { } async getCommitInfo(user: User, owner: string, repo: string, ref: string): Promise { - const commit = await this.github.getCommit(user, { repo, owner, ref }); + const commit = await this.gitea.getCommit(user, { repo, owner, ref }); return commit; } async getUserRepos(user: User): Promise { - // Hint: Use this to get richer results: - // node { - // nameWithOwner - // shortDescriptionHTML(limit: 120) - // url - // } - const result: any = await this.githubQueryApi.runQuery(user, ` - query { - viewer { - repositoriesContributedTo(includeUserRepositories: true, first: 100) { - edges { - node { - url - } - } - } - } - }`); + const result: any = await this.gitea.getUserRepositories(user); return (result.data.viewer?.repositoriesContributedTo?.edges || []).map((edge: any) => edge.node.url) } } diff --git a/components/server/src/gitea/gitea-token-validator.ts b/components/server/src/gitea/gitea-token-validator.ts index 3c557f6db051b9..ab09b3d72673d1 100644 --- a/components/server/src/gitea/gitea-token-validator.ts +++ b/components/server/src/gitea/gitea-token-validator.ts @@ -6,13 +6,12 @@ import { inject, injectable } from "inversify"; import { CheckWriteAccessResult, IGitTokenValidator, IGitTokenValidatorParams } from "../workspace/git-token-validator"; -import { GiteaApiError, GiteaGraphQlEndpoint, GiteaRestApi, GiteaResult } from "./api"; +import { GiteaApiError, GiteaRestApi, GiteaResult } from "./api"; import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; @injectable() export class GiteaTokenValidator implements IGitTokenValidator { @inject(GiteaRestApi) githubRestApi: GiteaRestApi; - @inject(GiteaGraphQlEndpoint) githubGraphQLEndpoint: GiteaGraphQlEndpoint; async checkWriteAccess(params: IGitTokenValidatorParams): Promise { diff --git a/yarn.lock b/yarn.lock index 5e395feecdcd9b..8b2b883e043a46 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1348,6 +1348,11 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" +"@exodus/schemasafe@^1.0.0-rc.2": + version "1.0.0-rc.6" + resolved "https://registry.yarnpkg.com/@exodus/schemasafe/-/schemasafe-1.0.0-rc.6.tgz#7985f681564cff4ffaebb5896eb4be20af3aae7a" + integrity sha512-dDnQizD94EdBwEj/fh3zPRa/HWCS9O5au2PuHhZBbuM3xWHxuaKzPBOEWze7Nn0xW68MIpZ7Xdyn1CoCpjKCuQ== + "@gar/promisify@^1.0.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.2.tgz#30aa825f11d438671d585bd44e7fd564535fc210" @@ -3196,6 +3201,11 @@ "@types/cookiejar" "*" "@types/node" "*" +"@types/swagger-schema-official@2.0.21": + version "2.0.21" + resolved "https://registry.yarnpkg.com/@types/swagger-schema-official/-/swagger-schema-official-2.0.21.tgz#56812a86dcd57ba60e5c51705ee96a2b2dc9b374" + integrity sha512-n9BbLOjR4Hre7B4TSGGMPohOgOg8tcp00uxqsIE00uuWQC0QuX57G1bqC1csLsk2DpTGtHkd0dEb3ipsCZ9dAA== + "@types/tapable@^1", "@types/tapable@^1.0.5": version "1.0.8" resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.8.tgz#b94a4391c85666c7b73299fd3ad79d4faa435310" @@ -4311,7 +4321,7 @@ axios-retry@^3.0.2: "@babel/runtime" "^7.15.4" is-retry-allowed "^2.2.0" -axios@^0.21.1: +axios@^0.21.1, axios@^0.21.4: version "0.21.4" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== @@ -5089,6 +5099,11 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" +call-me-maybe@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" + integrity sha1-JtII6onje1y95gJQoV8DHBak1ms= + caller-callsite@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" @@ -5678,7 +5693,7 @@ commander@^5.1.0: resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== -commander@^6.0.0, commander@^6.2.0: +commander@^6.0.0, commander@^6.2.0, commander@^6.2.1: version "6.2.1" resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== @@ -7266,6 +7281,11 @@ es6-iterator@2.0.3, es6-iterator@~2.0.3: es5-ext "^0.10.35" es6-symbol "^3.1.1" +es6-promise@^3.2.1: + version "3.3.1" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" + integrity sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM= + es6-promise@^4.1.1: version "4.2.8" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" @@ -7702,6 +7722,11 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== +eta@^1.12.1: + version "1.12.3" + resolved "https://registry.yarnpkg.com/eta/-/eta-1.12.3.tgz#2982d08adfbef39f9fa50e2fbd42d7337e7338b1" + integrity sha512-qHixwbDLtekO/d51Yr4glcaUJCIjGVJyTzuqV4GPlgZo1YpgOKG+avQynErZIYrfM6JIJdtiG2Kox8tbb+DoGg== + etag@~1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" @@ -8591,6 +8616,13 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" +gitea-js@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gitea-js/-/gitea-js-1.0.1.tgz#34e22c34e141171935644ce22e4c674fa9b30c8d" + integrity sha512-lZ1Fk2x30Jwsm5OEpfdU98v9ncpZITDotoQ2Ad8uEwEPW71HPBgbYpgVFuteTvdN1UEqaArUfD2zQz3uW+TB/g== + dependencies: + swagger-typescript-api "9.3.1" + glob-parent@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae" @@ -9261,6 +9293,11 @@ http-signature@~1.3.6: jsprim "^2.0.2" sshpk "^1.14.1" +http2-client@^1.2.5: + version "1.3.5" + resolved "https://registry.yarnpkg.com/http2-client/-/http2-client-1.3.5.tgz#20c9dc909e3cc98284dd20af2432c524086df181" + integrity sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA== + http2-wrapper@^1.0.0-beta.5.2: version "1.0.3" resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d" @@ -11951,6 +11988,11 @@ nan@^2.12.1, nan@^2.13.2: resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== +nanoid@^3.1.22: + version "3.2.0" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" + integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== + nanoid@^3.1.30: version "3.1.30" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362" @@ -12022,13 +12064,20 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" -node-emoji@^1.11.0: +node-emoji@^1.10.0, node-emoji@^1.11.0: version "1.11.0" resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c" integrity sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A== dependencies: lodash "^4.17.21" +node-fetch-h2@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz#c6188325f9bd3d834020bf0f2d6dc17ced2241ac" + integrity sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg== + dependencies: + http2-client "^1.2.5" + node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.5: version "2.6.6" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.6.tgz#1751a7c01834e8e1697758732e9efb6eeadfaf89" @@ -12124,6 +12173,13 @@ node-pre-gyp@^0.17.0: semver "^5.7.1" tar "^4.4.13" +node-readfiles@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/node-readfiles/-/node-readfiles-0.2.0.tgz#dbbd4af12134e2e635c245ef93ffcf6f60673a5d" + integrity sha1-271K8SE04uY1wkXvk//Pb2BnOl0= + dependencies: + es6-promise "^3.2.1" + node-releases@^1.1.61: version "1.1.77" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.77.tgz#50b0cfede855dd374e7585bf228ff34e57c1c32e" @@ -12285,6 +12341,52 @@ nwsapi@^2.2.0: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== +oas-kit-common@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/oas-kit-common/-/oas-kit-common-1.0.8.tgz#6d8cacf6e9097967a4c7ea8bcbcbd77018e1f535" + integrity sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ== + dependencies: + fast-safe-stringify "^2.0.7" + +oas-linter@^3.2.2: + version "3.2.2" + resolved "https://registry.yarnpkg.com/oas-linter/-/oas-linter-3.2.2.tgz#ab6a33736313490659035ca6802dc4b35d48aa1e" + integrity sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ== + dependencies: + "@exodus/schemasafe" "^1.0.0-rc.2" + should "^13.2.1" + yaml "^1.10.0" + +oas-resolver@^2.5.6: + version "2.5.6" + resolved "https://registry.yarnpkg.com/oas-resolver/-/oas-resolver-2.5.6.tgz#10430569cb7daca56115c915e611ebc5515c561b" + integrity sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ== + dependencies: + node-fetch-h2 "^2.3.0" + oas-kit-common "^1.0.8" + reftools "^1.1.9" + yaml "^1.10.0" + yargs "^17.0.1" + +oas-schema-walker@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/oas-schema-walker/-/oas-schema-walker-1.1.5.tgz#74c3cd47b70ff8e0b19adada14455b5d3ac38a22" + integrity sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ== + +oas-validator@^5.0.8: + version "5.0.8" + resolved "https://registry.yarnpkg.com/oas-validator/-/oas-validator-5.0.8.tgz#387e90df7cafa2d3ffc83b5fb976052b87e73c28" + integrity sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw== + dependencies: + call-me-maybe "^1.0.1" + oas-kit-common "^1.0.8" + oas-linter "^3.2.2" + oas-resolver "^2.5.6" + oas-schema-walker "^1.1.5" + reftools "^1.1.9" + should "^13.2.1" + yaml "^1.10.0" + oauth@0.9.x: version "0.9.15" resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1" @@ -14072,6 +14174,11 @@ prepend-http@^2.0.0: resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= +prettier@^2.2.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a" + integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg== + pretty-bytes@^5.3.0, pretty-bytes@^5.6.0: version "5.6.0" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" @@ -14858,6 +14965,11 @@ reflect.getprototypeof@^1.0.2: get-intrinsic "^1.1.1" which-builtin-type "^1.1.1" +reftools@^1.1.9: + version "1.1.9" + resolved "https://registry.yarnpkg.com/reftools/-/reftools-1.1.9.tgz#e16e19f662ccd4648605312c06d34e5da3a2b77e" + integrity sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w== + regenerate-unicode-properties@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz#54d09c7115e1f53dc2314a974b32c1c344efe326" @@ -15567,6 +15679,50 @@ shellwords@^0.1.1: resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== +should-equal@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3" + integrity sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA== + dependencies: + should-type "^1.4.0" + +should-format@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/should-format/-/should-format-3.0.3.tgz#9bfc8f74fa39205c53d38c34d717303e277124f1" + integrity sha1-m/yPdPo5IFxT04w01xcwPidxJPE= + dependencies: + should-type "^1.3.0" + should-type-adaptors "^1.0.1" + +should-type-adaptors@^1.0.1: + version "1.1.0" + resolved "https://registry.yarnpkg.com/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz#401e7f33b5533033944d5cd8bf2b65027792e27a" + integrity sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA== + dependencies: + should-type "^1.3.0" + should-util "^1.0.0" + +should-type@^1.3.0, should-type@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/should-type/-/should-type-1.4.0.tgz#0756d8ce846dfd09843a6947719dfa0d4cff5cf3" + integrity sha1-B1bYzoRt/QmEOmlHcZ36DUz/XPM= + +should-util@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/should-util/-/should-util-1.0.1.tgz#fb0d71338f532a3a149213639e2d32cbea8bcb28" + integrity sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g== + +should@^13.2.1: + version "13.2.3" + resolved "https://registry.yarnpkg.com/should/-/should-13.2.3.tgz#96d8e5acf3e97b49d89b51feaa5ae8d07ef58f10" + integrity sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ== + dependencies: + should-equal "^2.0.0" + should-format "^3.0.3" + should-type "^1.4.0" + should-type-adaptors "^1.0.1" + should-util "^1.0.0" + side-channel@^1.0.3, side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" @@ -16378,6 +16534,48 @@ svgo@^1.0.0, svgo@^1.2.2: unquote "~1.1.1" util.promisify "~1.0.0" +swagger-schema-official@2.0.0-bab6bed: + version "2.0.0-bab6bed" + resolved "https://registry.yarnpkg.com/swagger-schema-official/-/swagger-schema-official-2.0.0-bab6bed.tgz#70070468d6d2977ca5237b2e519ca7d06a2ea3fd" + integrity sha1-cAcEaNbSl3ylI3suUZyn0Gouo/0= + +swagger-typescript-api@9.3.1: + version "9.3.1" + resolved "https://registry.yarnpkg.com/swagger-typescript-api/-/swagger-typescript-api-9.3.1.tgz#07df3aa8e83a3897356a6e820e88b8348830aa6b" + integrity sha512-vtarFELmXmDKrtY2FvU2OjNvN1BqOj3kHWzz7mAresNKRgRubZpjlopECxbBekrLeX3lezAYSNcrFMVRGJAD4A== + dependencies: + "@types/swagger-schema-official" "2.0.21" + axios "^0.21.4" + commander "^6.2.1" + cosmiconfig "^7.0.0" + eta "^1.12.1" + js-yaml "^4.0.0" + lodash "^4.17.21" + make-dir "^3.1.0" + nanoid "^3.1.22" + node-emoji "^1.10.0" + prettier "^2.2.1" + swagger-schema-official "2.0.0-bab6bed" + swagger2openapi "^7.0.5" + typescript "^4.2.4" + +swagger2openapi@^7.0.5: + version "7.0.8" + resolved "https://registry.yarnpkg.com/swagger2openapi/-/swagger2openapi-7.0.8.tgz#12c88d5de776cb1cbba758994930f40ad0afac59" + integrity sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g== + dependencies: + call-me-maybe "^1.0.1" + node-fetch "^2.6.1" + node-fetch-h2 "^2.3.0" + node-readfiles "^0.2.0" + oas-kit-common "^1.0.8" + oas-resolver "^2.5.6" + oas-schema-walker "^1.1.5" + oas-validator "^5.0.8" + reftools "^1.1.9" + yaml "^1.10.0" + yargs "^17.0.1" + swot-js@^1.0.3: version "1.0.7" resolved "https://registry.yarnpkg.com/swot-js/-/swot-js-1.0.7.tgz#0fb6520d6bd466370b695c6bdd9489214effb112" From 86ba2fdf17923a3d5079e59f1bb6f9c5798f9703 Mon Sep 17 00:00:00 2001 From: Anbraten Date: Thu, 10 Feb 2022 02:22:34 +0000 Subject: [PATCH 03/16] adjust to use gitea api --- .../server/ee/src/gitea/container-module.ts | 13 + .../server/ee/src/gitea/gitea-app-support.ts | 62 + .../server/ee/src/prebuilds/gitea-app.ts | 131 ++ .../server/ee/src/prebuilds/gitea-service.ts | 15 + components/server/src/gitea/api.ts | 687 +--------- components/server/src/gitea/convert.ts | 28 + components/server/src/gitea/file-provider.ts | 33 +- .../server/src/gitea/gitea-auth-provider.ts | 207 ++- .../src/gitea/gitea-context-parser.spec.ts | 1180 ++++++++--------- .../server/src/gitea/gitea-context-parser.ts | 830 ++++++------ .../src/gitea/gitea-repository-provider.ts | 173 ++- .../server/src/gitea/gitea-token-helper.ts | 3 - .../server/src/gitea/gitea-token-validator.ts | 59 +- .../server/src/gitea/languages-provider.ts | 10 +- components/server/src/gitea/scopes.ts | 16 +- 15 files changed, 1492 insertions(+), 1955 deletions(-) create mode 100644 components/server/ee/src/gitea/container-module.ts create mode 100644 components/server/ee/src/gitea/gitea-app-support.ts create mode 100644 components/server/ee/src/prebuilds/gitea-app.ts create mode 100644 components/server/ee/src/prebuilds/gitea-service.ts create mode 100644 components/server/src/gitea/convert.ts diff --git a/components/server/ee/src/gitea/container-module.ts b/components/server/ee/src/gitea/container-module.ts new file mode 100644 index 00000000000000..391e0eedbc343a --- /dev/null +++ b/components/server/ee/src/gitea/container-module.ts @@ -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 { GiteaService } from "../prebuilds/gitea-service"; +import { RepositoryService } from "../../../src/repohost/repo-service"; + +export const giteaContainerModuleEE = new ContainerModule((_bind, _unbind, _isBound, rebind) => { + rebind(RepositoryService).to(GiteaService).inSingletonScope(); +}); diff --git a/components/server/ee/src/gitea/gitea-app-support.ts b/components/server/ee/src/gitea/gitea-app-support.ts new file mode 100644 index 00000000000000..8f94e82ec852be --- /dev/null +++ b/components/server/ee/src/gitea/gitea-app-support.ts @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2020 Gitpod GmbH. All rights reserved. + * Licensed under the Gitpod Enterprise Source Code License, + * See License.enterprise.txt in the project root folder. + */ + +import { AuthProviderInfo, ProviderRepository, User } from "@gitpod/gitpod-protocol"; +import { inject, injectable } from "inversify"; +import { TokenProvider } from "../../../src/user/token-provider"; +import { UserDB } from "@gitpod/gitpod-db/lib"; +import { Gitea } from "../../../src/gitea/api"; + +@injectable() +export class GiteaAppSupport { + + @inject(UserDB) protected readonly userDB: UserDB; + @inject(TokenProvider) protected readonly tokenProvider: TokenProvider; + + async getProviderRepositoriesForUser(params: { user: User, provider: AuthProviderInfo }): Promise { + const token = await this.tokenProvider.getTokenForHost(params.user, params.provider.host); + const oauthToken = token.value; + const api = Gitea.create(`https://${params.provider.host}`, oauthToken); + + const result: ProviderRepository[] = []; + const ownersRepos: ProviderRepository[] = []; + + const identity = params.user.identities.find(i => i.authProviderId === params.provider.authProviderId); + if (!identity) { + return result; + } + const usersGitLabAccount = identity.authName; + + // cf. https://docs.gitlab.com/ee/api/projects.html#list-all-projects + // we are listing only those projects with access level of maintainers. + // also cf. https://docs.gitlab.com/ee/api/members.html#valid-access-levels + // + // TODO: check if valid + const projectsWithAccess = await api.user.userCurrentListRepos({ limit: 100 }); + for (const project of projectsWithAccess.data) { + const path = project.full_name as string; + const cloneUrl = project.clone_url as string; + const updatedAt = project.updated_at as string; + const accountAvatarUrl = project.owner?.avatar_url as string; + const account = project.owner?.login as string; + + (account === usersGitLabAccount ? ownersRepos : result).push({ + name: project.name as string, + path, + account, + cloneUrl, + updatedAt, + accountAvatarUrl, + // inUse: // todo(at) compute usage via ProjectHooks API + }) + } + + // put owner's repos first. the frontend will pick first account to continue with + result.unshift(...ownersRepos); + return result; + } + +} \ No newline at end of file diff --git a/components/server/ee/src/prebuilds/gitea-app.ts b/components/server/ee/src/prebuilds/gitea-app.ts new file mode 100644 index 00000000000000..01a091a8097688 --- /dev/null +++ b/components/server/ee/src/prebuilds/gitea-app.ts @@ -0,0 +1,131 @@ +/** + * 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 * as express from 'express'; +import { postConstruct, injectable, inject } from 'inversify'; +import { ProjectDB, TeamDB, UserDB } from '@gitpod/gitpod-db/lib'; +import { Project, User, StartPrebuildResult } from '@gitpod/gitpod-protocol'; +import { PrebuildManager } from '../prebuilds/prebuild-manager'; +import { TraceContext } from '@gitpod/gitpod-protocol/lib/util/tracing'; +import { TokenService } from '../../../src/user/token-service'; +import { HostContextProvider } from '../../../src/auth/host-context-provider'; +// import { GiteaService } from './gitea-service'; +import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; + +@injectable() +export class GiteaApp { + + @inject(UserDB) protected readonly userDB: UserDB; + @inject(PrebuildManager) protected readonly prebuildManager: PrebuildManager; + @inject(TokenService) protected readonly tokenService: TokenService; + @inject(HostContextProvider) protected readonly hostCtxProvider: HostContextProvider; + @inject(ProjectDB) protected readonly projectDB: ProjectDB; + @inject(TeamDB) protected readonly teamDB: TeamDB; + + protected _router = express.Router(); + public static path = '/apps/gitea/'; + + @postConstruct() + protected init() { + this._router.post('/', async (req, res) => { + const event = req.header('X-Gitlab-Event'); + if (event === 'Push Hook') { + const context = req.body as GitLabPushHook; + const span = TraceContext.startSpan("GitLapApp.handleEvent", {}); + span.setTag("request", context); + log.debug("GitLab push hook received", { event, context }); + let user: User | undefined; + try { + user = await this.findUser({ span }, context, req); + } catch (error) { + log.error("Cannot find user.", error, { req }) + } + if (!user) { + res.statusCode = 503; + res.send(); + return; + } + await this.handlePushHook({ span }, context, user); + } else { + log.debug("Unknown GitLab event received", { event }); + } + res.send('OK'); + }); + } + + protected async findUser(ctx: TraceContext, context: GitLabPushHook, req: express.Request): Promise { + // TODO + return {} as User; + } + + protected async handlePushHook(ctx: TraceContext, body: GitLabPushHook, user: User): Promise { + // TODO + return undefined; + } + + /** + * 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 gitlab.com 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 }> { + // TODO + return {} as { user: User, project?: Project }; + } + + protected createContextUrl(body: GitLabPushHook) { + const repoUrl = body.repository.git_http_url; + const contextURL = `${repoUrl.substr(0, repoUrl.length - 4)}/-/tree${body.ref.substr('refs/head/'.length)}`; + return contextURL; + } + + get router(): express.Router { + return this._router; + } + + protected getBranchFromRef(ref: string): string | undefined { + const headsPrefix = "refs/heads/"; + if (ref.startsWith(headsPrefix)) { + return ref.substring(headsPrefix.length); + } + + return undefined; + } +} + +interface GitLabPushHook { + object_kind: 'push'; + before: string; + after: string; // commit + ref: string; // e.g. "refs/heads/master" + user_avatar: string; + user_name: string; + project: GitLabProject; + repository: GitLabRepository; +} + +interface GitLabRepository { + name: string, + git_http_url: string; // e.g. http://example.com/mike/diaspora.git + visibility_level: number, +} + +interface GitLabProject { + id: number, + namespace: string, + name: string, + path_with_namespace: string, // e.g. "mike/diaspora" + git_http_url: string; // e.g. http://example.com/mike/diaspora.git + web_url: string; // e.g. http://example.com/mike/diaspora + visibility_level: number, + avatar_url: string | null, +} \ No newline at end of file diff --git a/components/server/ee/src/prebuilds/gitea-service.ts b/components/server/ee/src/prebuilds/gitea-service.ts new file mode 100644 index 00000000000000..6e497fe5f788b4 --- /dev/null +++ b/components/server/ee/src/prebuilds/gitea-service.ts @@ -0,0 +1,15 @@ +/** + * 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 { inject, injectable } from "inversify"; +import { GiteaRestApi } from "../../../src/gitea/api"; + +@injectable() +export class GiteaService extends RepositoryService { + @inject(GiteaRestApi) protected readonly giteaApi: GiteaRestApi; + // TODO: complete? +} \ No newline at end of file diff --git a/components/server/src/gitea/api.ts b/components/server/src/gitea/api.ts index b1d29504f8515a..9f0c4c1e9222f7 100644 --- a/components/server/src/gitea/api.ts +++ b/components/server/src/gitea/api.ts @@ -4,44 +4,56 @@ * See License-AGPL.txt in the project root for license information. */ -import { Api } from "gitea-js" +import { Api, Commit as ICommit, Repository as IRepository, ContentsResponse as IContentsResponse, Branch as IBranch, Tag as ITag, PullRequest as IPullRequest, Issue as IIssue, User as IUser } from "gitea-js" +import fetch from 'node-fetch' -import { Branch, CommitInfo, User } from "@gitpod/gitpod-protocol" -import { GarbageCollectedCache } from "@gitpod/gitpod-protocol/lib/util/garbage-collected-cache"; +import { User } from "@gitpod/gitpod-protocol" import { injectable, inject } from 'inversify'; import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; import { GiteaScope } from './scopes'; import { AuthProviderParams } from '../auth/auth-provider'; import { GiteaTokenHelper } from './gitea-token-helper'; -import { Deferred } from '@gitpod/gitpod-protocol/lib/util/deferred'; -import { URL } from 'url'; - -export class GiteaApiError extends Error { - constructor(public readonly response: OctokitResponse) { - super(`Gitea API Error. Status: ${response.status}`); - this.name = 'GiteaApiError'; +export namespace Gitea { + export class ApiError extends Error { + readonly httpError: { name: string, description: string } | undefined; + constructor(msg?: string, httpError?: any) { + super(msg); + this.httpError = httpError; + this.name = 'GitLabApiError'; + } } -} -export namespace GiteaApiError { - export function is(error: Error | null): error is GiteaApiError { - return !!error && error.name === 'GiteaApiError'; + export namespace ApiError { + export function is(something: any): something is ApiError { + return !!something && something.name === 'GitLabApiError'; + } + export function isNotFound(error: ApiError): boolean { + return !!error.httpError?.description.startsWith("404"); + } + export function isInternalServerError(error: ApiError): boolean { + return !!error.httpError?.description.startsWith("500"); + } } -} - -export interface QueryResult { - data: D - errors?: QueryError[]; -} -export interface QueryError { - message: string - locations: QueryLocation -} + export function create(host: string, token: string) { + return new Api({ + customFetch: fetch, + baseUrl: `https://${host}`, + baseApiParams: { + headers: { + "Authorization": token, + }, + }}); + } -export interface QueryLocation { - line: number - column: number + export type Commit = ICommit; + export type Repository = IRepository; + export type ContentsResponse = IContentsResponse; + export type Branch = IBranch; + export type Tag = ITag; + export type PullRequest = IPullRequest; + export type Issue = IIssue; + export type User = IUser; } @injectable() @@ -50,614 +62,43 @@ export class GiteaRestApi { @inject(AuthProviderParams) readonly config: AuthProviderParams; @inject(GiteaTokenHelper) protected readonly tokenHelper: GiteaTokenHelper; protected async create(userOrToken: User | string) { - let token: string | undefined; + let oauthToken: string | undefined; if (typeof userOrToken === 'string') { - token = userOrToken; + oauthToken = userOrToken; } else { - const githubToken = await this.tokenHelper.getTokenWithScopes(userOrToken, GiteaScope.Requirements.DEFAULT); - token = githubToken.value; + const giteaToken = await this.tokenHelper.getTokenWithScopes(userOrToken, GiteaScope.Requirements.DEFAULT); + oauthToken = giteaToken.value; } - const api = new Api(this.getGiteaOptions(token)); + const api = Gitea.create(this.config.host, oauthToken); + const repo = api.repos.repoGet('anbraten', 'gitea-js'); + console.log(repo); return api; } - protected get userAgent() { - return new URL(this.config.oauth!.callBackUrl).hostname; - } - - /** - * +----+-------------------------------------+ - * | | Gitea | - * +----+-------------------------------------+ - * | v1 | https://[YOUR_HOST]/api/v1 | - * +----+-------------------------------------+ - */ - get baseURL() { - return `https://${this.config.host}/api/v1`; - } - - protected getGiteaOptions(auth: string): OctokitOptions { - return { - auth, - request: { - timeout: 5000 - }, - baseUrl: this.baseURL, - userAgent: this.userAgent - }; - } - - public async run(userOrToken: User | string, operation: (api: Octokit) => Promise>): Promise> { + public async run(userOrToken: User | string, operation: (g: Api) => Promise): Promise { const before = new Date().getTime(); const userApi = await this.create(userOrToken); - try { - const response = (await operation(userApi)); - const statusCode = response.status; - if (statusCode !== 200) { - throw new GiteaApiError(response); - } - return response; + const response = (await operation(userApi) as R); + return response as R; } catch (error) { - if (error.status) { - throw new GiteaApiError(error); - } - throw error; - } finally { - log.debug(`Gitea request took ${new Date().getTime() - before} ms`); - } - } - - protected readonly cachedResponses = new GarbageCollectedCache>(120, 150); - public async runWithCache(key: string, user: User, operation: (api: Octokit) => Promise>): Promise> { - const result = new Deferred>(); - const before = new Date().getTime(); - const cacheKey = `${this.config.host}-${key}`; - const cachedResponse = this.cachedResponses.get(cacheKey); - const api = await this.create(user); - - // using hooks in Octokits lifecycle for caching results - // cf. https://github.com/octokit/rest.js/blob/master/docs/src/pages/api/06_hooks.md - api.hook.wrap("request", async (request, options) => { - - // send etag on each request if there is something cached for the given key - if (cachedResponse) { - if (cachedResponse.headers.etag) { - options.headers['If-None-Match'] = cachedResponse.headers.etag; - } - if (cachedResponse.headers["last-modified"]) { - options.headers['If-Modified-Since'] = cachedResponse.headers["last-modified"]; - } + if (error && typeof error?.response?.status === "number" && error?.response?.status !== 200) { + return new Gitea.ApiError(`Gitea responded with code ${error.response.status}`, error); } - - try { - const response = await request(options); - - // on successful responses (HTTP 2xx) we fill the cache - this.cachedResponses.delete(cacheKey); - if (response.headers.etag || response.headers["last-modified"]) { - this.cachedResponses.set(cacheKey, response); - } - result.resolve(response); - return response; - } catch (error) { - - // resolve with cached resource if GH tells us that it's not modified (HTTP 304) - if (error.status === 304 && cachedResponse) { - result.resolve(cachedResponse); - return cachedResponse; - } - this.cachedResponses.delete(cacheKey); - throw error; + if (error && error?.name === "HTTPError") { + // e.g. + // { + // "name": "HTTPError", + // "timings": { }, + // "description": "404 Commit Not Found" + // } + + return new Gitea.ApiError(`Gitea Request Error: ${error?.description}`, error); } - }); - - try { - await operation(api); - } catch (e) { - result.reject(e); + log.error(`Gitea request error`, error); + throw error; } finally { - log.debug(`Gitea request took ${new Date().getTime() - before} ms`); + log.info(`Gitea request took ${new Date().getTime() - before} ms`); } - return result.promise; - } - - public async getRepository(user: User, params: RestEndpointMethodTypes["repos"]["get"]["parameters"]): Promise { - const key = `getRepository:${params.owner}/${params.owner}:${user.id}`; - const response = await this.runWithCache(key, user, (api) => api.repos.get(params)); - return response.data; - } - - public async getBranch(user: User, params: RestEndpointMethodTypes["repos"]["getBranch"]["parameters"]): Promise { - const key = `getBranch:${params.owner}/${params.owner}/${params.branch}:${user.id}`; - const getBranchResponse = (await this.runWithCache(key, user, (api) => api.repos.getBranch(params))) as RestEndpointMethodTypes["repos"]["getBranch"]["response"]; - const { commit: { sha }, name, _links: { html } } = getBranchResponse.data; - - const commit = await this.getCommit(user, { ...params, ref: sha }); - - return { - name, - commit, - htmlUrl: html - }; - } - - public async getBranches(user: User, params: RestEndpointMethodTypes["repos"]["listBranches"]["parameters"]): Promise { - const key = `getBranches:${params.owner}/${params.owner}:${user.id}`; - const listBranchesResponse = (await this.runWithCache(key, user, (api) => api.repos.listBranches(params))) as RestEndpointMethodTypes["repos"]["listBranches"]["response"]; - - const result: Branch[] = []; - - for (const branch of listBranchesResponse.data) { - const { commit: { sha } } = branch; - const commit = await this.getCommit(user, { ...params, ref: sha }); - - const key = `getBranch:${params.owner}/${params.owner}/${params.branch}:${user.id}`; - const getBranchResponse = (await this.runWithCache(key, user, (api) => api.repos.listBranches(params))) as RestEndpointMethodTypes["repos"]["getBranch"]["response"]; - const htmlUrl = getBranchResponse.data._links.html; - - result.push({ - name: branch.name, - commit, - htmlUrl - }); - } - - return result; - } - - public async getCommit(user: User, params: RestEndpointMethodTypes["repos"]["getCommit"]["parameters"]): Promise { - const key = `getCommit:${params.owner}/${params.owner}/${params.ref}:${user.id}`; - const getCommitResponse = (await this.runWithCache(key, user, (api) => api.repos.getCommit(params))) as RestEndpointMethodTypes["repos"]["getCommit"]["response"]; - const { sha, commit, author } = getCommitResponse.data; - return { - sha, - author: commit.author?.name || "nobody", - authorAvatarUrl: author?.avatar_url, - authorDate: commit.author?.date, - commitMessage: commit.message, - } - } - - public async getFileContents(user, repositoryOwner, repositoryName, revision, path): Promise { - return []; - } -} - -// export interface GiteaResult extends OctokitResponse { } -// export namespace GiteaResult { -// export function actualScopes(result: OctokitResponse): string[] { -// return (result.headers['x-oauth-scopes'] || "").split(",").map((s: any) => s.trim()); -// } -// export function mayReadOrgs(result: OctokitResponse): boolean { -// return actualScopes(result).some(scope => scope === "read:org" || scope === "user"); -// } -// export function mayWritePrivate(result: OctokitResponse): boolean { -// return actualScopes(result).some(scope => scope === "repo"); -// } -// export function mayWritePublic(result: OctokitResponse): boolean { -// return actualScopes(result).some(scope => scope === "repo" || scope === "public_repo"); -// } -// } - -// Git -export interface CommitUser { - date: string - name: string - email: string -} - -export interface CommitVerification { - verified: boolean // ??? - reason: "unsigned" // ??? - signature: null // ??? - payload: null // ??? -} - -export interface Commit extends CommitRef { - author: CommitUser - committer: CommitUser - message: string - tree: TreeRef - parents: TreeRef[] - verification: CommitVerification -} - -export interface CommitRef { - sha: string - url: string -} - -export interface Tree extends TreeRef { - tree: TreeNode[] - truncated: boolean -} - -export interface TreeRef { - sha: string - url: string -} - -export interface TreeNode { - path: string - mode: number //"100644", - type: string //"blob", - sha: string //"5f2f16bfff90e6620509c0cf442e7a3586dad8fb", - size: number // 5 ??? - url: string //"https://api.github.com/repos/somefox/test/git/blobs/5f2f16bfff90e6620509c0cf442e7a3586dad8fb" -} - -export interface BlobTreeNode extends TreeNode { - type: "blob" -} - -export interface Blob { - content: string, // always base64 encoded! (https://developer.github.com/v3/git/blobs/#get-a-blob) - encoding: "base64", - url: string, - sha: string, - size: number // bytes? -} - -export interface BranchRef { - name: string - commit: CommitRef - protected: boolean - protection_url?: string -} - -export interface CommitDetails { - url: string - sha: string - node_id: string - html_url: string - comments_url: string - commit: Commit - author: UserRef - committer: UserRef - parents: CommitRef[] -} -export type CommitResponse = CommitDetails[]; - -// Gitea -export type UserEmails = UserEmail[]; -export interface UserEmail { - email: string - verified: boolean - primary: boolean - visibility: "public" | "private" -} - -export interface UserRef { - login: string - id: number - avatar_url: string - gravatar_id: string - url: string - html_url: string - followers_url: string - following_url: string - gists_url: string - starred_url: string - subscriptions_url: string - organizations_url: string - repos_url: string - events_url: string - received_events_url: string - type: "User" | "Organization" - site_admin: boolean -} - - -export interface License { - key: "mit" - name: string - spdx_id: string - url: string - html_url: string -} - -export interface Repository { - id: number - owner: UserRef - name: string - full_name: string - description: string - private: boolean - fork: boolean - url: string - html_url: string - archive_url: string - assignees_url: string - blobs_url: string - branches_url: string - clone_url: string - collaborators_url: string - comments_url: string - commits_url: string - compare_url: string - contents_url: string - contributors_url: string - deployments_url: string - downloads_url: string - events_url: string - forks_url: string - git_commits_url: string - git_refs_url: string - git_tags_url: string - git_url: string - hooks_url: string - issue_comment_url: string - issue_events_url: string - issues_url: string - keys_url: string - labels_url: string - languages_url: string - merges_url: string - milestones_url: string - mirror_url: string - notifications_url: string - pulls_url: string - releases_url: string - ssh_url: string - stargazers_url: string - statuses_url: string - subscribers_url: string - subscription_url: string - svn_url: string - tags_url: string - teams_url: string - trees_url: string - homepage: string - language: null - forks_count: number - stargazers_count: number - watchers_count: number - size: number - default_branch: string - open_issues_count: number - topics: string[] - has_issues: boolean - has_wiki: boolean - has_pages: boolean - has_downloads: boolean - archived: boolean - pushed_at: string - created_at: string - updated_at: string - permissions?: { // No permissions means "no permissions" - admin: boolean - push: boolean - pull: boolean - }, - allow_rebase_merge: boolean - allow_squash_merge: boolean - allow_merge_commit: boolean - subscribers_count: number - network_count: number - license: License - organization: UserRef - parent: Repository - source: Repository -} - -export interface CommitRefInUserRepo { - label: string - ref: string - sha: string - user: UserRef - repo: Repository -} - -export interface PullRequest { - id: number - url: string - html_url: string - diff_url: string - patch_url: string - issue_url: string - commits_url: string - review_comments_url: string - review_comment_url: string - comments_url: string - statuses_url: string - number: number - state: "open" - title: string - body: string - assignee: UserRef - labels: Label[] - milestone: Milestone - locked: boolean - active_lock_reason?: "too heated" | "off-topic" | "resolved" | "spam" // The reason for locking the issue or pull request conversation. Lock will fail if you don't use one of these reasons: ... - created_at: string - updated_at: string - closed_at: string - merged_at: string - head: CommitRefInUserRepo - base: CommitRefInUserRepo - "_links": { - self: Link - html: Link - issue: Link - comments: Link - review_comments: Link - review_comment: Link - commits: Link - statuses: Link - } - user: UserRef - merge_commit_sha: string - merged: boolean - mergeable: boolean - merged_by: UserRef - comments: number - commits: number - additions: number - deletions: number - changed_files: number - maintainer_can_modify: boolean -} - -export interface Link { href: string } - -export interface Label { - id: number - url: string - name: string - description: string - color: string // f29513 - default: boolean -} - -export interface Milestone { - // ??? Not relevant yet -} - -export interface Issue { - id: number - url: string - number: number - title: string - user: UserRef - labels: Label[] - state: "open" | "closed" - html_url: string - pull_request?: { - url: string - html_url: string - diff_url: string - patch_url: string - } - repository_url: string - labels_url: string - comments_url: string - events_url: string - body: string - assignee: null | UserRef - assignees: UserRef[] - milestone: null | Milestone - locked: boolean - active_lock_reason?: "too heated" | "off-topic" | "resolved" | "spam" // The reason for locking the issue or pull request conversation. Lock will fail if you don't use one of these reasons: ... - created_at: string - updated_at: string - closed_at: null | string - closed_by: null | UserRef -} - - -export namespace Issue { - export function isPullRequest(issue: Issue): boolean { - return 'pull_request' in issue; - } -} - -// Contents -export type ContentType = 'file' | 'dir' | 'symlink' | 'submodule'; -export interface ContentMetadata { - type: ContentType - size: number - name: string - path: string - sha: string - url: string - git_url: string - html_url: string - download_url: string | null - _links: { - self: string - git: string - html: string } -} -export namespace ContentMetadata { - export function is(content: any): content is ContentMetadata { - return 'type' in content - && 'size' in content - && 'name' in content - && 'path' in content - && 'sha' in content; - } -} - -export interface FileMetadata extends ContentMetadata { - type: 'file' -} -export namespace FileMetadata { - export function is(content: any): content is FileMetadata { - return ContentMetadata.is(content) - && content.type === 'file'; - } -} - -export interface DirectoyMetadata extends ContentMetadata { - type: 'dir' -} -export namespace DirectoyMetadata { - export function is(content: any): content is DirectoyMetadata { - return ContentMetadata.is(content) - && content.type === 'dir'; - } -} - -export interface SymlinkMetadata extends ContentMetadata { - type: 'symlink' -} -export namespace SymlinkMetadata { - export function is(content: any): content is SymlinkMetadata { - return ContentMetadata.is(content) - && content.type === 'symlink'; - } -} - -export interface SubmoduleMetadata extends ContentMetadata { - type: 'submodule' -} -export namespace SubmoduleMetadata { - export function is(content: any): content is SubmoduleMetadata { - return ContentMetadata.is(content) - && content.type === 'submodule'; - } -} - -export interface FileContent extends FileMetadata { - encoding: 'base64' - content: string -} -export namespace FileContent { - export function is(content: any): content is FileContent { - return FileMetadata.is(content) - && 'encoding' in content - && 'content' in content; - } -} - -export type DirectoryContent = ContentMetadata[]; -export namespace DirectoryContent { - export function is(content: any): content is DirectoryContent { - return Array.isArray(content); - } -} - -export interface SymlinkContent extends SymlinkMetadata { - target: string -} -export namespace SymlinkContent { - export function is(content: any): content is SymlinkContent { - return SymlinkMetadata.is(content) - && 'target' in content; - } -} - -export interface SubmoduleContent extends SubmoduleMetadata { - submodule_git_url: string -} -export namespace SubmoduleContent { - export function is(content: any): content is SubmoduleContent { - return SubmoduleMetadata.is(content) - && 'submodule_git_url' in content; - } -} - -export type Content = ContentMetadata; -export type Contents = ContentMetadata | DirectoryContent; +} \ No newline at end of file diff --git a/components/server/src/gitea/convert.ts b/components/server/src/gitea/convert.ts new file mode 100644 index 00000000000000..a76e46074f31c0 --- /dev/null +++ b/components/server/src/gitea/convert.ts @@ -0,0 +1,28 @@ +import { Repository } from '@gitpod/gitpod-protocol'; +import { RepoURL } from '../repohost'; +import { Gitea } from "./api"; + +export function convertRepo(repo: Gitea.Repository): Repository | undefined { + if (!repo.clone_url || !repo.name || !repo.owner?.login) { + return undefined; + } + + const host = RepoURL.parseRepoUrl(repo.clone_url)!.host; + + return { + host, + owner: repo.owner.login, + name: repo.name, + cloneUrl: repo.clone_url, + description: repo.description, + avatarUrl: repo.avatar_url, + webUrl: repo.html_url, // TODO: is this correct? + defaultBranch: repo.default_branch, + private: repo.private, + fork: undefined // TODO: load fork somehow + } +} + +// export function convertBranch(repo: Gitea.Repository): Branch | undefined { + +// } \ No newline at end of file diff --git a/components/server/src/gitea/file-provider.ts b/components/server/src/gitea/file-provider.ts index 3bc67196fe4fd6..ae028c0bf840d0 100644 --- a/components/server/src/gitea/file-provider.ts +++ b/components/server/src/gitea/file-provider.ts @@ -8,8 +8,7 @@ import { injectable, inject } from 'inversify'; import { FileProvider, MaybeContent } from "../repohost/file-provider"; import { Commit, User, Repository } from "@gitpod/gitpod-protocol" -import { GiteaRestApi } from "./api"; -import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; +import { Gitea, GiteaRestApi } from "./api"; @injectable() export class GiteaFileProvider implements FileProvider { @@ -25,32 +24,34 @@ export class GiteaFileProvider implements FileProvider { } public async getLastChangeRevision(repository: Repository, revisionOrBranch: string, user: User, path: string): Promise { - const commits = (await this.giteaApi.run(user, (gh) => gh.repos.listCommits({ - owner: repository.owner, - repo: repository.name, + const commits = (await this.giteaApi.run(user, (api) => api.repos.repoGetAllCommits(repository.owner, repository.name, { sha: revisionOrBranch, - // per_page: 1, // we need just the last one right? + limit: 1, // we need just the last one right? path - }))).data; + }))); - const lastCommit = commits && commits[0]; - if (!lastCommit) { + if (Gitea.ApiError.is(commits) || commits.length === 0) { throw new Error(`File ${path} does not exist in repository ${repository.owner}/${repository.name}`); } - return lastCommit.sha; + const sha = commits[0].sha; + if (!sha) { + throw new Error(`File ${path} in repository ${repository.owner}/${repository.name} has no char. Is it a folder?`); + } + + return sha; } - public async getFileContent(commit: Commit, user: User, path: string) { + public async getFileContent(commit: Commit, user: User, path: string): Promise { if (!commit.revision) { return undefined; } - try { - const contents = await this.giteaApi.getFileContents(user, commit.repository.owner, commit.repository.name, commit.revision, path); - return contents; - } catch (err) { - log.error(err); + const contents = await this.giteaApi.run(user, api => api.repos.repoGetRawFile(commit.repository.owner, commit.repository.name, path, { ref: commit.revision })) + if (Gitea.ApiError.is(contents)) { + return undefined; // e.g. 404 error, because the file isn't found } + + return contents; } } diff --git a/components/server/src/gitea/gitea-auth-provider.ts b/components/server/src/gitea/gitea-auth-provider.ts index a8f82873726e77..6649149f1b9d7c 100644 --- a/components/server/src/gitea/gitea-auth-provider.ts +++ b/components/server/src/gitea/gitea-auth-provider.ts @@ -4,131 +4,106 @@ * See License-AGPL.txt in the project root for license information. */ -import { injectable } from 'inversify'; -import * as express from "express" -import { AuthProviderInfo } from '@gitpod/gitpod-protocol'; -import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; -import { GiteaScope } from "./scopes"; -import { AuthUserSetup } from "../auth/auth-provider"; -import { Octokit } from "@octokit/rest" -import { GiteaApiError } from "./api"; -import { GenericAuthProvider } from "../auth/generic-auth-provider"; -import { oauthUrls } from "./gitea-urls"; + import * as express from "express"; + import { injectable } from 'inversify'; + import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; + import { AuthProviderInfo } from '@gitpod/gitpod-protocol'; + import { GiteaScope } from "./scopes"; + import { UnconfirmedUserException } from "../auth/errors"; + import { Gitea } from "./api"; + import { GenericAuthProvider } from "../auth/generic-auth-provider"; + import { AuthUserSetup } from "../auth/auth-provider"; + import { oauthUrls } from "./gitea-urls"; -@injectable() -export class GiteaAuthProvider extends GenericAuthProvider { + @injectable() + export class GiteaAuthProvider extends GenericAuthProvider { - get info(): AuthProviderInfo { - return { - ...this.defaultInfo(), - scopes: GiteaScope.All, - requirements: { - default: GiteaScope.Requirements.DEFAULT, - publicRepo: GiteaScope.Requirements.PUBLIC_REPO, - privateRepo: GiteaScope.Requirements.PRIVATE_REPO, - }, - } - } - /** - * Augmented OAuthConfig for Gitea - */ - protected get oauthConfig() { - const oauth = this.params.oauth!; - const defaultUrls = oauthUrls(this.params.host); - const scopeSeparator = ","; - return { - ...oauth!, - authorizationUrl: oauth.authorizationUrl || defaultUrls.authorizationUrl, - tokenUrl: oauth.tokenUrl || defaultUrls.tokenUrl, - scope: GiteaScope.All.join(scopeSeparator), - scopeSeparator - }; - } + get info(): AuthProviderInfo { + return { + ...this.defaultInfo(), + scopes: GiteaScope.All, + requirements: { + default: GiteaScope.Requirements.DEFAULT, + publicRepo: GiteaScope.Requirements.PUBLIC_REPO, + privateRepo: GiteaScope.Requirements.PRIVATE_REPO, + }, + } + } - authorize(req: express.Request, res: express.Response, next: express.NextFunction, scope?: string[]): void { - super.authorize(req, res, next, scope ? scope : GiteaScope.Requirements.DEFAULT); - } + /** + * Augmented OAuthConfig for Gitea + */ + protected get oauthConfig() { + const oauth = this.params.oauth!; + const defaultUrls = oauthUrls(this.params.host); + const scopeSeparator = " "; + return { + ...oauth, + authorizationUrl: oauth.authorizationUrl || defaultUrls.authorizationUrl, + tokenUrl: oauth.tokenUrl || defaultUrls.tokenUrl, + // settingsUrl: oauth.settingsUrl || defaultUrls.settingsUrl, + scope: GiteaScope.All.join(scopeSeparator), + scopeSeparator + }; + } - protected get baseURL() { - return `https://${this.params.host}/api/v1`; - } + authorize(req: express.Request, res: express.Response, next: express.NextFunction, scope?: string[]): void { + super.authorize(req, res, next, scope ? scope : GiteaScope.Requirements.DEFAULT); + } - protected readAuthUserSetup = async (accessToken: string, _tokenResponse: object) => { - const api = new Octokit({ - auth: accessToken, - request: { - timeout: 5000, - }, - userAgent: this.USER_AGENT, - baseUrl: this.baseURL - }); - const fetchCurrentUser = async () => { - const response = await api.users.getAuthenticated(); - if (response.status !== 200) { - throw new GiteaApiError(response); - } - return response; - } - const fetchUserEmails = async () => { - const response = await api.users.listEmailsForAuthenticated({}); - if (response.status !== 200) { - throw new GiteaApiError(response); - } - return response.data; - } - const currentUserPromise = this.retry(() => fetchCurrentUser()); - const userEmailsPromise = this.retry(() => fetchUserEmails()); + protected get baseURL() { + return `https://${this.params.host}`; + } - try { - const [ { data: { id, login, avatar_url, name }, headers }, userEmails ] = await Promise.all([ currentUserPromise, userEmailsPromise ]); + protected readAuthUserSetup = async (accessToken: string, tokenResponse: object) => { + const api = Gitea.create(this.baseURL, accessToken); + const getCurrentUser = async () => { + const response = await api.user.userGetCurrent(); + return response.data as unknown as Gitea.User; + } + try { + const result = await getCurrentUser(); + if (result) { + if (!result.active || !result.created || !result.prohibit_login) { + throw UnconfirmedUserException.create("Please confirm and activate your Gitea account and try again.", result); + } + } - // https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/ - // e.g. X-OAuth-Scopes: repo, user - const currentScopes = this.normalizeScopes((headers as any)["x-oauth-scopes"] - .split(this.oauthConfig.scopeSeparator!) - .map((s: string) => s.trim()) - ); + return { + authUser: { + authId: String(result.id), + authName: result.login, + avatarUrl: result.avatar_url || undefined, + name: result.full_name, + primaryEmail: result.email + }, + currentScopes: this.readScopesFromVerifyParams(tokenResponse) + } + } catch (error) { + // TODO: cleanup + // if (error && typeof error.description === "string" && error.description.includes("403 Forbidden")) { + // // If Gitlab is configured to disallow OAuth-token based API access for unconfirmed users, we need to reject this attempt + // // 403 Forbidden - You (@...) must accept the Terms of Service in order to perform this action. Please access GitLab from a web browser to accept these terms. + // throw UnconfirmedUserException.create(error.description, error); + // } else { + log.error(`(${this.strategyName}) Reading current user info failed`, error, { accessToken, error }); + throw error; + // } + } - const filterPrimaryEmail = (emails: typeof userEmails) => { - if (this.config.blockNewUsers) { - // if there is any verified email with a domain that is in the blockNewUsersPassList then use this email as primary email - const emailDomainInPasslist = (mail: string) => this.config.blockNewUsers.passlist.some(e => mail.endsWith(`@${e}`)); - const result = emails.filter(e => e.verified).filter(e => emailDomainInPasslist(e.email)) - if (result.length > 0) { - return result[0].email; - } - } - // otherwise use Gitea's primary email as Gitpod's primary email - return emails.filter(e => e.primary)[0].email; - }; + } - return { - authUser: { - authId: String(id), - authName: login, - avatarUrl: avatar_url, - name, - primaryEmail: filterPrimaryEmail(userEmails) - }, - currentScopes - } + protected readScopesFromVerifyParams(params: any) { + if (params && typeof params.scope === 'string') { + return this.normalizeScopes(params.scope.split(' ')); + } + return []; + } - } catch (error) { - log.error(`(${this.strategyName}) Reading current user info failed`, error, { accessToken, error }); - throw error; - } - } + protected normalizeScopes(scopes: string[]) { + const set = new Set(scopes); + return Array.from(set).sort(); + } - protected normalizeScopes(scopes: string[]) { - const set = new Set(scopes); - if (set.has('repo')) { - set.add('public_repo'); - } - if (set.has('user')) { - set.add('user:email'); - } - return Array.from(set).sort(); - } - -} + } diff --git a/components/server/src/gitea/gitea-context-parser.spec.ts b/components/server/src/gitea/gitea-context-parser.spec.ts index dd7b9f39af09f2..9920a5a0948108 100644 --- a/components/server/src/gitea/gitea-context-parser.spec.ts +++ b/components/server/src/gitea/gitea-context-parser.spec.ts @@ -1,590 +1,590 @@ -/** - * Copyright (c) 2020 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. - */ - -// Use asyncIterators with es2015 -if (typeof (Symbol as any).asyncIterator === 'undefined') { - (Symbol as any).asyncIterator = Symbol.asyncIterator || Symbol('asyncIterator'); -} -import "reflect-metadata"; - -import { suite, test, timeout, retries } from "mocha-typescript"; -import * as chai from 'chai'; -const expect = chai.expect; - -import { BranchRef, GiteaGraphQlEndpoint } from './api'; -import { NotFoundError } from '../errors'; -import { GiteaContextParser } from './gitea-context-parser'; -import { User } from "@gitpod/gitpod-protocol"; -import { ContainerModule, Container } from "inversify"; -import { Config } from "../config"; -import { DevData } from "../dev/dev-data"; -import { AuthProviderParams } from "../auth/auth-provider"; -import { TokenProvider } from "../user/token-provider"; -import { GiteaTokenHelper } from "./gitea-token-helper"; -import { HostContextProvider } from "../auth/host-context-provider"; -import { skipIfEnvVarNotSet } from "@gitpod/gitpod-protocol/lib/util/skip-if"; - -@suite(timeout(10000), retries(2), skipIfEnvVarNotSet("GITPOD_TEST_TOKEN_GITEA")) -class TestGiteaContextParser { - - protected parser: GiteaContextParser; - protected user: User; - - public before() { - const container = new Container(); - container.load(new ContainerModule((bind, unbind, isBound, rebind) => { - bind(Config).toConstantValue({ - // meant to appease DI, but Config is never actually used here - }); - bind(GiteaContextParser).toSelf().inSingletonScope(); - bind(GiteaGraphQlEndpoint).toSelf().inSingletonScope(); - bind(AuthProviderParams).toConstantValue(TestGiteaContextParser.AUTH_HOST_CONFIG); - bind(GiteaTokenHelper).toSelf().inSingletonScope(); - bind(TokenProvider).toConstantValue({ - getTokenForHost: async (user: User, host: string) => { - return DevData.createGiteaTestToken(); - } - }); - bind(HostContextProvider).toConstantValue(DevData.createDummyHostContextProvider()); - })); - this.parser = container.get(GiteaContextParser); - this.user = DevData.createTestUser(); - } - - static readonly AUTH_HOST_CONFIG: Partial = { - id: "Public-Gitea", - type: "Gitea", - verified: true, - description: "", - icon: "", - host: "github.com", - oauth: "not-used" as any - } - - static readonly BRANCH_TEST = { - name: "test", - commit: { - sha: "testsha", - url: "testurl" - }, - protected: false, - protection_url: "" - }; - - static readonly BRANCH_ISSUE_974 = { - name: "ak/lmcbout-issue_974", - commit: { - sha: "sha974", - url: "url974" - }, - protected: false, - protection_url: "" - }; - - static readonly BLO_BLA_ERROR_DATA = { - host: "github.com", - lastUpdate: undefined, - owner: 'blo', - repoName: 'bla', - userIsOwner: false, - userScopes: ["user:email", "public_repo", "repo"], - }; - - protected getTestBranches(): BranchRef[] { - return [TestGiteaContextParser.BRANCH_TEST, TestGiteaContextParser.BRANCH_ISSUE_974]; - } - - protected get bloBlaErrorData() { - return TestGiteaContextParser.BLO_BLA_ERROR_DATA; - } - - @test public async testErrorContext_01() { - try { - await this.parser.handle({}, this.user, 'https://github.com/blo/bla'); - } catch (e) { - expect(NotFoundError.is(e)); - expect(e.data).to.deep.equal(this.bloBlaErrorData); - } - } - - @test public async testErrorContext_02() { - try { - await this.parser.handle({}, this.user, 'https://github.com/blo/bla/pull/42'); - } catch (e) { - expect(NotFoundError.is(e)); - expect(e.data).to.deep.equal(this.bloBlaErrorData); - } - } - - @test public async testErrorContext_03() { - try { - await this.parser.handle({}, this.user, 'https://github.com/blo/bla/issues/42'); - } catch (e) { - expect(NotFoundError.is(e)); - expect(e.data).to.deep.equal(this.bloBlaErrorData); - } - } - - @test public async testErrorContext_04() { - try { - await this.parser.handle({}, this.user, 'https://github.com/blo/bla/tree/my/branch/path/foo.ts'); - } catch (e) { - expect(NotFoundError.is(e)); - expect(e.data).to.deep.equal(this.bloBlaErrorData); - } - } - - @test public async testTreeContext_01() { - const result = await this.parser.handle({}, this.user, 'https://github.com/eclipse-theia/theia'); - expect(result).to.deep.include({ - "ref": "master", - "refType": "branch", - "path": "", - "isFile": false, - "repository": { - "host": "github.com", - "owner": "eclipse-theia", - "name": "theia", - "cloneUrl": "https://github.com/eclipse-theia/theia.git", - "private": false - }, - "title": "eclipse-theia/theia - master" - }) - } - - @test public async testTreeContext_02() { - const result = await this.parser.handle({}, this.user, 'https://github.com/eclipse-theia/theia/tree/master'); - expect(result).to.deep.include({ - "ref": "master", - "refType": "branch", - "path": "", - "isFile": false, - "repository": { - "host": "github.com", - "owner": "eclipse-theia", - "name": "theia", - "cloneUrl": "https://github.com/eclipse-theia/theia.git", - "private": false - }, - "title": "eclipse-theia/theia - master" - }) - } - - @test public async testTreeContext_03() { - const result = await this.parser.handle({}, this.user, 'https://github.com/eclipse-theia/theia/tree/master/LICENSE'); - expect(result).to.deep.include({ - "ref": "master", - "refType": "branch", - "path": "LICENSE", - "isFile": true, - "repository": { - "host": "github.com", - "owner": "eclipse-theia", - "name": "theia", - "cloneUrl": "https://github.com/eclipse-theia/theia.git", - "private": false - }, - "title": "eclipse-theia/theia - master" - }) - } - - @test public async testTreeContext_04() { - const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/blob/nametest/src/src/server.ts'); - expect(result).to.deep.include({ - "ref": "nametest/src", - "refType": "branch", - "path": "src/server.ts", - "isFile": true, - "repository": { - "host": "github.com", - "owner": "gitpod-io", - "name": "gitpod-test-repo", - "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", - "private": false - }, - "title": "gitpod-io/gitpod-test-repo - nametest/src" - }) - } - - @test public async testTreeContext_05() { - const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/tree/499efbbcb50e7e6e5e2883053f72a34cd5396be3/folder1/folder2'); - expect(result).to.deep.include( - { - "title": "gitpod-io/gitpod-test-repo - 499efbbc:folder1/folder2", - "repository": { - "host": "github.com", - "owner": "gitpod-io", - "name": "gitpod-test-repo", - "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", - "private": false - }, - "revision": "499efbbcb50e7e6e5e2883053f72a34cd5396be3", - "isFile": false, - "path": "folder1/folder2" - } - ) - } - - @test public async testTreeContext_06() { - const result = await this.parser.handle({}, this.user, 'https://github.com/Snailclimb/JavaGuide/blob/940982ebffa5f376b6baddeaf9ed41c91217a6b6/数据结构与算法/常见安全算法(MD5、SHA1、Base64等等)总结.md'); - expect(result).to.deep.include( - { - "title": "Snailclimb/JavaGuide - 940982eb:数据结构与算法/常见安全算法(MD5、SHA1、Base64等等)总结.md", - "repository": { - "host": "github.com", - "owner": "Snailclimb", - "name": "JavaGuide", - "cloneUrl": "https://github.com/Snailclimb/JavaGuide.git", - "private": false - }, - "revision": "940982ebffa5f376b6baddeaf9ed41c91217a6b6", - "isFile": true, - "path": "数据结构与算法/常见安全算法(MD5、SHA1、Base64等等)总结.md" - } - ) - } - - @test public async testTreeContext_07() { - const result = await this.parser.handle({}, this.user, 'https://github.com/eclipse-theia/theia#license'); - expect(result).to.deep.include({ - "ref": "master", - "refType": "branch", - "path": "", - "isFile": false, - "repository": { - "host": "github.com", - "owner": "eclipse-theia", - "name": "theia", - "cloneUrl": "https://github.com/eclipse-theia/theia.git", - "private": false - }, - "title": "eclipse-theia/theia - master" - }) - } - - @test public async testTreeContext_tag_01() { - const result = await this.parser.handle({}, this.user, 'https://github.com/eclipse-theia/theia/tree/v0.1.0'); - expect(result).to.deep.include( - { - "title": "eclipse-theia/theia - v0.1.0", - "repository": { - "host": "github.com", - "owner": "eclipse-theia", - "name": "theia", - "cloneUrl": "https://github.com/eclipse-theia/theia.git", - "private": false - }, - "revision": "f29626847a14ca50dd78483aebaf4b4fe26bcb73", - "isFile": false, - "ref": "v0.1.0", - "refType": "tag" - } - ) - } - - @test public async testReleasesContext_tag_01() { - const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod/releases/tag/v0.9.0'); - expect(result).to.deep.include( - { - "ref": "v0.9.0", - "refType": "tag", - "isFile": false, - "path": "", - "title": "gitpod-io/gitpod - v0.9.0", - "revision": "25ece59c495d525614f28971d41d5708a31bf1e3", - "repository": { - "cloneUrl": "https://github.com/gitpod-io/gitpod.git", - "host": "github.com", - "name": "gitpod", - "owner": "gitpod-io", - "private": false - } - } - ) - } - - @test public async testCommitsContext_01() { - const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/commits/4test'); - expect(result).to.deep.include({ - "ref": "4test", - "refType": "branch", - "path": "", - "isFile": false, - "repository": { - "host": "github.com", - "owner": "gitpod-io", - "name": "gitpod-test-repo", - "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", - "private": false - }, - "title": "gitpod-io/gitpod-test-repo - 4test" - }) - } - - @test public async testCommitContext_01() { - const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/commit/409ac2de49a53d679989d438735f78204f441634'); - expect(result).to.deep.include({ - "ref": "", - "refType": "revision", - "path": "", - "revision": "409ac2de49a53d679989d438735f78204f441634", - "isFile": false, - "repository": { - "host": "github.com", - "owner": "gitpod-io", - "name": "gitpod-test-repo", - "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", - "private": false - }, - "title": "gitpod-io/gitpod-test-repo - Test 3" - }) - } - - @test public async testCommitContext_02_notExistingCommit() { - try { - await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/commit/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); - // ensure that an error has been thrown - chai.assert.fail(); - } catch (e) { - expect(e.message).contains("Couldn't find commit"); - } - } - - @test public async testCommitContext_02_invalidSha() { - try { - await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/commit/invalid'); - // ensure that an error has been thrown - chai.assert.fail(); - } catch (e) { - expect(e.message).contains("Invalid commit ID"); - } - } - - @test public async testPullRequestContext_01() { - const result = await this.parser.handle({}, this.user, 'https://github.com/TypeFox/theia/pull/1'); - expect(result).to.deep.include( - { - "title": "Merge master", - "repository": { - "host": "github.com", - "owner": "eclipse-theia", - "name": "theia", - "cloneUrl": "https://github.com/eclipse-theia/theia.git", - "private": false - }, - "ref": "master", - "refType": "branch", - "nr": 1, - "base": { - "repository": { - "host": "github.com", - "owner": "TypeFox", - "name": "theia", - "cloneUrl": "https://github.com/TypeFox/theia.git", - "private": false, - "fork": { - "parent": { - "cloneUrl": "https://github.com/eclipse-theia/theia.git", - "host": "github.com", - "name": "theia", - "owner": "eclipse-theia", - "private": false - } - } - }, - "ref": "master", - "refType": "branch", - } - } - ) - } - - @test public async testPullRequestthroughIssueContext_04() { - const result = await this.parser.handle({}, this.user, 'https://github.com/TypeFox/theia/issues/1'); - expect(result).to.deep.include( - { - "title": "Merge master", - "repository": { - "host": "github.com", - "owner": "eclipse-theia", - "name": "theia", - "cloneUrl": "https://github.com/eclipse-theia/theia.git", - "private": false - }, - "ref": "master", - "refType": "branch", - "nr": 1, - "base": { - "repository": { - "host": "github.com", - "owner": "TypeFox", - "name": "theia", - "cloneUrl": "https://github.com/TypeFox/theia.git", - "private": false, - "fork": { - "parent": { - "cloneUrl": "https://github.com/eclipse-theia/theia.git", - "host": "github.com", - "name": "theia", - "owner": "eclipse-theia", - "private": false - } - } - }, - "ref": "master", - "refType": "branch", - } - } - ) - } - - @test public async testIssueContext_01() { - const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/issues/42'); - expect(result).to.deep.include( - { - "title": "Test issue web-extension", - "repository": { - "host": "github.com", - "owner": "gitpod-io", - "name": "gitpod-test-repo", - "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", - "private": false - }, - "owner": "gitpod-io", - "nr": 42, - "ref": "1test", - "refType": "branch", - "localBranch": "somefox/test-issue-web-extension-42" - } - ) - } - - @test public async testIssuePageContext() { - const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/issues'); - expect(result).to.deep.include( - { - "title": "gitpod-io/gitpod-test-repo - 1test", - "repository": { - "host": "github.com", - "owner": "gitpod-io", - "name": "gitpod-test-repo", - "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", - "private": false - }, - "ref": "1test", - "refType": "branch", - } - ) - } - - - @test public async testIssueThroughPullRequestContext() { - const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/pull/42'); - expect(result).to.deep.include( - { - "title": "Test issue web-extension", - "repository": { - "host": "github.com", - "owner": "gitpod-io", - "name": "gitpod-test-repo", - "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", - "private": false - }, - "owner": "gitpod-io", - "nr": 42, - "ref": "1test", - "refType": "branch", - "localBranch": "somefox/test-issue-web-extension-42" - } - ) - } - - @test public async testBlobContext_01() { - const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/blob/aba298d5084a817cdde3dd1f26692bc2a216e2b9/test-comment-01.md'); - expect(result).to.deep.include( - { - "title": "gitpod-io/gitpod-test-repo - aba298d5:test-comment-01.md", - "repository": { - "host": "github.com", - "owner": "gitpod-io", - "name": "gitpod-test-repo", - "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", - "private": false - }, - "revision": "aba298d5084a817cdde3dd1f26692bc2a216e2b9", - "isFile": true, - "path": "test-comment-01.md" - } - ) - } - - @test public async testBlobContext_02() { - const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/blob/499efbbcb50e7e6e5e2883053f72a34cd5396be3/folder1/folder2/content2'); - expect(result).to.deep.include( - { - "title": "gitpod-io/gitpod-test-repo - 499efbbc:folder1/folder2/content2", - "repository": { - "host": "github.com", - "owner": "gitpod-io", - "name": "gitpod-test-repo", - "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", - "private": false - }, - "revision": "499efbbcb50e7e6e5e2883053f72a34cd5396be3", - "isFile": true, - "path": "folder1/folder2/content2" - } - ) - } - - @test public async testBlobContextShort_01() { - const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/blob/499efbbc/folder1/folder2/content2'); - expect(result).to.deep.include( - { - "title": "gitpod-io/gitpod-test-repo - 499efbbc:folder1/folder2/content2", - "repository": { - "host": "github.com", - "owner": "gitpod-io", - "name": "gitpod-test-repo", - "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", - "private": false - }, - "revision": "499efbbcb50e7e6e5e2883053f72a34cd5396be3", - "isFile": true, - "path": "folder1/folder2/content2" - } - ) - } - - @test public async testBlobContextShort_02() { - const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/blob/499ef/folder1/folder2/content2'); - expect(result).to.deep.include( - { - "title": "gitpod-io/gitpod-test-repo - 499efbbc:folder1/folder2/content2", - "repository": { - "host": "github.com", - "owner": "gitpod-io", - "name": "gitpod-test-repo", - "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", - "private": false - }, - "revision": "499efbbcb50e7e6e5e2883053f72a34cd5396be3", - "isFile": true, - "path": "folder1/folder2/content2" - } - ) - } - - @test public async testFetchCommitHistory() { - const result = await this.parser.fetchCommitHistory({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo', '409ac2de49a53d679989d438735f78204f441634', 100); - expect(result).to.deep.equal([ - '506e5aed317f28023994ecf8ca6ed91430e9c1a4', - 'f5b041513bfab914b5fbf7ae55788d9835004d76', - ]) - } - -} -module.exports = new TestGiteaContextParser() // Only to circumvent no usage warning :-/ \ No newline at end of file +// /** +// * Copyright (c) 2020 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. +// */ + +// // Use asyncIterators with es2015 +// if (typeof (Symbol as any).asyncIterator === 'undefined') { +// (Symbol as any).asyncIterator = Symbol.asyncIterator || Symbol('asyncIterator'); +// } +// import "reflect-metadata"; + +// import { suite, test, timeout, retries } from "mocha-typescript"; +// import * as chai from 'chai'; +// const expect = chai.expect; + +// import { BranchRef, GiteaGraphQlEndpoint } from './api'; +// import { NotFoundError } from '../errors'; +// import { GiteaContextParser } from './gitea-context-parser'; +// import { User } from "@gitpod/gitpod-protocol"; +// import { ContainerModule, Container } from "inversify"; +// import { Config } from "../config"; +// import { DevData } from "../dev/dev-data"; +// import { AuthProviderParams } from "../auth/auth-provider"; +// import { TokenProvider } from "../user/token-provider"; +// import { GiteaTokenHelper } from "./gitea-token-helper"; +// import { HostContextProvider } from "../auth/host-context-provider"; +// import { skipIfEnvVarNotSet } from "@gitpod/gitpod-protocol/lib/util/skip-if"; + +// @suite(timeout(10000), retries(2), skipIfEnvVarNotSet("GITPOD_TEST_TOKEN_GITEA")) +// class TestGiteaContextParser { + +// protected parser: GiteaContextParser; +// protected user: User; + +// public before() { +// const container = new Container(); +// container.load(new ContainerModule((bind, unbind, isBound, rebind) => { +// bind(Config).toConstantValue({ +// // meant to appease DI, but Config is never actually used here +// }); +// bind(GiteaContextParser).toSelf().inSingletonScope(); +// bind(GiteaGraphQlEndpoint).toSelf().inSingletonScope(); +// bind(AuthProviderParams).toConstantValue(TestGiteaContextParser.AUTH_HOST_CONFIG); +// bind(GiteaTokenHelper).toSelf().inSingletonScope(); +// bind(TokenProvider).toConstantValue({ +// getTokenForHost: async (user: User, host: string) => { +// return DevData.createGiteaTestToken(); +// } +// }); +// bind(HostContextProvider).toConstantValue(DevData.createDummyHostContextProvider()); +// })); +// this.parser = container.get(GiteaContextParser); +// this.user = DevData.createTestUser(); +// } + +// static readonly AUTH_HOST_CONFIG: Partial = { +// id: "Public-Gitea", +// type: "Gitea", +// verified: true, +// description: "", +// icon: "", +// host: "github.com", +// oauth: "not-used" as any +// } + +// static readonly BRANCH_TEST = { +// name: "test", +// commit: { +// sha: "testsha", +// url: "testurl" +// }, +// protected: false, +// protection_url: "" +// }; + +// static readonly BRANCH_ISSUE_974 = { +// name: "ak/lmcbout-issue_974", +// commit: { +// sha: "sha974", +// url: "url974" +// }, +// protected: false, +// protection_url: "" +// }; + +// static readonly BLO_BLA_ERROR_DATA = { +// host: "github.com", +// lastUpdate: undefined, +// owner: 'blo', +// repoName: 'bla', +// userIsOwner: false, +// userScopes: ["user:email", "public_repo", "repo"], +// }; + +// protected getTestBranches(): BranchRef[] { +// return [TestGiteaContextParser.BRANCH_TEST, TestGiteaContextParser.BRANCH_ISSUE_974]; +// } + +// protected get bloBlaErrorData() { +// return TestGiteaContextParser.BLO_BLA_ERROR_DATA; +// } + +// @test public async testErrorContext_01() { +// try { +// await this.parser.handle({}, this.user, 'https://github.com/blo/bla'); +// } catch (e) { +// expect(NotFoundError.is(e)); +// expect(e.data).to.deep.equal(this.bloBlaErrorData); +// } +// } + +// @test public async testErrorContext_02() { +// try { +// await this.parser.handle({}, this.user, 'https://github.com/blo/bla/pull/42'); +// } catch (e) { +// expect(NotFoundError.is(e)); +// expect(e.data).to.deep.equal(this.bloBlaErrorData); +// } +// } + +// @test public async testErrorContext_03() { +// try { +// await this.parser.handle({}, this.user, 'https://github.com/blo/bla/issues/42'); +// } catch (e) { +// expect(NotFoundError.is(e)); +// expect(e.data).to.deep.equal(this.bloBlaErrorData); +// } +// } + +// @test public async testErrorContext_04() { +// try { +// await this.parser.handle({}, this.user, 'https://github.com/blo/bla/tree/my/branch/path/foo.ts'); +// } catch (e) { +// expect(NotFoundError.is(e)); +// expect(e.data).to.deep.equal(this.bloBlaErrorData); +// } +// } + +// @test public async testTreeContext_01() { +// const result = await this.parser.handle({}, this.user, 'https://github.com/eclipse-theia/theia'); +// expect(result).to.deep.include({ +// "ref": "master", +// "refType": "branch", +// "path": "", +// "isFile": false, +// "repository": { +// "host": "github.com", +// "owner": "eclipse-theia", +// "name": "theia", +// "cloneUrl": "https://github.com/eclipse-theia/theia.git", +// "private": false +// }, +// "title": "eclipse-theia/theia - master" +// }) +// } + +// @test public async testTreeContext_02() { +// const result = await this.parser.handle({}, this.user, 'https://github.com/eclipse-theia/theia/tree/master'); +// expect(result).to.deep.include({ +// "ref": "master", +// "refType": "branch", +// "path": "", +// "isFile": false, +// "repository": { +// "host": "github.com", +// "owner": "eclipse-theia", +// "name": "theia", +// "cloneUrl": "https://github.com/eclipse-theia/theia.git", +// "private": false +// }, +// "title": "eclipse-theia/theia - master" +// }) +// } + +// @test public async testTreeContext_03() { +// const result = await this.parser.handle({}, this.user, 'https://github.com/eclipse-theia/theia/tree/master/LICENSE'); +// expect(result).to.deep.include({ +// "ref": "master", +// "refType": "branch", +// "path": "LICENSE", +// "isFile": true, +// "repository": { +// "host": "github.com", +// "owner": "eclipse-theia", +// "name": "theia", +// "cloneUrl": "https://github.com/eclipse-theia/theia.git", +// "private": false +// }, +// "title": "eclipse-theia/theia - master" +// }) +// } + +// @test public async testTreeContext_04() { +// const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/blob/nametest/src/src/server.ts'); +// expect(result).to.deep.include({ +// "ref": "nametest/src", +// "refType": "branch", +// "path": "src/server.ts", +// "isFile": true, +// "repository": { +// "host": "github.com", +// "owner": "gitpod-io", +// "name": "gitpod-test-repo", +// "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", +// "private": false +// }, +// "title": "gitpod-io/gitpod-test-repo - nametest/src" +// }) +// } + +// @test public async testTreeContext_05() { +// const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/tree/499efbbcb50e7e6e5e2883053f72a34cd5396be3/folder1/folder2'); +// expect(result).to.deep.include( +// { +// "title": "gitpod-io/gitpod-test-repo - 499efbbc:folder1/folder2", +// "repository": { +// "host": "github.com", +// "owner": "gitpod-io", +// "name": "gitpod-test-repo", +// "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", +// "private": false +// }, +// "revision": "499efbbcb50e7e6e5e2883053f72a34cd5396be3", +// "isFile": false, +// "path": "folder1/folder2" +// } +// ) +// } + +// @test public async testTreeContext_06() { +// const result = await this.parser.handle({}, this.user, 'https://github.com/Snailclimb/JavaGuide/blob/940982ebffa5f376b6baddeaf9ed41c91217a6b6/数据结构与算法/常见安全算法(MD5、SHA1、Base64等等)总结.md'); +// expect(result).to.deep.include( +// { +// "title": "Snailclimb/JavaGuide - 940982eb:数据结构与算法/常见安全算法(MD5、SHA1、Base64等等)总结.md", +// "repository": { +// "host": "github.com", +// "owner": "Snailclimb", +// "name": "JavaGuide", +// "cloneUrl": "https://github.com/Snailclimb/JavaGuide.git", +// "private": false +// }, +// "revision": "940982ebffa5f376b6baddeaf9ed41c91217a6b6", +// "isFile": true, +// "path": "数据结构与算法/常见安全算法(MD5、SHA1、Base64等等)总结.md" +// } +// ) +// } + +// @test public async testTreeContext_07() { +// const result = await this.parser.handle({}, this.user, 'https://github.com/eclipse-theia/theia#license'); +// expect(result).to.deep.include({ +// "ref": "master", +// "refType": "branch", +// "path": "", +// "isFile": false, +// "repository": { +// "host": "github.com", +// "owner": "eclipse-theia", +// "name": "theia", +// "cloneUrl": "https://github.com/eclipse-theia/theia.git", +// "private": false +// }, +// "title": "eclipse-theia/theia - master" +// }) +// } + +// @test public async testTreeContext_tag_01() { +// const result = await this.parser.handle({}, this.user, 'https://github.com/eclipse-theia/theia/tree/v0.1.0'); +// expect(result).to.deep.include( +// { +// "title": "eclipse-theia/theia - v0.1.0", +// "repository": { +// "host": "github.com", +// "owner": "eclipse-theia", +// "name": "theia", +// "cloneUrl": "https://github.com/eclipse-theia/theia.git", +// "private": false +// }, +// "revision": "f29626847a14ca50dd78483aebaf4b4fe26bcb73", +// "isFile": false, +// "ref": "v0.1.0", +// "refType": "tag" +// } +// ) +// } + +// @test public async testReleasesContext_tag_01() { +// const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod/releases/tag/v0.9.0'); +// expect(result).to.deep.include( +// { +// "ref": "v0.9.0", +// "refType": "tag", +// "isFile": false, +// "path": "", +// "title": "gitpod-io/gitpod - v0.9.0", +// "revision": "25ece59c495d525614f28971d41d5708a31bf1e3", +// "repository": { +// "cloneUrl": "https://github.com/gitpod-io/gitpod.git", +// "host": "github.com", +// "name": "gitpod", +// "owner": "gitpod-io", +// "private": false +// } +// } +// ) +// } + +// @test public async testCommitsContext_01() { +// const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/commits/4test'); +// expect(result).to.deep.include({ +// "ref": "4test", +// "refType": "branch", +// "path": "", +// "isFile": false, +// "repository": { +// "host": "github.com", +// "owner": "gitpod-io", +// "name": "gitpod-test-repo", +// "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", +// "private": false +// }, +// "title": "gitpod-io/gitpod-test-repo - 4test" +// }) +// } + +// @test public async testCommitContext_01() { +// const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/commit/409ac2de49a53d679989d438735f78204f441634'); +// expect(result).to.deep.include({ +// "ref": "", +// "refType": "revision", +// "path": "", +// "revision": "409ac2de49a53d679989d438735f78204f441634", +// "isFile": false, +// "repository": { +// "host": "github.com", +// "owner": "gitpod-io", +// "name": "gitpod-test-repo", +// "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", +// "private": false +// }, +// "title": "gitpod-io/gitpod-test-repo - Test 3" +// }) +// } + +// @test public async testCommitContext_02_notExistingCommit() { +// try { +// await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/commit/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); +// // ensure that an error has been thrown +// chai.assert.fail(); +// } catch (e) { +// expect(e.message).contains("Couldn't find commit"); +// } +// } + +// @test public async testCommitContext_02_invalidSha() { +// try { +// await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/commit/invalid'); +// // ensure that an error has been thrown +// chai.assert.fail(); +// } catch (e) { +// expect(e.message).contains("Invalid commit ID"); +// } +// } + +// @test public async testPullRequestContext_01() { +// const result = await this.parser.handle({}, this.user, 'https://github.com/TypeFox/theia/pull/1'); +// expect(result).to.deep.include( +// { +// "title": "Merge master", +// "repository": { +// "host": "github.com", +// "owner": "eclipse-theia", +// "name": "theia", +// "cloneUrl": "https://github.com/eclipse-theia/theia.git", +// "private": false +// }, +// "ref": "master", +// "refType": "branch", +// "nr": 1, +// "base": { +// "repository": { +// "host": "github.com", +// "owner": "TypeFox", +// "name": "theia", +// "cloneUrl": "https://github.com/TypeFox/theia.git", +// "private": false, +// "fork": { +// "parent": { +// "cloneUrl": "https://github.com/eclipse-theia/theia.git", +// "host": "github.com", +// "name": "theia", +// "owner": "eclipse-theia", +// "private": false +// } +// } +// }, +// "ref": "master", +// "refType": "branch", +// } +// } +// ) +// } + +// @test public async testPullRequestthroughIssueContext_04() { +// const result = await this.parser.handle({}, this.user, 'https://github.com/TypeFox/theia/issues/1'); +// expect(result).to.deep.include( +// { +// "title": "Merge master", +// "repository": { +// "host": "github.com", +// "owner": "eclipse-theia", +// "name": "theia", +// "cloneUrl": "https://github.com/eclipse-theia/theia.git", +// "private": false +// }, +// "ref": "master", +// "refType": "branch", +// "nr": 1, +// "base": { +// "repository": { +// "host": "github.com", +// "owner": "TypeFox", +// "name": "theia", +// "cloneUrl": "https://github.com/TypeFox/theia.git", +// "private": false, +// "fork": { +// "parent": { +// "cloneUrl": "https://github.com/eclipse-theia/theia.git", +// "host": "github.com", +// "name": "theia", +// "owner": "eclipse-theia", +// "private": false +// } +// } +// }, +// "ref": "master", +// "refType": "branch", +// } +// } +// ) +// } + +// @test public async testIssueContext_01() { +// const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/issues/42'); +// expect(result).to.deep.include( +// { +// "title": "Test issue web-extension", +// "repository": { +// "host": "github.com", +// "owner": "gitpod-io", +// "name": "gitpod-test-repo", +// "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", +// "private": false +// }, +// "owner": "gitpod-io", +// "nr": 42, +// "ref": "1test", +// "refType": "branch", +// "localBranch": "somefox/test-issue-web-extension-42" +// } +// ) +// } + +// @test public async testIssuePageContext() { +// const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/issues'); +// expect(result).to.deep.include( +// { +// "title": "gitpod-io/gitpod-test-repo - 1test", +// "repository": { +// "host": "github.com", +// "owner": "gitpod-io", +// "name": "gitpod-test-repo", +// "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", +// "private": false +// }, +// "ref": "1test", +// "refType": "branch", +// } +// ) +// } + + +// @test public async testIssueThroughPullRequestContext() { +// const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/pull/42'); +// expect(result).to.deep.include( +// { +// "title": "Test issue web-extension", +// "repository": { +// "host": "github.com", +// "owner": "gitpod-io", +// "name": "gitpod-test-repo", +// "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", +// "private": false +// }, +// "owner": "gitpod-io", +// "nr": 42, +// "ref": "1test", +// "refType": "branch", +// "localBranch": "somefox/test-issue-web-extension-42" +// } +// ) +// } + +// @test public async testBlobContext_01() { +// const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/blob/aba298d5084a817cdde3dd1f26692bc2a216e2b9/test-comment-01.md'); +// expect(result).to.deep.include( +// { +// "title": "gitpod-io/gitpod-test-repo - aba298d5:test-comment-01.md", +// "repository": { +// "host": "github.com", +// "owner": "gitpod-io", +// "name": "gitpod-test-repo", +// "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", +// "private": false +// }, +// "revision": "aba298d5084a817cdde3dd1f26692bc2a216e2b9", +// "isFile": true, +// "path": "test-comment-01.md" +// } +// ) +// } + +// @test public async testBlobContext_02() { +// const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/blob/499efbbcb50e7e6e5e2883053f72a34cd5396be3/folder1/folder2/content2'); +// expect(result).to.deep.include( +// { +// "title": "gitpod-io/gitpod-test-repo - 499efbbc:folder1/folder2/content2", +// "repository": { +// "host": "github.com", +// "owner": "gitpod-io", +// "name": "gitpod-test-repo", +// "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", +// "private": false +// }, +// "revision": "499efbbcb50e7e6e5e2883053f72a34cd5396be3", +// "isFile": true, +// "path": "folder1/folder2/content2" +// } +// ) +// } + +// @test public async testBlobContextShort_01() { +// const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/blob/499efbbc/folder1/folder2/content2'); +// expect(result).to.deep.include( +// { +// "title": "gitpod-io/gitpod-test-repo - 499efbbc:folder1/folder2/content2", +// "repository": { +// "host": "github.com", +// "owner": "gitpod-io", +// "name": "gitpod-test-repo", +// "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", +// "private": false +// }, +// "revision": "499efbbcb50e7e6e5e2883053f72a34cd5396be3", +// "isFile": true, +// "path": "folder1/folder2/content2" +// } +// ) +// } + +// @test public async testBlobContextShort_02() { +// const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/blob/499ef/folder1/folder2/content2'); +// expect(result).to.deep.include( +// { +// "title": "gitpod-io/gitpod-test-repo - 499efbbc:folder1/folder2/content2", +// "repository": { +// "host": "github.com", +// "owner": "gitpod-io", +// "name": "gitpod-test-repo", +// "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", +// "private": false +// }, +// "revision": "499efbbcb50e7e6e5e2883053f72a34cd5396be3", +// "isFile": true, +// "path": "folder1/folder2/content2" +// } +// ) +// } + +// @test public async testFetchCommitHistory() { +// const result = await this.parser.fetchCommitHistory({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo', '409ac2de49a53d679989d438735f78204f441634', 100); +// expect(result).to.deep.equal([ +// '506e5aed317f28023994ecf8ca6ed91430e9c1a4', +// 'f5b041513bfab914b5fbf7ae55788d9835004d76', +// ]) +// } + +// } +// module.exports = new TestGiteaContextParser() // Only to circumvent no usage warning :-/ \ No newline at end of file diff --git a/components/server/src/gitea/gitea-context-parser.ts b/components/server/src/gitea/gitea-context-parser.ts index 3c1a53988202ac..81370079cd8ac7 100644 --- a/components/server/src/gitea/gitea-context-parser.ts +++ b/components/server/src/gitea/gitea-context-parser.ts @@ -4,477 +4,389 @@ * See License-AGPL.txt in the project root for license information. */ -import { injectable, inject } from 'inversify'; - -import { Repository, PullRequestContext, NavigatorContext, IssueContext, User, CommitContext, RefType } from '@gitpod/gitpod-protocol'; -import { GiteaGraphQlEndpoint } from './api'; -import { NotFoundError, UnauthorizedError } from '../errors'; -import { log, LogContext, LogPayload } from '@gitpod/gitpod-protocol/lib/util/logging'; -import { IContextParser, IssueContexts, AbstractContextParser } from '../workspace/context-parser'; -import { GiteaScope } from './scopes'; -import { GiteaTokenHelper } from './gitea-token-helper'; -import { TraceContext } from '@gitpod/gitpod-protocol/lib/util/tracing'; - -@injectable() -export class GiteaContextParser extends AbstractContextParser implements IContextParser { - - @inject(GiteaGraphQlEndpoint) protected readonly githubQueryApi: GiteaGraphQlEndpoint; - @inject(GiteaTokenHelper) protected readonly tokenHelper: GiteaTokenHelper; - - protected get authProviderId() { - return this.config.id; - } - - public async handle(ctx: TraceContext, user: User, contextUrl: string): Promise { - const span = TraceContext.startSpan("GiteaContextParser.handle", ctx); - - try { - const { host, owner, repoName, moreSegments } = await this.parseURL(user, contextUrl); - if (moreSegments.length > 0) { - switch (moreSegments[0]) { - case 'pull': { - return await this.handlePullRequestContext({span}, user, host, owner, repoName, parseInt(moreSegments[1], 10)); - } - case 'tree': - case 'blob': - case 'commits': { - return await this.handleTreeContext({span}, user, host, owner, repoName, moreSegments.slice(1)); - } - case 'releases': { - if (moreSegments.length > 1 && moreSegments[1] === "tag") { - return await this.handleTreeContext({ span }, user, host, owner, repoName, moreSegments.slice(2)); - } - break; - } - case 'issues': { - const issueNr = parseInt(moreSegments[1], 10); - if (isNaN(issueNr)) - break; - return await this.handleIssueContext({span}, user, host, owner, repoName, issueNr); - } - case 'commit': { - return await this.handleCommitContext({span}, user, host, owner, repoName, moreSegments[1]); - } + import { injectable, inject } from 'inversify'; + + import { NavigatorContext, User, CommitContext, Repository, PullRequestContext, IssueContext, RefType } from '@gitpod/gitpod-protocol'; + import { GiteaRestApi, Gitea } from './api'; + import { UnauthorizedError, NotFoundError } from '../errors'; + import { GiteaScope } from './scopes'; + import { IContextParser, IssueContexts, AbstractContextParser } from '../workspace/context-parser'; + import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; + import { GiteaTokenHelper } from './gitea-token-helper'; + import { TraceContext } from '@gitpod/gitpod-protocol/lib/util/tracing'; + const path = require('path'); +import { convertRepo } from './convert'; + + @injectable() + export class GiteaContextParser extends AbstractContextParser implements IContextParser { + + @inject(GiteaRestApi) protected readonly giteaApi: GiteaRestApi; + @inject(GiteaTokenHelper) protected readonly tokenHelper: GiteaTokenHelper; + + protected get authProviderId() { + return this.config.id; + } + + public async handle(ctx: TraceContext, user: User, contextUrl: string): Promise { + const span = TraceContext.startSpan("GitlabContextParser", ctx); + span.setTag("contextUrl", contextUrl); + + try { + const { host, owner, repoName, moreSegments } = await this.parseURL(user, contextUrl); + if (moreSegments.length > 0) { + switch (moreSegments[0]) { + case 'merge_requests': { + return await this.handlePullRequestContext(user, host, owner, repoName, parseInt(moreSegments[1])); + } + case 'tree': + case 'blob': + case 'commits': { + return await this.handleTreeContext(user, host, owner, repoName, moreSegments.slice(1)); + } + case 'issues': { + return await this.handleIssueContext(user, host, owner, repoName, parseInt(moreSegments[1])); + } + case 'commit': { + return await this.handleCommitContext(user, host, owner, repoName, moreSegments[1]); + } + } + } + + return await this.handleDefaultContext(user, host, owner, repoName); + } catch (error) { + if (error && error.code === 401) { + const token = await this.tokenHelper.getCurrentToken(user); + if (token) { + const scopes = token.scopes; + // most likely the token needs to be updated after revoking by user. + throw UnauthorizedError.create(this.config.host, scopes, "http-unauthorized"); + } + throw UnauthorizedError.create(this.config.host, GiteaScope.Requirements.DEFAULT); + } + throw error; + } finally { + span.finish(); + } + } + + // https://gitlab.com/AlexTugarev/gp-test + protected async handleDefaultContext(user: User, host: string, owner: string, repoName: string): Promise { + try { + const repository = await this.fetchRepo(user, owner, repoName); + if (!repository.defaultBranch) { + return { + isFile: false, + path: '', + title: `${owner}/${repoName}`, + repository + } + } + + try { + const branchOrTag = await this.getBranchOrTag(user, owner, repoName, [repository.defaultBranch!]); + return { + isFile: false, + path: '', + title: `${owner}/${repoName} - ${branchOrTag.name}`, + ref: branchOrTag.name, + revision: branchOrTag.revision, + refType: branchOrTag.type, + repository + }; + } catch (error) { + if (error && error.message && (error.message as string).startsWith("Cannot find tag/branch for context")) { + // the repo is empty (has no branches) + return { + isFile: false, + path: '', + title: `${owner}/${repoName} - ${repository.defaultBranch}`, + revision: '', + repository + } + } else { + throw error; + } + } + } catch (error) { + if (UnauthorizedError.is(error)) { + throw error; + } + // log.error({ userId: user.id }, error); + throw await NotFoundError.create(await this.tokenHelper.getCurrentToken(user), user, host, owner, repoName); + } + } + + // https://gitlab.com/AlexTugarev/gp-test/tree/wip + // https://gitlab.com/AlexTugarev/gp-test/tree/wip/folder + // https://gitlab.com/AlexTugarev/gp-test/blob/wip/folder/empty.file.jpeg + protected async handleTreeContext(user: User, host: string, owner: string, repoName: string, segments: string[]): Promise { + + try { + const branchOrTagPromise = segments.length > 0 ? this.getBranchOrTag(user, owner, repoName, segments) : undefined; + const repository = await this.fetchRepo(user, owner, repoName); + const branchOrTag = await branchOrTagPromise; + const context = { + isFile: false, + path: '', + title: `${owner}/${repoName}` + (branchOrTag ? ` - ${branchOrTag.name}` : ''), + ref: branchOrTag && branchOrTag.name, + revision: branchOrTag && branchOrTag.revision, + refType: branchOrTag && branchOrTag.type, + repository + }; + if (!branchOrTag) { + return context; + } + if (segments.length === 1 || branchOrTag.fullPath.length === 0) { + return context; + } + + const result = await this.giteaApi.run(user, async g => { + return g.repos.repoGetContents(owner, repoName, path.dirname(branchOrTag.fullPath), { ref: branchOrTag.name }); + }); + if (Gitea.ApiError.is(result)) { + throw new Error(`Error reading TREE ${owner}/${repoName}/tree/${segments.join('/')}: ${result}`); + } else { + const object = result.find(o => o.path === branchOrTag.fullPath); + if (object) { + const isFile = object.type === "blob"; + context.isFile = isFile; + context.path = branchOrTag.fullPath; + } + } + return context; + } catch (e) { + log.debug("Gitea context parser: Error handle tree context.", e); + throw e; + } + } + + protected async getBranchOrTag(user: User, owner: string, repoName: string, segments: string[]): Promise<{ type: RefType, name: string, revision: string, fullPath: string }> { + + let branchOrTagObject: { type: RefType, name: string, revision: string } | undefined = undefined; + + // `segments` could have branch/tag name parts as well as file path parts. + // We never know which segments belong to the branch/tag name and which are already folder names. + // Here we generate a list of candidates for branch/tag names. + const branchOrTagCandidates: string[] = []; + // Try the concatination of all segments first. + branchOrTagCandidates.push(segments.join("/")); + // Then all subsets. + for (let i = 1; i < segments.length; i++) { + branchOrTagCandidates.push(segments.slice(0, i).join("/")); + } + + for (const candidate of branchOrTagCandidates) { + + // Check if there is a BRANCH with name `candidate`: + const possibleBranch = await this.giteaApi.run(user, async g => { + return g.repos.repoGetBranch(owner, repoName, candidate); + }); + // If the branch does not exist, the Gitea API returns with NotFound or InternalServerError. + const isNotFoundBranch = Gitea.ApiError.is(possibleBranch) && (Gitea.ApiError.isNotFound(possibleBranch) || Gitea.ApiError.isInternalServerError(possibleBranch)); + if (!isNotFoundBranch) { + if (Gitea.ApiError.is(possibleBranch)) { + throw new Error(`Gitea ApiError on searching for possible branches for ${owner}/${repoName}/tree/${segments.join('/')}: ${possibleBranch}`); } - } - return await this.handleDefaultContext({span}, user, host, owner, repoName); - } catch (error) { - if (error && error.code === 401) { - const token = await this.tokenHelper.getCurrentToken(user); - if (token) { - const scopes = token.scopes; - // most likely the token needs to be updated after revoking by user. - throw UnauthorizedError.create(this.config.host, scopes, "http-unauthorized"); - } - // todo@alex: this is very unlikely. is coercing it into a valid case helpful? - // here, GH API responded with a 401 code, and we are missing a token. OTOH, a missing token would not lead to a request. - throw UnauthorizedError.create(this.config.host, GiteaScope.Requirements.PUBLIC_REPO, "missing-identity"); - } - throw error; - } finally { - span.finish(); - } - } - - protected async handleDefaultContext(ctx: TraceContext, user: User, host: string, owner: string, repoName: string): Promise { - const span = TraceContext.startSpan("GiteaContextParser.handleDefaultContext", ctx); - - try { - const result: any = await this.githubQueryApi.runQuery(user, ` - query { - repository(name: "${repoName}", owner: "${owner}") { - ${this.repoProperties()} - defaultBranchRef { - name, - target { - oid - } - }, - } - } - `); - span.log({"request.finished": ""}); - if (result.data.repository === null) { - throw await NotFoundError.create(await this.tokenHelper.getCurrentToken(user), user, this.config.host, owner, repoName); - } - const defaultBranch = result.data.repository.defaultBranchRef; - const ref = defaultBranch && defaultBranch.name || undefined; - const refType = ref ? "branch" : undefined; - return { - isFile: false, - path: '', - title: `${owner}/${repoName} ${defaultBranch ? '- ' + defaultBranch.name : ''}`, - ref, - refType, - revision: defaultBranch && defaultBranch.target.oid || '', - repository: this.toRepository(host, result.data.repository) - } - } catch (e) { - span.log({error: e}); - throw e; - } finally { - span.finish(); - } - } - - protected async handleTreeContext(ctx: TraceContext, user: User, host: string, owner: string, repoName: string, segments: string[]): Promise { - const span = TraceContext.startSpan("handleTreeContext", ctx); - - try { - if (segments.length === 0) { - return this.handleDefaultContext({span}, user, host, owner, repoName); - } - - for (let i = 1; i <= segments.length; i++) { - const branchNameOrCommitHash = decodeURIComponent(segments.slice(0, i).join('/')); - const couldBeHash = i === 1; - const path = decodeURIComponent(segments.slice(i).join('/')); - // Sanitize path expression to prevent GraphQL injections (e.g. escape any `"` or `\n`). - const pathExpression = JSON.stringify(`${branchNameOrCommitHash}:${path}`); - const result: any = await this.githubQueryApi.runQuery(user, ` - query { - repository(name: "${repoName}", owner: "${owner}") { - ${this.repoProperties()} - path: object(expression: ${pathExpression}) { - ... on Blob { - oid - } - } - commit: object(expression: "${branchNameOrCommitHash}") { - oid - } - ref(qualifiedName: "${branchNameOrCommitHash}") { - name - prefix - target { - oid - } - } - } - } - `); - span.log({"request.finished": ""}); - - const repo = result.data.repository; - if (repo === null) { - throw await NotFoundError.create(await this.tokenHelper.getCurrentToken(user), user, this.config.host, owner, repoName); + if (!possibleBranch.commit?.id || !possibleBranch.name) { + throw new Error(`Gitea ApiError on searching for possible branches for ${owner}/${repoName}/tree/${segments.join('/')}: ${possibleBranch}`); } - const isFile = !!(repo.path && repo.path.oid); - const repository = this.toRepository(host, repo); - if (repo.ref !== null) { - return { - ref: repo.ref.name, - refType: this.toRefType({ userId: user.id }, { host, owner, repoName }, repo.ref.prefix), - isFile, - path, - title: `${owner}/${repoName} - ${repo.ref.name}`, - revision: repo.ref.target.oid, - repository - }; - } - if (couldBeHash && repo.commit !== null) { - const revision = repo.commit.oid as string; - const shortRevision = revision.substr(0, 8); - return { - isFile, - path, - title: `${owner}/${repoName} - ${shortRevision}:${path}`, - revision, - repository - }; + branchOrTagObject = { type: 'branch', name: possibleBranch.name, revision: possibleBranch.commit.id }; + break; + } + + // Check if there is a TAG with name `candidate`: + const possibleTag = await this.giteaApi.run(user, async g => { + return g.repos.repoGetTag(owner, repoName, candidate); + }); + // If the tag does not exist, the GitLab API returns with NotFound or InternalServerError. + const isNotFoundTag = Gitea.ApiError.is(possibleTag) && (Gitea.ApiError.isNotFound(possibleTag) || Gitea.ApiError.isInternalServerError(possibleTag)); + if (!isNotFoundTag) { + if (Gitea.ApiError.is(possibleTag)) { + throw new Error(`GitLab ApiError on searching for possible tags for ${owner}/${repoName}/tree/${segments.join('/')}: ${possibleTag}`); } - } - throw new Error(`Couldn't find branch and path for ${segments.join('/')} in repo ${owner}/${repoName}.`); - } catch (e) { - span.log({error: e}); - throw e; - } finally { - span.finish(); - } - } - - protected toRefType(logCtx: LogContext, logPayload: LogPayload, refPrefix: string): RefType { - switch (refPrefix) { - case 'refs/tags/': { - return 'tag'; - } - case 'refs/heads/': { - return 'branch'; - } - default: { - log.warn(logCtx, "Unexpected refPrefix: " + refPrefix, logPayload); - return 'branch'; - } - } - } - protected async handleCommitContext(ctx: TraceContext, user: User, host: string, owner: string, repoName: string, sha: string): Promise { - const span = TraceContext.startSpan("handleCommitContext", ctx); - - if (sha.length != 40) { - throw new Error(`Invalid commit ID ${sha}.`); - } - - try { - const result: any = await this.githubQueryApi.runQuery(user, ` - query { - repository(name: "${repoName}", owner: "${owner}") { - object(oid: "${sha}") { - oid, - ... on Commit { - messageHeadline - } - } - ${this.repoProperties()} - defaultBranchRef { - name, - target { - oid - } - }, - } + if (!possibleTag.commit?.sha || !possibleTag.name) { + throw new Error(`Gitea ApiError on searching for possible branches for ${owner}/${repoName}/tree/${segments.join('/')}: ${possibleBranch}`); } - `); - span.log({"request.finished": ""}); - if (result.data.repository === null) { - throw await NotFoundError.create(await this.tokenHelper.getCurrentToken(user), user, this.config.host, owner, repoName); - } - - const commit = result.data.repository.object; - if (commit === null || commit.message === null) { - throw new Error(`Couldn't find commit ${sha} in repository ${owner}/${repoName}.`); - } - - return { - path: '', - ref: '', - refType: 'revision', - isFile: false, - title: `${owner}/${repoName} - ${commit.messageHeadline}`, - owner, - revision: sha, - repository: this.toRepository(host, result.data.repository), - }; - } catch (e) { - span.log({"error": e}); - throw e; - } finally { - span.finish(); + branchOrTagObject = { type: 'tag', name: possibleTag.name, revision: possibleTag.commit.sha }; + break; + } + } + + // There seems to be no matching branch or tag. + if (branchOrTagObject === undefined) { + log.debug(`Cannot find tag/branch for context: ${owner}/${repoName}/tree/${segments.join('/')}.`, + { branchOrTagCandidates } + ); + throw new Error(`Cannot find tag/branch for context: ${owner}/${repoName}/tree/${segments.join('/')}.`); + } + + const remainingSegmentsIndex = branchOrTagObject.name.split('/').length; + const fullPath = decodeURIComponent(segments.slice(remainingSegmentsIndex).filter(s => s.length > 0).join('/')); + + return { ...branchOrTagObject, fullPath }; + } + + // https://gitlab.com/AlexTugarev/gp-test/merge_requests/1 + protected async handlePullRequestContext(user: User, host: string, owner: string, repoName: string, nr: number): Promise { + const result = await this.giteaApi.run(user, async g => { + return g.repos.repoGetPullRequest(owner, repoName, nr); + }); + if (Gitea.ApiError.is(result)) { + throw await NotFoundError.create(await this.tokenHelper.getCurrentToken(user), user, host, owner, repoName); + } + + if (!result.base?.repo || !result.head?.repo || !result.title) { + throw new Error(`Missing relevant commit information for pull-request ${nr} from repository ${owner}/${repoName}`); } - } - - protected async handlePullRequestContext(ctx: TraceContext, user: User, host: string, owner: string, repoName: string, pullRequestNr: number, tryIssueContext: boolean = true): Promise { - const span = TraceContext.startSpan("handlePullRequestContext", ctx); - - try { - const result: any = await this.githubQueryApi.runQuery(user, ` - query { - repository(name: "${repoName}", owner: "${owner}") { - pullRequest(number: ${pullRequestNr}) { - title - headRef { - name - repository { - ${this.repoProperties()} - } - target { - oid - } - } - baseRef { - name - repository { - ${this.repoProperties()} - } - target { - oid - } - } - } - } - } - `); - span.log({"request.finished": ""}); - if (result.data.repository === null) { - throw await NotFoundError.create(await this.tokenHelper.getCurrentToken(user), user, this.config.host, owner, repoName); - } - const pr = result.data.repository.pullRequest; - if (pr === null) { - log.info(`PR ${owner}/${repoName}/pull/${pullRequestNr} not found. Trying issue context.`); - if (tryIssueContext) { - return this.handleIssueContext({span}, user, host, owner, repoName, pullRequestNr, false); - } else { - throw new Error(`Could not find issue or pull request #${pullRequestNr} in repository ${owner}/${repoName}.`) - } + const sourceRepo = convertRepo(result.head?.repo); + const targetRepo = convertRepo(result.base?.repo); + + const U = { + title: result.title, + repository: sourceRepo, + ref: result.head.ref, + refType: 'branch', + revision: result.head.sha, + nr, + base: { + repository: targetRepo, + ref: result.base.ref, + refType: 'branch', } - if (pr.headRef === null) { - throw new Error(`Could not open pull request ${owner}/${repoName}#${pullRequestNr}. Source branch may have been removed.`); - } - return { - title: pr.title, - repository: this.toRepository(host, pr.headRef.repository), - ref: pr.headRef.name, - refType: "branch", - revision: pr.headRef.target.oid, - nr: pullRequestNr, - base: { - repository: this.toRepository(host, pr.baseRef.repository), - ref: pr.baseRef.name, - refType: "branch", - } - }; - } catch (e) { - span.log({"error": e}); - throw e; - } finally { - span.finish(); } - } - - protected async handleIssueContext(ctx: TraceContext, user: User, host: string, owner: string, repoName: string, issueNr: number, tryPullrequestContext: boolean = true): Promise { - const span = TraceContext.startSpan("handleIssueContext", ctx); - - try { - const result: any = await this.githubQueryApi.runQuery(user, ` - query { - repository(name: "${repoName}", owner: "${owner}") { - issue(number: ${issueNr}) { - title - } - ${this.repoProperties()} - defaultBranchRef { - name, - target { - oid - } - }, - } - } - `); - span.log({"request.finished": ""}); - if (result.data.repository === null) { - throw await NotFoundError.create(await this.tokenHelper.getCurrentToken(user), user, this.config.host, owner, repoName); + return U; + } + + protected async fetchRepo(user: User, owner: string, repoName: string): Promise { + // host might be a relative URL + const host = this.host; // as per contract, cf. `canHandle(user, contextURL)` + + const result = await this.giteaApi.run(user, async g => { + return g.repos.repoGet(owner, repoName); + }); + if (Gitea.ApiError.is(result)) { + throw result; + } + const repo = { + host, + name: repoName, + owner: owner, + cloneUrl: result.clone_url, + defaultBranch: result.default_branch, + private: result.private + } + // TODO: support forks + // if (result.fork) { + // // host might be a relative URL, let's compute the prefix + // const url = new URL(forked_from_project.http_url_to_repo.split(forked_from_project.namespace.full_path)[0]); + // const relativePath = url.pathname.slice(1); // hint: pathname always starts with `/` + // const host = relativePath ? `${url.hostname}/${relativePath}` : url.hostname; + + // repo.fork = { + // parent: { + // name: forked_from_project.path, + // host, + // owner: forked_from_project.namespace.full_path, + // cloneUrl: forked_from_project.http_url_to_repo, + // defaultBranch: forked_from_project.default_branch + // } + // } + // } + return repo; + } + + protected async fetchCommit(user: User, owner: string, repoName: string, sha: string) { + const result = await this.giteaApi.run(user, async g => { + return g.repos.repoGetSingleCommit(owner, repoName, sha); + }); + if (Gitea.ApiError.is(result)) { + if (result.message === 'GitLab responded with code 404') { + throw new Error(`Couldn't find commit #${sha} in repository ${owner}/${repoName}.`); + } + throw result; + } + + if (!result.sha || !result.commit?.message) { + throw new Error(`The commit does not have all needed data ${owner}/${repoName}.`); + } + + return { + id: result.sha, // TODO: how can we use a proper commit-id instead of the sha + title: result.commit?.message + } + } + + // https://gitlab.com/AlexTugarev/gp-test/issues/1 + protected async handleIssueContext(user: User, host: string, owner: string, repoName: string, nr: number): Promise { + const ctxPromise = this.handleDefaultContext(user, host, owner, repoName); + const result = await this.giteaApi.run(user, async g => { + return g.repos.issueGetIssue(owner, repoName, nr); + }); + if (Gitea.ApiError.is(result) || !result.title || !result.id) { + throw await NotFoundError.create(await this.tokenHelper.getCurrentToken(user), user, host, owner, repoName); + } + const context = await ctxPromise; + return { + ... context, + title: result.title, + owner, + nr, + localBranch: IssueContexts.toBranchName(user, result.title, result.id) + }; + } + + // https://gitlab.com/AlexTugarev/gp-test/-/commit/80948e8cc8f0e851e89a10bc7c2ee234d1a5fbe7 + protected async handleCommitContext(user: User, host: string, owner: string, repoName: string, sha: string): Promise { + const repository = await this.fetchRepo(user, owner, repoName); + if (Gitea.ApiError.is(repository)) { + throw await NotFoundError.create(await this.tokenHelper.getCurrentToken(user), user, host, owner, repoName); + } + const commit = await this.fetchCommit(user, owner, repoName, sha); + if (Gitea.ApiError.is(commit)) { + throw new Error(`Couldn't find commit #${sha} in repository ${owner}/${repoName}.`); + } + return { + path: '', + ref: '', + refType: 'revision', + isFile: false, + title: `${owner}/${repoName} - ${commit.title}`, + owner, + revision: sha, + repository, + }; + } + + public async fetchCommitHistory(ctx: TraceContext, user: User, contextUrl: string, sha: string, maxDepth: number): Promise { + // TODO(janx): To get more results than Gitea API's max per_page (seems to be 100), pagination should be handled. + const { owner, repoName } = await this.parseURL(user, contextUrl); + const result = await this.giteaApi.run(user, async g => { + return g.repos.repoGetAllCommits(owner, repoName, { + sha, + limit: maxDepth, + page: 1, + }); + }); + if (Gitea.ApiError.is(result)) { + if (result.message === 'Gitea responded with code 404') { + throw new Error(`Couldn't find commit #${sha} in repository ${owner}/${repoName}.`); + } + throw result; + } + return result.slice(1).map((c) => { + // TODO: how can we use a proper commit-id instead of the sha + if (!c.sha) { + throw new Error(`Commit #${sha} does not have commit.`); } - const issue = result.data.repository.issue; - if (issue === null) { - if (tryPullrequestContext) { - log.info(`Issue ${owner}/${repoName}/issues/${issueNr} not found. Trying issue context.`); - return this.handlePullRequestContext({span}, user, host, owner, repoName, issueNr, false); - } else { - throw new Error(`Couldn't find issue or pull request #${issueNr} in repository ${owner}/${repoName}.`) - } - } - const branchRef = result.data.repository.defaultBranchRef; - const ref = branchRef && branchRef.name || undefined; - const refType = ref ? "branch" : undefined; - - - return { - title: result.data.repository.issue.title, - owner, - nr: issueNr, - localBranch: IssueContexts.toBranchName(user, result.data.repository.issue.title as string || '', issueNr), - ref, - refType, - revision: branchRef && branchRef.target.oid || '', - repository: this.toRepository(host, result.data.repository) - }; - } catch (e) { - span.log({error: e}); - throw e; - } finally { - span.finish(); - } - } - protected toRepository(host: string, repoQueryResult: any): Repository { - if (repoQueryResult === null) { - throw new Error('Unknown repository.'); - } - const result: Repository = { - cloneUrl: repoQueryResult.url + '.git', - host, - name: repoQueryResult.name, - owner: repoQueryResult.owner.login, - private: !!repoQueryResult.isPrivate - } - if (repoQueryResult.parent !== null) { - result.fork = { - parent: this.toRepository(host, repoQueryResult.parent) - }; - } - - return result; - } - - protected repoProperties(parents: number = 10): string { - return ` - name, - owner { - login - } - url, - isPrivate, - ${parents > 0 ? `parent { - ${this.repoProperties(parents - 1)} - }` : ''} - `; - } - - public async fetchCommitHistory(ctx: TraceContext, user: User, contextUrl: string, sha: string, maxDepth: number): Promise { - const span = TraceContext.startSpan("GiteaContextParser.fetchCommitHistory", ctx); - - try { - if (sha.length != 40) { - throw new Error(`Invalid commit ID ${sha}.`); - } - - // TODO(janx): To get more results than Gitea API's max page size (seems to be 100), pagination should be handled. - // These additional history properties may be helfpul: - // totalCount, - // pageInfo { - // haxNextPage, - // }, - const { owner, repoName } = await this.parseURL(user, contextUrl); - const result: any = await this.githubQueryApi.runQuery(user, ` - query { - repository(name: "${repoName}", owner: "${owner}") { - object(oid: "${sha}") { - ... on Commit { - history(first: ${maxDepth}) { - edges { - node { - oid - } - } - } - } - } - } - } - `); - span.log({"request.finished": ""}); - - if (result.data.repository === null) { - throw await NotFoundError.create(await this.tokenHelper.getCurrentToken(user), user, this.config.host, owner, repoName); - } - - const commit = result.data.repository.object; - if (commit === null) { - throw new Error(`Couldn't find commit ${sha} in repository ${owner}/${repoName}.`); - } - - return commit.history.edges.slice(1).map((e: any) => e.node.oid) || []; - } catch (e) { - span.log({"error": e}); - throw e; - } finally { - span.finish(); - } - } -} + return c.sha; + }); + } + } \ No newline at end of file diff --git a/components/server/src/gitea/gitea-repository-provider.ts b/components/server/src/gitea/gitea-repository-provider.ts index 2de5889917c962..542f8b7adeaa02 100644 --- a/components/server/src/gitea/gitea-repository-provider.ts +++ b/components/server/src/gitea/gitea-repository-provider.ts @@ -7,107 +7,106 @@ import { injectable, inject } from 'inversify'; import { User, Repository } from "@gitpod/gitpod-protocol" -import { GiteaRestApi } from "./api"; +import { Gitea, GiteaRestApi } from "./api"; import { RepositoryProvider } from '../repohost/repository-provider'; import { RepoURL } from '../repohost/repo-url'; import { Branch, CommitInfo } from '@gitpod/gitpod-protocol/src/protocol'; @injectable() -export class GithubRepositoryProvider implements RepositoryProvider { - @inject(GiteaRestApi) protected readonly gitea: GiteaRestApi; - - async getRepo(user: User, owner: string, repo: string): Promise { - const repository = await this.gitea.getRepository(user, { owner, repo }); - const cloneUrl = repository.clone_url; - const host = RepoURL.parseRepoUrl(cloneUrl)!.host; - const description = repository.description; - const avatarUrl = repository.owner.avatar_url; - const webUrl = repository.html_url; - const defaultBranch = repository.default_branch; - return { host, owner, name: repo, cloneUrl, description, avatarUrl, webUrl, defaultBranch }; +export class GiteaRepositoryProvider implements RepositoryProvider { + @inject(GiteaRestApi) protected readonly giteaApi: GiteaRestApi; + + async getRepo(user: User, owner: string, repoName: string): Promise { + const result = await this.giteaApi.run(user, (g => g.repos.repoGet(owner, repoName))); + if (Gitea.ApiError.is(result)) { + throw new Error(`Can't get repository ${owner}/${repoName}`); + } + + if (!result.clone_url) { + throw new Error(`Can't find clone_url for repository ${owner}/${repoName}`); + } + + const host = RepoURL.parseRepoUrl(result.clone_url)!.host; + return { host, owner, name: repoName, cloneUrl: result.clone_url, description: result.description, avatarUrl: result.avatar_url, webUrl: result.html_url, defaultBranch: result.default_branch }; } - async getBranch(user: User, owner: string, repo: string, branch: string): Promise { - const result = await this.gitea.getBranch(user, { repo, owner, branch }); - return result; + async getBranch(user: User, owner: string, repoName: string, branch: string): Promise { + // TODO: we currently use ref as sha :thinking: + const result = await this.giteaApi.run(user, (g => g.repos.repoGetBranch(owner, repoName, branch))); + if (Gitea.ApiError.is(result)) { + throw new Error(`Can't get branch ${branch} from repository ${owner}/${repoName}`); + } + + if (!result.name || !result.commit?.author?.name || !result.commit?.message || !result.commit?.added) { + throw new Error(`Missing relevant commit information for branch ${branch} from repository ${owner}/${repoName}`); + } + + return { + name: result.name, + htmlUrl: '', // TODO: find way to get branch url / create it manually + commit: { + author: result.commit.author.name, + sha: '', // TODO: find way to get branch sha + commitMessage: result.commit.message, + authorAvatarUrl: '', // TODO: find way to get author avatar + authorDate: '', // TODO: find way to get author date + }, + }; } - async getBranches(user: User, owner: string, repo: string): Promise { - const branches: Branch[] = []; - let endCursor: string | undefined; - let hasNextPage: boolean = true; - - while (hasNextPage) { - const result: any = await this.gitea.runQuery(user, ` - query { - repository(name: "${repo}", owner: "${owner}") { - refs(refPrefix: "refs/heads/", orderBy: {field: TAG_COMMIT_DATE, direction: ASC}, first: 100 ${endCursor ? `, after: "${endCursor}"` : ""}) { - nodes { - name - target { - ... on Commit { - oid - history(first: 1) { - nodes { - messageHeadline - committedDate - oid - authoredDate - tree { - id - } - treeUrl - author { - avatarUrl - name - date - } - } - } - } - } - } - pageInfo { - endCursor - hasNextPage - hasPreviousPage - startCursor - } - totalCount - } - } - } - `); - - endCursor = result.data.repository?.refs?.pageInfo?.endCursor; - hasNextPage = result.data.repository?.refs?.pageInfo?.hasNextPage; - - const nodes = result.data.repository?.refs?.nodes; - for (const node of (nodes || [])) { - - branches.push({ - name: node.name, - commit: { - sha: node.target.oid, - commitMessage: node.target.history.nodes[0].messageHeadline, - author: node.target.history.nodes[0].author.name, - authorAvatarUrl: node.target.history.nodes[0].author.avatarUrl, - authorDate: node.target.history.nodes[0].author.date, - }, - htmlUrl: node.target.history.nodes[0].treeUrl.replace(node.target.oid, node.name) - }); + async getBranches(user: User, owner: string, repoName: string): Promise { + // TODO: we currently use ref as sha :thinking: + const result = await this.giteaApi.run(user, (g => g.repos.repoListBranches(owner, repoName))); + if (Gitea.ApiError.is(result)) { + throw new Error(`Can't get branches from repository ${owner}/${repoName}`); + } + + return result.map((branch) => { + if (!branch.name || !branch.commit?.author?.name || !branch.commit?.message || branch.commit?.added) { + throw new Error(`Missing relevant commit information for branch ${branch.name} from repository ${owner}/${repoName}`); } - } - return branches; + + return { + name: branch.name, + htmlUrl: '', // TODO: find way to get branch url / create it manually + commit: { + author: branch.commit.author.name, + sha: '', // TODO: find way to get branch sha + commitMessage: branch.commit.message, + authorAvatarUrl: '', // TODO: find way to get author avatar + authorDate: '', // TODO: find way to get author date + }, + }; + }); } - async getCommitInfo(user: User, owner: string, repo: string, ref: string): Promise { - const commit = await this.gitea.getCommit(user, { repo, owner, ref }); - return commit; + async getCommitInfo(user: User, owner: string, repoName: string, ref: string): Promise { + // TODO: we currently use ref as sha :thinking: + const result = await this.giteaApi.run(user, (g => g.repos.repoGetSingleCommit(owner, repoName, ref))); + if (Gitea.ApiError.is(result)) { + throw new Error(`Can't get commit for ref ${ref} from repository ${owner}/${repoName}`); + } + + if (!result.author?.login || !result.commit?.message || !result.sha) { + throw new Error(`Missing relevant commit information for ref ${ref} from repository ${owner}/${repoName}`); + } + + return { + author: result.author?.login, // TODO: is this correct? + commitMessage: result.commit?.message, + sha: result.sha, + authorAvatarUrl: result.author?.avatar_url, + authorDate: result.created, // TODO: is this correct? + }; } async getUserRepos(user: User): Promise { - const result: any = await this.gitea.getUserRepositories(user); - return (result.data.viewer?.repositoriesContributedTo?.edges || []).map((edge: any) => edge.node.url) + const result = await this.giteaApi.run(user, (g => g.user.userCurrentListRepos())); + if (Gitea.ApiError.is(result)) { + throw new Error(`Can't get repositories for user ${user.name}`); + } + + // TODO: do we need the html url or clone urls? + return (result || []).map((repo) => repo.html_url || '').filter(s => s !== "") } } diff --git a/components/server/src/gitea/gitea-token-helper.ts b/components/server/src/gitea/gitea-token-helper.ts index 30753c54aa3d68..cfe4171d2473da 100644 --- a/components/server/src/gitea/gitea-token-helper.ts +++ b/components/server/src/gitea/gitea-token-helper.ts @@ -42,9 +42,6 @@ export class GiteaTokenHelper { protected containsScopes(token: Token, wantedScopes: string[] | undefined): boolean { const wantedSet = new Set(wantedScopes); const currentScopes = [...token.scopes]; - if (currentScopes.some(s => s === GiteaScope.PRIVATE)) { - currentScopes.push(GiteaScope.PUBLIC); // normalize private_repo, which includes public_repo - } currentScopes.forEach(s => wantedSet.delete(s)); return wantedSet.size === 0; } diff --git a/components/server/src/gitea/gitea-token-validator.ts b/components/server/src/gitea/gitea-token-validator.ts index ab09b3d72673d1..fedf7d3b44aa55 100644 --- a/components/server/src/gitea/gitea-token-validator.ts +++ b/components/server/src/gitea/gitea-token-validator.ts @@ -6,12 +6,12 @@ import { inject, injectable } from "inversify"; import { CheckWriteAccessResult, IGitTokenValidator, IGitTokenValidatorParams } from "../workspace/git-token-validator"; -import { GiteaApiError, GiteaRestApi, GiteaResult } from "./api"; +import { Gitea, GiteaRestApi } from "./api"; import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; @injectable() export class GiteaTokenValidator implements IGitTokenValidator { - @inject(GiteaRestApi) githubRestApi: GiteaRestApi; + @inject(GiteaRestApi) giteaApi: GiteaRestApi; async checkWriteAccess(params: IGitTokenValidatorParams): Promise { @@ -21,58 +21,23 @@ export class GiteaTokenValidator implements IGitTokenValidator { if (!parsedRepoName) { throw new Error(`Could not parse repo name: ${repoFullName}`); } - let repo; - try { - repo = await this.githubRestApi.run(token, api => api.repos.get(parsedRepoName)); - } catch (error) { - if (GiteaApiError.is(error) && error.response?.status === 404) { - return { found: false }; - } - log.error('Error getting repo information from Gitea', error, { repoFullName, parsedRepoName }) - return { found: false, error }; + const repo = await this.giteaApi.run(token, api => api.repos.repoGet(parsedRepoName.owner, parsedRepoName.repo)); + if (Gitea.ApiError.is(repo) && Gitea.ApiError.isNotFound(repo)) { + return { found: false }; + } else if (Gitea.ApiError.is(repo)) { + log.error('Error getting repo information from Gitea', repo, { repoFullName, parsedRepoName }) + return { found: false, error: repo }; } - const mayWritePrivate = GiteaResult.mayWritePrivate(repo); - const mayWritePublic = GiteaResult.mayWritePublic(repo); - - const isPrivateRepo = repo.data.private; - let writeAccessToRepo = repo.data.permissions?.push; - const inOrg = repo.data.owner?.type === "Organization"; - - if (inOrg) { - // if this repository belongs to an organization and Gitpod is not authorized, - // we're not allowed to list repositories using this a token issues for - // Gitpod's OAuth App. + const isPrivateRepo = repo.private; + let writeAccessToRepo = repo.permissions?.push; - const request = { - query: ` - query { - organization(login: "${parsedRepoName.owner}") { - repositories(first: 1) { - totalCount - } - } - } - `.trim() - }; - try { - await this.githubGraphQLEndpoint.runQueryWithToken(token, request) - } catch (error) { - const errors = error.result?.errors; - if (errors && errors[0] && (errors[0] as any)["type"] === "FORBIDDEN") { - writeAccessToRepo = false; - } else { - log.error('Error getting organization information from Gitea', error, { org: parsedRepoName.owner }) - throw error; - } - } - } return { found: true, isPrivateRepo, writeAccessToRepo, - mayWritePrivate, - mayWritePublic + mayWritePrivate: true, + mayWritePublic: true } } diff --git a/components/server/src/gitea/languages-provider.ts b/components/server/src/gitea/languages-provider.ts index aa11863850a867..66e22826343c41 100644 --- a/components/server/src/gitea/languages-provider.ts +++ b/components/server/src/gitea/languages-provider.ts @@ -7,7 +7,7 @@ import { injectable, inject } from 'inversify'; import { User, Repository } from "@gitpod/gitpod-protocol" -import { GiteaRestApi } from "./api"; +import { Gitea, GiteaRestApi } from "./api"; import { LanguagesProvider } from '../repohost/languages-provider'; @injectable() @@ -16,8 +16,12 @@ export class GiteaLanguagesProvider implements LanguagesProvider { @inject(GiteaRestApi) protected readonly gitea: GiteaRestApi; async getLanguages(repository: Repository, user: User): Promise { - const languages = await this.gitea.run(user, (gitea) => gitea.repos.listLanguages({ owner: repository.owner, repo: repository.name })); + const languages = await this.gitea.run(user, (gitea) => gitea.repos.repoGetLanguages(repository.owner, repository.name )); - return languages.data; + if (Gitea.ApiError.is(languages)) { + throw new Error(`Can\' get languages from repository ${repository.owner}/${repository.name}`); + } + + return languages; } } diff --git a/components/server/src/gitea/scopes.ts b/components/server/src/gitea/scopes.ts index 4199510caba76d..439594118d3366 100644 --- a/components/server/src/gitea/scopes.ts +++ b/components/server/src/gitea/scopes.ts @@ -6,22 +6,16 @@ export namespace GiteaScope { - export const EMAIL = "user:email"; - export const READ_USER = "read:user"; - export const PUBLIC = "public_repo"; - export const PRIVATE = "repo"; - export const ORGS = "read:org"; - export const WORKFLOW = "workflow"; - - export const All = [EMAIL, READ_USER, PUBLIC, PRIVATE, ORGS, WORKFLOW]; + // TODO: currently Gitea does not support scopes (https://github.com/go-gitea/gitea/issues/4300) + export const All = []; export const Requirements = { /** * Minimal required permission. * Gitea's API is not restricted any further. */ - DEFAULT: [EMAIL], + DEFAULT: [], - PUBLIC_REPO: [PUBLIC], - PRIVATE_REPO: [PRIVATE], + PUBLIC_REPO: [], + PRIVATE_REPO: [], } } From 9fa680e5a456dcbbb58b23c9a5fc4d7fba0ce3c1 Mon Sep 17 00:00:00 2001 From: Anbraten Date: Thu, 10 Feb 2022 02:57:48 +0000 Subject: [PATCH 04/16] update gitea-js --- components/server/package.json | 2 +- yarn.lock | 207 ++------------------------------- 2 files changed, 8 insertions(+), 201 deletions(-) diff --git a/components/server/package.json b/components/server/package.json index f3e8ac76cfb177..08008a84e89918 100644 --- a/components/server/package.json +++ b/components/server/package.json @@ -56,7 +56,7 @@ "express-mysql-session": "^2.1.0", "express-session": "^1.15.6", "fs-extra": "^10.0.0", - "gitea-js": "1.0.1", + "gitea-js": "^1.1.0", "google-protobuf": "^3.18.0-rc.2", "heapdump": "^0.3.15", "inversify": "^5.0.1", diff --git a/yarn.lock b/yarn.lock index 8b2b883e043a46..dec9ec88245bea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1348,11 +1348,6 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" -"@exodus/schemasafe@^1.0.0-rc.2": - version "1.0.0-rc.6" - resolved "https://registry.yarnpkg.com/@exodus/schemasafe/-/schemasafe-1.0.0-rc.6.tgz#7985f681564cff4ffaebb5896eb4be20af3aae7a" - integrity sha512-dDnQizD94EdBwEj/fh3zPRa/HWCS9O5au2PuHhZBbuM3xWHxuaKzPBOEWze7Nn0xW68MIpZ7Xdyn1CoCpjKCuQ== - "@gar/promisify@^1.0.1": version "1.1.2" resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.2.tgz#30aa825f11d438671d585bd44e7fd564535fc210" @@ -3201,11 +3196,6 @@ "@types/cookiejar" "*" "@types/node" "*" -"@types/swagger-schema-official@2.0.21": - version "2.0.21" - resolved "https://registry.yarnpkg.com/@types/swagger-schema-official/-/swagger-schema-official-2.0.21.tgz#56812a86dcd57ba60e5c51705ee96a2b2dc9b374" - integrity sha512-n9BbLOjR4Hre7B4TSGGMPohOgOg8tcp00uxqsIE00uuWQC0QuX57G1bqC1csLsk2DpTGtHkd0dEb3ipsCZ9dAA== - "@types/tapable@^1", "@types/tapable@^1.0.5": version "1.0.8" resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.8.tgz#b94a4391c85666c7b73299fd3ad79d4faa435310" @@ -4321,7 +4311,7 @@ axios-retry@^3.0.2: "@babel/runtime" "^7.15.4" is-retry-allowed "^2.2.0" -axios@^0.21.1, axios@^0.21.4: +axios@^0.21.1: version "0.21.4" resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.4.tgz#c67b90dc0568e5c1cf2b0b858c43ba28e2eda575" integrity sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg== @@ -5099,11 +5089,6 @@ call-bind@^1.0.0, call-bind@^1.0.2: function-bind "^1.1.1" get-intrinsic "^1.0.2" -call-me-maybe@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/call-me-maybe/-/call-me-maybe-1.0.1.tgz#26d208ea89e37b5cbde60250a15f031c16a4d66b" - integrity sha1-JtII6onje1y95gJQoV8DHBak1ms= - caller-callsite@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/caller-callsite/-/caller-callsite-2.0.0.tgz#847e0fce0a223750a9a027c54b33731ad3154134" @@ -5693,7 +5678,7 @@ commander@^5.1.0: resolved "https://registry.yarnpkg.com/commander/-/commander-5.1.0.tgz#46abbd1652f8e059bddaef99bbdcb2ad9cf179ae" integrity sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg== -commander@^6.0.0, commander@^6.2.0, commander@^6.2.1: +commander@^6.0.0, commander@^6.2.0: version "6.2.1" resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== @@ -7281,11 +7266,6 @@ es6-iterator@2.0.3, es6-iterator@~2.0.3: es5-ext "^0.10.35" es6-symbol "^3.1.1" -es6-promise@^3.2.1: - version "3.3.1" - resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" - integrity sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM= - es6-promise@^4.1.1: version "4.2.8" resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.8.tgz#4eb21594c972bc40553d276e510539143db53e0a" @@ -7722,11 +7702,6 @@ esutils@^2.0.2: resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -eta@^1.12.1: - version "1.12.3" - resolved "https://registry.yarnpkg.com/eta/-/eta-1.12.3.tgz#2982d08adfbef39f9fa50e2fbd42d7337e7338b1" - integrity sha512-qHixwbDLtekO/d51Yr4glcaUJCIjGVJyTzuqV4GPlgZo1YpgOKG+avQynErZIYrfM6JIJdtiG2Kox8tbb+DoGg== - etag@~1.8.1: version "1.8.1" resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" @@ -8616,12 +8591,10 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" -gitea-js@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/gitea-js/-/gitea-js-1.0.1.tgz#34e22c34e141171935644ce22e4c674fa9b30c8d" - integrity sha512-lZ1Fk2x30Jwsm5OEpfdU98v9ncpZITDotoQ2Ad8uEwEPW71HPBgbYpgVFuteTvdN1UEqaArUfD2zQz3uW+TB/g== - dependencies: - swagger-typescript-api "9.3.1" +gitea-js@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/gitea-js/-/gitea-js-1.1.0.tgz#d538ce5ed1452346d5397720a8ed09f71abe8988" + integrity sha512-MtEEZjFIfsGNku7KEfELI6LIHy5JdZ8FVHEmvWdAgZWucC8GMj5wgsMN/t4N2qYIFG1RKDudSKXQNtCDOvulkQ== glob-parent@^3.1.0: version "3.1.0" @@ -9293,11 +9266,6 @@ http-signature@~1.3.6: jsprim "^2.0.2" sshpk "^1.14.1" -http2-client@^1.2.5: - version "1.3.5" - resolved "https://registry.yarnpkg.com/http2-client/-/http2-client-1.3.5.tgz#20c9dc909e3cc98284dd20af2432c524086df181" - integrity sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA== - http2-wrapper@^1.0.0-beta.5.2: version "1.0.3" resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d" @@ -11988,11 +11956,6 @@ nan@^2.12.1, nan@^2.13.2: resolved "https://registry.yarnpkg.com/nan/-/nan-2.15.0.tgz#3f34a473ff18e15c1b5626b62903b5ad6e665fee" integrity sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ== -nanoid@^3.1.22: - version "3.2.0" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.2.0.tgz#62667522da6673971cca916a6d3eff3f415ff80c" - integrity sha512-fmsZYa9lpn69Ad5eDn7FMcnnSR+8R34W9qJEijxYhTbfOWzr22n1QxCMzXLK+ODyW2973V3Fux959iQoUxzUIA== - nanoid@^3.1.30: version "3.1.30" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.1.30.tgz#63f93cc548d2a113dc5dfbc63bfa09e2b9b64362" @@ -12064,20 +12027,13 @@ no-case@^3.0.4: lower-case "^2.0.2" tslib "^2.0.3" -node-emoji@^1.10.0, node-emoji@^1.11.0: +node-emoji@^1.11.0: version "1.11.0" resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c" integrity sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A== dependencies: lodash "^4.17.21" -node-fetch-h2@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/node-fetch-h2/-/node-fetch-h2-2.3.0.tgz#c6188325f9bd3d834020bf0f2d6dc17ced2241ac" - integrity sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg== - dependencies: - http2-client "^1.2.5" - node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.5: version "2.6.6" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.6.tgz#1751a7c01834e8e1697758732e9efb6eeadfaf89" @@ -12173,13 +12129,6 @@ node-pre-gyp@^0.17.0: semver "^5.7.1" tar "^4.4.13" -node-readfiles@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/node-readfiles/-/node-readfiles-0.2.0.tgz#dbbd4af12134e2e635c245ef93ffcf6f60673a5d" - integrity sha1-271K8SE04uY1wkXvk//Pb2BnOl0= - dependencies: - es6-promise "^3.2.1" - node-releases@^1.1.61: version "1.1.77" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.77.tgz#50b0cfede855dd374e7585bf228ff34e57c1c32e" @@ -12341,52 +12290,6 @@ nwsapi@^2.2.0: resolved "https://registry.yarnpkg.com/nwsapi/-/nwsapi-2.2.0.tgz#204879a9e3d068ff2a55139c2c772780681a38b7" integrity sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ== -oas-kit-common@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/oas-kit-common/-/oas-kit-common-1.0.8.tgz#6d8cacf6e9097967a4c7ea8bcbcbd77018e1f535" - integrity sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ== - dependencies: - fast-safe-stringify "^2.0.7" - -oas-linter@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/oas-linter/-/oas-linter-3.2.2.tgz#ab6a33736313490659035ca6802dc4b35d48aa1e" - integrity sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ== - dependencies: - "@exodus/schemasafe" "^1.0.0-rc.2" - should "^13.2.1" - yaml "^1.10.0" - -oas-resolver@^2.5.6: - version "2.5.6" - resolved "https://registry.yarnpkg.com/oas-resolver/-/oas-resolver-2.5.6.tgz#10430569cb7daca56115c915e611ebc5515c561b" - integrity sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ== - dependencies: - node-fetch-h2 "^2.3.0" - oas-kit-common "^1.0.8" - reftools "^1.1.9" - yaml "^1.10.0" - yargs "^17.0.1" - -oas-schema-walker@^1.1.5: - version "1.1.5" - resolved "https://registry.yarnpkg.com/oas-schema-walker/-/oas-schema-walker-1.1.5.tgz#74c3cd47b70ff8e0b19adada14455b5d3ac38a22" - integrity sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ== - -oas-validator@^5.0.8: - version "5.0.8" - resolved "https://registry.yarnpkg.com/oas-validator/-/oas-validator-5.0.8.tgz#387e90df7cafa2d3ffc83b5fb976052b87e73c28" - integrity sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw== - dependencies: - call-me-maybe "^1.0.1" - oas-kit-common "^1.0.8" - oas-linter "^3.2.2" - oas-resolver "^2.5.6" - oas-schema-walker "^1.1.5" - reftools "^1.1.9" - should "^13.2.1" - yaml "^1.10.0" - oauth@0.9.x: version "0.9.15" resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1" @@ -14174,11 +14077,6 @@ prepend-http@^2.0.0: resolved "https://registry.yarnpkg.com/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= -prettier@^2.2.1: - version "2.5.1" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.5.1.tgz#fff75fa9d519c54cf0fce328c1017d94546bc56a" - integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg== - pretty-bytes@^5.3.0, pretty-bytes@^5.6.0: version "5.6.0" resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" @@ -14965,11 +14863,6 @@ reflect.getprototypeof@^1.0.2: get-intrinsic "^1.1.1" which-builtin-type "^1.1.1" -reftools@^1.1.9: - version "1.1.9" - resolved "https://registry.yarnpkg.com/reftools/-/reftools-1.1.9.tgz#e16e19f662ccd4648605312c06d34e5da3a2b77e" - integrity sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w== - regenerate-unicode-properties@^9.0.0: version "9.0.0" resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-9.0.0.tgz#54d09c7115e1f53dc2314a974b32c1c344efe326" @@ -15679,50 +15572,6 @@ shellwords@^0.1.1: resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== -should-equal@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/should-equal/-/should-equal-2.0.0.tgz#6072cf83047360867e68e98b09d71143d04ee0c3" - integrity sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA== - dependencies: - should-type "^1.4.0" - -should-format@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/should-format/-/should-format-3.0.3.tgz#9bfc8f74fa39205c53d38c34d717303e277124f1" - integrity sha1-m/yPdPo5IFxT04w01xcwPidxJPE= - dependencies: - should-type "^1.3.0" - should-type-adaptors "^1.0.1" - -should-type-adaptors@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/should-type-adaptors/-/should-type-adaptors-1.1.0.tgz#401e7f33b5533033944d5cd8bf2b65027792e27a" - integrity sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA== - dependencies: - should-type "^1.3.0" - should-util "^1.0.0" - -should-type@^1.3.0, should-type@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/should-type/-/should-type-1.4.0.tgz#0756d8ce846dfd09843a6947719dfa0d4cff5cf3" - integrity sha1-B1bYzoRt/QmEOmlHcZ36DUz/XPM= - -should-util@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/should-util/-/should-util-1.0.1.tgz#fb0d71338f532a3a149213639e2d32cbea8bcb28" - integrity sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g== - -should@^13.2.1: - version "13.2.3" - resolved "https://registry.yarnpkg.com/should/-/should-13.2.3.tgz#96d8e5acf3e97b49d89b51feaa5ae8d07ef58f10" - integrity sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ== - dependencies: - should-equal "^2.0.0" - should-format "^3.0.3" - should-type "^1.4.0" - should-type-adaptors "^1.0.1" - should-util "^1.0.0" - side-channel@^1.0.3, side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" @@ -16534,48 +16383,6 @@ svgo@^1.0.0, svgo@^1.2.2: unquote "~1.1.1" util.promisify "~1.0.0" -swagger-schema-official@2.0.0-bab6bed: - version "2.0.0-bab6bed" - resolved "https://registry.yarnpkg.com/swagger-schema-official/-/swagger-schema-official-2.0.0-bab6bed.tgz#70070468d6d2977ca5237b2e519ca7d06a2ea3fd" - integrity sha1-cAcEaNbSl3ylI3suUZyn0Gouo/0= - -swagger-typescript-api@9.3.1: - version "9.3.1" - resolved "https://registry.yarnpkg.com/swagger-typescript-api/-/swagger-typescript-api-9.3.1.tgz#07df3aa8e83a3897356a6e820e88b8348830aa6b" - integrity sha512-vtarFELmXmDKrtY2FvU2OjNvN1BqOj3kHWzz7mAresNKRgRubZpjlopECxbBekrLeX3lezAYSNcrFMVRGJAD4A== - dependencies: - "@types/swagger-schema-official" "2.0.21" - axios "^0.21.4" - commander "^6.2.1" - cosmiconfig "^7.0.0" - eta "^1.12.1" - js-yaml "^4.0.0" - lodash "^4.17.21" - make-dir "^3.1.0" - nanoid "^3.1.22" - node-emoji "^1.10.0" - prettier "^2.2.1" - swagger-schema-official "2.0.0-bab6bed" - swagger2openapi "^7.0.5" - typescript "^4.2.4" - -swagger2openapi@^7.0.5: - version "7.0.8" - resolved "https://registry.yarnpkg.com/swagger2openapi/-/swagger2openapi-7.0.8.tgz#12c88d5de776cb1cbba758994930f40ad0afac59" - integrity sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g== - dependencies: - call-me-maybe "^1.0.1" - node-fetch "^2.6.1" - node-fetch-h2 "^2.3.0" - node-readfiles "^0.2.0" - oas-kit-common "^1.0.8" - oas-resolver "^2.5.6" - oas-schema-walker "^1.1.5" - oas-validator "^5.0.8" - reftools "^1.1.9" - yaml "^1.10.0" - yargs "^17.0.1" - swot-js@^1.0.3: version "1.0.7" resolved "https://registry.yarnpkg.com/swot-js/-/swot-js-1.0.7.tgz#0fb6520d6bd466370b695c6bdd9489214effb112" From e5057263eec713260df707a8c6cb202a64f76604 Mon Sep 17 00:00:00 2001 From: Anbraten Date: Thu, 10 Feb 2022 02:58:03 +0000 Subject: [PATCH 05/16] replace gitlab names with gitea --- .../server/ee/src/gitea/gitea-app-support.ts | 8 +-- .../server/ee/src/prebuilds/gitea-app.ts | 63 +++---------------- 2 files changed, 11 insertions(+), 60 deletions(-) diff --git a/components/server/ee/src/gitea/gitea-app-support.ts b/components/server/ee/src/gitea/gitea-app-support.ts index 8f94e82ec852be..cd756aabf1027a 100644 --- a/components/server/ee/src/gitea/gitea-app-support.ts +++ b/components/server/ee/src/gitea/gitea-app-support.ts @@ -28,12 +28,8 @@ export class GiteaAppSupport { if (!identity) { return result; } - const usersGitLabAccount = identity.authName; + const usersAccount = identity.authName; - // cf. https://docs.gitlab.com/ee/api/projects.html#list-all-projects - // we are listing only those projects with access level of maintainers. - // also cf. https://docs.gitlab.com/ee/api/members.html#valid-access-levels - // // TODO: check if valid const projectsWithAccess = await api.user.userCurrentListRepos({ limit: 100 }); for (const project of projectsWithAccess.data) { @@ -43,7 +39,7 @@ export class GiteaAppSupport { const accountAvatarUrl = project.owner?.avatar_url as string; const account = project.owner?.login as string; - (account === usersGitLabAccount ? ownersRepos : result).push({ + (account === usersAccount ? ownersRepos : result).push({ name: project.name as string, path, account, diff --git a/components/server/ee/src/prebuilds/gitea-app.ts b/components/server/ee/src/prebuilds/gitea-app.ts index 01a091a8097688..87cfa7c6f81091 100644 --- a/components/server/ee/src/prebuilds/gitea-app.ts +++ b/components/server/ee/src/prebuilds/gitea-app.ts @@ -13,7 +13,6 @@ import { TraceContext } from '@gitpod/gitpod-protocol/lib/util/tracing'; import { TokenService } from '../../../src/user/token-service'; import { HostContextProvider } from '../../../src/auth/host-context-provider'; // import { GiteaService } from './gitea-service'; -import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; @injectable() export class GiteaApp { @@ -30,38 +29,15 @@ export class GiteaApp { @postConstruct() protected init() { - this._router.post('/', async (req, res) => { - const event = req.header('X-Gitlab-Event'); - if (event === 'Push Hook') { - const context = req.body as GitLabPushHook; - const span = TraceContext.startSpan("GitLapApp.handleEvent", {}); - span.setTag("request", context); - log.debug("GitLab push hook received", { event, context }); - let user: User | undefined; - try { - user = await this.findUser({ span }, context, req); - } catch (error) { - log.error("Cannot find user.", error, { req }) - } - if (!user) { - res.statusCode = 503; - res.send(); - return; - } - await this.handlePushHook({ span }, context, user); - } else { - log.debug("Unknown GitLab event received", { event }); - } - res.send('OK'); - }); + // TODO } - protected async findUser(ctx: TraceContext, context: GitLabPushHook, req: express.Request): Promise { + protected async findUser(ctx: TraceContext, context: GiteaPushHook, req: express.Request): Promise { // TODO return {} as User; } - protected async handlePushHook(ctx: TraceContext, body: GitLabPushHook, user: User): Promise { + protected async handlePushHook(ctx: TraceContext, body: GiteaPushHook, user: User): Promise { // TODO return undefined; } @@ -82,10 +58,9 @@ export class GiteaApp { return {} as { user: User, project?: Project }; } - protected createContextUrl(body: GitLabPushHook) { - const repoUrl = body.repository.git_http_url; - const contextURL = `${repoUrl.substr(0, repoUrl.length - 4)}/-/tree${body.ref.substr('refs/head/'.length)}`; - return contextURL; + protected createContextUrl(body: GiteaPushHook) { + // TODO + return {}; } get router(): express.Router { @@ -102,30 +77,10 @@ export class GiteaApp { } } -interface GitLabPushHook { - object_kind: 'push'; - before: string; - after: string; // commit - ref: string; // e.g. "refs/heads/master" - user_avatar: string; - user_name: string; - project: GitLabProject; - repository: GitLabRepository; +interface GiteaPushHook { } -interface GitLabRepository { - name: string, - git_http_url: string; // e.g. http://example.com/mike/diaspora.git - visibility_level: number, +interface GiteaRepository { } -interface GitLabProject { - id: number, - namespace: string, - name: string, - path_with_namespace: string, // e.g. "mike/diaspora" - git_http_url: string; // e.g. http://example.com/mike/diaspora.git - web_url: string; // e.g. http://example.com/mike/diaspora - visibility_level: number, - avatar_url: string | null, -} \ No newline at end of file +interface GiteaProject {} \ No newline at end of file From 7ae3857a9c958586ad4f3b4a33133f92f7695579 Mon Sep 17 00:00:00 2001 From: Anbraten Date: Sat, 5 Mar 2022 01:01:56 +0000 Subject: [PATCH 06/16] further dashboard adjustments --- components/dashboard/src/App.tsx | 2 +- components/dashboard/src/Setup.tsx | 2 +- components/dashboard/src/settings/Integrations.tsx | 9 ++++++++- 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/components/dashboard/src/App.tsx b/components/dashboard/src/App.tsx index 1cd3d457088c05..93b62cfb437295 100644 --- a/components/dashboard/src/App.tsx +++ b/components/dashboard/src/App.tsx @@ -416,7 +416,7 @@ function App() { toRender = ; } else if (isWsStart) { toRender = ; - } else if (/^(github|gitlab)\.com\/.+?/i.test(window.location.pathname)) { + } else if (/^(github|gitlab|gitea)\.com\/.+?/i.test(window.location.pathname)) { let url = new URL(window.location.href) url.hash = url.pathname url.pathname = '/' diff --git a/components/dashboard/src/Setup.tsx b/components/dashboard/src/Setup.tsx index 7f2804e0ec83d8..3720016e114c20 100644 --- a/components/dashboard/src/Setup.tsx +++ b/components/dashboard/src/Setup.tsx @@ -34,7 +34,7 @@ export default function Setup() { })(); } - const headerText = "Configure a Git integration with a GitLab or GitHub instance." + const headerText = "Configure a Git integration with a GitLab, GitHub or Gitea instance." return
{!showModal && ( diff --git a/components/dashboard/src/settings/Integrations.tsx b/components/dashboard/src/settings/Integrations.tsx index 0cb9a06b5060b7..65d8a1b6025204 100644 --- a/components/dashboard/src/settings/Integrations.tsx +++ b/components/dashboard/src/settings/Integrations.tsx @@ -384,7 +384,7 @@ function GitIntegrations() {

Git Integrations

-

Manage Git integrations for GitLab or GitHub self-hosted instances.

+

Manage Git integrations for GitLab, GitHub or Gitea self-hosted instances.

{providers.length !== 0 ? @@ -603,6 +603,9 @@ export function GitIntegrationModal(props: ({ case "GitLab": settingsUrl = `${host}/-/profile/applications`; break; + case "Gitea": + settingsUrl = `${host}/user/settings/applications`; + break; default: return undefined; } let docsUrl = ``; @@ -613,6 +616,9 @@ export function GitIntegrationModal(props: ({ case "GitLab": docsUrl = `https://www.gitpod.io/docs/gitlab-integration/#oauth-application`; break; + case "Gitea": + docsUrl = `https://www.gitpod.io/docs/gitea-integration/#oauth-application`; + break; default: return undefined; } @@ -653,6 +659,7 @@ export function GitIntegrationModal(props: ({ onChange={(e) => setType(e.target.value)}> +
)} From 5f5067830d3d3c57339f5daa0c642f973efa30f2 Mon Sep 17 00:00:00 2001 From: Anbraten Date: Sat, 5 Mar 2022 02:41:02 +0000 Subject: [PATCH 07/16] fix option value --- components/dashboard/src/settings/Integrations.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/dashboard/src/settings/Integrations.tsx b/components/dashboard/src/settings/Integrations.tsx index 65d8a1b6025204..2347dd6528c960 100644 --- a/components/dashboard/src/settings/Integrations.tsx +++ b/components/dashboard/src/settings/Integrations.tsx @@ -659,7 +659,7 @@ export function GitIntegrationModal(props: ({ onChange={(e) => setType(e.target.value)}> - +
)} From 3ff0f29a09a8a7b3827c470900bc35b32bfb4e43 Mon Sep 17 00:00:00 2001 From: Anbraten Date: Sat, 5 Mar 2022 11:03:54 +0000 Subject: [PATCH 08/16] nits --- components/dashboard/src/settings/Integrations.tsx | 2 +- components/server/ee/src/prebuilds/gitea-app.ts | 6 +++--- .../server/src/auth/auth-provider-service.ts | 9 ++++++++- components/server/src/gitea/api.ts | 6 ++---- components/server/src/gitea/gitea-auth-provider.ts | 2 +- .../server/src/gitea/gitea-context-parser.ts | 14 ++++---------- .../server/src/gitea/gitea-repository-provider.ts | 14 ++++++++++++++ components/server/src/gitea/gitea-urls.ts | 1 + 8 files changed, 34 insertions(+), 20 deletions(-) diff --git a/components/dashboard/src/settings/Integrations.tsx b/components/dashboard/src/settings/Integrations.tsx index 2347dd6528c960..ddcbc604fe9149 100644 --- a/components/dashboard/src/settings/Integrations.tsx +++ b/components/dashboard/src/settings/Integrations.tsx @@ -648,7 +648,7 @@ export function GitIntegrationModal(props: ({ You need to activate this integration. )}
- {props.headerText || "Configure a Git integration with a GitLab or GitHub self-hosted instance."} + {props.headerText || "Configure a Git integration with a GitLab, GitHub or Gitea self-hosted instance."}
diff --git a/components/server/ee/src/prebuilds/gitea-app.ts b/components/server/ee/src/prebuilds/gitea-app.ts index 87cfa7c6f81091..e9d1a396bb0b92 100644 --- a/components/server/ee/src/prebuilds/gitea-app.ts +++ b/components/server/ee/src/prebuilds/gitea-app.ts @@ -80,7 +80,7 @@ export class GiteaApp { interface GiteaPushHook { } -interface GiteaRepository { -} +// interface GiteaRepository { +// } -interface GiteaProject {} \ No newline at end of file +// interface GiteaProject {} \ No newline at end of file diff --git a/components/server/src/auth/auth-provider-service.ts b/components/server/src/auth/auth-provider-service.ts index 169f3c29869092..249e52100fc12e 100644 --- a/components/server/src/auth/auth-provider-service.ts +++ b/components/server/src/auth/auth-provider-service.ts @@ -108,7 +108,14 @@ export class AuthProviderService { } protected initializeNewProvider(newEntry: AuthProviderEntry.NewEntry): AuthProviderEntry { const { host, type, clientId, clientSecret } = newEntry; - const urls = type === "GitHub" ? githubUrls(host) : (type === "GitLab" ? gitlabUrls(host) : (type === "Gitea" ? giteaUrls(host) : undefined)); + let urls: { authorizationUrl: string, tokenUrl: string } | undefined = undefined; + if (type === "GitHub") { + urls =githubUrls(host); + } else if (type === "GitLab") { + urls =gitlabUrls(host); + } else if (type === "Gitea") { + urls = giteaUrls(host); + } if (!urls) { throw new Error("Unexpected service type."); } diff --git a/components/server/src/gitea/api.ts b/components/server/src/gitea/api.ts index 9f0c4c1e9222f7..e5f58d5756eedc 100644 --- a/components/server/src/gitea/api.ts +++ b/components/server/src/gitea/api.ts @@ -20,12 +20,12 @@ export namespace Gitea { constructor(msg?: string, httpError?: any) { super(msg); this.httpError = httpError; - this.name = 'GitLabApiError'; + this.name = 'GiteaApiError'; } } export namespace ApiError { export function is(something: any): something is ApiError { - return !!something && something.name === 'GitLabApiError'; + return !!something && something.name === 'GiteaApiError'; } export function isNotFound(error: ApiError): boolean { return !!error.httpError?.description.startsWith("404"); @@ -70,8 +70,6 @@ export class GiteaRestApi { oauthToken = giteaToken.value; } const api = Gitea.create(this.config.host, oauthToken); - const repo = api.repos.repoGet('anbraten', 'gitea-js'); - console.log(repo); return api; } diff --git a/components/server/src/gitea/gitea-auth-provider.ts b/components/server/src/gitea/gitea-auth-provider.ts index 6649149f1b9d7c..6d8e60dbcfd912 100644 --- a/components/server/src/gitea/gitea-auth-provider.ts +++ b/components/server/src/gitea/gitea-auth-provider.ts @@ -81,7 +81,7 @@ currentScopes: this.readScopesFromVerifyParams(tokenResponse) } } catch (error) { - // TODO: cleanup + // TODO: cleanup & check for Gitea instead of Gitlab // if (error && typeof error.description === "string" && error.description.includes("403 Forbidden")) { // // If Gitlab is configured to disallow OAuth-token based API access for unconfirmed users, we need to reject this attempt // // 403 Forbidden - You (@...) must accept the Terms of Service in order to perform this action. Please access GitLab from a web browser to accept these terms. diff --git a/components/server/src/gitea/gitea-context-parser.ts b/components/server/src/gitea/gitea-context-parser.ts index 81370079cd8ac7..492d63b9a90a54 100644 --- a/components/server/src/gitea/gitea-context-parser.ts +++ b/components/server/src/gitea/gitea-context-parser.ts @@ -28,7 +28,7 @@ import { convertRepo } from './convert'; } public async handle(ctx: TraceContext, user: User, contextUrl: string): Promise { - const span = TraceContext.startSpan("GitlabContextParser", ctx); + const span = TraceContext.startSpan("GiteaContextParser", ctx); span.setTag("contextUrl", contextUrl); try { @@ -69,7 +69,6 @@ import { convertRepo } from './convert'; } } - // https://gitlab.com/AlexTugarev/gp-test protected async handleDefaultContext(user: User, host: string, owner: string, repoName: string): Promise { try { const repository = await this.fetchRepo(user, owner, repoName); @@ -116,9 +115,6 @@ import { convertRepo } from './convert'; } } - // https://gitlab.com/AlexTugarev/gp-test/tree/wip - // https://gitlab.com/AlexTugarev/gp-test/tree/wip/folder - // https://gitlab.com/AlexTugarev/gp-test/blob/wip/folder/empty.file.jpeg protected async handleTreeContext(user: User, host: string, owner: string, repoName: string, segments: string[]): Promise { try { @@ -201,11 +197,12 @@ import { convertRepo } from './convert'; const possibleTag = await this.giteaApi.run(user, async g => { return g.repos.repoGetTag(owner, repoName, candidate); }); + // TODO // If the tag does not exist, the GitLab API returns with NotFound or InternalServerError. const isNotFoundTag = Gitea.ApiError.is(possibleTag) && (Gitea.ApiError.isNotFound(possibleTag) || Gitea.ApiError.isInternalServerError(possibleTag)); if (!isNotFoundTag) { if (Gitea.ApiError.is(possibleTag)) { - throw new Error(`GitLab ApiError on searching for possible tags for ${owner}/${repoName}/tree/${segments.join('/')}: ${possibleTag}`); + throw new Error(`Gitea ApiError on searching for possible tags for ${owner}/${repoName}/tree/${segments.join('/')}: ${possibleTag}`); } if (!possibleTag.commit?.sha || !possibleTag.name) { @@ -231,7 +228,6 @@ import { convertRepo } from './convert'; return { ...branchOrTagObject, fullPath }; } - // https://gitlab.com/AlexTugarev/gp-test/merge_requests/1 protected async handlePullRequestContext(user: User, host: string, owner: string, repoName: string, nr: number): Promise { const result = await this.giteaApi.run(user, async g => { return g.repos.repoGetPullRequest(owner, repoName, nr); @@ -307,7 +303,7 @@ import { convertRepo } from './convert'; return g.repos.repoGetSingleCommit(owner, repoName, sha); }); if (Gitea.ApiError.is(result)) { - if (result.message === 'GitLab responded with code 404') { + if (result.message === 'Gitea responded with code 404') { throw new Error(`Couldn't find commit #${sha} in repository ${owner}/${repoName}.`); } throw result; @@ -323,7 +319,6 @@ import { convertRepo } from './convert'; } } - // https://gitlab.com/AlexTugarev/gp-test/issues/1 protected async handleIssueContext(user: User, host: string, owner: string, repoName: string, nr: number): Promise { const ctxPromise = this.handleDefaultContext(user, host, owner, repoName); const result = await this.giteaApi.run(user, async g => { @@ -342,7 +337,6 @@ import { convertRepo } from './convert'; }; } - // https://gitlab.com/AlexTugarev/gp-test/-/commit/80948e8cc8f0e851e89a10bc7c2ee234d1a5fbe7 protected async handleCommitContext(user: User, host: string, owner: string, repoName: string, sha: string): Promise { const repository = await this.fetchRepo(user, owner, repoName); if (Gitea.ApiError.is(repository)) { diff --git a/components/server/src/gitea/gitea-repository-provider.ts b/components/server/src/gitea/gitea-repository-provider.ts index 542f8b7adeaa02..23dbf19b4000ca 100644 --- a/components/server/src/gitea/gitea-repository-provider.ts +++ b/components/server/src/gitea/gitea-repository-provider.ts @@ -109,4 +109,18 @@ export class GiteaRepositoryProvider implements RepositoryProvider { // TODO: do we need the html url or clone urls? return (result || []).map((repo) => repo.html_url || '').filter(s => s !== "") } + + async hasReadAccess(user: User, owner: string, repo: string): Promise { + try { + // If we can "see" a project we are allowed to read it + const api = await this.giteaApi; + const response = await api.run(user, (g => g.repos.repoGet(owner, repo))); + if (Gitea.ApiError.is(response)) { + return false; + } + return true; + } catch (err) { + return false; + } + } } diff --git a/components/server/src/gitea/gitea-urls.ts b/components/server/src/gitea/gitea-urls.ts index 00b61a65814570..eaf177e15f7ea6 100644 --- a/components/server/src/gitea/gitea-urls.ts +++ b/components/server/src/gitea/gitea-urls.ts @@ -9,5 +9,6 @@ export function oauthUrls(host: string) { return { authorizationUrl: `https://${host}/login/oauth/authorize`, tokenUrl: `https://${host}/login/oauth/access_token`, + settingsUrl: `https://${host}/user/settings/applications`, } } From 1d75b9dc84f44b5a5045108f2e6f45e0c7b87acd Mon Sep 17 00:00:00 2001 From: Anbraten Date: Sat, 5 Mar 2022 14:03:08 +0000 Subject: [PATCH 09/16] update gitea-js --- components/server/package.json | 3 ++- components/server/src/gitea/api.ts | 14 +++++--------- yarn.lock | 17 ++++++++++++----- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/components/server/package.json b/components/server/package.json index 2c273830176e33..74166246625172 100644 --- a/components/server/package.json +++ b/components/server/package.json @@ -50,6 +50,7 @@ "cookie": "^0.4.2", "cookie-parser": "^1.4.6", "cors": "^2.8.4", + "cross-fetch": "^3.1.5", "deep-equal": "^2.0.5", "deepmerge": "^4.2.2", "express": "^4.17.3", @@ -57,7 +58,7 @@ "express-mysql-session": "^2.1.0", "express-session": "^1.15.6", "fs-extra": "^10.0.0", - "gitea-js": "^1.1.0", + "gitea-js": "^1.2.0", "google-protobuf": "^3.18.0-rc.2", "heapdump": "^0.3.15", "inversify": "^5.0.1", diff --git a/components/server/src/gitea/api.ts b/components/server/src/gitea/api.ts index e5f58d5756eedc..a1c01f00652398 100644 --- a/components/server/src/gitea/api.ts +++ b/components/server/src/gitea/api.ts @@ -4,8 +4,8 @@ * See License-AGPL.txt in the project root for license information. */ -import { Api, Commit as ICommit, Repository as IRepository, ContentsResponse as IContentsResponse, Branch as IBranch, Tag as ITag, PullRequest as IPullRequest, Issue as IIssue, User as IUser } from "gitea-js" -import fetch from 'node-fetch' +import { giteaApi, Api, Commit as ICommit, Repository as IRepository, ContentsResponse as IContentsResponse, Branch as IBranch, Tag as ITag, PullRequest as IPullRequest, Issue as IIssue, User as IUser } from "gitea-js" +import fetch from 'cross-fetch' import { User } from "@gitpod/gitpod-protocol" import { injectable, inject } from 'inversify'; @@ -36,14 +36,10 @@ export namespace Gitea { } export function create(host: string, token: string) { - return new Api({ + return giteaApi(`https://${host}`, { customFetch: fetch, - baseUrl: `https://${host}`, - baseApiParams: { - headers: { - "Authorization": token, - }, - }}); + token, + }); } export type Commit = ICommit; diff --git a/yarn.lock b/yarn.lock index 0d6bb5289520e2..c1e3ea5cb39c31 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6091,6 +6091,13 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cross-fetch@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" + integrity sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw== + dependencies: + node-fetch "2.6.7" + cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -8685,10 +8692,10 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" -gitea-js@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/gitea-js/-/gitea-js-1.1.0.tgz#d538ce5ed1452346d5397720a8ed09f71abe8988" - integrity sha512-MtEEZjFIfsGNku7KEfELI6LIHy5JdZ8FVHEmvWdAgZWucC8GMj5wgsMN/t4N2qYIFG1RKDudSKXQNtCDOvulkQ== +gitea-js@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gitea-js/-/gitea-js-1.2.0.tgz#b118b003c1ca4499d9ad0a4f15f343967a362cb4" + integrity sha512-/1Xqs8wVWnSTtIJ3JffXbPwVtmJjA+qjVnmdZ0DShj1RKgKzYlqfXT8JUJ7lBdLHXauZrp5VFqldIKrbmWCqKg== glob-parent@^3.1.0: version "3.1.0" @@ -12144,7 +12151,7 @@ node-emoji@^1.11.0: dependencies: lodash "^4.17.21" -node-fetch@^2.1.2, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.5, node-fetch@^2.6.7: +node-fetch@2.6.7, 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: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== From bd3bc1578c7515b3dd2beada19f9f7ef20224bfa Mon Sep 17 00:00:00 2001 From: Anbraten Date: Mon, 7 Mar 2022 18:31:53 +0000 Subject: [PATCH 10/16] fix login --- components/server/src/gitea/gitea-auth-provider.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/components/server/src/gitea/gitea-auth-provider.ts b/components/server/src/gitea/gitea-auth-provider.ts index 6d8e60dbcfd912..b9c7b10a4fc441 100644 --- a/components/server/src/gitea/gitea-auth-provider.ts +++ b/components/server/src/gitea/gitea-auth-provider.ts @@ -42,7 +42,7 @@ ...oauth, authorizationUrl: oauth.authorizationUrl || defaultUrls.authorizationUrl, tokenUrl: oauth.tokenUrl || defaultUrls.tokenUrl, - // settingsUrl: oauth.settingsUrl || defaultUrls.settingsUrl, + settingsUrl: oauth.settingsUrl || defaultUrls.settingsUrl, scope: GiteaScope.All.join(scopeSeparator), scopeSeparator }; @@ -52,12 +52,8 @@ super.authorize(req, res, next, scope ? scope : GiteaScope.Requirements.DEFAULT); } - protected get baseURL() { - return `https://${this.params.host}`; - } - protected readAuthUserSetup = async (accessToken: string, tokenResponse: object) => { - const api = Gitea.create(this.baseURL, accessToken); + const api = Gitea.create(this.params.host, accessToken); const getCurrentUser = async () => { const response = await api.user.userGetCurrent(); return response.data as unknown as Gitea.User; @@ -65,7 +61,7 @@ try { const result = await getCurrentUser(); if (result) { - if (!result.active || !result.created || !result.prohibit_login) { + if (!result.active || result.prohibit_login) { throw UnconfirmedUserException.create("Please confirm and activate your Gitea account and try again.", result); } } From f1bb3ca7d87445e1407b86c149fbe965e679ded0 Mon Sep 17 00:00:00 2001 From: Anbraten Date: Sun, 13 Mar 2022 11:25:13 +0000 Subject: [PATCH 11/16] improve context parser --- components/server/src/dev/dev-data.ts | 7 + components/server/src/gitea/api.ts | 2 +- .../src/gitea/gitea-context-parser.spec.ts | 1176 ++++++++--------- .../server/src/gitea/gitea-context-parser.ts | 677 +++++----- .../src/gitea/gitea-repository-provider.ts | 21 + 5 files changed, 968 insertions(+), 915 deletions(-) diff --git a/components/server/src/dev/dev-data.ts b/components/server/src/dev/dev-data.ts index 52ee4a267f2133..d5584b91403d5b 100644 --- a/components/server/src/dev/dev-data.ts +++ b/components/server/src/dev/dev-data.ts @@ -83,6 +83,13 @@ export namespace DevData { return result; } + export function createGiteaTestToken(): Token { + return { + ...getTokenFromEnv("GITPOD_TEST_TOKEN_GITEA"), + scopes: [] + }; + } + function getTokenFromEnv(varname: string): Token { const secret = process.env[varname]; if (!secret) { diff --git a/components/server/src/gitea/api.ts b/components/server/src/gitea/api.ts index a1c01f00652398..6e82087011a8cc 100644 --- a/components/server/src/gitea/api.ts +++ b/components/server/src/gitea/api.ts @@ -1,5 +1,5 @@ /** - * Copyright (c) 2020 Gitpod GmbH. All rights reserved. + * 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. */ diff --git a/components/server/src/gitea/gitea-context-parser.spec.ts b/components/server/src/gitea/gitea-context-parser.spec.ts index 9920a5a0948108..eb8745d3a72d25 100644 --- a/components/server/src/gitea/gitea-context-parser.spec.ts +++ b/components/server/src/gitea/gitea-context-parser.spec.ts @@ -1,590 +1,586 @@ -// /** -// * Copyright (c) 2020 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. -// */ - -// // Use asyncIterators with es2015 -// if (typeof (Symbol as any).asyncIterator === 'undefined') { -// (Symbol as any).asyncIterator = Symbol.asyncIterator || Symbol('asyncIterator'); -// } -// import "reflect-metadata"; - -// import { suite, test, timeout, retries } from "mocha-typescript"; -// import * as chai from 'chai'; -// const expect = chai.expect; - -// import { BranchRef, GiteaGraphQlEndpoint } from './api'; -// import { NotFoundError } from '../errors'; -// import { GiteaContextParser } from './gitea-context-parser'; -// import { User } from "@gitpod/gitpod-protocol"; -// import { ContainerModule, Container } from "inversify"; -// import { Config } from "../config"; -// import { DevData } from "../dev/dev-data"; -// import { AuthProviderParams } from "../auth/auth-provider"; -// import { TokenProvider } from "../user/token-provider"; -// import { GiteaTokenHelper } from "./gitea-token-helper"; -// import { HostContextProvider } from "../auth/host-context-provider"; -// import { skipIfEnvVarNotSet } from "@gitpod/gitpod-protocol/lib/util/skip-if"; - -// @suite(timeout(10000), retries(2), skipIfEnvVarNotSet("GITPOD_TEST_TOKEN_GITEA")) -// class TestGiteaContextParser { - -// protected parser: GiteaContextParser; -// protected user: User; - -// public before() { -// const container = new Container(); -// container.load(new ContainerModule((bind, unbind, isBound, rebind) => { -// bind(Config).toConstantValue({ -// // meant to appease DI, but Config is never actually used here -// }); -// bind(GiteaContextParser).toSelf().inSingletonScope(); -// bind(GiteaGraphQlEndpoint).toSelf().inSingletonScope(); -// bind(AuthProviderParams).toConstantValue(TestGiteaContextParser.AUTH_HOST_CONFIG); -// bind(GiteaTokenHelper).toSelf().inSingletonScope(); -// bind(TokenProvider).toConstantValue({ -// getTokenForHost: async (user: User, host: string) => { -// return DevData.createGiteaTestToken(); -// } -// }); -// bind(HostContextProvider).toConstantValue(DevData.createDummyHostContextProvider()); -// })); -// this.parser = container.get(GiteaContextParser); -// this.user = DevData.createTestUser(); -// } - -// static readonly AUTH_HOST_CONFIG: Partial = { -// id: "Public-Gitea", -// type: "Gitea", -// verified: true, -// description: "", -// icon: "", -// host: "github.com", -// oauth: "not-used" as any -// } - -// static readonly BRANCH_TEST = { -// name: "test", -// commit: { -// sha: "testsha", -// url: "testurl" -// }, -// protected: false, -// protection_url: "" -// }; - -// static readonly BRANCH_ISSUE_974 = { -// name: "ak/lmcbout-issue_974", -// commit: { -// sha: "sha974", -// url: "url974" -// }, -// protected: false, -// protection_url: "" -// }; - -// static readonly BLO_BLA_ERROR_DATA = { -// host: "github.com", -// lastUpdate: undefined, -// owner: 'blo', -// repoName: 'bla', -// userIsOwner: false, -// userScopes: ["user:email", "public_repo", "repo"], -// }; - -// protected getTestBranches(): BranchRef[] { -// return [TestGiteaContextParser.BRANCH_TEST, TestGiteaContextParser.BRANCH_ISSUE_974]; -// } - -// protected get bloBlaErrorData() { -// return TestGiteaContextParser.BLO_BLA_ERROR_DATA; -// } - -// @test public async testErrorContext_01() { -// try { -// await this.parser.handle({}, this.user, 'https://github.com/blo/bla'); -// } catch (e) { -// expect(NotFoundError.is(e)); -// expect(e.data).to.deep.equal(this.bloBlaErrorData); -// } -// } - -// @test public async testErrorContext_02() { -// try { -// await this.parser.handle({}, this.user, 'https://github.com/blo/bla/pull/42'); -// } catch (e) { -// expect(NotFoundError.is(e)); -// expect(e.data).to.deep.equal(this.bloBlaErrorData); -// } -// } - -// @test public async testErrorContext_03() { -// try { -// await this.parser.handle({}, this.user, 'https://github.com/blo/bla/issues/42'); -// } catch (e) { -// expect(NotFoundError.is(e)); -// expect(e.data).to.deep.equal(this.bloBlaErrorData); -// } -// } - -// @test public async testErrorContext_04() { -// try { -// await this.parser.handle({}, this.user, 'https://github.com/blo/bla/tree/my/branch/path/foo.ts'); -// } catch (e) { -// expect(NotFoundError.is(e)); -// expect(e.data).to.deep.equal(this.bloBlaErrorData); -// } -// } - -// @test public async testTreeContext_01() { -// const result = await this.parser.handle({}, this.user, 'https://github.com/eclipse-theia/theia'); -// expect(result).to.deep.include({ -// "ref": "master", -// "refType": "branch", -// "path": "", -// "isFile": false, -// "repository": { -// "host": "github.com", -// "owner": "eclipse-theia", -// "name": "theia", -// "cloneUrl": "https://github.com/eclipse-theia/theia.git", -// "private": false -// }, -// "title": "eclipse-theia/theia - master" -// }) -// } - -// @test public async testTreeContext_02() { -// const result = await this.parser.handle({}, this.user, 'https://github.com/eclipse-theia/theia/tree/master'); -// expect(result).to.deep.include({ -// "ref": "master", -// "refType": "branch", -// "path": "", -// "isFile": false, -// "repository": { -// "host": "github.com", -// "owner": "eclipse-theia", -// "name": "theia", -// "cloneUrl": "https://github.com/eclipse-theia/theia.git", -// "private": false -// }, -// "title": "eclipse-theia/theia - master" -// }) -// } - -// @test public async testTreeContext_03() { -// const result = await this.parser.handle({}, this.user, 'https://github.com/eclipse-theia/theia/tree/master/LICENSE'); -// expect(result).to.deep.include({ -// "ref": "master", -// "refType": "branch", -// "path": "LICENSE", -// "isFile": true, -// "repository": { -// "host": "github.com", -// "owner": "eclipse-theia", -// "name": "theia", -// "cloneUrl": "https://github.com/eclipse-theia/theia.git", -// "private": false -// }, -// "title": "eclipse-theia/theia - master" -// }) -// } - -// @test public async testTreeContext_04() { -// const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/blob/nametest/src/src/server.ts'); -// expect(result).to.deep.include({ -// "ref": "nametest/src", -// "refType": "branch", -// "path": "src/server.ts", -// "isFile": true, -// "repository": { -// "host": "github.com", -// "owner": "gitpod-io", -// "name": "gitpod-test-repo", -// "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", -// "private": false -// }, -// "title": "gitpod-io/gitpod-test-repo - nametest/src" -// }) -// } - -// @test public async testTreeContext_05() { -// const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/tree/499efbbcb50e7e6e5e2883053f72a34cd5396be3/folder1/folder2'); -// expect(result).to.deep.include( -// { -// "title": "gitpod-io/gitpod-test-repo - 499efbbc:folder1/folder2", -// "repository": { -// "host": "github.com", -// "owner": "gitpod-io", -// "name": "gitpod-test-repo", -// "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", -// "private": false -// }, -// "revision": "499efbbcb50e7e6e5e2883053f72a34cd5396be3", -// "isFile": false, -// "path": "folder1/folder2" -// } -// ) -// } - -// @test public async testTreeContext_06() { -// const result = await this.parser.handle({}, this.user, 'https://github.com/Snailclimb/JavaGuide/blob/940982ebffa5f376b6baddeaf9ed41c91217a6b6/数据结构与算法/常见安全算法(MD5、SHA1、Base64等等)总结.md'); -// expect(result).to.deep.include( -// { -// "title": "Snailclimb/JavaGuide - 940982eb:数据结构与算法/常见安全算法(MD5、SHA1、Base64等等)总结.md", -// "repository": { -// "host": "github.com", -// "owner": "Snailclimb", -// "name": "JavaGuide", -// "cloneUrl": "https://github.com/Snailclimb/JavaGuide.git", -// "private": false -// }, -// "revision": "940982ebffa5f376b6baddeaf9ed41c91217a6b6", -// "isFile": true, -// "path": "数据结构与算法/常见安全算法(MD5、SHA1、Base64等等)总结.md" -// } -// ) -// } - -// @test public async testTreeContext_07() { -// const result = await this.parser.handle({}, this.user, 'https://github.com/eclipse-theia/theia#license'); -// expect(result).to.deep.include({ -// "ref": "master", -// "refType": "branch", -// "path": "", -// "isFile": false, -// "repository": { -// "host": "github.com", -// "owner": "eclipse-theia", -// "name": "theia", -// "cloneUrl": "https://github.com/eclipse-theia/theia.git", -// "private": false -// }, -// "title": "eclipse-theia/theia - master" -// }) -// } - -// @test public async testTreeContext_tag_01() { -// const result = await this.parser.handle({}, this.user, 'https://github.com/eclipse-theia/theia/tree/v0.1.0'); -// expect(result).to.deep.include( -// { -// "title": "eclipse-theia/theia - v0.1.0", -// "repository": { -// "host": "github.com", -// "owner": "eclipse-theia", -// "name": "theia", -// "cloneUrl": "https://github.com/eclipse-theia/theia.git", -// "private": false -// }, -// "revision": "f29626847a14ca50dd78483aebaf4b4fe26bcb73", -// "isFile": false, -// "ref": "v0.1.0", -// "refType": "tag" -// } -// ) -// } - -// @test public async testReleasesContext_tag_01() { -// const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod/releases/tag/v0.9.0'); -// expect(result).to.deep.include( -// { -// "ref": "v0.9.0", -// "refType": "tag", -// "isFile": false, -// "path": "", -// "title": "gitpod-io/gitpod - v0.9.0", -// "revision": "25ece59c495d525614f28971d41d5708a31bf1e3", -// "repository": { -// "cloneUrl": "https://github.com/gitpod-io/gitpod.git", -// "host": "github.com", -// "name": "gitpod", -// "owner": "gitpod-io", -// "private": false -// } -// } -// ) -// } - -// @test public async testCommitsContext_01() { -// const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/commits/4test'); -// expect(result).to.deep.include({ -// "ref": "4test", -// "refType": "branch", -// "path": "", -// "isFile": false, -// "repository": { -// "host": "github.com", -// "owner": "gitpod-io", -// "name": "gitpod-test-repo", -// "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", -// "private": false -// }, -// "title": "gitpod-io/gitpod-test-repo - 4test" -// }) -// } - -// @test public async testCommitContext_01() { -// const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/commit/409ac2de49a53d679989d438735f78204f441634'); -// expect(result).to.deep.include({ -// "ref": "", -// "refType": "revision", -// "path": "", -// "revision": "409ac2de49a53d679989d438735f78204f441634", -// "isFile": false, -// "repository": { -// "host": "github.com", -// "owner": "gitpod-io", -// "name": "gitpod-test-repo", -// "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", -// "private": false -// }, -// "title": "gitpod-io/gitpod-test-repo - Test 3" -// }) -// } - -// @test public async testCommitContext_02_notExistingCommit() { -// try { -// await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/commit/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); -// // ensure that an error has been thrown -// chai.assert.fail(); -// } catch (e) { -// expect(e.message).contains("Couldn't find commit"); -// } -// } - -// @test public async testCommitContext_02_invalidSha() { -// try { -// await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/commit/invalid'); -// // ensure that an error has been thrown -// chai.assert.fail(); -// } catch (e) { -// expect(e.message).contains("Invalid commit ID"); -// } -// } - -// @test public async testPullRequestContext_01() { -// const result = await this.parser.handle({}, this.user, 'https://github.com/TypeFox/theia/pull/1'); -// expect(result).to.deep.include( -// { -// "title": "Merge master", -// "repository": { -// "host": "github.com", -// "owner": "eclipse-theia", -// "name": "theia", -// "cloneUrl": "https://github.com/eclipse-theia/theia.git", -// "private": false -// }, -// "ref": "master", -// "refType": "branch", -// "nr": 1, -// "base": { -// "repository": { -// "host": "github.com", -// "owner": "TypeFox", -// "name": "theia", -// "cloneUrl": "https://github.com/TypeFox/theia.git", -// "private": false, -// "fork": { -// "parent": { -// "cloneUrl": "https://github.com/eclipse-theia/theia.git", -// "host": "github.com", -// "name": "theia", -// "owner": "eclipse-theia", -// "private": false -// } -// } -// }, -// "ref": "master", -// "refType": "branch", -// } -// } -// ) -// } - -// @test public async testPullRequestthroughIssueContext_04() { -// const result = await this.parser.handle({}, this.user, 'https://github.com/TypeFox/theia/issues/1'); -// expect(result).to.deep.include( -// { -// "title": "Merge master", -// "repository": { -// "host": "github.com", -// "owner": "eclipse-theia", -// "name": "theia", -// "cloneUrl": "https://github.com/eclipse-theia/theia.git", -// "private": false -// }, -// "ref": "master", -// "refType": "branch", -// "nr": 1, -// "base": { -// "repository": { -// "host": "github.com", -// "owner": "TypeFox", -// "name": "theia", -// "cloneUrl": "https://github.com/TypeFox/theia.git", -// "private": false, -// "fork": { -// "parent": { -// "cloneUrl": "https://github.com/eclipse-theia/theia.git", -// "host": "github.com", -// "name": "theia", -// "owner": "eclipse-theia", -// "private": false -// } -// } -// }, -// "ref": "master", -// "refType": "branch", -// } -// } -// ) -// } - -// @test public async testIssueContext_01() { -// const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/issues/42'); -// expect(result).to.deep.include( -// { -// "title": "Test issue web-extension", -// "repository": { -// "host": "github.com", -// "owner": "gitpod-io", -// "name": "gitpod-test-repo", -// "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", -// "private": false -// }, -// "owner": "gitpod-io", -// "nr": 42, -// "ref": "1test", -// "refType": "branch", -// "localBranch": "somefox/test-issue-web-extension-42" -// } -// ) -// } - -// @test public async testIssuePageContext() { -// const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/issues'); -// expect(result).to.deep.include( -// { -// "title": "gitpod-io/gitpod-test-repo - 1test", -// "repository": { -// "host": "github.com", -// "owner": "gitpod-io", -// "name": "gitpod-test-repo", -// "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", -// "private": false -// }, -// "ref": "1test", -// "refType": "branch", -// } -// ) -// } - - -// @test public async testIssueThroughPullRequestContext() { -// const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/pull/42'); -// expect(result).to.deep.include( -// { -// "title": "Test issue web-extension", -// "repository": { -// "host": "github.com", -// "owner": "gitpod-io", -// "name": "gitpod-test-repo", -// "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", -// "private": false -// }, -// "owner": "gitpod-io", -// "nr": 42, -// "ref": "1test", -// "refType": "branch", -// "localBranch": "somefox/test-issue-web-extension-42" -// } -// ) -// } - -// @test public async testBlobContext_01() { -// const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/blob/aba298d5084a817cdde3dd1f26692bc2a216e2b9/test-comment-01.md'); -// expect(result).to.deep.include( -// { -// "title": "gitpod-io/gitpod-test-repo - aba298d5:test-comment-01.md", -// "repository": { -// "host": "github.com", -// "owner": "gitpod-io", -// "name": "gitpod-test-repo", -// "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", -// "private": false -// }, -// "revision": "aba298d5084a817cdde3dd1f26692bc2a216e2b9", -// "isFile": true, -// "path": "test-comment-01.md" -// } -// ) -// } - -// @test public async testBlobContext_02() { -// const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/blob/499efbbcb50e7e6e5e2883053f72a34cd5396be3/folder1/folder2/content2'); -// expect(result).to.deep.include( -// { -// "title": "gitpod-io/gitpod-test-repo - 499efbbc:folder1/folder2/content2", -// "repository": { -// "host": "github.com", -// "owner": "gitpod-io", -// "name": "gitpod-test-repo", -// "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", -// "private": false -// }, -// "revision": "499efbbcb50e7e6e5e2883053f72a34cd5396be3", -// "isFile": true, -// "path": "folder1/folder2/content2" -// } -// ) -// } - -// @test public async testBlobContextShort_01() { -// const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/blob/499efbbc/folder1/folder2/content2'); -// expect(result).to.deep.include( -// { -// "title": "gitpod-io/gitpod-test-repo - 499efbbc:folder1/folder2/content2", -// "repository": { -// "host": "github.com", -// "owner": "gitpod-io", -// "name": "gitpod-test-repo", -// "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", -// "private": false -// }, -// "revision": "499efbbcb50e7e6e5e2883053f72a34cd5396be3", -// "isFile": true, -// "path": "folder1/folder2/content2" -// } -// ) -// } - -// @test public async testBlobContextShort_02() { -// const result = await this.parser.handle({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo/blob/499ef/folder1/folder2/content2'); -// expect(result).to.deep.include( -// { -// "title": "gitpod-io/gitpod-test-repo - 499efbbc:folder1/folder2/content2", -// "repository": { -// "host": "github.com", -// "owner": "gitpod-io", -// "name": "gitpod-test-repo", -// "cloneUrl": "https://github.com/gitpod-io/gitpod-test-repo.git", -// "private": false -// }, -// "revision": "499efbbcb50e7e6e5e2883053f72a34cd5396be3", -// "isFile": true, -// "path": "folder1/folder2/content2" -// } -// ) -// } - -// @test public async testFetchCommitHistory() { -// const result = await this.parser.fetchCommitHistory({}, this.user, 'https://github.com/gitpod-io/gitpod-test-repo', '409ac2de49a53d679989d438735f78204f441634', 100); -// expect(result).to.deep.equal([ -// '506e5aed317f28023994ecf8ca6ed91430e9c1a4', -// 'f5b041513bfab914b5fbf7ae55788d9835004d76', -// ]) -// } - -// } -// module.exports = new TestGiteaContextParser() // Only to circumvent no usage warning :-/ \ No newline at end of file +/** + * Copyright (c) 2020 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. + */ + +// Use asyncIterators with es2015 +if (typeof (Symbol as any).asyncIterator === 'undefined') { + (Symbol as any).asyncIterator = Symbol.asyncIterator || Symbol('asyncIterator'); +} +import "reflect-metadata"; + +import { suite, test, timeout, retries } from "mocha-typescript"; +import * as chai from 'chai'; +const expect = chai.expect; + +import { GiteaRestApi } from './api'; +import { NotFoundError } from '../errors'; +import { GiteaContextParser } from './gitea-context-parser'; +import { User } from "@gitpod/gitpod-protocol"; +import { ContainerModule, Container } from "inversify"; +import { Config } from "../config"; +import { DevData } from "../dev/dev-data"; +import { AuthProviderParams } from "../auth/auth-provider"; +import { TokenProvider } from "../user/token-provider"; +import { GiteaTokenHelper } from "./gitea-token-helper"; +import { HostContextProvider } from "../auth/host-context-provider"; +import { skipIfEnvVarNotSet } from "@gitpod/gitpod-protocol/lib/util/skip-if"; + +@suite(timeout(10000), retries(2), skipIfEnvVarNotSet("GITPOD_TEST_TOKEN_GITEA")) +class TestGiteaContextParser { + + protected parser: GiteaContextParser; + protected user: User; + + public before() { + const container = new Container(); + container.load(new ContainerModule((bind, unbind, isBound, rebind) => { + bind(Config).toConstantValue({ + // meant to appease DI, but Config is never actually used here + }); + bind(GiteaContextParser).toSelf().inSingletonScope(); + bind(GiteaRestApi).toSelf().inSingletonScope(); + bind(AuthProviderParams).toConstantValue(TestGiteaContextParser.AUTH_HOST_CONFIG); + bind(GiteaTokenHelper).toSelf().inSingletonScope(); + bind(TokenProvider).toConstantValue({ + getTokenForHost: async (user: User, host: string) => { + return DevData.createGiteaTestToken(); + } + }); + bind(HostContextProvider).toConstantValue(DevData.createDummyHostContextProvider()); + })); + this.parser = container.get(GiteaContextParser); + this.user = DevData.createTestUser(); + } + + static readonly AUTH_HOST_CONFIG: Partial = { + id: "Public-Gitea", + type: "Gitea", + verified: true, + description: "", + icon: "", + host: "gitea.com", + oauth: "not-used" as any + } + + static readonly BRANCH_TEST = { + name: "test", + commit: { + sha: "testsha", + url: "testurl" + }, + protected: false, + protection_url: "" + }; + + static readonly BRANCH_ISSUE_974 = { + name: "ak/lmcbout-issue_974", + commit: { + sha: "sha974", + url: "url974" + }, + protected: false, + protection_url: "" + }; + + static readonly BLO_BLA_ERROR_DATA = { + host: "gitea.com", + lastUpdate: undefined, + owner: 'blo', + repoName: 'bla', + userIsOwner: false, + userScopes: ["user:email", "public_repo", "repo"], + }; + + protected get bloBlaErrorData() { + return TestGiteaContextParser.BLO_BLA_ERROR_DATA; + } + + @test public async testErrorContext_01() { + try { + await this.parser.handle({}, this.user, 'https://gitea.com/blo/bla'); + } catch (e) { + expect(NotFoundError.is(e)); + expect(e.data).to.deep.equal(this.bloBlaErrorData); + } + } + + @test public async testErrorContext_02() { + try { + await this.parser.handle({}, this.user, 'https://gitea.com/blo/bla/pull/42'); + } catch (e) { + expect(NotFoundError.is(e)); + expect(e.data).to.deep.equal(this.bloBlaErrorData); + } + } + + @test public async testErrorContext_03() { + try { + await this.parser.handle({}, this.user, 'https://gitea.com/blo/bla/issues/42'); + } catch (e) { + expect(NotFoundError.is(e)); + expect(e.data).to.deep.equal(this.bloBlaErrorData); + } + } + + @test public async testErrorContext_04() { + try { + await this.parser.handle({}, this.user, 'https://gitea.com/blo/bla/tree/my/branch/path/foo.ts'); + } catch (e) { + expect(NotFoundError.is(e)); + expect(e.data).to.deep.equal(this.bloBlaErrorData); + } + } + + @test public async testTreeContext_01() { + const result = await this.parser.handle({}, this.user, 'https://gitea.com/eclipse-theia/theia'); + expect(result).to.deep.include({ + "ref": "master", + "refType": "branch", + "path": "", + "isFile": false, + "repository": { + "host": "gitea.com", + "owner": "eclipse-theia", + "name": "theia", + "cloneUrl": "https://gitea.com/eclipse-theia/theia.git", + "private": false + }, + "title": "eclipse-theia/theia - master" + }) + } + + @test public async testTreeContext_02() { + const result = await this.parser.handle({}, this.user, 'https://gitea.com/eclipse-theia/theia/tree/master'); + expect(result).to.deep.include({ + "ref": "master", + "refType": "branch", + "path": "", + "isFile": false, + "repository": { + "host": "gitea.com", + "owner": "eclipse-theia", + "name": "theia", + "cloneUrl": "https://gitea.com/eclipse-theia/theia.git", + "private": false + }, + "title": "eclipse-theia/theia - master" + }) + } + + @test public async testTreeContext_03() { + const result = await this.parser.handle({}, this.user, 'https://gitea.com/eclipse-theia/theia/tree/master/LICENSE'); + expect(result).to.deep.include({ + "ref": "master", + "refType": "branch", + "path": "LICENSE", + "isFile": true, + "repository": { + "host": "gitea.com", + "owner": "eclipse-theia", + "name": "theia", + "cloneUrl": "https://gitea.com/eclipse-theia/theia.git", + "private": false + }, + "title": "eclipse-theia/theia - master" + }) + } + + @test public async testTreeContext_04() { + const result = await this.parser.handle({}, this.user, 'https://gitea.com/gitpod-io/gitpod-test-repo/blob/nametest/src/src/server.ts'); + expect(result).to.deep.include({ + "ref": "nametest/src", + "refType": "branch", + "path": "src/server.ts", + "isFile": true, + "repository": { + "host": "gitea.com", + "owner": "gitpod-io", + "name": "gitpod-test-repo", + "cloneUrl": "https://gitea.com/gitpod-io/gitpod-test-repo.git", + "private": false + }, + "title": "gitpod-io/gitpod-test-repo - nametest/src" + }) + } + + @test public async testTreeContext_05() { + const result = await this.parser.handle({}, this.user, 'https://gitea.com/gitpod-io/gitpod-test-repo/tree/499efbbcb50e7e6e5e2883053f72a34cd5396be3/folder1/folder2'); + expect(result).to.deep.include( + { + "title": "gitpod-io/gitpod-test-repo - 499efbbc:folder1/folder2", + "repository": { + "host": "gitea.com", + "owner": "gitpod-io", + "name": "gitpod-test-repo", + "cloneUrl": "https://gitea.com/gitpod-io/gitpod-test-repo.git", + "private": false + }, + "revision": "499efbbcb50e7e6e5e2883053f72a34cd5396be3", + "isFile": false, + "path": "folder1/folder2" + } + ) + } + + @test public async testTreeContext_06() { + const result = await this.parser.handle({}, this.user, 'https://gitea.com/Snailclimb/JavaGuide/blob/940982ebffa5f376b6baddeaf9ed41c91217a6b6/数据结构与算法/常见安全算法(MD5、SHA1、Base64等等)总结.md'); + expect(result).to.deep.include( + { + "title": "Snailclimb/JavaGuide - 940982eb:数据结构与算法/常见安全算法(MD5、SHA1、Base64等等)总结.md", + "repository": { + "host": "gitea.com", + "owner": "Snailclimb", + "name": "JavaGuide", + "cloneUrl": "https://gitea.com/Snailclimb/JavaGuide.git", + "private": false + }, + "revision": "940982ebffa5f376b6baddeaf9ed41c91217a6b6", + "isFile": true, + "path": "数据结构与算法/常见安全算法(MD5、SHA1、Base64等等)总结.md" + } + ) + } + + @test public async testTreeContext_07() { + const result = await this.parser.handle({}, this.user, 'https://gitea.com/eclipse-theia/theia#license'); + expect(result).to.deep.include({ + "ref": "master", + "refType": "branch", + "path": "", + "isFile": false, + "repository": { + "host": "gitea.com", + "owner": "eclipse-theia", + "name": "theia", + "cloneUrl": "https://gitea.com/eclipse-theia/theia.git", + "private": false + }, + "title": "eclipse-theia/theia - master" + }) + } + + @test public async testTreeContext_tag_01() { + const result = await this.parser.handle({}, this.user, 'https://gitea.com/eclipse-theia/theia/tree/v0.1.0'); + expect(result).to.deep.include( + { + "title": "eclipse-theia/theia - v0.1.0", + "repository": { + "host": "gitea.com", + "owner": "eclipse-theia", + "name": "theia", + "cloneUrl": "https://gitea.com/eclipse-theia/theia.git", + "private": false + }, + "revision": "f29626847a14ca50dd78483aebaf4b4fe26bcb73", + "isFile": false, + "ref": "v0.1.0", + "refType": "tag" + } + ) + } + + @test public async testReleasesContext_tag_01() { + const result = await this.parser.handle({}, this.user, 'https://gitea.com/gitpod-io/gitpod/releases/tag/v0.9.0'); + expect(result).to.deep.include( + { + "ref": "v0.9.0", + "refType": "tag", + "isFile": false, + "path": "", + "title": "gitpod-io/gitpod - v0.9.0", + "revision": "25ece59c495d525614f28971d41d5708a31bf1e3", + "repository": { + "cloneUrl": "https://gitea.com/gitpod-io/gitpod.git", + "host": "gitea.com", + "name": "gitpod", + "owner": "gitpod-io", + "private": false + } + } + ) + } + + @test public async testCommitsContext_01() { + const result = await this.parser.handle({}, this.user, 'https://gitea.com/gitpod-io/gitpod-test-repo/commits/4test'); + expect(result).to.deep.include({ + "ref": "4test", + "refType": "branch", + "path": "", + "isFile": false, + "repository": { + "host": "gitea.com", + "owner": "gitpod-io", + "name": "gitpod-test-repo", + "cloneUrl": "https://gitea.com/gitpod-io/gitpod-test-repo.git", + "private": false + }, + "title": "gitpod-io/gitpod-test-repo - 4test" + }) + } + + @test public async testCommitContext_01() { + const result = await this.parser.handle({}, this.user, 'https://gitea.com/gitpod-io/gitpod-test-repo/commit/409ac2de49a53d679989d438735f78204f441634'); + expect(result).to.deep.include({ + "ref": "", + "refType": "revision", + "path": "", + "revision": "409ac2de49a53d679989d438735f78204f441634", + "isFile": false, + "repository": { + "host": "gitea.com", + "owner": "gitpod-io", + "name": "gitpod-test-repo", + "cloneUrl": "https://gitea.com/gitpod-io/gitpod-test-repo.git", + "private": false + }, + "title": "gitpod-io/gitpod-test-repo - Test 3" + }) + } + + @test public async testCommitContext_02_notExistingCommit() { + try { + await this.parser.handle({}, this.user, 'https://gitea.com/gitpod-io/gitpod-test-repo/commit/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); + // ensure that an error has been thrown + chai.assert.fail(); + } catch (e) { + expect(e.message).contains("Couldn't find commit"); + } + } + + @test public async testCommitContext_02_invalidSha() { + try { + await this.parser.handle({}, this.user, 'https://gitea.com/gitpod-io/gitpod-test-repo/commit/invalid'); + // ensure that an error has been thrown + chai.assert.fail(); + } catch (e) { + expect(e.message).contains("Invalid commit ID"); + } + } + + @test public async testPullRequestContext_01() { + const result = await this.parser.handle({}, this.user, 'https://gitea.com/TypeFox/theia/pull/1'); + expect(result).to.deep.include( + { + "title": "Merge master", + "repository": { + "host": "gitea.com", + "owner": "eclipse-theia", + "name": "theia", + "cloneUrl": "https://gitea.com/eclipse-theia/theia.git", + "private": false + }, + "ref": "master", + "refType": "branch", + "nr": 1, + "base": { + "repository": { + "host": "gitea.com", + "owner": "TypeFox", + "name": "theia", + "cloneUrl": "https://gitea.com/TypeFox/theia.git", + "private": false, + "fork": { + "parent": { + "cloneUrl": "https://gitea.com/eclipse-theia/theia.git", + "host": "gitea.com", + "name": "theia", + "owner": "eclipse-theia", + "private": false + } + } + }, + "ref": "master", + "refType": "branch", + } + } + ) + } + + @test public async testPullRequestthroughIssueContext_04() { + const result = await this.parser.handle({}, this.user, 'https://gitea.com/TypeFox/theia/issues/1'); + expect(result).to.deep.include( + { + "title": "Merge master", + "repository": { + "host": "gitea.com", + "owner": "eclipse-theia", + "name": "theia", + "cloneUrl": "https://gitea.com/eclipse-theia/theia.git", + "private": false + }, + "ref": "master", + "refType": "branch", + "nr": 1, + "base": { + "repository": { + "host": "gitea.com", + "owner": "TypeFox", + "name": "theia", + "cloneUrl": "https://gitea.com/TypeFox/theia.git", + "private": false, + "fork": { + "parent": { + "cloneUrl": "https://gitea.com/eclipse-theia/theia.git", + "host": "gitea.com", + "name": "theia", + "owner": "eclipse-theia", + "private": false + } + } + }, + "ref": "master", + "refType": "branch", + } + } + ) + } + + @test public async testIssueContext_01() { + const result = await this.parser.handle({}, this.user, 'https://gitea.com/gitpod-io/gitpod-test-repo/issues/42'); + expect(result).to.deep.include( + { + "title": "Test issue web-extension", + "repository": { + "host": "gitea.com", + "owner": "gitpod-io", + "name": "gitpod-test-repo", + "cloneUrl": "https://gitea.com/gitpod-io/gitpod-test-repo.git", + "private": false + }, + "owner": "gitpod-io", + "nr": 42, + "ref": "1test", + "refType": "branch", + "localBranch": "somefox/test-issue-web-extension-42" + } + ) + } + + @test public async testIssuePageContext() { + const result = await this.parser.handle({}, this.user, 'https://gitea.com/gitpod-io/gitpod-test-repo/issues'); + expect(result).to.deep.include( + { + "title": "gitpod-io/gitpod-test-repo - 1test", + "repository": { + "host": "gitea.com", + "owner": "gitpod-io", + "name": "gitpod-test-repo", + "cloneUrl": "https://gitea.com/gitpod-io/gitpod-test-repo.git", + "private": false + }, + "ref": "1test", + "refType": "branch", + } + ) + } + + + @test public async testIssueThroughPullRequestContext() { + const result = await this.parser.handle({}, this.user, 'https://gitea.com/gitpod-io/gitpod-test-repo/pull/42'); + expect(result).to.deep.include( + { + "title": "Test issue web-extension", + "repository": { + "host": "gitea.com", + "owner": "gitpod-io", + "name": "gitpod-test-repo", + "cloneUrl": "https://gitea.com/gitpod-io/gitpod-test-repo.git", + "private": false + }, + "owner": "gitpod-io", + "nr": 42, + "ref": "1test", + "refType": "branch", + "localBranch": "somefox/test-issue-web-extension-42" + } + ) + } + + @test public async testBlobContext_01() { + const result = await this.parser.handle({}, this.user, 'https://gitea.com/gitpod-io/gitpod-test-repo/blob/aba298d5084a817cdde3dd1f26692bc2a216e2b9/test-comment-01.md'); + expect(result).to.deep.include( + { + "title": "gitpod-io/gitpod-test-repo - aba298d5:test-comment-01.md", + "repository": { + "host": "gitea.com", + "owner": "gitpod-io", + "name": "gitpod-test-repo", + "cloneUrl": "https://gitea.com/gitpod-io/gitpod-test-repo.git", + "private": false + }, + "revision": "aba298d5084a817cdde3dd1f26692bc2a216e2b9", + "isFile": true, + "path": "test-comment-01.md" + } + ) + } + + @test public async testBlobContext_02() { + const result = await this.parser.handle({}, this.user, 'https://gitea.com/gitpod-io/gitpod-test-repo/blob/499efbbcb50e7e6e5e2883053f72a34cd5396be3/folder1/folder2/content2'); + expect(result).to.deep.include( + { + "title": "gitpod-io/gitpod-test-repo - 499efbbc:folder1/folder2/content2", + "repository": { + "host": "gitea.com", + "owner": "gitpod-io", + "name": "gitpod-test-repo", + "cloneUrl": "https://gitea.com/gitpod-io/gitpod-test-repo.git", + "private": false + }, + "revision": "499efbbcb50e7e6e5e2883053f72a34cd5396be3", + "isFile": true, + "path": "folder1/folder2/content2" + } + ) + } + + @test public async testBlobContextShort_01() { + const result = await this.parser.handle({}, this.user, 'https://gitea.com/gitpod-io/gitpod-test-repo/blob/499efbbc/folder1/folder2/content2'); + expect(result).to.deep.include( + { + "title": "gitpod-io/gitpod-test-repo - 499efbbc:folder1/folder2/content2", + "repository": { + "host": "gitea.com", + "owner": "gitpod-io", + "name": "gitpod-test-repo", + "cloneUrl": "https://gitea.com/gitpod-io/gitpod-test-repo.git", + "private": false + }, + "revision": "499efbbcb50e7e6e5e2883053f72a34cd5396be3", + "isFile": true, + "path": "folder1/folder2/content2" + } + ) + } + + @test public async testBlobContextShort_02() { + const result = await this.parser.handle({}, this.user, 'https://gitea.com/gitpod-io/gitpod-test-repo/blob/499ef/folder1/folder2/content2'); + expect(result).to.deep.include( + { + "title": "gitpod-io/gitpod-test-repo - 499efbbc:folder1/folder2/content2", + "repository": { + "host": "gitea.com", + "owner": "gitpod-io", + "name": "gitpod-test-repo", + "cloneUrl": "https://gitea.com/gitpod-io/gitpod-test-repo.git", + "private": false + }, + "revision": "499efbbcb50e7e6e5e2883053f72a34cd5396be3", + "isFile": true, + "path": "folder1/folder2/content2" + } + ) + } + + @test public async testFetchCommitHistory() { + const result = await this.parser.fetchCommitHistory({}, this.user, 'https://gitea.com/gitpod-io/gitpod-test-repo', '409ac2de49a53d679989d438735f78204f441634', 100); + expect(result).to.deep.equal([ + '506e5aed317f28023994ecf8ca6ed91430e9c1a4', + 'f5b041513bfab914b5fbf7ae55788d9835004d76', + ]) + } + +} +module.exports = new TestGiteaContextParser() // Only to circumvent no usage warning :-/ \ No newline at end of file diff --git a/components/server/src/gitea/gitea-context-parser.ts b/components/server/src/gitea/gitea-context-parser.ts index 492d63b9a90a54..6ed2621e434bf6 100644 --- a/components/server/src/gitea/gitea-context-parser.ts +++ b/components/server/src/gitea/gitea-context-parser.ts @@ -4,183 +4,195 @@ * See License-AGPL.txt in the project root for license information. */ - import { injectable, inject } from 'inversify'; - - import { NavigatorContext, User, CommitContext, Repository, PullRequestContext, IssueContext, RefType } from '@gitpod/gitpod-protocol'; - import { GiteaRestApi, Gitea } from './api'; - import { UnauthorizedError, NotFoundError } from '../errors'; - import { GiteaScope } from './scopes'; - import { IContextParser, IssueContexts, AbstractContextParser } from '../workspace/context-parser'; - import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; - import { GiteaTokenHelper } from './gitea-token-helper'; - import { TraceContext } from '@gitpod/gitpod-protocol/lib/util/tracing'; - const path = require('path'); +import { injectable, inject } from 'inversify'; + +import { Repository, PullRequestContext, NavigatorContext, IssueContext, User, CommitContext, RefType } from '@gitpod/gitpod-protocol'; +import { Gitea, GiteaRestApi } from './api'; +import { NotFoundError, UnauthorizedError } from '../errors'; +import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; +import { IContextParser, IssueContexts, AbstractContextParser } from '../workspace/context-parser'; +import { GiteaScope } from './scopes'; +import { GiteaTokenHelper } from './gitea-token-helper'; +import { TraceContext } from '@gitpod/gitpod-protocol/lib/util/tracing'; import { convertRepo } from './convert'; - @injectable() - export class GiteaContextParser extends AbstractContextParser implements IContextParser { - - @inject(GiteaRestApi) protected readonly giteaApi: GiteaRestApi; - @inject(GiteaTokenHelper) protected readonly tokenHelper: GiteaTokenHelper; - - protected get authProviderId() { - return this.config.id; - } - - public async handle(ctx: TraceContext, user: User, contextUrl: string): Promise { - const span = TraceContext.startSpan("GiteaContextParser", ctx); - span.setTag("contextUrl", contextUrl); - - try { - const { host, owner, repoName, moreSegments } = await this.parseURL(user, contextUrl); - if (moreSegments.length > 0) { - switch (moreSegments[0]) { - case 'merge_requests': { - return await this.handlePullRequestContext(user, host, owner, repoName, parseInt(moreSegments[1])); - } - case 'tree': - case 'blob': - case 'commits': { - return await this.handleTreeContext(user, host, owner, repoName, moreSegments.slice(1)); - } - case 'issues': { - return await this.handleIssueContext(user, host, owner, repoName, parseInt(moreSegments[1])); - } - case 'commit': { - return await this.handleCommitContext(user, host, owner, repoName, moreSegments[1]); - } - } - } - - return await this.handleDefaultContext(user, host, owner, repoName); - } catch (error) { - if (error && error.code === 401) { - const token = await this.tokenHelper.getCurrentToken(user); - if (token) { - const scopes = token.scopes; - // most likely the token needs to be updated after revoking by user. - throw UnauthorizedError.create(this.config.host, scopes, "http-unauthorized"); - } - throw UnauthorizedError.create(this.config.host, GiteaScope.Requirements.DEFAULT); - } - throw error; - } finally { - span.finish(); - } - } - - protected async handleDefaultContext(user: User, host: string, owner: string, repoName: string): Promise { - try { - const repository = await this.fetchRepo(user, owner, repoName); - if (!repository.defaultBranch) { - return { - isFile: false, - path: '', - title: `${owner}/${repoName}`, - repository - } - } - - try { - const branchOrTag = await this.getBranchOrTag(user, owner, repoName, [repository.defaultBranch!]); - return { - isFile: false, - path: '', - title: `${owner}/${repoName} - ${branchOrTag.name}`, - ref: branchOrTag.name, - revision: branchOrTag.revision, - refType: branchOrTag.type, - repository - }; - } catch (error) { - if (error && error.message && (error.message as string).startsWith("Cannot find tag/branch for context")) { - // the repo is empty (has no branches) - return { - isFile: false, - path: '', - title: `${owner}/${repoName} - ${repository.defaultBranch}`, - revision: '', - repository - } - } else { - throw error; - } - } - } catch (error) { - if (UnauthorizedError.is(error)) { - throw error; - } - // log.error({ userId: user.id }, error); - throw await NotFoundError.create(await this.tokenHelper.getCurrentToken(user), user, host, owner, repoName); - } - } - - protected async handleTreeContext(user: User, host: string, owner: string, repoName: string, segments: string[]): Promise { - - try { - const branchOrTagPromise = segments.length > 0 ? this.getBranchOrTag(user, owner, repoName, segments) : undefined; - const repository = await this.fetchRepo(user, owner, repoName); - const branchOrTag = await branchOrTagPromise; - const context = { - isFile: false, - path: '', - title: `${owner}/${repoName}` + (branchOrTag ? ` - ${branchOrTag.name}` : ''), - ref: branchOrTag && branchOrTag.name, - revision: branchOrTag && branchOrTag.revision, - refType: branchOrTag && branchOrTag.type, - repository - }; - if (!branchOrTag) { - return context; - } - if (segments.length === 1 || branchOrTag.fullPath.length === 0) { - return context; - } - - const result = await this.giteaApi.run(user, async g => { - return g.repos.repoGetContents(owner, repoName, path.dirname(branchOrTag.fullPath), { ref: branchOrTag.name }); - }); - if (Gitea.ApiError.is(result)) { - throw new Error(`Error reading TREE ${owner}/${repoName}/tree/${segments.join('/')}: ${result}`); - } else { - const object = result.find(o => o.path === branchOrTag.fullPath); - if (object) { - const isFile = object.type === "blob"; - context.isFile = isFile; - context.path = branchOrTag.fullPath; - } - } - return context; - } catch (e) { - log.debug("Gitea context parser: Error handle tree context.", e); - throw e; - } - } - - protected async getBranchOrTag(user: User, owner: string, repoName: string, segments: string[]): Promise<{ type: RefType, name: string, revision: string, fullPath: string }> { - - let branchOrTagObject: { type: RefType, name: string, revision: string } | undefined = undefined; - - // `segments` could have branch/tag name parts as well as file path parts. - // We never know which segments belong to the branch/tag name and which are already folder names. - // Here we generate a list of candidates for branch/tag names. - const branchOrTagCandidates: string[] = []; - // Try the concatination of all segments first. - branchOrTagCandidates.push(segments.join("/")); - // Then all subsets. - for (let i = 1; i < segments.length; i++) { - branchOrTagCandidates.push(segments.slice(0, i).join("/")); - } - - for (const candidate of branchOrTagCandidates) { - - // Check if there is a BRANCH with name `candidate`: - const possibleBranch = await this.giteaApi.run(user, async g => { - return g.repos.repoGetBranch(owner, repoName, candidate); - }); - // If the branch does not exist, the Gitea API returns with NotFound or InternalServerError. - const isNotFoundBranch = Gitea.ApiError.is(possibleBranch) && (Gitea.ApiError.isNotFound(possibleBranch) || Gitea.ApiError.isInternalServerError(possibleBranch)); - if (!isNotFoundBranch) { +const path = require('path'); // TODO(anbraten): is this really needed / correct? + +@injectable() +export class GiteaContextParser extends AbstractContextParser implements IContextParser { + + @inject(GiteaRestApi) protected readonly giteaApi: GiteaRestApi; + @inject(GiteaTokenHelper) protected readonly tokenHelper: GiteaTokenHelper; + + protected get authProviderId() { + return this.config.id; + } + + public async handle(ctx: TraceContext, user: User, contextUrl: string): Promise { + const span = TraceContext.startSpan("GiteaContextParser.handle", ctx); + + try { + const { host, owner, repoName, moreSegments } = await this.parseURL(user, contextUrl); + if (moreSegments.length > 0) { + switch (moreSegments[0]) { + case 'pulls': { // https://host/owner/repo/pulls/123 + const prNr = parseInt(moreSegments[1], 10); + if (isNaN(prNr)) + break; + return await this.handlePullRequestContext({ span }, user, host, owner, repoName, prNr); + } + case 'src': // https://host/owner/repo/src/branch/main/path/to/folder/or/file.yml + case 'commits': { // https://host/owner/repo/commits/branch/main + return await this.handleTreeContext({ span }, user, host, owner, repoName, moreSegments.slice(1)); + } + case 'releases': { // https://host/owner/repo/releases/tag/1.0.0 + if (moreSegments.length > 1 && moreSegments[1] === "tag") { + return await this.handleTreeContext({ span }, user, host, owner, repoName, moreSegments.slice(2)); + } + break; + } + case 'issues': { // https://host/owner/repo/issues/123 + const issueNr = parseInt(moreSegments[1], 10); + if (isNaN(issueNr)) + break; + return await this.handleIssueContext({ span }, user, host, owner, repoName, issueNr); + } + case 'commit': { // https://host/owner/repo/commit/cfbaea9ee7d24d95e30e0bf2d4f75e83481815bc + return await this.handleCommitContext({ span }, user, host, owner, repoName, moreSegments[1]); + } + } + } + return await this.handleDefaultContext({ span }, user, host, owner, repoName); + } catch (error) { + if (error && error.code === 401) { + const token = await this.tokenHelper.getCurrentToken(user); + if (token) { + const scopes = token.scopes; + // most likely the token needs to be updated after revoking by user. + throw UnauthorizedError.create(this.config.host, scopes, "http-unauthorized"); + } + // todo@alex: this is very unlikely. is coercing it into a valid case helpful? + // here, GH API responded with a 401 code, and we are missing a token. OTOH, a missing token would not lead to a request. + throw UnauthorizedError.create(this.config.host, GiteaScope.Requirements.PUBLIC_REPO, "missing-identity"); + } + throw error; + } finally { + span.finish(); + } + } + + protected async handleDefaultContext(ctx: TraceContext, user: User, host: string, owner: string, repoName: string): Promise { + try { + const repository = await this.fetchRepo(user, owner, repoName); + if (!repository.defaultBranch) { + return { + isFile: false, + path: '', + title: `${owner}/${repoName}`, + repository + } + } + + try { + const branchOrTag = await this.getBranchOrTag(user, owner, repoName, [repository.defaultBranch!]); + return { + isFile: false, + path: '', + title: `${owner}/${repoName} - ${branchOrTag.name}`, + ref: branchOrTag.name, + revision: branchOrTag.revision, + refType: branchOrTag.type, + repository + }; + } catch (error) { + if (error && error.message && (error.message as string).startsWith("Cannot find tag/branch for context")) { + // the repo is empty (has no branches) + return { + isFile: false, + path: '', + title: `${owner}/${repoName} - ${repository.defaultBranch}`, + revision: '', + repository + } + } else { + throw error; + } + } + } catch (error) { + if (UnauthorizedError.is(error)) { + throw error; + } + // log.error({ userId: user.id }, error); + throw await NotFoundError.create(await this.tokenHelper.getCurrentToken(user), user, host, owner, repoName); + } + } + + protected async handleTreeContext(ctx: TraceContext, user: User, host: string, owner: string, repoName: string, segments: string[]): Promise { + + try { + const branchOrTagPromise = segments.length > 0 ? this.getBranchOrTag(user, owner, repoName, segments) : undefined; + const repository = await this.fetchRepo(user, owner, repoName); + const branchOrTag = await branchOrTagPromise; + const context = { + isFile: false, + path: '', + title: `${owner}/${repoName}` + (branchOrTag ? ` - ${branchOrTag.name}` : ''), + ref: branchOrTag && branchOrTag.name, + revision: branchOrTag && branchOrTag.revision, + refType: branchOrTag && branchOrTag.type, + repository + }; + if (!branchOrTag) { + return context; + } + if (segments.length === 1 || branchOrTag.fullPath.length === 0) { + return context; + } + + const result = await this.giteaApi.run(user, async g => { + return g.repos.repoGetContents(owner, repoName, path.dirname(branchOrTag.fullPath), { ref: branchOrTag.name }); + }); + if (Gitea.ApiError.is(result)) { + throw new Error(`Error reading TREE ${owner}/${repoName}/tree/${segments.join('/')}: ${result}`); + } else { + const object = result.find(o => o.path === branchOrTag.fullPath); + if (object) { + const isFile = object.type === "blob"; + context.isFile = isFile; + context.path = branchOrTag.fullPath; + } + } + return context; + } catch (e) { + log.debug("Gitea context parser: Error handle tree context.", e); + throw e; + } + } + + protected async getBranchOrTag(user: User, owner: string, repoName: string, segments: string[]): Promise<{ type: RefType, name: string, revision: string, fullPath: string }> { + + let branchOrTagObject: { type: RefType, name: string, revision: string } | undefined = undefined; + + // `segments` could have branch/tag name parts as well as file path parts. + // We never know which segments belong to the branch/tag name and which are already folder names. + // Here we generate a list of candidates for branch/tag names. + const branchOrTagCandidates: string[] = []; + // Try the concatination of all segments first. + branchOrTagCandidates.push(segments.join("/")); + // Then all subsets. + for (let i = 1; i < segments.length; i++) { + branchOrTagCandidates.push(segments.slice(0, i).join("/")); + } + + for (const candidate of branchOrTagCandidates) { + + // Check if there is a BRANCH with name `candidate`: + const possibleBranch = await this.giteaApi.run(user, async g => { + return g.repos.repoGetBranch(owner, repoName, candidate); + }); + // If the branch does not exist, the Gitea API returns with NotFound or InternalServerError. + const isNotFoundBranch = Gitea.ApiError.is(possibleBranch) && (Gitea.ApiError.isNotFound(possibleBranch) || Gitea.ApiError.isInternalServerError(possibleBranch)); + if (!isNotFoundBranch) { if (Gitea.ApiError.is(possibleBranch)) { throw new Error(`Gitea ApiError on searching for possible branches for ${owner}/${repoName}/tree/${segments.join('/')}: ${possibleBranch}`); } @@ -191,16 +203,16 @@ import { convertRepo } from './convert'; branchOrTagObject = { type: 'branch', name: possibleBranch.name, revision: possibleBranch.commit.id }; break; - } - - // Check if there is a TAG with name `candidate`: - const possibleTag = await this.giteaApi.run(user, async g => { - return g.repos.repoGetTag(owner, repoName, candidate); - }); - // TODO - // If the tag does not exist, the GitLab API returns with NotFound or InternalServerError. - const isNotFoundTag = Gitea.ApiError.is(possibleTag) && (Gitea.ApiError.isNotFound(possibleTag) || Gitea.ApiError.isInternalServerError(possibleTag)); - if (!isNotFoundTag) { + } + + // Check if there is a TAG with name `candidate`: + const possibleTag = await this.giteaApi.run(user, async g => { + return g.repos.repoGetTag(owner, repoName, candidate); + }); + // TODO + // If the tag does not exist, the GitLab API returns with NotFound or InternalServerError. + const isNotFoundTag = Gitea.ApiError.is(possibleTag) && (Gitea.ApiError.isNotFound(possibleTag) || Gitea.ApiError.isInternalServerError(possibleTag)); + if (!isNotFoundTag) { if (Gitea.ApiError.is(possibleTag)) { throw new Error(`Gitea ApiError on searching for possible tags for ${owner}/${repoName}/tree/${segments.join('/')}: ${possibleTag}`); } @@ -211,39 +223,39 @@ import { convertRepo } from './convert'; branchOrTagObject = { type: 'tag', name: possibleTag.name, revision: possibleTag.commit.sha }; break; - } - } - - // There seems to be no matching branch or tag. - if (branchOrTagObject === undefined) { - log.debug(`Cannot find tag/branch for context: ${owner}/${repoName}/tree/${segments.join('/')}.`, - { branchOrTagCandidates } - ); - throw new Error(`Cannot find tag/branch for context: ${owner}/${repoName}/tree/${segments.join('/')}.`); - } - - const remainingSegmentsIndex = branchOrTagObject.name.split('/').length; - const fullPath = decodeURIComponent(segments.slice(remainingSegmentsIndex).filter(s => s.length > 0).join('/')); - - return { ...branchOrTagObject, fullPath }; - } - - protected async handlePullRequestContext(user: User, host: string, owner: string, repoName: string, nr: number): Promise { - const result = await this.giteaApi.run(user, async g => { - return g.repos.repoGetPullRequest(owner, repoName, nr); - }); - if (Gitea.ApiError.is(result)) { - throw await NotFoundError.create(await this.tokenHelper.getCurrentToken(user), user, host, owner, repoName); - } - - if (!result.base?.repo || !result.head?.repo || !result.title) { + } + } + + // There seems to be no matching branch or tag. + if (branchOrTagObject === undefined) { + log.debug(`Cannot find tag/branch for context: ${owner}/${repoName}/tree/${segments.join('/')}.`, + { branchOrTagCandidates } + ); + throw new Error(`Cannot find tag/branch for context: ${owner}/${repoName}/tree/${segments.join('/')}.`); + } + + const remainingSegmentsIndex = branchOrTagObject.name.split('/').length; + const fullPath = decodeURIComponent(segments.slice(remainingSegmentsIndex).filter(s => s.length > 0).join('/')); + + return { ...branchOrTagObject, fullPath }; + } + + protected async handlePullRequestContext(ctx: TraceContext, user: User, host: string, owner: string, repoName: string, nr: number): Promise { + const result = await this.giteaApi.run(user, async g => { + return g.repos.repoGetPullRequest(owner, repoName, nr); + }); + if (Gitea.ApiError.is(result)) { + throw await NotFoundError.create(await this.tokenHelper.getCurrentToken(user), user, host, owner, repoName); + } + + if (!result.base?.repo || !result.head?.repo || !result.title) { throw new Error(`Missing relevant commit information for pull-request ${nr} from repository ${owner}/${repoName}`); } - const sourceRepo = convertRepo(result.head?.repo); - const targetRepo = convertRepo(result.base?.repo); + const sourceRepo = convertRepo(result.head?.repo); + const targetRepo = convertRepo(result.base?.repo); - const U = { + return { title: result.title, repository: sourceRepo, ref: result.head.ref, @@ -255,31 +267,29 @@ import { convertRepo } from './convert'; ref: result.base.ref, refType: 'branch', } - } + }; + } + + protected async fetchRepo(user: User, owner: string, repoName: string): Promise { + // host might be a relative URL + const host = this.host; // as per contract, cf. `canHandle(user, contextURL)` - return U; - } - - protected async fetchRepo(user: User, owner: string, repoName: string): Promise { - // host might be a relative URL - const host = this.host; // as per contract, cf. `canHandle(user, contextURL)` - - const result = await this.giteaApi.run(user, async g => { - return g.repos.repoGet(owner, repoName); - }); - if (Gitea.ApiError.is(result)) { - throw result; - } - const repo = { - host, - name: repoName, - owner: owner, - cloneUrl: result.clone_url, - defaultBranch: result.default_branch, - private: result.private - } - // TODO: support forks - // if (result.fork) { + const result = await this.giteaApi.run(user, async g => { + return g.repos.repoGet(owner, repoName); + }); + if (Gitea.ApiError.is(result)) { + throw result; + } + const repo = { + host, + name: repoName, + owner: owner, + cloneUrl: result.clone_url, + defaultBranch: result.default_branch, + private: result.private + } + // TODO: support forks + // if (result.fork) { // // host might be a relative URL, let's compute the prefix // const url = new URL(forked_from_project.http_url_to_repo.split(forked_from_project.namespace.full_path)[0]); // const relativePath = url.pathname.slice(1); // hint: pathname always starts with `/` @@ -295,92 +305,111 @@ import { convertRepo } from './convert'; // } // } // } - return repo; - } - - protected async fetchCommit(user: User, owner: string, repoName: string, sha: string) { - const result = await this.giteaApi.run(user, async g => { - return g.repos.repoGetSingleCommit(owner, repoName, sha); - }); - if (Gitea.ApiError.is(result)) { - if (result.message === 'Gitea responded with code 404') { - throw new Error(`Couldn't find commit #${sha} in repository ${owner}/${repoName}.`); - } - throw result; - } - - if (!result.sha || !result.commit?.message) { - throw new Error(`The commit does not have all needed data ${owner}/${repoName}.`); - } - - return { - id: result.sha, // TODO: how can we use a proper commit-id instead of the sha - title: result.commit?.message - } - } - - protected async handleIssueContext(user: User, host: string, owner: string, repoName: string, nr: number): Promise { - const ctxPromise = this.handleDefaultContext(user, host, owner, repoName); - const result = await this.giteaApi.run(user, async g => { - return g.repos.issueGetIssue(owner, repoName, nr); - }); - if (Gitea.ApiError.is(result) || !result.title || !result.id) { - throw await NotFoundError.create(await this.tokenHelper.getCurrentToken(user), user, host, owner, repoName); - } - const context = await ctxPromise; - return { - ... context, - title: result.title, - owner, - nr, - localBranch: IssueContexts.toBranchName(user, result.title, result.id) - }; - } - - protected async handleCommitContext(user: User, host: string, owner: string, repoName: string, sha: string): Promise { - const repository = await this.fetchRepo(user, owner, repoName); - if (Gitea.ApiError.is(repository)) { - throw await NotFoundError.create(await this.tokenHelper.getCurrentToken(user), user, host, owner, repoName); - } - const commit = await this.fetchCommit(user, owner, repoName, sha); - if (Gitea.ApiError.is(commit)) { - throw new Error(`Couldn't find commit #${sha} in repository ${owner}/${repoName}.`); - } - return { - path: '', - ref: '', - refType: 'revision', - isFile: false, - title: `${owner}/${repoName} - ${commit.title}`, - owner, - revision: sha, - repository, - }; - } - - public async fetchCommitHistory(ctx: TraceContext, user: User, contextUrl: string, sha: string, maxDepth: number): Promise { - // TODO(janx): To get more results than Gitea API's max per_page (seems to be 100), pagination should be handled. - const { owner, repoName } = await this.parseURL(user, contextUrl); - const result = await this.giteaApi.run(user, async g => { - return g.repos.repoGetAllCommits(owner, repoName, { - sha, - limit: maxDepth, - page: 1, - }); - }); - if (Gitea.ApiError.is(result)) { - if (result.message === 'Gitea responded with code 404') { - throw new Error(`Couldn't find commit #${sha} in repository ${owner}/${repoName}.`); - } - throw result; - } - return result.slice(1).map((c) => { - // TODO: how can we use a proper commit-id instead of the sha - if (!c.sha) { - throw new Error(`Commit #${sha} does not have commit.`); + return repo; + } + + protected async fetchCommit(user: User, owner: string, repoName: string, sha: string) { + const result = await this.giteaApi.run(user, async g => { + return g.repos.repoGetSingleCommit(owner, repoName, sha); + }); + if (Gitea.ApiError.is(result)) { + if (result.message === 'Gitea responded with code 404') { + throw new Error(`Couldn't find commit #${sha} in repository ${owner}/${repoName}.`); } + throw result; + } + + if (!result.sha || !result.commit?.message) { + throw new Error(`The commit does not have all needed data ${owner}/${repoName}.`); + } - return c.sha; + return { + id: result.sha, // TODO: how can we use a proper commit-id instead of the sha + title: result.commit?.message + } + } + + protected async handleIssueContext(ctx: TraceContext, user: User, host: string, owner: string, repoName: string, nr: number): Promise { + const ctxPromise = this.handleDefaultContext(ctx, user, host, owner, repoName); + const result = await this.giteaApi.run(user, async g => { + return g.repos.issueGetIssue(owner, repoName, nr); }); - } - } \ No newline at end of file + if (Gitea.ApiError.is(result) || !result.title || !result.id) { + throw await NotFoundError.create(await this.tokenHelper.getCurrentToken(user), user, host, owner, repoName); + } + const context = await ctxPromise; + return { + ...context, + title: result.title, + owner, + nr, + localBranch: IssueContexts.toBranchName(user, result.title, result.id) + }; + } + + protected async handleCommitContext(ctx: TraceContext, user: User, host: string, owner: string, repoName: string, sha: string): Promise { + const repository = await this.fetchRepo(user, owner, repoName); + if (Gitea.ApiError.is(repository)) { + throw await NotFoundError.create(await this.tokenHelper.getCurrentToken(user), user, host, owner, repoName); + } + const commit = await this.fetchCommit(user, owner, repoName, sha); + if (Gitea.ApiError.is(commit)) { + throw new Error(`Couldn't find commit #${sha} in repository ${owner}/${repoName}.`); + } + return { + path: '', + ref: '', + refType: 'revision', + isFile: false, + title: `${owner}/${repoName} - ${commit.title}`, + owner, + revision: sha, + repository, + }; + } + + public async fetchCommitHistory(ctx: TraceContext, user: User, contextUrl: string, sha: string, maxDepth: number): Promise { + const span = TraceContext.startSpan("GiteaContextParser.fetchCommitHistory", ctx); + + try { + if (sha.length != 40) { + throw new Error(`Invalid commit ID ${sha}.`); + } + + // TODO(janx): To get more results than Gitea API's max page size (seems to be 100), pagination should be handled. + // These additional history properties may be helfpul: + // totalCount, + // pageInfo { + // haxNextPage, + // }, + const { owner, repoName } = await this.parseURL(user, contextUrl); + const result = await this.giteaApi.run(user, async g => { + return g.repos.repoGetAllCommits(owner, repoName, { + sha, + limit: maxDepth, + page: 1, + }); + }); + if (Gitea.ApiError.is(result)) { + if (result.message === 'Gitea responded with code 404') { + throw new Error(`Couldn't find commit #${sha} in repository ${owner}/${repoName}.`); + } + throw result; + } + + return result.slice(1).map((c) => { + // TODO: how can we use a proper commit-id instead of the sha + if (!c.sha) { + throw new Error(`Commit #${sha} does not have commit.`); + } + + return c.sha; + }); + } catch (e) { + span.log({ "error": e }); + throw e; + } finally { + span.finish(); + } + } +} diff --git a/components/server/src/gitea/gitea-repository-provider.ts b/components/server/src/gitea/gitea-repository-provider.ts index 23dbf19b4000ca..764a2c7d474abe 100644 --- a/components/server/src/gitea/gitea-repository-provider.ts +++ b/components/server/src/gitea/gitea-repository-provider.ts @@ -100,6 +100,27 @@ export class GiteaRepositoryProvider implements RepositoryProvider { }; } + public async getCommitHistory(user: User, owner: string, repo: string, ref: string, maxDepth: number = 100): Promise { + // TODO(janx): To get more results than GitLab API's max per_page (seems to be 100), pagination should be handled. + const result = await this.giteaApi.run(user, async g => { + return g.repos.repoGetAllCommits(owner, repo, { + // TODO(anbraten): pass ref to call + // ref_name: ref, + // per_page: maxDepth, + page: 1, + }); + }); + if (Gitea.ApiError.is(result)) { + if (result.message === 'GitLab responded with code 404') { + throw new Error(`Couldn't find commit #${ref} in repository ${owner}/${repo}.`); + } + throw result; + } + + // TODO(anbraten): use some unique id instead of sha + return result.slice(1).map((c) => c.sha || ''); + } + async getUserRepos(user: User): Promise { const result = await this.giteaApi.run(user, (g => g.user.userCurrentListRepos())); if (Gitea.ApiError.is(result)) { From ad2d2b212b8c1f77416f20ff252205334d07df7f Mon Sep 17 00:00:00 2001 From: Geoffrey Huntley Date: Thu, 17 Mar 2022 12:13:29 +0000 Subject: [PATCH 12/16] format code --- components/server/src/gitea/convert.ts | 46 +++++++++++++++----------- 1 file changed, 26 insertions(+), 20 deletions(-) diff --git a/components/server/src/gitea/convert.ts b/components/server/src/gitea/convert.ts index a76e46074f31c0..8d75823bdb034d 100644 --- a/components/server/src/gitea/convert.ts +++ b/components/server/src/gitea/convert.ts @@ -1,28 +1,34 @@ -import { Repository } from '@gitpod/gitpod-protocol'; -import { RepoURL } from '../repohost'; +/** + * 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 } from "@gitpod/gitpod-protocol"; +import { RepoURL } from "../repohost"; import { Gitea } from "./api"; -export function convertRepo(repo: Gitea.Repository): Repository | undefined { - if (!repo.clone_url || !repo.name || !repo.owner?.login) { - return undefined; - } +export function convertRepo(repo: Gitea.Repository): Repository | undefined { + if (!repo.clone_url || !repo.name || !repo.owner?.login) { + return undefined; + } - const host = RepoURL.parseRepoUrl(repo.clone_url)!.host; + const host = RepoURL.parseRepoUrl(repo.clone_url)!.host; - return { - host, - owner: repo.owner.login, - name: repo.name, - cloneUrl: repo.clone_url, - description: repo.description, - avatarUrl: repo.avatar_url, - webUrl: repo.html_url, // TODO: is this correct? - defaultBranch: repo.default_branch, - private: repo.private, - fork: undefined // TODO: load fork somehow - } + return { + host, + owner: repo.owner.login, + name: repo.name, + cloneUrl: repo.clone_url, + description: repo.description, + avatarUrl: repo.avatar_url, + webUrl: repo.html_url, // TODO: is this correct? + defaultBranch: repo.default_branch, + private: repo.private, + fork: undefined, // TODO: load fork somehow + }; } // export function convertBranch(repo: Gitea.Repository): Branch | undefined { -// } \ No newline at end of file +// } From 9018b8457d1d21154cfe74f67f18df5da746d245 Mon Sep 17 00:00:00 2001 From: Anbraten Date: Fri, 20 May 2022 07:04:41 +0000 Subject: [PATCH 13/16] fix lockfile --- yarn.lock | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index 3292893c01633c..cdf136ac67239f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12155,11 +12155,7 @@ node-emoji@^1.11.0: dependencies: lodash "^4.17.21" -<<<<<<< HEAD -node-fetch@2.6.7, 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: ->>>>>>> upstream/main +node-fetch@2.6.7, node-fetch@^2.6.0, node-fetch@^2.6.1, node-fetch@^2.6.5, node-fetch@^2.6.7: version "2.6.7" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== From de295669343edf6c74e5b52f683246598bc41b1e Mon Sep 17 00:00:00 2001 From: Anbraten Date: Sat, 4 Jun 2022 06:53:10 +0000 Subject: [PATCH 14/16] fix some context parser tests --- components/server/src/dev/dev-data.ts | 2 +- components/server/src/gitea/api.ts | 45 +- .../src/gitea/gitea-context-parser.spec.ts | 809 +++++++++--------- .../server/src/gitea/gitea-context-parser.ts | 289 +++++-- .../server/src/gitea/gitea-token-validator.ts | 67 +- 5 files changed, 673 insertions(+), 539 deletions(-) diff --git a/components/server/src/dev/dev-data.ts b/components/server/src/dev/dev-data.ts index c63a67c5932bf0..21d5c582192ffe 100644 --- a/components/server/src/dev/dev-data.ts +++ b/components/server/src/dev/dev-data.ts @@ -77,7 +77,7 @@ export namespace DevData { export function createGiteaTestToken(): Token { return { ...getTokenFromEnv("GITPOD_TEST_TOKEN_GITEA"), - scopes: [] + scopes: [], }; } diff --git a/components/server/src/gitea/api.ts b/components/server/src/gitea/api.ts index 6e82087011a8cc..723e7323a4a745 100644 --- a/components/server/src/gitea/api.ts +++ b/components/server/src/gitea/api.ts @@ -4,28 +4,39 @@ * See License-AGPL.txt in the project root for license information. */ -import { giteaApi, Api, Commit as ICommit, Repository as IRepository, ContentsResponse as IContentsResponse, Branch as IBranch, Tag as ITag, PullRequest as IPullRequest, Issue as IIssue, User as IUser } from "gitea-js" -import fetch from 'cross-fetch' +import { + giteaApi, + Api, + Commit as ICommit, + Repository as IRepository, + ContentsResponse as IContentsResponse, + Branch as IBranch, + Tag as ITag, + PullRequest as IPullRequest, + Issue as IIssue, + User as IUser, +} from "gitea-js"; +import fetch from "cross-fetch"; -import { User } from "@gitpod/gitpod-protocol" -import { injectable, inject } from 'inversify'; -import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; -import { GiteaScope } from './scopes'; -import { AuthProviderParams } from '../auth/auth-provider'; -import { GiteaTokenHelper } from './gitea-token-helper'; +import { User } from "@gitpod/gitpod-protocol"; +import { injectable, inject } from "inversify"; +import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; +import { GiteaScope } from "./scopes"; +import { AuthProviderParams } from "../auth/auth-provider"; +import { GiteaTokenHelper } from "./gitea-token-helper"; export namespace Gitea { export class ApiError extends Error { - readonly httpError: { name: string, description: string } | undefined; + readonly httpError: { name: string; description: string } | undefined; constructor(msg?: string, httpError?: any) { super(msg); this.httpError = httpError; - this.name = 'GiteaApiError'; + this.name = "GiteaApiError"; } } export namespace ApiError { export function is(something: any): something is ApiError { - return !!something && something.name === 'GiteaApiError'; + return !!something && something.name === "GiteaApiError"; } export function isNotFound(error: ApiError): boolean { return !!error.httpError?.description.startsWith("404"); @@ -54,12 +65,11 @@ export namespace Gitea { @injectable() export class GiteaRestApi { - @inject(AuthProviderParams) readonly config: AuthProviderParams; @inject(GiteaTokenHelper) protected readonly tokenHelper: GiteaTokenHelper; protected async create(userOrToken: User | string) { let oauthToken: string | undefined; - if (typeof userOrToken === 'string') { + if (typeof userOrToken === "string") { oauthToken = userOrToken; } else { const giteaToken = await this.tokenHelper.getTokenWithScopes(userOrToken, GiteaScope.Requirements.DEFAULT); @@ -69,11 +79,14 @@ export class GiteaRestApi { return api; } - public async run(userOrToken: User | string, operation: (g: Api) => Promise): Promise { + public async run( + userOrToken: User | string, + operation: (g: Api) => Promise, + ): Promise { const before = new Date().getTime(); const userApi = await this.create(userOrToken); try { - const response = (await operation(userApi) as R); + const response = (await operation(userApi)) as R; return response as R; } catch (error) { if (error && typeof error?.response?.status === "number" && error?.response?.status !== 200) { @@ -95,4 +108,4 @@ export class GiteaRestApi { log.info(`Gitea request took ${new Date().getTime() - before} ms`); } } -} \ No newline at end of file +} diff --git a/components/server/src/gitea/gitea-context-parser.spec.ts b/components/server/src/gitea/gitea-context-parser.spec.ts index eb8745d3a72d25..0c3aa14d473d65 100644 --- a/components/server/src/gitea/gitea-context-parser.spec.ts +++ b/components/server/src/gitea/gitea-context-parser.spec.ts @@ -5,18 +5,18 @@ */ // Use asyncIterators with es2015 -if (typeof (Symbol as any).asyncIterator === 'undefined') { - (Symbol as any).asyncIterator = Symbol.asyncIterator || Symbol('asyncIterator'); +if (typeof (Symbol as any).asyncIterator === "undefined") { + (Symbol as any).asyncIterator = Symbol.asyncIterator || Symbol("asyncIterator"); } import "reflect-metadata"; import { suite, test, timeout, retries } from "mocha-typescript"; -import * as chai from 'chai'; +import * as chai from "chai"; const expect = chai.expect; -import { GiteaRestApi } from './api'; -import { NotFoundError } from '../errors'; -import { GiteaContextParser } from './gitea-context-parser'; +import { GiteaRestApi } from "./api"; +import { NotFoundError } from "../errors"; +import { GiteaContextParser } from "./gitea-context-parser"; import { User } from "@gitpod/gitpod-protocol"; import { ContainerModule, Container } from "inversify"; import { Config } from "../config"; @@ -27,29 +27,30 @@ import { GiteaTokenHelper } from "./gitea-token-helper"; import { HostContextProvider } from "../auth/host-context-provider"; import { skipIfEnvVarNotSet } from "@gitpod/gitpod-protocol/lib/util/skip-if"; -@suite(timeout(10000), retries(2), skipIfEnvVarNotSet("GITPOD_TEST_TOKEN_GITEA")) +@suite(timeout(10000), retries(1), skipIfEnvVarNotSet("GITPOD_TEST_TOKEN_GITEA")) class TestGiteaContextParser { - protected parser: GiteaContextParser; protected user: User; public before() { const container = new Container(); - container.load(new ContainerModule((bind, unbind, isBound, rebind) => { - bind(Config).toConstantValue({ - // meant to appease DI, but Config is never actually used here - }); - bind(GiteaContextParser).toSelf().inSingletonScope(); - bind(GiteaRestApi).toSelf().inSingletonScope(); - bind(AuthProviderParams).toConstantValue(TestGiteaContextParser.AUTH_HOST_CONFIG); - bind(GiteaTokenHelper).toSelf().inSingletonScope(); - bind(TokenProvider).toConstantValue({ - getTokenForHost: async (user: User, host: string) => { - return DevData.createGiteaTestToken(); - } - }); - bind(HostContextProvider).toConstantValue(DevData.createDummyHostContextProvider()); - })); + container.load( + new ContainerModule((bind, unbind, isBound, rebind) => { + bind(Config).toConstantValue({ + // meant to appease DI, but Config is never actually used here + }); + bind(GiteaContextParser).toSelf().inSingletonScope(); + bind(GiteaRestApi).toSelf().inSingletonScope(); + bind(AuthProviderParams).toConstantValue(TestGiteaContextParser.AUTH_HOST_CONFIG); + bind(GiteaTokenHelper).toSelf().inSingletonScope(); + bind(TokenProvider).toConstantValue({ + getTokenForHost: async (user: User, host: string) => { + return DevData.createGiteaTestToken(); + }, + }); + bind(HostContextProvider).toConstantValue(DevData.createDummyHostContextProvider()); + }), + ); this.parser = container.get(GiteaContextParser); this.user = DevData.createTestUser(); } @@ -61,36 +62,36 @@ class TestGiteaContextParser { description: "", icon: "", host: "gitea.com", - oauth: "not-used" as any - } + oauth: "not-used" as any, + }; static readonly BRANCH_TEST = { name: "test", commit: { sha: "testsha", - url: "testurl" + url: "testurl", }, protected: false, - protection_url: "" + protection_url: "", }; static readonly BRANCH_ISSUE_974 = { name: "ak/lmcbout-issue_974", commit: { sha: "sha974", - url: "url974" + url: "url974", }, protected: false, - protection_url: "" + protection_url: "", }; static readonly BLO_BLA_ERROR_DATA = { host: "gitea.com", lastUpdate: undefined, - owner: 'blo', - repoName: 'bla', + owner: "blo", + repoName: "bla", userIsOwner: false, - userScopes: ["user:email", "public_repo", "repo"], + userScopes: [], }; protected get bloBlaErrorData() { @@ -99,7 +100,7 @@ class TestGiteaContextParser { @test public async testErrorContext_01() { try { - await this.parser.handle({}, this.user, 'https://gitea.com/blo/bla'); + await this.parser.handle({}, this.user, "https://gitea.com/blo/bla"); } catch (e) { expect(NotFoundError.is(e)); expect(e.data).to.deep.equal(this.bloBlaErrorData); @@ -108,7 +109,7 @@ class TestGiteaContextParser { @test public async testErrorContext_02() { try { - await this.parser.handle({}, this.user, 'https://gitea.com/blo/bla/pull/42'); + await this.parser.handle({}, this.user, "https://gitea.com/blo/bla/pull/42"); } catch (e) { expect(NotFoundError.is(e)); expect(e.data).to.deep.equal(this.bloBlaErrorData); @@ -117,7 +118,7 @@ class TestGiteaContextParser { @test public async testErrorContext_03() { try { - await this.parser.handle({}, this.user, 'https://gitea.com/blo/bla/issues/42'); + await this.parser.handle({}, this.user, "https://gitea.com/blo/bla/issues/42"); } catch (e) { expect(NotFoundError.is(e)); expect(e.data).to.deep.equal(this.bloBlaErrorData); @@ -126,7 +127,7 @@ class TestGiteaContextParser { @test public async testErrorContext_04() { try { - await this.parser.handle({}, this.user, 'https://gitea.com/blo/bla/tree/my/branch/path/foo.ts'); + await this.parser.handle({}, this.user, "https://gitea.com/blo/bla/tree/my/branch/path/foo.ts"); } catch (e) { expect(NotFoundError.is(e)); expect(e.data).to.deep.equal(this.bloBlaErrorData); @@ -134,214 +135,238 @@ class TestGiteaContextParser { } @test public async testTreeContext_01() { - const result = await this.parser.handle({}, this.user, 'https://gitea.com/eclipse-theia/theia'); + const result = await this.parser.handle({}, this.user, "https://gitea.com/eclipse-theia/theia"); expect(result).to.deep.include({ - "ref": "master", - "refType": "branch", - "path": "", - "isFile": false, - "repository": { - "host": "gitea.com", - "owner": "eclipse-theia", - "name": "theia", - "cloneUrl": "https://gitea.com/eclipse-theia/theia.git", - "private": false + ref: "master", + refType: "branch", + path: "", + isFile: false, + repository: { + host: "gitea.com", + owner: "eclipse-theia", + name: "theia", + cloneUrl: "https://gitea.com/eclipse-theia/theia.git", + private: false, }, - "title": "eclipse-theia/theia - master" - }) + title: "eclipse-theia/theia - master", + }); } @test public async testTreeContext_02() { - const result = await this.parser.handle({}, this.user, 'https://gitea.com/eclipse-theia/theia/tree/master'); + const result = await this.parser.handle({}, this.user, "https://gitea.com/eclipse-theia/theia/tree/master"); expect(result).to.deep.include({ - "ref": "master", - "refType": "branch", - "path": "", - "isFile": false, - "repository": { - "host": "gitea.com", - "owner": "eclipse-theia", - "name": "theia", - "cloneUrl": "https://gitea.com/eclipse-theia/theia.git", - "private": false + ref: "master", + refType: "branch", + path: "", + isFile: false, + repository: { + host: "gitea.com", + owner: "eclipse-theia", + name: "theia", + cloneUrl: "https://gitea.com/eclipse-theia/theia.git", + private: false, }, - "title": "eclipse-theia/theia - master" - }) + title: "eclipse-theia/theia - master", + }); } @test public async testTreeContext_03() { - const result = await this.parser.handle({}, this.user, 'https://gitea.com/eclipse-theia/theia/tree/master/LICENSE'); + const result = await this.parser.handle( + {}, + this.user, + "https://gitea.com/eclipse-theia/theia/tree/master/LICENSE", + ); expect(result).to.deep.include({ - "ref": "master", - "refType": "branch", - "path": "LICENSE", - "isFile": true, - "repository": { - "host": "gitea.com", - "owner": "eclipse-theia", - "name": "theia", - "cloneUrl": "https://gitea.com/eclipse-theia/theia.git", - "private": false + ref: "master", + refType: "branch", + path: "LICENSE", + isFile: true, + repository: { + host: "gitea.com", + owner: "eclipse-theia", + name: "theia", + cloneUrl: "https://gitea.com/eclipse-theia/theia.git", + private: false, }, - "title": "eclipse-theia/theia - master" - }) + title: "eclipse-theia/theia - master", + }); } @test public async testTreeContext_04() { - const result = await this.parser.handle({}, this.user, 'https://gitea.com/gitpod-io/gitpod-test-repo/blob/nametest/src/src/server.ts'); + const result = await this.parser.handle( + {}, + this.user, + "https://gitea.com/gitpod-io/gitpod-test-repo/blob/nametest/src/src/server.ts", + ); expect(result).to.deep.include({ - "ref": "nametest/src", - "refType": "branch", - "path": "src/server.ts", - "isFile": true, - "repository": { - "host": "gitea.com", - "owner": "gitpod-io", - "name": "gitpod-test-repo", - "cloneUrl": "https://gitea.com/gitpod-io/gitpod-test-repo.git", - "private": false + ref: "nametest/src", + refType: "branch", + path: "src/server.ts", + isFile: true, + repository: { + host: "gitea.com", + owner: "gitpod-io", + name: "gitpod-test-repo", + cloneUrl: "https://gitea.com/gitpod-io/gitpod-test-repo.git", + private: false, }, - "title": "gitpod-io/gitpod-test-repo - nametest/src" - }) + title: "gitpod-io/gitpod-test-repo - nametest/src", + }); } @test public async testTreeContext_05() { - const result = await this.parser.handle({}, this.user, 'https://gitea.com/gitpod-io/gitpod-test-repo/tree/499efbbcb50e7e6e5e2883053f72a34cd5396be3/folder1/folder2'); - expect(result).to.deep.include( - { - "title": "gitpod-io/gitpod-test-repo - 499efbbc:folder1/folder2", - "repository": { - "host": "gitea.com", - "owner": "gitpod-io", - "name": "gitpod-test-repo", - "cloneUrl": "https://gitea.com/gitpod-io/gitpod-test-repo.git", - "private": false - }, - "revision": "499efbbcb50e7e6e5e2883053f72a34cd5396be3", - "isFile": false, - "path": "folder1/folder2" - } - ) + const result = await this.parser.handle( + {}, + this.user, + "https://gitea.com/gitpod-io/gitpod-test-repo/tree/499efbbcb50e7e6e5e2883053f72a34cd5396be3/folder1/folder2", + ); + expect(result).to.deep.include({ + title: "gitpod-io/gitpod-test-repo - 499efbbc:folder1/folder2", + repository: { + host: "gitea.com", + owner: "gitpod-io", + name: "gitpod-test-repo", + cloneUrl: "https://gitea.com/gitpod-io/gitpod-test-repo.git", + private: false, + }, + revision: "499efbbcb50e7e6e5e2883053f72a34cd5396be3", + isFile: false, + path: "folder1/folder2", + }); } @test public async testTreeContext_06() { - const result = await this.parser.handle({}, this.user, 'https://gitea.com/Snailclimb/JavaGuide/blob/940982ebffa5f376b6baddeaf9ed41c91217a6b6/数据结构与算法/常见安全算法(MD5、SHA1、Base64等等)总结.md'); - expect(result).to.deep.include( - { - "title": "Snailclimb/JavaGuide - 940982eb:数据结构与算法/常见安全算法(MD5、SHA1、Base64等等)总结.md", - "repository": { - "host": "gitea.com", - "owner": "Snailclimb", - "name": "JavaGuide", - "cloneUrl": "https://gitea.com/Snailclimb/JavaGuide.git", - "private": false - }, - "revision": "940982ebffa5f376b6baddeaf9ed41c91217a6b6", - "isFile": true, - "path": "数据结构与算法/常见安全算法(MD5、SHA1、Base64等等)总结.md" - } - ) + const result = await this.parser.handle( + {}, + this.user, + "https://gitea.com/Snailclimb/JavaGuide/blob/940982ebffa5f376b6baddeaf9ed41c91217a6b6/数据结构与算法/常见安全算法(MD5、SHA1、Base64等等)总结.md", + ); + expect(result).to.deep.include({ + title: "Snailclimb/JavaGuide - 940982eb:数据结构与算法/常见安全算法(MD5、SHA1、Base64等等)总结.md", + repository: { + host: "gitea.com", + owner: "Snailclimb", + name: "JavaGuide", + cloneUrl: "https://gitea.com/Snailclimb/JavaGuide.git", + private: false, + }, + revision: "940982ebffa5f376b6baddeaf9ed41c91217a6b6", + isFile: true, + path: "数据结构与算法/常见安全算法(MD5、SHA1、Base64等等)总结.md", + }); } @test public async testTreeContext_07() { - const result = await this.parser.handle({}, this.user, 'https://gitea.com/eclipse-theia/theia#license'); + const result = await this.parser.handle({}, this.user, "https://gitea.com/eclipse-theia/theia#license"); expect(result).to.deep.include({ - "ref": "master", - "refType": "branch", - "path": "", - "isFile": false, - "repository": { - "host": "gitea.com", - "owner": "eclipse-theia", - "name": "theia", - "cloneUrl": "https://gitea.com/eclipse-theia/theia.git", - "private": false + ref: "master", + refType: "branch", + path: "", + isFile: false, + repository: { + host: "gitea.com", + owner: "eclipse-theia", + name: "theia", + cloneUrl: "https://gitea.com/eclipse-theia/theia.git", + private: false, }, - "title": "eclipse-theia/theia - master" - }) + title: "eclipse-theia/theia - master", + }); } @test public async testTreeContext_tag_01() { - const result = await this.parser.handle({}, this.user, 'https://gitea.com/eclipse-theia/theia/tree/v0.1.0'); - expect(result).to.deep.include( - { - "title": "eclipse-theia/theia - v0.1.0", - "repository": { - "host": "gitea.com", - "owner": "eclipse-theia", - "name": "theia", - "cloneUrl": "https://gitea.com/eclipse-theia/theia.git", - "private": false - }, - "revision": "f29626847a14ca50dd78483aebaf4b4fe26bcb73", - "isFile": false, - "ref": "v0.1.0", - "refType": "tag" - } - ) + const result = await this.parser.handle({}, this.user, "https://gitea.com/eclipse-theia/theia/tree/v0.1.0"); + expect(result).to.deep.include({ + title: "eclipse-theia/theia - v0.1.0", + repository: { + host: "gitea.com", + owner: "eclipse-theia", + name: "theia", + cloneUrl: "https://gitea.com/eclipse-theia/theia.git", + private: false, + }, + revision: "f29626847a14ca50dd78483aebaf4b4fe26bcb73", + isFile: false, + ref: "v0.1.0", + refType: "tag", + }); } @test public async testReleasesContext_tag_01() { - const result = await this.parser.handle({}, this.user, 'https://gitea.com/gitpod-io/gitpod/releases/tag/v0.9.0'); - expect(result).to.deep.include( - { - "ref": "v0.9.0", - "refType": "tag", - "isFile": false, - "path": "", - "title": "gitpod-io/gitpod - v0.9.0", - "revision": "25ece59c495d525614f28971d41d5708a31bf1e3", - "repository": { - "cloneUrl": "https://gitea.com/gitpod-io/gitpod.git", - "host": "gitea.com", - "name": "gitpod", - "owner": "gitpod-io", - "private": false - } - } - ) + const result = await this.parser.handle( + {}, + this.user, + "https://gitea.com/gitpod-io/gitpod/releases/tag/v0.9.0", + ); + expect(result).to.deep.include({ + ref: "v0.9.0", + refType: "tag", + isFile: false, + path: "", + title: "gitpod-io/gitpod - v0.9.0", + revision: "25ece59c495d525614f28971d41d5708a31bf1e3", + repository: { + cloneUrl: "https://gitea.com/gitpod-io/gitpod.git", + host: "gitea.com", + name: "gitpod", + owner: "gitpod-io", + private: false, + }, + }); } @test public async testCommitsContext_01() { - const result = await this.parser.handle({}, this.user, 'https://gitea.com/gitpod-io/gitpod-test-repo/commits/4test'); + const result = await this.parser.handle( + {}, + this.user, + "https://gitea.com/gitpod-io/gitpod-test-repo/commits/4test", + ); expect(result).to.deep.include({ - "ref": "4test", - "refType": "branch", - "path": "", - "isFile": false, - "repository": { - "host": "gitea.com", - "owner": "gitpod-io", - "name": "gitpod-test-repo", - "cloneUrl": "https://gitea.com/gitpod-io/gitpod-test-repo.git", - "private": false + ref: "4test", + refType: "branch", + path: "", + isFile: false, + repository: { + host: "gitea.com", + owner: "gitpod-io", + name: "gitpod-test-repo", + cloneUrl: "https://gitea.com/gitpod-io/gitpod-test-repo.git", + private: false, }, - "title": "gitpod-io/gitpod-test-repo - 4test" - }) + title: "gitpod-io/gitpod-test-repo - 4test", + }); } @test public async testCommitContext_01() { - const result = await this.parser.handle({}, this.user, 'https://gitea.com/gitpod-io/gitpod-test-repo/commit/409ac2de49a53d679989d438735f78204f441634'); + const result = await this.parser.handle( + {}, + this.user, + "https://gitea.com/gitpod-io/gitpod-test-repo/commit/409ac2de49a53d679989d438735f78204f441634", + ); expect(result).to.deep.include({ - "ref": "", - "refType": "revision", - "path": "", - "revision": "409ac2de49a53d679989d438735f78204f441634", - "isFile": false, - "repository": { - "host": "gitea.com", - "owner": "gitpod-io", - "name": "gitpod-test-repo", - "cloneUrl": "https://gitea.com/gitpod-io/gitpod-test-repo.git", - "private": false + ref: "", + refType: "revision", + path: "", + revision: "409ac2de49a53d679989d438735f78204f441634", + isFile: false, + repository: { + host: "gitea.com", + owner: "gitpod-io", + name: "gitpod-test-repo", + cloneUrl: "https://gitea.com/gitpod-io/gitpod-test-repo.git", + private: false, }, - "title": "gitpod-io/gitpod-test-repo - Test 3" - }) + title: "gitpod-io/gitpod-test-repo - Test 3", + }); } @test public async testCommitContext_02_notExistingCommit() { try { - await this.parser.handle({}, this.user, 'https://gitea.com/gitpod-io/gitpod-test-repo/commit/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); + await this.parser.handle( + {}, + this.user, + "https://gitea.com/gitpod-io/gitpod-test-repo/commit/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + ); // ensure that an error has been thrown chai.assert.fail(); } catch (e) { @@ -351,7 +376,7 @@ class TestGiteaContextParser { @test public async testCommitContext_02_invalidSha() { try { - await this.parser.handle({}, this.user, 'https://gitea.com/gitpod-io/gitpod-test-repo/commit/invalid'); + await this.parser.handle({}, this.user, "https://gitea.com/gitpod-io/gitpod-test-repo/commit/invalid"); // ensure that an error has been thrown chai.assert.fail(); } catch (e) { @@ -360,227 +385,233 @@ class TestGiteaContextParser { } @test public async testPullRequestContext_01() { - const result = await this.parser.handle({}, this.user, 'https://gitea.com/TypeFox/theia/pull/1'); - expect(result).to.deep.include( - { - "title": "Merge master", - "repository": { - "host": "gitea.com", - "owner": "eclipse-theia", - "name": "theia", - "cloneUrl": "https://gitea.com/eclipse-theia/theia.git", - "private": false - }, - "ref": "master", - "refType": "branch", - "nr": 1, - "base": { - "repository": { - "host": "gitea.com", - "owner": "TypeFox", - "name": "theia", - "cloneUrl": "https://gitea.com/TypeFox/theia.git", - "private": false, - "fork": { - "parent": { - "cloneUrl": "https://gitea.com/eclipse-theia/theia.git", - "host": "gitea.com", - "name": "theia", - "owner": "eclipse-theia", - "private": false - } - } + const result = await this.parser.handle({}, this.user, "https://gitea.com/TypeFox/theia/pull/1"); + expect(result).to.deep.include({ + title: "Merge master", + repository: { + host: "gitea.com", + owner: "eclipse-theia", + name: "theia", + cloneUrl: "https://gitea.com/eclipse-theia/theia.git", + private: false, + }, + ref: "master", + refType: "branch", + nr: 1, + base: { + repository: { + host: "gitea.com", + owner: "TypeFox", + name: "theia", + cloneUrl: "https://gitea.com/TypeFox/theia.git", + private: false, + fork: { + parent: { + cloneUrl: "https://gitea.com/eclipse-theia/theia.git", + host: "gitea.com", + name: "theia", + owner: "eclipse-theia", + private: false, + }, }, - "ref": "master", - "refType": "branch", - } - } - ) + }, + ref: "master", + refType: "branch", + }, + }); } @test public async testPullRequestthroughIssueContext_04() { - const result = await this.parser.handle({}, this.user, 'https://gitea.com/TypeFox/theia/issues/1'); - expect(result).to.deep.include( - { - "title": "Merge master", - "repository": { - "host": "gitea.com", - "owner": "eclipse-theia", - "name": "theia", - "cloneUrl": "https://gitea.com/eclipse-theia/theia.git", - "private": false - }, - "ref": "master", - "refType": "branch", - "nr": 1, - "base": { - "repository": { - "host": "gitea.com", - "owner": "TypeFox", - "name": "theia", - "cloneUrl": "https://gitea.com/TypeFox/theia.git", - "private": false, - "fork": { - "parent": { - "cloneUrl": "https://gitea.com/eclipse-theia/theia.git", - "host": "gitea.com", - "name": "theia", - "owner": "eclipse-theia", - "private": false - } - } + const result = await this.parser.handle({}, this.user, "https://gitea.com/TypeFox/theia/issues/1"); + expect(result).to.deep.include({ + title: "Merge master", + repository: { + host: "gitea.com", + owner: "eclipse-theia", + name: "theia", + cloneUrl: "https://gitea.com/eclipse-theia/theia.git", + private: false, + }, + ref: "master", + refType: "branch", + nr: 1, + base: { + repository: { + host: "gitea.com", + owner: "TypeFox", + name: "theia", + cloneUrl: "https://gitea.com/TypeFox/theia.git", + private: false, + fork: { + parent: { + cloneUrl: "https://gitea.com/eclipse-theia/theia.git", + host: "gitea.com", + name: "theia", + owner: "eclipse-theia", + private: false, + }, }, - "ref": "master", - "refType": "branch", - } - } - ) + }, + ref: "master", + refType: "branch", + }, + }); } @test public async testIssueContext_01() { - const result = await this.parser.handle({}, this.user, 'https://gitea.com/gitpod-io/gitpod-test-repo/issues/42'); - expect(result).to.deep.include( - { - "title": "Test issue web-extension", - "repository": { - "host": "gitea.com", - "owner": "gitpod-io", - "name": "gitpod-test-repo", - "cloneUrl": "https://gitea.com/gitpod-io/gitpod-test-repo.git", - "private": false - }, - "owner": "gitpod-io", - "nr": 42, - "ref": "1test", - "refType": "branch", - "localBranch": "somefox/test-issue-web-extension-42" - } - ) + const result = await this.parser.handle( + {}, + this.user, + "https://gitea.com/gitpod-io/gitpod-test-repo/issues/42", + ); + expect(result).to.deep.include({ + title: "Test issue web-extension", + repository: { + host: "gitea.com", + owner: "gitpod-io", + name: "gitpod-test-repo", + cloneUrl: "https://gitea.com/gitpod-io/gitpod-test-repo.git", + private: false, + }, + owner: "gitpod-io", + nr: 42, + ref: "1test", + refType: "branch", + localBranch: "somefox/test-issue-web-extension-42", + }); } @test public async testIssuePageContext() { - const result = await this.parser.handle({}, this.user, 'https://gitea.com/gitpod-io/gitpod-test-repo/issues'); - expect(result).to.deep.include( - { - "title": "gitpod-io/gitpod-test-repo - 1test", - "repository": { - "host": "gitea.com", - "owner": "gitpod-io", - "name": "gitpod-test-repo", - "cloneUrl": "https://gitea.com/gitpod-io/gitpod-test-repo.git", - "private": false - }, - "ref": "1test", - "refType": "branch", - } - ) + const result = await this.parser.handle({}, this.user, "https://gitea.com/gitpod-io/gitpod-test-repo/issues"); + expect(result).to.deep.include({ + title: "gitpod-io/gitpod-test-repo - 1test", + repository: { + host: "gitea.com", + owner: "gitpod-io", + name: "gitpod-test-repo", + cloneUrl: "https://gitea.com/gitpod-io/gitpod-test-repo.git", + private: false, + }, + ref: "1test", + refType: "branch", + }); } - @test public async testIssueThroughPullRequestContext() { - const result = await this.parser.handle({}, this.user, 'https://gitea.com/gitpod-io/gitpod-test-repo/pull/42'); - expect(result).to.deep.include( - { - "title": "Test issue web-extension", - "repository": { - "host": "gitea.com", - "owner": "gitpod-io", - "name": "gitpod-test-repo", - "cloneUrl": "https://gitea.com/gitpod-io/gitpod-test-repo.git", - "private": false - }, - "owner": "gitpod-io", - "nr": 42, - "ref": "1test", - "refType": "branch", - "localBranch": "somefox/test-issue-web-extension-42" - } - ) + const result = await this.parser.handle({}, this.user, "https://gitea.com/gitpod-io/gitpod-test-repo/pull/42"); + expect(result).to.deep.include({ + title: "Test issue web-extension", + repository: { + host: "gitea.com", + owner: "gitpod-io", + name: "gitpod-test-repo", + cloneUrl: "https://gitea.com/gitpod-io/gitpod-test-repo.git", + private: false, + }, + owner: "gitpod-io", + nr: 42, + ref: "1test", + refType: "branch", + localBranch: "somefox/test-issue-web-extension-42", + }); } @test public async testBlobContext_01() { - const result = await this.parser.handle({}, this.user, 'https://gitea.com/gitpod-io/gitpod-test-repo/blob/aba298d5084a817cdde3dd1f26692bc2a216e2b9/test-comment-01.md'); - expect(result).to.deep.include( - { - "title": "gitpod-io/gitpod-test-repo - aba298d5:test-comment-01.md", - "repository": { - "host": "gitea.com", - "owner": "gitpod-io", - "name": "gitpod-test-repo", - "cloneUrl": "https://gitea.com/gitpod-io/gitpod-test-repo.git", - "private": false - }, - "revision": "aba298d5084a817cdde3dd1f26692bc2a216e2b9", - "isFile": true, - "path": "test-comment-01.md" - } - ) + const result = await this.parser.handle( + {}, + this.user, + "https://gitea.com/gitpod-io/gitpod-test-repo/blob/aba298d5084a817cdde3dd1f26692bc2a216e2b9/test-comment-01.md", + ); + expect(result).to.deep.include({ + title: "gitpod-io/gitpod-test-repo - aba298d5:test-comment-01.md", + repository: { + host: "gitea.com", + owner: "gitpod-io", + name: "gitpod-test-repo", + cloneUrl: "https://gitea.com/gitpod-io/gitpod-test-repo.git", + private: false, + }, + revision: "aba298d5084a817cdde3dd1f26692bc2a216e2b9", + isFile: true, + path: "test-comment-01.md", + }); } @test public async testBlobContext_02() { - const result = await this.parser.handle({}, this.user, 'https://gitea.com/gitpod-io/gitpod-test-repo/blob/499efbbcb50e7e6e5e2883053f72a34cd5396be3/folder1/folder2/content2'); - expect(result).to.deep.include( - { - "title": "gitpod-io/gitpod-test-repo - 499efbbc:folder1/folder2/content2", - "repository": { - "host": "gitea.com", - "owner": "gitpod-io", - "name": "gitpod-test-repo", - "cloneUrl": "https://gitea.com/gitpod-io/gitpod-test-repo.git", - "private": false - }, - "revision": "499efbbcb50e7e6e5e2883053f72a34cd5396be3", - "isFile": true, - "path": "folder1/folder2/content2" - } - ) + const result = await this.parser.handle( + {}, + this.user, + "https://gitea.com/gitpod-io/gitpod-test-repo/blob/499efbbcb50e7e6e5e2883053f72a34cd5396be3/folder1/folder2/content2", + ); + expect(result).to.deep.include({ + title: "gitpod-io/gitpod-test-repo - 499efbbc:folder1/folder2/content2", + repository: { + host: "gitea.com", + owner: "gitpod-io", + name: "gitpod-test-repo", + cloneUrl: "https://gitea.com/gitpod-io/gitpod-test-repo.git", + private: false, + }, + revision: "499efbbcb50e7e6e5e2883053f72a34cd5396be3", + isFile: true, + path: "folder1/folder2/content2", + }); } @test public async testBlobContextShort_01() { - const result = await this.parser.handle({}, this.user, 'https://gitea.com/gitpod-io/gitpod-test-repo/blob/499efbbc/folder1/folder2/content2'); - expect(result).to.deep.include( - { - "title": "gitpod-io/gitpod-test-repo - 499efbbc:folder1/folder2/content2", - "repository": { - "host": "gitea.com", - "owner": "gitpod-io", - "name": "gitpod-test-repo", - "cloneUrl": "https://gitea.com/gitpod-io/gitpod-test-repo.git", - "private": false - }, - "revision": "499efbbcb50e7e6e5e2883053f72a34cd5396be3", - "isFile": true, - "path": "folder1/folder2/content2" - } - ) + const result = await this.parser.handle( + {}, + this.user, + "https://gitea.com/gitpod-io/gitpod-test-repo/blob/499efbbc/folder1/folder2/content2", + ); + expect(result).to.deep.include({ + title: "gitpod-io/gitpod-test-repo - 499efbbc:folder1/folder2/content2", + repository: { + host: "gitea.com", + owner: "gitpod-io", + name: "gitpod-test-repo", + cloneUrl: "https://gitea.com/gitpod-io/gitpod-test-repo.git", + private: false, + }, + revision: "499efbbcb50e7e6e5e2883053f72a34cd5396be3", + isFile: true, + path: "folder1/folder2/content2", + }); } @test public async testBlobContextShort_02() { - const result = await this.parser.handle({}, this.user, 'https://gitea.com/gitpod-io/gitpod-test-repo/blob/499ef/folder1/folder2/content2'); - expect(result).to.deep.include( - { - "title": "gitpod-io/gitpod-test-repo - 499efbbc:folder1/folder2/content2", - "repository": { - "host": "gitea.com", - "owner": "gitpod-io", - "name": "gitpod-test-repo", - "cloneUrl": "https://gitea.com/gitpod-io/gitpod-test-repo.git", - "private": false - }, - "revision": "499efbbcb50e7e6e5e2883053f72a34cd5396be3", - "isFile": true, - "path": "folder1/folder2/content2" - } - ) + const result = await this.parser.handle( + {}, + this.user, + "https://gitea.com/gitpod-io/gitpod-test-repo/blob/499ef/folder1/folder2/content2", + ); + expect(result).to.deep.include({ + title: "gitpod-io/gitpod-test-repo - 499efbbc:folder1/folder2/content2", + repository: { + host: "gitea.com", + owner: "gitpod-io", + name: "gitpod-test-repo", + cloneUrl: "https://gitea.com/gitpod-io/gitpod-test-repo.git", + private: false, + }, + revision: "499efbbcb50e7e6e5e2883053f72a34cd5396be3", + isFile: true, + path: "folder1/folder2/content2", + }); } @test public async testFetchCommitHistory() { - const result = await this.parser.fetchCommitHistory({}, this.user, 'https://gitea.com/gitpod-io/gitpod-test-repo', '409ac2de49a53d679989d438735f78204f441634', 100); + const result = await this.parser.fetchCommitHistory( + {}, + this.user, + "https://gitea.com/gitpod-io/gitpod-test-repo", + "409ac2de49a53d679989d438735f78204f441634", + 100, + ); expect(result).to.deep.equal([ - '506e5aed317f28023994ecf8ca6ed91430e9c1a4', - 'f5b041513bfab914b5fbf7ae55788d9835004d76', - ]) + "506e5aed317f28023994ecf8ca6ed91430e9c1a4", + "f5b041513bfab914b5fbf7ae55788d9835004d76", + ]); } - } -module.exports = new TestGiteaContextParser() // Only to circumvent no usage warning :-/ \ No newline at end of file +module.exports = new TestGiteaContextParser(); // Only to circumvent no usage warning :-/ diff --git a/components/server/src/gitea/gitea-context-parser.ts b/components/server/src/gitea/gitea-context-parser.ts index 6ed2621e434bf6..c45b3e4e8a71df 100644 --- a/components/server/src/gitea/gitea-context-parser.ts +++ b/components/server/src/gitea/gitea-context-parser.ts @@ -4,23 +4,30 @@ * See License-AGPL.txt in the project root for license information. */ -import { injectable, inject } from 'inversify'; - -import { Repository, PullRequestContext, NavigatorContext, IssueContext, User, CommitContext, RefType } from '@gitpod/gitpod-protocol'; -import { Gitea, GiteaRestApi } from './api'; -import { NotFoundError, UnauthorizedError } from '../errors'; -import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; -import { IContextParser, IssueContexts, AbstractContextParser } from '../workspace/context-parser'; -import { GiteaScope } from './scopes'; -import { GiteaTokenHelper } from './gitea-token-helper'; -import { TraceContext } from '@gitpod/gitpod-protocol/lib/util/tracing'; -import { convertRepo } from './convert'; - -const path = require('path'); // TODO(anbraten): is this really needed / correct? +import { injectable, inject } from "inversify"; + +import { + Repository, + PullRequestContext, + NavigatorContext, + IssueContext, + User, + CommitContext, + RefType, +} from "@gitpod/gitpod-protocol"; +import { Gitea, GiteaRestApi } from "./api"; +import { NotFoundError, UnauthorizedError } from "../errors"; +import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; +import { IContextParser, IssueContexts, AbstractContextParser } from "../workspace/context-parser"; +import { GiteaScope } from "./scopes"; +import { GiteaTokenHelper } from "./gitea-token-helper"; +import { TraceContext } from "@gitpod/gitpod-protocol/lib/util/tracing"; +import { convertRepo } from "./convert"; + +const path = require("path"); // TODO(anbraten): is this really needed / correct? @injectable() export class GiteaContextParser extends AbstractContextParser implements IContextParser { - @inject(GiteaRestApi) protected readonly giteaApi: GiteaRestApi; @inject(GiteaTokenHelper) protected readonly tokenHelper: GiteaTokenHelper; @@ -35,35 +42,53 @@ export class GiteaContextParser extends AbstractContextParser implements IContex const { host, owner, repoName, moreSegments } = await this.parseURL(user, contextUrl); if (moreSegments.length > 0) { switch (moreSegments[0]) { - case 'pulls': { // https://host/owner/repo/pulls/123 + case "pulls": { + // https://host/owner/repo/pulls/123 const prNr = parseInt(moreSegments[1], 10); - if (isNaN(prNr)) - break; + if (isNaN(prNr)) break; return await this.handlePullRequestContext({ span }, user, host, owner, repoName, prNr); } - case 'src': // https://host/owner/repo/src/branch/main/path/to/folder/or/file.yml - case 'commits': { // https://host/owner/repo/commits/branch/main - return await this.handleTreeContext({ span }, user, host, owner, repoName, moreSegments.slice(1)); + case "src": // https://host/owner/repo/src/branch/main/path/to/folder/or/file.yml + case "commits": { + // https://host/owner/repo/commits/branch/main + return await this.handleTreeContext( + { span }, + user, + host, + owner, + repoName, + moreSegments.slice(1), + ); } - case 'releases': { // https://host/owner/repo/releases/tag/1.0.0 + case "releases": { + // https://host/owner/repo/releases/tag/1.0.0 if (moreSegments.length > 1 && moreSegments[1] === "tag") { - return await this.handleTreeContext({ span }, user, host, owner, repoName, moreSegments.slice(2)); + return await this.handleTreeContext( + { span }, + user, + host, + owner, + repoName, + moreSegments.slice(2), + ); } break; } - case 'issues': { // https://host/owner/repo/issues/123 + case "issues": { + // https://host/owner/repo/issues/123 const issueNr = parseInt(moreSegments[1], 10); - if (isNaN(issueNr)) - break; + if (isNaN(issueNr)) break; return await this.handleIssueContext({ span }, user, host, owner, repoName, issueNr); } - case 'commit': { // https://host/owner/repo/commit/cfbaea9ee7d24d95e30e0bf2d4f75e83481815bc + case "commit": { + // https://host/owner/repo/commit/cfbaea9ee7d24d95e30e0bf2d4f75e83481815bc return await this.handleCommitContext({ span }, user, host, owner, repoName, moreSegments[1]); } } } return await this.handleDefaultContext({ span }, user, host, owner, repoName); } catch (error) { + // console.log("ass", error); if (error && error.code === 401) { const token = await this.tokenHelper.getCurrentToken(user); if (token) { @@ -73,7 +98,11 @@ export class GiteaContextParser extends AbstractContextParser implements IContex } // todo@alex: this is very unlikely. is coercing it into a valid case helpful? // here, GH API responded with a 401 code, and we are missing a token. OTOH, a missing token would not lead to a request. - throw UnauthorizedError.create(this.config.host, GiteaScope.Requirements.PUBLIC_REPO, "missing-identity"); + throw UnauthorizedError.create( + this.config.host, + GiteaScope.Requirements.PUBLIC_REPO, + "missing-identity", + ); } throw error; } finally { @@ -81,39 +110,49 @@ export class GiteaContextParser extends AbstractContextParser implements IContex } } - protected async handleDefaultContext(ctx: TraceContext, user: User, host: string, owner: string, repoName: string): Promise { + protected async handleDefaultContext( + ctx: TraceContext, + user: User, + host: string, + owner: string, + repoName: string, + ): Promise { try { const repository = await this.fetchRepo(user, owner, repoName); if (!repository.defaultBranch) { return { isFile: false, - path: '', + path: "", title: `${owner}/${repoName}`, - repository - } + repository, + }; } try { const branchOrTag = await this.getBranchOrTag(user, owner, repoName, [repository.defaultBranch!]); return { isFile: false, - path: '', + path: "", title: `${owner}/${repoName} - ${branchOrTag.name}`, ref: branchOrTag.name, revision: branchOrTag.revision, refType: branchOrTag.type, - repository + repository, }; } catch (error) { - if (error && error.message && (error.message as string).startsWith("Cannot find tag/branch for context")) { + if ( + error && + error.message && + (error.message as string).startsWith("Cannot find tag/branch for context") + ) { // the repo is empty (has no branches) return { isFile: false, - path: '', + path: "", title: `${owner}/${repoName} - ${repository.defaultBranch}`, - revision: '', - repository - } + revision: "", + repository, + }; } else { throw error; } @@ -127,20 +166,27 @@ export class GiteaContextParser extends AbstractContextParser implements IContex } } - protected async handleTreeContext(ctx: TraceContext, user: User, host: string, owner: string, repoName: string, segments: string[]): Promise { - + protected async handleTreeContext( + ctx: TraceContext, + user: User, + host: string, + owner: string, + repoName: string, + segments: string[], + ): Promise { try { - const branchOrTagPromise = segments.length > 0 ? this.getBranchOrTag(user, owner, repoName, segments) : undefined; + const branchOrTagPromise = + segments.length > 0 ? this.getBranchOrTag(user, owner, repoName, segments) : undefined; const repository = await this.fetchRepo(user, owner, repoName); const branchOrTag = await branchOrTagPromise; const context = { isFile: false, - path: '', - title: `${owner}/${repoName}` + (branchOrTag ? ` - ${branchOrTag.name}` : ''), + path: "", + title: `${owner}/${repoName}` + (branchOrTag ? ` - ${branchOrTag.name}` : ""), ref: branchOrTag && branchOrTag.name, revision: branchOrTag && branchOrTag.revision, refType: branchOrTag && branchOrTag.type, - repository + repository, }; if (!branchOrTag) { return context; @@ -149,13 +195,15 @@ export class GiteaContextParser extends AbstractContextParser implements IContex return context; } - const result = await this.giteaApi.run(user, async g => { - return g.repos.repoGetContents(owner, repoName, path.dirname(branchOrTag.fullPath), { ref: branchOrTag.name }); + const result = await this.giteaApi.run(user, async (g) => { + return g.repos.repoGetContents(owner, repoName, path.dirname(branchOrTag.fullPath), { + ref: branchOrTag.name, + }); }); if (Gitea.ApiError.is(result)) { - throw new Error(`Error reading TREE ${owner}/${repoName}/tree/${segments.join('/')}: ${result}`); + throw new Error(`Error reading TREE ${owner}/${repoName}/tree/${segments.join("/")}: ${result}`); } else { - const object = result.find(o => o.path === branchOrTag.fullPath); + const object = result.find((o) => o.path === branchOrTag.fullPath); if (object) { const isFile = object.type === "blob"; context.isFile = isFile; @@ -169,9 +217,13 @@ export class GiteaContextParser extends AbstractContextParser implements IContex } } - protected async getBranchOrTag(user: User, owner: string, repoName: string, segments: string[]): Promise<{ type: RefType, name: string, revision: string, fullPath: string }> { - - let branchOrTagObject: { type: RefType, name: string, revision: string } | undefined = undefined; + protected async getBranchOrTag( + user: User, + owner: string, + repoName: string, + segments: string[], + ): Promise<{ type: RefType; name: string; revision: string; fullPath: string }> { + let branchOrTagObject: { type: RefType; name: string; revision: string } | undefined = undefined; // `segments` could have branch/tag name parts as well as file path parts. // We never know which segments belong to the branch/tag name and which are already folder names. @@ -185,63 +237,94 @@ export class GiteaContextParser extends AbstractContextParser implements IContex } for (const candidate of branchOrTagCandidates) { - // Check if there is a BRANCH with name `candidate`: - const possibleBranch = await this.giteaApi.run(user, async g => { + const possibleBranch = await this.giteaApi.run(user, async (g) => { return g.repos.repoGetBranch(owner, repoName, candidate); }); // If the branch does not exist, the Gitea API returns with NotFound or InternalServerError. - const isNotFoundBranch = Gitea.ApiError.is(possibleBranch) && (Gitea.ApiError.isNotFound(possibleBranch) || Gitea.ApiError.isInternalServerError(possibleBranch)); + const isNotFoundBranch = + Gitea.ApiError.is(possibleBranch) && + (Gitea.ApiError.isNotFound(possibleBranch) || Gitea.ApiError.isInternalServerError(possibleBranch)); if (!isNotFoundBranch) { if (Gitea.ApiError.is(possibleBranch)) { - throw new Error(`Gitea ApiError on searching for possible branches for ${owner}/${repoName}/tree/${segments.join('/')}: ${possibleBranch}`); + throw new Error( + `Gitea ApiError on searching for possible branches for ${owner}/${repoName}/tree/${segments.join( + "/", + )}: ${possibleBranch}`, + ); } if (!possibleBranch.commit?.id || !possibleBranch.name) { - throw new Error(`Gitea ApiError on searching for possible branches for ${owner}/${repoName}/tree/${segments.join('/')}: ${possibleBranch}`); + throw new Error( + `Gitea ApiError on searching for possible branches for ${owner}/${repoName}/tree/${segments.join( + "/", + )}: ${possibleBranch}`, + ); } - branchOrTagObject = { type: 'branch', name: possibleBranch.name, revision: possibleBranch.commit.id }; + branchOrTagObject = { type: "branch", name: possibleBranch.name, revision: possibleBranch.commit.id }; break; } // Check if there is a TAG with name `candidate`: - const possibleTag = await this.giteaApi.run(user, async g => { + const possibleTag = await this.giteaApi.run(user, async (g) => { return g.repos.repoGetTag(owner, repoName, candidate); }); // TODO // If the tag does not exist, the GitLab API returns with NotFound or InternalServerError. - const isNotFoundTag = Gitea.ApiError.is(possibleTag) && (Gitea.ApiError.isNotFound(possibleTag) || Gitea.ApiError.isInternalServerError(possibleTag)); + const isNotFoundTag = + Gitea.ApiError.is(possibleTag) && + (Gitea.ApiError.isNotFound(possibleTag) || Gitea.ApiError.isInternalServerError(possibleTag)); if (!isNotFoundTag) { if (Gitea.ApiError.is(possibleTag)) { - throw new Error(`Gitea ApiError on searching for possible tags for ${owner}/${repoName}/tree/${segments.join('/')}: ${possibleTag}`); + throw new Error( + `Gitea ApiError on searching for possible tags for ${owner}/${repoName}/tree/${segments.join( + "/", + )}: ${possibleTag}`, + ); } if (!possibleTag.commit?.sha || !possibleTag.name) { - throw new Error(`Gitea ApiError on searching for possible branches for ${owner}/${repoName}/tree/${segments.join('/')}: ${possibleBranch}`); + throw new Error( + `Gitea ApiError on searching for possible branches for ${owner}/${repoName}/tree/${segments.join( + "/", + )}: ${possibleBranch}`, + ); } - branchOrTagObject = { type: 'tag', name: possibleTag.name, revision: possibleTag.commit.sha }; + branchOrTagObject = { type: "tag", name: possibleTag.name, revision: possibleTag.commit.sha }; break; } } // There seems to be no matching branch or tag. if (branchOrTagObject === undefined) { - log.debug(`Cannot find tag/branch for context: ${owner}/${repoName}/tree/${segments.join('/')}.`, - { branchOrTagCandidates } - ); - throw new Error(`Cannot find tag/branch for context: ${owner}/${repoName}/tree/${segments.join('/')}.`); + log.debug(`Cannot find tag/branch for context: ${owner}/${repoName}/tree/${segments.join("/")}.`, { + branchOrTagCandidates, + }); + throw new Error(`Cannot find tag/branch for context: ${owner}/${repoName}/tree/${segments.join("/")}.`); } - const remainingSegmentsIndex = branchOrTagObject.name.split('/').length; - const fullPath = decodeURIComponent(segments.slice(remainingSegmentsIndex).filter(s => s.length > 0).join('/')); + const remainingSegmentsIndex = branchOrTagObject.name.split("/").length; + const fullPath = decodeURIComponent( + segments + .slice(remainingSegmentsIndex) + .filter((s) => s.length > 0) + .join("/"), + ); return { ...branchOrTagObject, fullPath }; } - protected async handlePullRequestContext(ctx: TraceContext, user: User, host: string, owner: string, repoName: string, nr: number): Promise { - const result = await this.giteaApi.run(user, async g => { + protected async handlePullRequestContext( + ctx: TraceContext, + user: User, + host: string, + owner: string, + repoName: string, + nr: number, + ): Promise { + const result = await this.giteaApi.run(user, async (g) => { return g.repos.repoGetPullRequest(owner, repoName, nr); }); if (Gitea.ApiError.is(result)) { @@ -249,7 +332,9 @@ export class GiteaContextParser extends AbstractContextParser implements IContex } if (!result.base?.repo || !result.head?.repo || !result.title) { - throw new Error(`Missing relevant commit information for pull-request ${nr} from repository ${owner}/${repoName}`); + throw new Error( + `Missing relevant commit information for pull-request ${nr} from repository ${owner}/${repoName}`, + ); } const sourceRepo = convertRepo(result.head?.repo); @@ -259,14 +344,14 @@ export class GiteaContextParser extends AbstractContextParser implements IContex title: result.title, repository: sourceRepo, ref: result.head.ref, - refType: 'branch', + refType: "branch", revision: result.head.sha, nr, base: { repository: targetRepo, ref: result.base.ref, - refType: 'branch', - } + refType: "branch", + }, }; } @@ -274,7 +359,7 @@ export class GiteaContextParser extends AbstractContextParser implements IContex // host might be a relative URL const host = this.host; // as per contract, cf. `canHandle(user, contextURL)` - const result = await this.giteaApi.run(user, async g => { + const result = await this.giteaApi.run(user, async (g) => { return g.repos.repoGet(owner, repoName); }); if (Gitea.ApiError.is(result)) { @@ -286,8 +371,8 @@ export class GiteaContextParser extends AbstractContextParser implements IContex owner: owner, cloneUrl: result.clone_url, defaultBranch: result.default_branch, - private: result.private - } + private: result.private, + }; // TODO: support forks // if (result.fork) { // // host might be a relative URL, let's compute the prefix @@ -309,11 +394,11 @@ export class GiteaContextParser extends AbstractContextParser implements IContex } protected async fetchCommit(user: User, owner: string, repoName: string, sha: string) { - const result = await this.giteaApi.run(user, async g => { + const result = await this.giteaApi.run(user, async (g) => { return g.repos.repoGetSingleCommit(owner, repoName, sha); }); if (Gitea.ApiError.is(result)) { - if (result.message === 'Gitea responded with code 404') { + if (result.message === "Gitea responded with code 404") { throw new Error(`Couldn't find commit #${sha} in repository ${owner}/${repoName}.`); } throw result; @@ -325,13 +410,20 @@ export class GiteaContextParser extends AbstractContextParser implements IContex return { id: result.sha, // TODO: how can we use a proper commit-id instead of the sha - title: result.commit?.message - } + title: result.commit?.message, + }; } - protected async handleIssueContext(ctx: TraceContext, user: User, host: string, owner: string, repoName: string, nr: number): Promise { + protected async handleIssueContext( + ctx: TraceContext, + user: User, + host: string, + owner: string, + repoName: string, + nr: number, + ): Promise { const ctxPromise = this.handleDefaultContext(ctx, user, host, owner, repoName); - const result = await this.giteaApi.run(user, async g => { + const result = await this.giteaApi.run(user, async (g) => { return g.repos.issueGetIssue(owner, repoName, nr); }); if (Gitea.ApiError.is(result) || !result.title || !result.id) { @@ -343,11 +435,18 @@ export class GiteaContextParser extends AbstractContextParser implements IContex title: result.title, owner, nr, - localBranch: IssueContexts.toBranchName(user, result.title, result.id) + localBranch: IssueContexts.toBranchName(user, result.title, result.id), }; } - protected async handleCommitContext(ctx: TraceContext, user: User, host: string, owner: string, repoName: string, sha: string): Promise { + protected async handleCommitContext( + ctx: TraceContext, + user: User, + host: string, + owner: string, + repoName: string, + sha: string, + ): Promise { const repository = await this.fetchRepo(user, owner, repoName); if (Gitea.ApiError.is(repository)) { throw await NotFoundError.create(await this.tokenHelper.getCurrentToken(user), user, host, owner, repoName); @@ -357,9 +456,9 @@ export class GiteaContextParser extends AbstractContextParser implements IContex throw new Error(`Couldn't find commit #${sha} in repository ${owner}/${repoName}.`); } return { - path: '', - ref: '', - refType: 'revision', + path: "", + ref: "", + refType: "revision", isFile: false, title: `${owner}/${repoName} - ${commit.title}`, owner, @@ -368,7 +467,13 @@ export class GiteaContextParser extends AbstractContextParser implements IContex }; } - public async fetchCommitHistory(ctx: TraceContext, user: User, contextUrl: string, sha: string, maxDepth: number): Promise { + public async fetchCommitHistory( + ctx: TraceContext, + user: User, + contextUrl: string, + sha: string, + maxDepth: number, + ): Promise { const span = TraceContext.startSpan("GiteaContextParser.fetchCommitHistory", ctx); try { @@ -383,7 +488,7 @@ export class GiteaContextParser extends AbstractContextParser implements IContex // haxNextPage, // }, const { owner, repoName } = await this.parseURL(user, contextUrl); - const result = await this.giteaApi.run(user, async g => { + const result = await this.giteaApi.run(user, async (g) => { return g.repos.repoGetAllCommits(owner, repoName, { sha, limit: maxDepth, @@ -391,7 +496,7 @@ export class GiteaContextParser extends AbstractContextParser implements IContex }); }); if (Gitea.ApiError.is(result)) { - if (result.message === 'Gitea responded with code 404') { + if (result.message === "Gitea responded with code 404") { throw new Error(`Couldn't find commit #${sha} in repository ${owner}/${repoName}.`); } throw result; @@ -406,7 +511,7 @@ export class GiteaContextParser extends AbstractContextParser implements IContex return c.sha; }); } catch (e) { - span.log({ "error": e }); + span.log({ error: e }); throw e; } finally { span.finish(); diff --git a/components/server/src/gitea/gitea-token-validator.ts b/components/server/src/gitea/gitea-token-validator.ts index fedf7d3b44aa55..5bae06fd84dffe 100644 --- a/components/server/src/gitea/gitea-token-validator.ts +++ b/components/server/src/gitea/gitea-token-validator.ts @@ -7,47 +7,32 @@ import { inject, injectable } from "inversify"; import { CheckWriteAccessResult, IGitTokenValidator, IGitTokenValidatorParams } from "../workspace/git-token-validator"; import { Gitea, GiteaRestApi } from "./api"; -import { log } from '@gitpod/gitpod-protocol/lib/util/logging'; +import { log } from "@gitpod/gitpod-protocol/lib/util/logging"; @injectable() export class GiteaTokenValidator implements IGitTokenValidator { - @inject(GiteaRestApi) giteaApi: GiteaRestApi; - - async checkWriteAccess(params: IGitTokenValidatorParams): Promise { - - const { token, repoFullName } = params; - - const parsedRepoName = this.parseGiteaRepoName(repoFullName); - if (!parsedRepoName) { - throw new Error(`Could not parse repo name: ${repoFullName}`); - } - const repo = await this.giteaApi.run(token, api => api.repos.repoGet(parsedRepoName.owner, parsedRepoName.repo)); - if (Gitea.ApiError.is(repo) && Gitea.ApiError.isNotFound(repo)) { - return { found: false }; - } else if (Gitea.ApiError.is(repo)) { - log.error('Error getting repo information from Gitea', repo, { repoFullName, parsedRepoName }) - return { found: false, error: repo }; - } - - const isPrivateRepo = repo.private; - let writeAccessToRepo = repo.permissions?.push; - - return { - found: true, - isPrivateRepo, - writeAccessToRepo, - mayWritePrivate: true, - mayWritePublic: true - } - } - - protected parseGiteaRepoName(repoFullName: string) { - const parts = repoFullName.split("/"); - if (parts.length === 2) { - return { - owner: parts[0], - repo: parts[1] - } - } - } -} \ No newline at end of file + @inject(GiteaRestApi) giteaApi: GiteaRestApi; + + async checkWriteAccess(params: IGitTokenValidatorParams): Promise { + const { token, owner, repo: repoName } = params; + + const repo = await this.giteaApi.run(token, (api) => api.repos.repoGet(owner, repoName)); + if (Gitea.ApiError.is(repo) && Gitea.ApiError.isNotFound(repo)) { + return { found: false }; + } else if (Gitea.ApiError.is(repo)) { + log.error("Error getting repo information from Gitea", repo, { repo, owner }); + return { found: false, error: repo }; + } + + const isPrivateRepo = repo.private; + let writeAccessToRepo = repo.permissions?.push; + + return { + found: true, + isPrivateRepo, + writeAccessToRepo, + mayWritePrivate: true, + mayWritePublic: true, + }; + } +} From dc3fdc806de6d94a78904335fafd58cd8745793c Mon Sep 17 00:00:00 2001 From: Anbraten Date: Sat, 4 Jun 2022 07:54:51 +0000 Subject: [PATCH 15/16] improve error handling --- components/server/src/gitea/api.ts | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/components/server/src/gitea/api.ts b/components/server/src/gitea/api.ts index 723e7323a4a745..fa427ff46d8ba3 100644 --- a/components/server/src/gitea/api.ts +++ b/components/server/src/gitea/api.ts @@ -89,20 +89,14 @@ export class GiteaRestApi { const response = (await operation(userApi)) as R; return response as R; } catch (error) { - if (error && typeof error?.response?.status === "number" && error?.response?.status !== 200) { - return new Gitea.ApiError(`Gitea responded with code ${error.response.status}`, error); + if (error && error?.type === "system") { + return new Gitea.ApiError(`Gitea Fetch Error: ${error?.message}`, error); } - if (error && error?.name === "HTTPError") { - // e.g. - // { - // "name": "HTTPError", - // "timings": { }, - // "description": "404 Commit Not Found" - // } - - return new Gitea.ApiError(`Gitea Request Error: ${error?.description}`, error); + if (error?.error && !error?.data && error?.error?.errors) { + return new Gitea.ApiError(`Gitea Api Error: ${error?.error?.message}`, error?.error); } - log.error(`Gitea request error`, error); + + // log.error(`Gitea request error`, error); throw error; } finally { log.info(`Gitea request took ${new Date().getTime() - before} ms`); From 10a6b406c9fd17bee21456ef67a0efdc0119506a Mon Sep 17 00:00:00 2001 From: Anbraten Date: Sat, 4 Jun 2022 07:55:06 +0000 Subject: [PATCH 16/16] fix issue context --- components/server/src/gitea/gitea-context-parser.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/server/src/gitea/gitea-context-parser.ts b/components/server/src/gitea/gitea-context-parser.ts index c45b3e4e8a71df..3e01f787931d27 100644 --- a/components/server/src/gitea/gitea-context-parser.ts +++ b/components/server/src/gitea/gitea-context-parser.ts @@ -422,14 +422,14 @@ export class GiteaContextParser extends AbstractContextParser implements IContex repoName: string, nr: number, ): Promise { - const ctxPromise = this.handleDefaultContext(ctx, user, host, owner, repoName); const result = await this.giteaApi.run(user, async (g) => { return g.repos.issueGetIssue(owner, repoName, nr); }); if (Gitea.ApiError.is(result) || !result.title || !result.id) { throw await NotFoundError.create(await this.tokenHelper.getCurrentToken(user), user, host, owner, repoName); } - const context = await ctxPromise; + + const context = await this.handleDefaultContext(ctx, user, host, owner, repoName); return { ...context, title: result.title,