Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Gitea support #8131

Draft
wants to merge 23 commits into
base: main
Choose a base branch
from
Draft
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
adjust gitea api
  • Loading branch information
anbraten committed Feb 9, 2022
commit 1900190edcac39dcf32b58f8e4955628e3102260
1 change: 1 addition & 0 deletions components/server/package.json
Original file line number Diff line number Diff line change
@@ -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",
127 changes: 26 additions & 101 deletions components/server/src/gitea/api.ts
Original file line number Diff line number Diff line change
@@ -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<string | undefined> {
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<T>(user: User, query: string, variables?: object): Promise<QueryResult<T>> {
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<T>(token: string, request: object): Promise<QueryResult<T>> {
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<T> = 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<D> {
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<T> extends OctokitResponse<T> { }
export namespace GiteaResult {
export function actualScopes(result: OctokitResponse<any>): string[] {
return (result.headers['x-oauth-scopes'] || "").split(",").map((s: any) => s.trim());
}
export function mayReadOrgs(result: OctokitResponse<any>): boolean {
return actualScopes(result).some(scope => scope === "read:org" || scope === "user");
}
export function mayWritePrivate(result: OctokitResponse<any>): boolean {
return actualScopes(result).some(scope => scope === "repo");
}
export function mayWritePublic(result: OctokitResponse<any>): boolean {
return actualScopes(result).some(scope => scope === "repo" || scope === "public_repo");
public async getFileContents(user, repositoryOwner, repositoryName, revision, path): Promise<Branch[]> {
return [];
}
}

// export interface GiteaResult<T> extends OctokitResponse<T> { }
// export namespace GiteaResult {
// export function actualScopes(result: OctokitResponse<any>): string[] {
// return (result.headers['x-oauth-scopes'] || "").split(",").map((s: any) => s.trim());
// }
// export function mayReadOrgs(result: OctokitResponse<any>): boolean {
// return actualScopes(result).some(scope => scope === "read:org" || scope === "user");
// }
// export function mayWritePrivate(result: OctokitResponse<any>): boolean {
// return actualScopes(result).some(scope => scope === "repo");
// }
// export function mayWritePublic(result: OctokitResponse<any>): boolean {
// return actualScopes(result).some(scope => scope === "repo" || scope === "public_repo");
// }
// }

// Git
export interface CommitUser {
date: string
9 changes: 4 additions & 5 deletions components/server/src/gitea/file-provider.ts
Original file line number Diff line number Diff line change
@@ -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<MaybeContent> {
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<string> {
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);
10 changes: 1 addition & 9 deletions components/server/src/gitea/gitea-auth-provider.ts
Original file line number Diff line number Diff line change
@@ -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) => {
3 changes: 1 addition & 2 deletions components/server/src/gitea/gitea-container-module.ts
Original file line number Diff line number Diff line change
@@ -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();
32 changes: 7 additions & 25 deletions components/server/src/gitea/gitea-repository-provider.ts
Original file line number Diff line number Diff line change
@@ -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<Repository> {
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<Branch> {
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<CommitInfo | undefined> {
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<string[]> {
// 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)
}
}
3 changes: 1 addition & 2 deletions components/server/src/gitea/gitea-token-validator.ts
Original file line number Diff line number Diff line change
@@ -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<CheckWriteAccessResult> {

Loading
Oops, something went wrong.