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
Show file tree
Hide file tree
Changes from all commits
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
3 changes: 3 additions & 0 deletions components/dashboard/src/images/gitea.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 6 additions & 3 deletions components/dashboard/src/provider-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@
*/

import { AuthProviderType } from "@gitpod/public-api/lib/gitpod/v1/authprovider_pb";
import bitbucket from "./images/bitbucket.svg";
import github from "./images/github.svg";
import gitlab from "./images/gitlab.svg";
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 | AuthProviderType) {
Expand All @@ -18,6 +19,8 @@ function iconForAuthProvider(type: string | AuthProviderType) {
case "GitLab":
case AuthProviderType.GITLAB:
return <img className="fill-current filter-grayscale w-5 h-5 ml-3 mr-3 my-auto" src={gitlab} alt="" />;
case "Gitea":
return <img className="fill-current filter-grayscale w-5 h-5 ml-3 mr-3 my-auto" src={gitea} />;
case "Bitbucket":
case AuthProviderType.BITBUCKET:
return <img className="fill-current filter-grayscale w-5 h-5 ml-3 mr-3 my-auto" src={bitbucket} alt="" />;
Expand Down
11 changes: 9 additions & 2 deletions components/dashboard/src/user-settings/Integrations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ function GitIntegrations() {
<div>
<Heading2>Git Integrations</Heading2>
<Subheading>
Manage Git integrations for self-managed instances of GitLab, GitHub, or Bitbucket.
Manage Git integrations for self-managed instances of GitLab, GitHub, Gitea or Bitbucket.
</Subheading>
</div>
{/* Hide create button if ff is disabled */}
Expand Down Expand Up @@ -714,6 +714,9 @@ export function GitIntegrationModal(
settingsUrl = `${host}/-/profile/applications`;
}
break;
case "Gitea":
settingsUrl = `${host}/user/settings/applications`;
break;
default:
return undefined;
}
Expand All @@ -725,6 +728,9 @@ export function GitIntegrationModal(
case AuthProviderType.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;
}
Expand Down Expand Up @@ -787,7 +793,7 @@ export function GitIntegrationModal(
<div className="flex flex-col">
<span className="text-gray-500">
{props.headerText ||
"Configure an integration with a self-managed instance of GitLab, GitHub, or Bitbucket."}
"Configure an integration with a self-managed instance of GitLab, GitHub, Bitbucket or Gitea."}
</span>
</div>

Expand All @@ -806,6 +812,7 @@ export function GitIntegrationModal(
>
<option value={AuthProviderType.GITHUB}>GitHub</option>
<option value={AuthProviderType.GITLAB}>GitLab</option>
<option value={AuthProviderType.GITEA}>Gitea</option>
<option value={AuthProviderType.BITBUCKET_SERVER}>Bitbucket Server</option>
</select>
</div>
Expand Down
13 changes: 13 additions & 0 deletions components/server/ee/src/gitea/container-module.ts
Original file line number Diff line number Diff line change
@@ -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();
});
58 changes: 58 additions & 0 deletions components/server/ee/src/gitea/gitea-app-support.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/**
* 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<ProviderRepository[]> {
const token = await this.tokenProvider.getTokenForHost(params.user, params.provider.host);
const oauthToken = token.value;
const api = Gitea.create(`https://${params.provider.host}`, oauthToken);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two tiny nitpicks, 1. It is possible for gitea to exist on a subpath (eg. https://example.com/gitea/ etc..), and 2. some users may wish not to use https (not a great idea, but one that people make for a variety of reasons).

You may wish instead of host, to use something like baseURL where users can fill the url scheme and full url (incl. a sub-path if they so require).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good points. I will have a look and do some tests later. Currently my main problem is still getting a Gitpod instance with those changes up and running. 🙈

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the issue a matter of compute resources available to you, or being able to apply your changes to a running deploy? If the former, the Gitea project likely can assist in providing compute resources, if the later I'm sure if you post the issue you are running into then someone can help.

Copy link
Author

@anbraten anbraten Mar 5, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I currently using a cluster on vultr with with some credit I had lying around, so should be fine for the beginning, but I will come back to your offer if needed. Main problem is building images of the needed components and deploying those atm. It requires some manual intervention which loads to a pretty slow dev cycle as I have to rebuild the server, deploy it and look for changes ... The gitpod team seems to use telepresence for that, but I am not quite sure how to integrate that with my own cluster.


const result: ProviderRepository[] = [];
const ownersRepos: ProviderRepository[] = [];

const identity = params.user.identities.find(i => i.authProviderId === params.provider.authProviderId);
if (!identity) {
return result;
}
const usersAccount = identity.authName;

// 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 === usersAccount ? 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;
}

}
2 changes: 2 additions & 0 deletions components/server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,14 @@
"cookie": "^0.4.2",
"cookie-parser": "^1.4.6",
"cors": "^2.8.4",
"cross-fetch": "^3.1.5",
"countries-list": "^2.6.1",
"deep-equal": "^2.0.5",
"deepmerge": "^4.2.2",
"express": "^4.17.3",
"express-http-proxy": "^1.0.7",
"fs-extra": "^10.0.0",
"gitea-js": "^1.20.1",
"google-protobuf": "^3.19.1",
"inversify": "^6.0.1",
"ioredis": "^5.3.2",
Expand Down
4 changes: 4 additions & 0 deletions components/server/src/auth/auth-provider-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { oauthUrls as bbsUrls } from "../bitbucket-server/bitbucket-server-urls";
import { oauthUrls as bbUrls } from "../bitbucket/bitbucket-urls";
import { log } from "@gitpod/gitpod-protocol/lib/util/logging";
Expand Down Expand Up @@ -335,6 +336,9 @@ export class AuthProviderService {
case "BitbucketServer":
urls = bbsUrls(host);
break;
case "Gitea":
urls = giteaUrls(host);
break;
case "Bitbucket":
urls = bbUrls(host);
break;
Expand Down
3 changes: 3 additions & 0 deletions components/server/src/auth/host-container-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
import { bitbucketServerContainerModule } from "../bitbucket-server/bitbucket-server-container-module";

@injectable()
Expand All @@ -19,6 +20,8 @@ export class HostContainerMapping {
return [githubContainerModule];
case "GitLab":
return [gitlabContainerModule];
case "Gitea":
return [giteaContainerModule];
case "OAuth":
return [genericAuthContainerModule];
case "Bitbucket":
Expand Down
7 changes: 7 additions & 0 deletions components/server/src/dev/dev-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,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) {
Expand Down
105 changes: 105 additions & 0 deletions components/server/src/gitea/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* 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 {
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";

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 = "GiteaApiError";
}
}
export namespace ApiError {
export function is(something: any): something is ApiError {
return !!something && something.name === "GiteaApiError";
}
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 function create(host: string, token: string) {
return giteaApi(`https://${host}`, {
customFetch: fetch,
token,
});
}

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()
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") {
oauthToken = userOrToken;
} else {
const giteaToken = await this.tokenHelper.getTokenWithScopes(userOrToken, GiteaScope.Requirements.DEFAULT);
oauthToken = giteaToken.value;
}
const api = Gitea.create(this.config.host, oauthToken);
return api;
}

public async run<R>(
userOrToken: User | string,
operation: (g: Api<unknown>) => Promise<any>,
): Promise<R | Gitea.ApiError> {
const before = new Date().getTime();
const userApi = await this.create(userOrToken);
try {
const response = (await operation(userApi)) as R;
return response as R;
} catch (error) {
if (error && error?.type === "system") {
return new Gitea.ApiError(`Gitea Fetch Error: ${error?.message}`, 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);
throw error;
} finally {
log.info(`Gitea request took ${new Date().getTime() - before} ms`);
}
}
}
34 changes: 34 additions & 0 deletions components/server/src/gitea/convert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* 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;
}

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 {

// }
Loading