add back gitlab, gitea, and gerrit support (#184)

* add non github config definitions

* refactor github config compilation to seperate file

* add gitlab config compilation

* Connection management (#183)

* wip gitlab repo sync support

* fix gitlab zoekt metadata

* add gitea support

* add gerrit support

* Connection management (#183)

* add gerrit config compilation

* Connection management (#183)

---------

Co-authored-by: Brendan Kellam <bshizzle1234@gmail.com>
This commit is contained in:
Michael Sukkarieh 2025-02-14 10:58:53 -08:00 committed by GitHub
parent e2e5433d20
commit da33220289
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 1576 additions and 213 deletions

View file

@ -5,8 +5,7 @@ import { ConnectionConfig } from "@sourcebot/schemas/v3/connection.type";
import { createLogger } from "./logger.js";
import os from 'os';
import { Redis } from 'ioredis';
import { marshalBool } from "./utils.js";
import { getGitHubReposFromConfig } from "./github.js";
import { RepoData, compileGithubConfig, compileGitlabConfig, compileGiteaConfig, compileGerritConfig } from "./repoCompileUtils.js";
interface IConnectionManager {
scheduleConnectionSync: (connection: Connection) => Promise<void>;
@ -79,64 +78,28 @@ export class ConnectionManager implements IConnectionManager {
// @note: We aren't actually doing anything with this atm.
const abortController = new AbortController();
type RepoData = WithRequired<Prisma.RepoCreateInput, 'connections'>;
const repoData: RepoData[] = (
await (async () => {
switch (config.type) {
case 'github': {
const gitHubRepos = await getGitHubReposFromConfig(config, orgId, this.db, abortController.signal);
const hostUrl = config.url ?? 'https://github.com';
const hostname = config.url ? new URL(config.url).hostname : 'github.com';
return gitHubRepos.map((repo) => {
const repoName = `${hostname}/${repo.full_name}`;
const cloneUrl = new URL(repo.clone_url!);
const record: RepoData = {
external_id: repo.id.toString(),
external_codeHostType: 'github',
external_codeHostUrl: hostUrl,
cloneUrl: cloneUrl.toString(),
imageUrl: repo.owner.avatar_url,
name: repoName,
isFork: repo.fork,
isArchived: !!repo.archived,
org: {
connect: {
id: orgId,
},
},
connections: {
create: {
connectionId: job.data.connectionId,
}
},
metadata: {
'zoekt.web-url-type': 'github',
'zoekt.web-url': repo.html_url,
'zoekt.name': repoName,
'zoekt.github-stars': (repo.stargazers_count ?? 0).toString(),
'zoekt.github-watchers': (repo.watchers_count ?? 0).toString(),
'zoekt.github-subscribers': (repo.subscribers_count ?? 0).toString(),
'zoekt.github-forks': (repo.forks_count ?? 0).toString(),
'zoekt.archived': marshalBool(repo.archived),
'zoekt.fork': marshalBool(repo.fork),
'zoekt.public': marshalBool(repo.private === false)
},
};
return record;
})
}
case 'gitlab': {
// @todo
return [];
}
const repoData: RepoData[] = await (async () => {
switch (config.type) {
case 'github': {
return await compileGithubConfig(config, job.data.connectionId, orgId, this.db, abortController);
}
})()
)
case 'gitlab': {
return await compileGitlabConfig(config, job.data.connectionId, orgId, this.db);
}
case 'gitea': {
return await compileGiteaConfig(config, job.data.connectionId, orgId, this.db);
}
case 'gerrit': {
return await compileGerritConfig(config, job.data.connectionId, orgId);
}
default: {
return [];
}
}
})();
// Filter out any duplicates by external_id and external_codeHostUrl.
.filter((repo, index, self) => {
repoData.filter((repo, index, self) => {
return index === self.findIndex(r =>
r.external_id === repo.external_id &&
r.external_codeHostUrl === repo.external_codeHostUrl

View file

@ -1,8 +1,7 @@
import fetch from 'cross-fetch';
import { GerritConfig } from "@sourcebot/schemas/v2/index.type"
import { AppContext, GitRepository } from './types.js';
import { createLogger } from './logger.js';
import path from 'path';
import micromatch from "micromatch";
import { measure, marshalBool, excludeReposByName, includeReposByName } from './utils.js';
// https://gerrit-review.googlesource.com/Documentation/rest-api.html
@ -16,6 +15,13 @@ interface GerritProjectInfo {
web_links?: GerritWebLink[];
}
interface GerritProject {
name: string;
id: string;
state?: string;
web_links?: GerritWebLink[];
}
interface GerritWebLink {
name: string;
url: string;
@ -23,12 +29,12 @@ interface GerritWebLink {
const logger = createLogger('Gerrit');
export const getGerritReposFromConfig = async (config: GerritConfig, ctx: AppContext): Promise<GitRepository[]> => {
export const getGerritReposFromConfig = async (config: GerritConfig): Promise<GerritProject[]> => {
const url = config.url.endsWith('/') ? config.url : `${config.url}/`;
const hostname = new URL(config.url).hostname;
const { durationMs, data: projects } = await measure(async () => {
let { durationMs, data: projects } = await measure(async () => {
try {
return fetchAllProjects(url)
} catch (err) {
@ -42,67 +48,29 @@ export const getGerritReposFromConfig = async (config: GerritConfig, ctx: AppCon
}
// exclude "All-Projects" and "All-Users" projects
delete projects['All-Projects'];
delete projects['All-Users'];
delete projects['All-Avatars']
delete projects['All-Archived-Projects']
logger.debug(`Fetched ${Object.keys(projects).length} projects in ${durationMs}ms.`);
let repos: GitRepository[] = Object.keys(projects).map((projectName) => {
const project = projects[projectName];
let webUrl = "https://www.gerritcodereview.com/";
// Gerrit projects can have multiple web links; use the first one
if (project.web_links) {
const webLink = project.web_links[0];
if (webLink) {
webUrl = webLink.url;
}
}
const repoId = `${hostname}/${projectName}`;
const repoPath = path.resolve(path.join(ctx.reposPath, `${repoId}.git`));
const cloneUrl = `${url}${encodeURIComponent(projectName)}`;
return {
vcs: 'git',
codeHost: 'gerrit',
name: projectName,
id: repoId,
cloneUrl: cloneUrl,
path: repoPath,
isStale: false, // Gerrit projects are typically not stale
isFork: false, // Gerrit doesn't have forks in the same way as GitHub
isArchived: false,
gitConfigMetadata: {
// Gerrit uses Gitiles for web UI. This can sometimes be "browse" type in zoekt
'zoekt.web-url-type': 'gitiles',
'zoekt.web-url': webUrl,
'zoekt.name': repoId,
'zoekt.archived': marshalBool(false),
'zoekt.fork': marshalBool(false),
'zoekt.public': marshalBool(true), // Assuming projects are public; adjust as needed
},
branches: [],
tags: []
} satisfies GitRepository;
});
const excludedProjects = ['All-Projects', 'All-Users', 'All-Avatars', 'All-Archived-Projects'];
projects = projects.filter(project => !excludedProjects.includes(project.name));
// include repos by glob if specified in config
if (config.projects) {
repos = includeReposByName(repos, config.projects);
projects = projects.filter((project) => {
return micromatch.isMatch(project.name, config.projects!);
});
}
if (config.exclude && config.exclude.projects) {
repos = excludeReposByName(repos, config.exclude.projects);
projects = projects.filter((project) => {
return !micromatch.isMatch(project.name, config.exclude!.projects!);
});
}
return repos;
logger.debug(`Fetched ${Object.keys(projects).length} projects in ${durationMs}ms.`);
return projects;
};
const fetchAllProjects = async (url: string): Promise<GerritProjects> => {
const fetchAllProjects = async (url: string): Promise<GerritProject[]> => {
const projectsEndpoint = `${url}projects/`;
let allProjects: GerritProjects = {};
let allProjects: GerritProject[] = [];
let start = 0; // Start offset for pagination
let hasMoreProjects = true;
@ -119,8 +87,15 @@ const fetchAllProjects = async (url: string): Promise<GerritProjects> => {
const jsonText = text.replace(")]}'\n", ''); // Remove XSSI protection prefix
const data: GerritProjects = JSON.parse(jsonText);
// Merge the current batch of projects with allProjects
Object.assign(allProjects, data);
// Add fetched projects to allProjects
for (const [projectName, projectInfo] of Object.entries(data)) {
allProjects.push({
name: projectName,
id: projectInfo.id,
state: projectInfo.state,
web_links: projectInfo.web_links
})
}
// Check if there are more projects to fetch
hasMoreProjects = Object.values(data).some(

View file

@ -1,17 +1,16 @@
import { Api, giteaApi, HttpResponse, Repository as GiteaRepository } from 'gitea-js';
import { GiteaConfig } from "@sourcebot/schemas/v2/index.type"
import { excludeArchivedRepos, excludeForkedRepos, excludeReposByName, getTokenFromConfig, marshalBool, measure } from './utils.js';
import { AppContext, GitRepository } from './types.js';
import { GiteaConnectionConfig } from '@sourcebot/schemas/v3/gitea.type';
import { getTokenFromConfig, measure } from './utils.js';
import fetch from 'cross-fetch';
import { createLogger } from './logger.js';
import path from 'path';
import micromatch from 'micromatch';
import { PrismaClient } from '@sourcebot/db';
const logger = createLogger('Gitea');
export const getGiteaReposFromConfig = async (config: GiteaConfig, orgId: number, ctx: AppContext) => {
export const getGiteaReposFromConfig = async (config: GiteaConnectionConfig, orgId: number, db: PrismaClient) => {
// TODO: pass in DB here to fetch secret properly
const token = config.token ? await getTokenFromConfig(config.token, orgId) : undefined;
const token = config.token ? await getTokenFromConfig(config.token, orgId, db) : undefined;
const api = giteaApi(config.url ?? 'https://gitea.com', {
token,
@ -34,66 +33,26 @@ export const getGiteaReposFromConfig = async (config: GiteaConfig, orgId: number
const _repos = await getReposOwnedByUsers(config.users, api);
allRepos = allRepos.concat(_repos);
}
let repos: GitRepository[] = allRepos
.map((repo) => {
const hostname = config.url ? new URL(config.url).hostname : 'gitea.com';
const repoId = `${hostname}/${repo.full_name!}`;
const repoPath = path.resolve(path.join(ctx.reposPath, `${repoId}.git`));
const cloneUrl = new URL(repo.clone_url!);
if (token) {
cloneUrl.username = token;
}
return {
vcs: 'git',
codeHost: 'gitea',
name: repo.full_name!,
id: repoId,
cloneUrl: cloneUrl.toString(),
path: repoPath,
isStale: false,
isFork: repo.fork!,
isArchived: !!repo.archived,
gitConfigMetadata: {
'zoekt.web-url-type': 'gitea',
'zoekt.web-url': repo.html_url!,
'zoekt.name': repoId,
'zoekt.archived': marshalBool(repo.archived),
'zoekt.fork': marshalBool(repo.fork!),
'zoekt.public': marshalBool(repo.internal === false && repo.private === false),
},
branches: [],
tags: []
} satisfies GitRepository;
});
if (config.exclude) {
if (!!config.exclude.forks) {
repos = excludeForkedRepos(repos, logger);
allRepos = allRepos.filter(repo => repo.full_name !== undefined);
allRepos = allRepos.filter(repo => {
if (repo.full_name === undefined) {
logger.warn(`Repository with undefined full_name found: orgId=${orgId}, repoId=${repo.id}`);
return false;
}
return true;
});
if (!!config.exclude.archived) {
repos = excludeArchivedRepos(repos, logger);
}
if (config.exclude.repos) {
repos = excludeReposByName(repos, config.exclude.repos, logger);
}
}
logger.debug(`Found ${repos.length} total repositories.`);
if (config.revisions) {
if (config.revisions.branches) {
const branchGlobs = config.revisions.branches;
repos = await Promise.all(
repos.map(async (repo) => {
const [owner, name] = repo.name.split('/');
allRepos = await Promise.all(
allRepos.map(async (repo) => {
const [owner, name] = repo.full_name!.split('/');
let branches = (await getBranchesForRepo(owner, name, api)).map(branch => branch.name!);
branches = micromatch.match(branches, branchGlobs);
return {
...repo,
branches,
@ -101,27 +60,80 @@ export const getGiteaReposFromConfig = async (config: GiteaConfig, orgId: number
})
)
}
if (config.revisions.tags) {
const tagGlobs = config.revisions.tags;
repos = await Promise.all(
repos.map(async (repo) => {
const [owner, name] = repo.name.split('/');
allRepos = await Promise.all(
allRepos.map(async (allRepos) => {
const [owner, name] = allRepos.name!.split('/');
let tags = (await getTagsForRepo(owner, name, api)).map(tag => tag.name!);
tags = micromatch.match(tags, tagGlobs);
return {
...repo,
...allRepos,
tags,
};
})
)
}
}
let repos = allRepos
.filter((repo) => {
const isExcluded = shouldExcludeRepo({
repo,
exclude: config.exclude,
});
return !isExcluded;
});
logger.debug(`Found ${repos.length} total repositories.`);
return repos;
}
const shouldExcludeRepo = ({
repo,
exclude
} : {
repo: GiteaRepository,
exclude?: {
forks?: boolean,
archived?: boolean,
repos?: string[],
}
}) => {
let reason = '';
const repoName = repo.full_name!;
const shouldExclude = (() => {
if (!!exclude?.forks && repo.fork) {
reason = `\`exclude.forks\` is true`;
return true;
}
if (!!exclude?.archived && !!repo.archived) {
reason = `\`exclude.archived\` is true`;
return true;
}
if (exclude?.repos) {
if (micromatch.isMatch(repoName, exclude.repos)) {
reason = `\`exclude.repos\` contains ${repoName}`;
return true;
}
}
return false;
})();
if (shouldExclude) {
logger.debug(`Excluding repo ${repoName}. Reason: ${reason}`);
}
return shouldExclude;
}
const getTagsForRepo = async <T>(owner: string, repo: string, api: Api<T>) => {
try {
logger.debug(`Fetching tags for repo ${owner}/${repo}...`);

View file

@ -1,16 +1,15 @@
import { Gitlab, ProjectSchema } from "@gitbeaker/rest";
import micromatch from "micromatch";
import { createLogger } from "./logger.js";
import { GitLabConfig } from "@sourcebot/schemas/v2/index.type"
import { AppContext } from "./types.js";
import { GitlabConnectionConfig } from "@sourcebot/schemas/v3/gitlab.type"
import { getTokenFromConfig, measure } from "./utils.js";
import { PrismaClient } from "@sourcebot/db";
const logger = createLogger("GitLab");
export const GITLAB_CLOUD_HOSTNAME = "gitlab.com";
export const getGitLabReposFromConfig = async (config: GitLabConfig, orgId: number, ctx: AppContext) => {
// TODO: pass in DB here to fetch secret properly
const token = config.token ? await getTokenFromConfig(config.token, orgId) : undefined;
export const getGitLabReposFromConfig = async (config: GitlabConnectionConfig, orgId: number, db: PrismaClient) => {
const token = config.token ? await getTokenFromConfig(config.token, orgId, db) : undefined;
const api = new Gitlab({
...(config.token ? {
token,
@ -37,7 +36,7 @@ export const getGitLabReposFromConfig = async (config: GitLabConfig, orgId: numb
logger.error(`Failed to fetch all projects visible in ${config.url}.`, e);
}
} else {
logger.warn(`Ignoring option all:true in ${ctx.configPath} : host is ${GITLAB_CLOUD_HOSTNAME}`);
logger.warn(`Ignoring option all:true in config : host is ${GITLAB_CLOUD_HOSTNAME}`);
}
}
@ -119,9 +118,9 @@ export const shouldExcludeProject = ({
}: {
project: ProjectSchema,
include?: {
topics?: GitLabConfig['topics'],
topics?: GitlabConnectionConfig['topics'],
},
exclude?: GitLabConfig['exclude'],
exclude?: GitlabConnectionConfig['exclude'],
}) => {
const projectName = project.path_with_namespace;
let reason = '';

View file

@ -0,0 +1,208 @@
import { GithubConnectionConfig } from '@sourcebot/schemas/v3/github.type';
import { getGitHubReposFromConfig } from "./github.js";
import { getGitLabReposFromConfig } from "./gitlab.js";
import { getGiteaReposFromConfig } from "./gitea.js";
import { getGerritReposFromConfig } from "./gerrit.js";
import { Prisma, PrismaClient } from '@sourcebot/db';
import { WithRequired } from "./types.js"
import { marshalBool } from "./utils.js";
import { GerritConnectionConfig, GiteaConnectionConfig, GitlabConnectionConfig } from '@sourcebot/schemas/v3/connection.type';
export type RepoData = WithRequired<Prisma.RepoCreateInput, 'connections'>;
export const compileGithubConfig = async (
config: GithubConnectionConfig,
connectionId: number,
orgId: number,
db: PrismaClient,
abortController: AbortController): Promise<RepoData[]> => {
const gitHubRepos = await getGitHubReposFromConfig(config, orgId, db, abortController.signal);
const hostUrl = config.url ?? 'https://github.com';
const hostname = config.url ? new URL(config.url).hostname : 'github.com';
return gitHubRepos.map((repo) => {
const repoName = `${hostname}/${repo.full_name}`;
const cloneUrl = new URL(repo.clone_url!);
const record: RepoData = {
external_id: repo.id.toString(),
external_codeHostType: 'github',
external_codeHostUrl: hostUrl,
cloneUrl: cloneUrl.toString(),
name: repoName,
isFork: repo.fork,
isArchived: !!repo.archived,
org: {
connect: {
id: orgId,
},
},
connections: {
create: {
connectionId: connectionId,
}
},
metadata: {
'zoekt.web-url-type': 'github',
'zoekt.web-url': repo.html_url,
'zoekt.name': repoName,
'zoekt.github-stars': (repo.stargazers_count ?? 0).toString(),
'zoekt.github-watchers': (repo.watchers_count ?? 0).toString(),
'zoekt.github-subscribers': (repo.subscribers_count ?? 0).toString(),
'zoekt.github-forks': (repo.forks_count ?? 0).toString(),
'zoekt.archived': marshalBool(repo.archived),
'zoekt.fork': marshalBool(repo.fork),
'zoekt.public': marshalBool(repo.private === false)
},
};
return record;
})
}
export const compileGitlabConfig = async (
config: GitlabConnectionConfig,
connectionId: number,
orgId: number,
db: PrismaClient) => {
const gitlabRepos = await getGitLabReposFromConfig(config, orgId, db);
const hostUrl = config.url ?? 'https://gitlab.com';
return gitlabRepos.map((project) => {
const projectUrl = `${hostUrl}/${project.path_with_namespace}`;
const cloneUrl = new URL(project.http_url_to_repo);
const isFork = project.forked_from_project !== undefined;
const record: RepoData = {
external_id: project.id.toString(),
external_codeHostType: 'gitlab',
external_codeHostUrl: hostUrl,
cloneUrl: cloneUrl.toString(),
name: project.path_with_namespace,
isFork: isFork,
isArchived: !!project.archived,
org: {
connect: {
id: orgId,
},
},
connections: {
create: {
connectionId: connectionId,
}
},
metadata: {
'zoekt.web-url-type': 'gitlab',
'zoekt.web-url': projectUrl,
'zoekt.name': project.path_with_namespace,
'zoekt.gitlab-stars': (project.stargazers_count ?? 0).toString(),
'zoekt.gitlab-forks': (project.forks_count ?? 0).toString(),
'zoekt.archived': marshalBool(project.archived),
'zoekt.fork': marshalBool(isFork),
'zoekt.public': marshalBool(project.private === false)
},
};
return record;
})
}
export const compileGiteaConfig = async (
config: GiteaConnectionConfig,
connectionId: number,
orgId: number,
db: PrismaClient) => {
const giteaRepos = await getGiteaReposFromConfig(config, orgId, db);
const hostUrl = config.url ?? 'https://gitea.com';
return giteaRepos.map((repo) => {
const repoUrl = `${hostUrl}/${repo.full_name}`;
const cloneUrl = new URL(repo.clone_url!);
const record: RepoData = {
external_id: repo.id!.toString(),
external_codeHostType: 'gitea',
external_codeHostUrl: hostUrl,
cloneUrl: cloneUrl.toString(),
name: repo.full_name!,
isFork: repo.fork!,
isArchived: !!repo.archived,
org: {
connect: {
id: orgId,
},
},
connections: {
create: {
connectionId: connectionId,
}
},
metadata: {
'zoekt.web-url-type': 'gitea',
'zoekt.web-url': repo.html_url!,
'zoekt.name': repo.full_name!,
'zoekt.archived': marshalBool(repo.archived),
'zoekt.fork': marshalBool(repo.fork!),
'zoekt.public': marshalBool(repo.internal === false && repo.private === false),
},
};
return record;
})
}
export const compileGerritConfig = async (
config: GerritConnectionConfig,
connectionId: number,
orgId: number) => {
const gerritRepos = await getGerritReposFromConfig(config);
const hostUrl = config.url ?? 'https://gerritcodereview.com';
const hostname = new URL(hostUrl).hostname;
return gerritRepos.map((project) => {
const repoId = `${hostname}/${project.name}`;
const cloneUrl = new URL(`${config.url}/${encodeURIComponent(project.name)}`);
let webUrl = "https://www.gerritcodereview.com/";
// Gerrit projects can have multiple web links; use the first one
if (project.web_links) {
const webLink = project.web_links[0];
if (webLink) {
webUrl = webLink.url;
}
}
const record: RepoData = {
external_id: project.id.toString(),
external_codeHostType: 'gerrit',
external_codeHostUrl: hostUrl,
cloneUrl: cloneUrl.toString(),
name: project.name,
isFork: false,
isArchived: false,
org: {
connect: {
id: orgId,
},
},
connections: {
create: {
connectionId: connectionId,
}
},
metadata: {
'zoekt.web-url-type': 'gitiles',
'zoekt.web-url': webUrl,
'zoekt.name': repoId,
'zoekt.archived': marshalBool(false),
'zoekt.fork': marshalBool(false),
'zoekt.public': marshalBool(true),
},
};
return record;
})
}

View file

@ -2,7 +2,7 @@ import { Job, Queue, Worker } from 'bullmq';
import { Redis } from 'ioredis';
import { createLogger } from "./logger.js";
import { Connection, PrismaClient, Repo, RepoToConnection, RepoIndexingStatus } from "@sourcebot/db";
import { ConnectionConfig } from '@sourcebot/schemas/v3/connection.type';
import { GithubConnectionConfig, GitlabConnectionConfig, GiteaConnectionConfig } from '@sourcebot/schemas/v3/connection.type';
import { AppContext, Settings } from "./types.js";
import { captureEvent } from "./posthog.js";
import { getRepoPath, getTokenFromConfig, measure, getShardPrefix } from "./utils.js";
@ -74,7 +74,7 @@ export class RepoManager implements IRepoManager {
const repos = await this.db.repo.findMany({
where: {
repoIndexingStatus: {
notIn: [RepoIndexingStatus.IN_INDEX_QUEUE, RepoIndexingStatus.FAILED]
notIn: [RepoIndexingStatus.IN_INDEX_QUEUE, RepoIndexingStatus.INDEXING, RepoIndexingStatus.FAILED]
},
OR: [
{ indexedAt: null },
@ -147,10 +147,15 @@ export class RepoManager implements IRepoManager {
return;
}
let token: string | undefined;
for (const repoConnection of repoConnections) {
const connection = repoConnection.connection;
const config = connection.config as unknown as ConnectionConfig;
if (connection.connectionType !== 'github' && connection.connectionType !== 'gitlab' && connection.connectionType !== 'gitea') {
continue;
}
const config = connection.config as unknown as GithubConnectionConfig | GitlabConnectionConfig | GiteaConnectionConfig;
if (config.token) {
token = await getTokenFromConfig(config.token, connection.orgId, db);
if (token) {

View file

@ -218,7 +218,7 @@ const schema = {
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "GitLabConnectionConfig",
"title": "GitlabConnectionConfig",
"properties": {
"type": {
"const": "gitlab",
@ -352,6 +352,233 @@ const schema = {
"type"
],
"additionalProperties": false
},
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "GiteaConnectionConfig",
"properties": {
"type": {
"const": "gitea",
"description": "Gitea Configuration"
},
"token": {
"$ref": "#/oneOf/0/properties/token",
"description": "A Personal Access Token (PAT).",
"examples": [
"secret-token",
{
"env": "ENV_VAR_CONTAINING_TOKEN"
}
]
},
"url": {
"type": "string",
"format": "url",
"default": "https://gitea.com",
"description": "The URL of the Gitea host. Defaults to https://gitea.com",
"examples": [
"https://gitea.com",
"https://gitea.example.com"
],
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$"
},
"orgs": {
"type": "array",
"items": {
"type": "string"
},
"examples": [
[
"my-org-name"
]
],
"description": "List of organizations to sync with. All repositories in the organization visible to the provided `token` (if any) will be synced, unless explicitly defined in the `exclude` property. If a `token` is provided, it must have the read:organization scope."
},
"repos": {
"type": "array",
"items": {
"type": "string",
"pattern": "^[\\w.-]+\\/[\\w.-]+$"
},
"description": "List of individual repositories to sync with. Expected to be formatted as '{orgName}/{repoName}' or '{userName}/{repoName}'."
},
"users": {
"type": "array",
"items": {
"type": "string"
},
"examples": [
[
"username-1",
"username-2"
]
],
"description": "List of users to sync with. All repositories that the user owns will be synced, unless explicitly defined in the `exclude` property. If a `token` is provided, it must have the read:user scope."
},
"exclude": {
"type": "object",
"properties": {
"forks": {
"type": "boolean",
"default": false,
"description": "Exclude forked repositories from syncing."
},
"archived": {
"type": "boolean",
"default": false,
"description": "Exclude archived repositories from syncing."
},
"repos": {
"type": "array",
"items": {
"type": "string"
},
"default": [],
"description": "List of individual repositories to exclude from syncing. Glob patterns are supported."
}
},
"additionalProperties": false
},
"revisions": {
"$ref": "#/oneOf/0/properties/revisions"
}
},
"required": [
"type"
],
"additionalProperties": false
},
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "GerritConnectionConfig",
"properties": {
"type": {
"const": "gerrit",
"description": "Gerrit Configuration"
},
"url": {
"type": "string",
"format": "url",
"description": "The URL of the Gerrit host.",
"examples": [
"https://gerrit.example.com"
],
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$"
},
"projects": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of specific projects to sync. If not specified, all projects will be synced. Glob patterns are supported",
"examples": [
[
"project1/repo1",
"project2/**"
]
]
},
"exclude": {
"type": "object",
"properties": {
"projects": {
"type": "array",
"items": {
"type": "string"
},
"examples": [
[
"project1/repo1",
"project2/**"
]
],
"description": "List of specific projects to exclude from syncing."
}
},
"additionalProperties": false
}
},
"required": [
"type",
"url"
],
"additionalProperties": false
},
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "GitConnectionConfig",
"properties": {
"type": {
"const": "git",
"description": "Git Configuration"
},
"url": {
"type": "string",
"format": "url",
"description": "The URL to the git repository."
},
"revisions": {
"$ref": "#/oneOf/0/properties/revisions"
}
},
"required": [
"type",
"url"
],
"additionalProperties": false
},
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "GerritConnectionConfig",
"properties": {
"type": {
"const": "local",
"description": "Local Configuration"
},
"path": {
"type": "string",
"description": "Path to the local directory to sync with. Relative paths are relative to the configuration file's directory.",
"pattern": ".+"
},
"watch": {
"type": "boolean",
"default": true,
"description": "Enables a file watcher that will automatically re-sync when changes are made within `path` (recursively). Defaults to true."
},
"exclude": {
"type": "object",
"properties": {
"paths": {
"type": "array",
"items": {
"type": "string",
"pattern": ".+"
},
"description": "List of paths relative to the provided `path` to exclude from the index. .git, .hg, and .svn are always exluded.",
"default": [],
"examples": [
[
"node_modules",
"bin",
"dist",
"build",
"out"
]
]
}
},
"additionalProperties": false
}
},
"required": [
"type",
"path"
],
"additionalProperties": false
}
]
} as const;

View file

@ -1,6 +1,12 @@
// THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY!
export type ConnectionConfig = GithubConnectionConfig | GitLabConnectionConfig;
export type ConnectionConfig =
| GithubConnectionConfig
| GitlabConnectionConfig
| GiteaConnectionConfig
| GerritConnectionConfig
| GitConnectionConfig
| GerritConnectionConfig1;
export interface GithubConnectionConfig {
/**
@ -92,7 +98,7 @@ export interface GitRevisions {
*/
tags?: string[];
}
export interface GitLabConnectionConfig {
export interface GitlabConnectionConfig {
/**
* GitLab Configuration
*/
@ -160,3 +166,108 @@ export interface GitLabConnectionConfig {
};
revisions?: GitRevisions;
}
export interface GiteaConnectionConfig {
/**
* Gitea Configuration
*/
type: "gitea";
/**
* A Personal Access Token (PAT).
*/
token?:
| string
| {
/**
* The name of the environment variable that contains the token.
*/
env: string;
}
| {
/**
* The name of the secret that contains the token.
*/
secret: string;
};
/**
* The URL of the Gitea host. Defaults to https://gitea.com
*/
url?: string;
/**
* List of organizations to sync with. All repositories in the organization visible to the provided `token` (if any) will be synced, unless explicitly defined in the `exclude` property. If a `token` is provided, it must have the read:organization scope.
*/
orgs?: string[];
/**
* List of individual repositories to sync with. Expected to be formatted as '{orgName}/{repoName}' or '{userName}/{repoName}'.
*/
repos?: string[];
/**
* List of users to sync with. All repositories that the user owns will be synced, unless explicitly defined in the `exclude` property. If a `token` is provided, it must have the read:user scope.
*/
users?: string[];
exclude?: {
/**
* Exclude forked repositories from syncing.
*/
forks?: boolean;
/**
* Exclude archived repositories from syncing.
*/
archived?: boolean;
/**
* List of individual repositories to exclude from syncing. Glob patterns are supported.
*/
repos?: string[];
};
revisions?: GitRevisions;
}
export interface GerritConnectionConfig {
/**
* Gerrit Configuration
*/
type: "gerrit";
/**
* The URL of the Gerrit host.
*/
url: string;
/**
* List of specific projects to sync. If not specified, all projects will be synced. Glob patterns are supported
*/
projects?: string[];
exclude?: {
/**
* List of specific projects to exclude from syncing.
*/
projects?: string[];
};
}
export interface GitConnectionConfig {
/**
* Git Configuration
*/
type: "git";
/**
* The URL to the git repository.
*/
url: string;
revisions?: GitRevisions;
}
export interface GerritConnectionConfig1 {
/**
* Local Configuration
*/
type: "local";
/**
* Path to the local directory to sync with. Relative paths are relative to the configuration file's directory.
*/
path: string;
/**
* Enables a file watcher that will automatically re-sync when changes are made within `path` (recursively). Defaults to true.
*/
watch?: boolean;
exclude?: {
/**
* List of paths relative to the provided `path` to exclude from the index. .git, .hg, and .svn are always exluded.
*/
paths?: string[];
};
}

View file

@ -0,0 +1,59 @@
// THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY!
const schema = {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "GerritConnectionConfig",
"properties": {
"type": {
"const": "gerrit",
"description": "Gerrit Configuration"
},
"url": {
"type": "string",
"format": "url",
"description": "The URL of the Gerrit host.",
"examples": [
"https://gerrit.example.com"
],
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$"
},
"projects": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of specific projects to sync. If not specified, all projects will be synced. Glob patterns are supported",
"examples": [
[
"project1/repo1",
"project2/**"
]
]
},
"exclude": {
"type": "object",
"properties": {
"projects": {
"type": "array",
"items": {
"type": "string"
},
"examples": [
[
"project1/repo1",
"project2/**"
]
],
"description": "List of specific projects to exclude from syncing."
}
},
"additionalProperties": false
}
},
"required": [
"type",
"url"
],
"additionalProperties": false
} as const;
export { schema as gerritSchema };

View file

@ -0,0 +1,22 @@
// THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY!
export interface GerritConnectionConfig {
/**
* Gerrit Configuration
*/
type: "gerrit";
/**
* The URL of the Gerrit host.
*/
url: string;
/**
* List of specific projects to sync. If not specified, all projects will be synced. Glob patterns are supported
*/
projects?: string[];
exclude?: {
/**
* List of specific projects to exclude from syncing.
*/
projects?: string[];
};
}

View file

@ -0,0 +1,64 @@
// THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY!
const schema = {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "GitConnectionConfig",
"properties": {
"type": {
"const": "git",
"description": "Git Configuration"
},
"url": {
"type": "string",
"format": "url",
"description": "The URL to the git repository."
},
"revisions": {
"type": "object",
"description": "The revisions (branches, tags) that should be included when indexing. The default branch (HEAD) is always indexed.",
"properties": {
"branches": {
"type": "array",
"description": "List of branches to include when indexing. For a given repo, only the branches that exist on the repo's remote *and* match at least one of the provided `branches` will be indexed. The default branch (HEAD) is always indexed. Glob patterns are supported.",
"items": {
"type": "string"
},
"examples": [
[
"main",
"release/*"
],
[
"**"
]
],
"default": []
},
"tags": {
"type": "array",
"description": "List of tags to include when indexing. For a given repo, only the tags that exist on the repo's remote *and* match at least one of the provided `tags` will be indexed. Glob patterns are supported.",
"items": {
"type": "string"
},
"examples": [
[
"latest",
"v2.*.*"
],
[
"**"
]
],
"default": []
}
},
"additionalProperties": false
}
},
"required": [
"type",
"url"
],
"additionalProperties": false
} as const;
export { schema as gitSchema };

View file

@ -0,0 +1,26 @@
// THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY!
export interface GitConnectionConfig {
/**
* Git Configuration
*/
type: "git";
/**
* The URL to the git repository.
*/
url: string;
revisions?: GitRevisions;
}
/**
* The revisions (branches, tags) that should be included when indexing. The default branch (HEAD) is always indexed.
*/
export interface GitRevisions {
/**
* List of branches to include when indexing. For a given repo, only the branches that exist on the repo's remote *and* match at least one of the provided `branches` will be indexed. The default branch (HEAD) is always indexed. Glob patterns are supported.
*/
branches?: string[];
/**
* List of tags to include when indexing. For a given repo, only the tags that exist on the repo's remote *and* match at least one of the provided `tags` will be indexed. Glob patterns are supported.
*/
tags?: string[];
}

View file

@ -0,0 +1,166 @@
// THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY!
const schema = {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "GiteaConnectionConfig",
"properties": {
"type": {
"const": "gitea",
"description": "Gitea Configuration"
},
"token": {
"description": "A Personal Access Token (PAT).",
"examples": [
"secret-token",
{
"env": "ENV_VAR_CONTAINING_TOKEN"
}
],
"anyOf": [
{
"type": "string"
},
{
"type": "object",
"properties": {
"env": {
"type": "string",
"description": "The name of the environment variable that contains the token."
}
},
"required": [
"env"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"secret": {
"type": "string",
"description": "The name of the secret that contains the token."
}
},
"required": [
"secret"
],
"additionalProperties": false
}
]
},
"url": {
"type": "string",
"format": "url",
"default": "https://gitea.com",
"description": "The URL of the Gitea host. Defaults to https://gitea.com",
"examples": [
"https://gitea.com",
"https://gitea.example.com"
],
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$"
},
"orgs": {
"type": "array",
"items": {
"type": "string"
},
"examples": [
[
"my-org-name"
]
],
"description": "List of organizations to sync with. All repositories in the organization visible to the provided `token` (if any) will be synced, unless explicitly defined in the `exclude` property. If a `token` is provided, it must have the read:organization scope."
},
"repos": {
"type": "array",
"items": {
"type": "string",
"pattern": "^[\\w.-]+\\/[\\w.-]+$"
},
"description": "List of individual repositories to sync with. Expected to be formatted as '{orgName}/{repoName}' or '{userName}/{repoName}'."
},
"users": {
"type": "array",
"items": {
"type": "string"
},
"examples": [
[
"username-1",
"username-2"
]
],
"description": "List of users to sync with. All repositories that the user owns will be synced, unless explicitly defined in the `exclude` property. If a `token` is provided, it must have the read:user scope."
},
"exclude": {
"type": "object",
"properties": {
"forks": {
"type": "boolean",
"default": false,
"description": "Exclude forked repositories from syncing."
},
"archived": {
"type": "boolean",
"default": false,
"description": "Exclude archived repositories from syncing."
},
"repos": {
"type": "array",
"items": {
"type": "string"
},
"default": [],
"description": "List of individual repositories to exclude from syncing. Glob patterns are supported."
}
},
"additionalProperties": false
},
"revisions": {
"type": "object",
"description": "The revisions (branches, tags) that should be included when indexing. The default branch (HEAD) is always indexed.",
"properties": {
"branches": {
"type": "array",
"description": "List of branches to include when indexing. For a given repo, only the branches that exist on the repo's remote *and* match at least one of the provided `branches` will be indexed. The default branch (HEAD) is always indexed. Glob patterns are supported.",
"items": {
"type": "string"
},
"examples": [
[
"main",
"release/*"
],
[
"**"
]
],
"default": []
},
"tags": {
"type": "array",
"description": "List of tags to include when indexing. For a given repo, only the tags that exist on the repo's remote *and* match at least one of the provided `tags` will be indexed. Glob patterns are supported.",
"items": {
"type": "string"
},
"examples": [
[
"latest",
"v2.*.*"
],
[
"**"
]
],
"default": []
}
},
"additionalProperties": false
}
},
"required": [
"type"
],
"additionalProperties": false
} as const;
export { schema as giteaSchema };

View file

@ -0,0 +1,69 @@
// THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY!
export interface GiteaConnectionConfig {
/**
* Gitea Configuration
*/
type: "gitea";
/**
* A Personal Access Token (PAT).
*/
token?:
| string
| {
/**
* The name of the environment variable that contains the token.
*/
env: string;
}
| {
/**
* The name of the secret that contains the token.
*/
secret: string;
};
/**
* The URL of the Gitea host. Defaults to https://gitea.com
*/
url?: string;
/**
* List of organizations to sync with. All repositories in the organization visible to the provided `token` (if any) will be synced, unless explicitly defined in the `exclude` property. If a `token` is provided, it must have the read:organization scope.
*/
orgs?: string[];
/**
* List of individual repositories to sync with. Expected to be formatted as '{orgName}/{repoName}' or '{userName}/{repoName}'.
*/
repos?: string[];
/**
* List of users to sync with. All repositories that the user owns will be synced, unless explicitly defined in the `exclude` property. If a `token` is provided, it must have the read:user scope.
*/
users?: string[];
exclude?: {
/**
* Exclude forked repositories from syncing.
*/
forks?: boolean;
/**
* Exclude archived repositories from syncing.
*/
archived?: boolean;
/**
* List of individual repositories to exclude from syncing. Glob patterns are supported.
*/
repos?: string[];
};
revisions?: GitRevisions;
}
/**
* The revisions (branches, tags) that should be included when indexing. The default branch (HEAD) is always indexed.
*/
export interface GitRevisions {
/**
* List of branches to include when indexing. For a given repo, only the branches that exist on the repo's remote *and* match at least one of the provided `branches` will be indexed. The default branch (HEAD) is always indexed. Glob patterns are supported.
*/
branches?: string[];
/**
* List of tags to include when indexing. For a given repo, only the tags that exist on the repo's remote *and* match at least one of the provided `tags` will be indexed. Glob patterns are supported.
*/
tags?: string[];
}

View file

@ -2,7 +2,7 @@
const schema = {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "GitLabConnectionConfig",
"title": "GitlabConnectionConfig",
"properties": {
"type": {
"const": "gitlab",

View file

@ -1,6 +1,6 @@
// THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY!
export interface GitLabConnectionConfig {
export interface GitlabConnectionConfig {
/**
* GitLab Configuration
*/

View file

@ -0,0 +1,52 @@
// THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY!
const schema = {
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "GerritConnectionConfig",
"properties": {
"type": {
"const": "local",
"description": "Local Configuration"
},
"path": {
"type": "string",
"description": "Path to the local directory to sync with. Relative paths are relative to the configuration file's directory.",
"pattern": ".+"
},
"watch": {
"type": "boolean",
"default": true,
"description": "Enables a file watcher that will automatically re-sync when changes are made within `path` (recursively). Defaults to true."
},
"exclude": {
"type": "object",
"properties": {
"paths": {
"type": "array",
"items": {
"type": "string",
"pattern": ".+"
},
"description": "List of paths relative to the provided `path` to exclude from the index. .git, .hg, and .svn are always exluded.",
"default": [],
"examples": [
[
"node_modules",
"bin",
"dist",
"build",
"out"
]
]
}
},
"additionalProperties": false
}
},
"required": [
"type",
"path"
],
"additionalProperties": false
} as const;
export { schema as localSchema };

View file

@ -0,0 +1,22 @@
// THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY!
export interface GerritConnectionConfig {
/**
* Local Configuration
*/
type: "local";
/**
* Path to the local directory to sync with. Relative paths are relative to the configuration file's directory.
*/
path: string;
/**
* Enables a file watcher that will automatically re-sync when changes are made within `path` (recursively). Defaults to true.
*/
watch?: boolean;
exclude?: {
/**
* List of paths relative to the provided `path` to exclude from the index. .git, .hg, and .svn are always exluded.
*/
paths?: string[];
};
}

View file

@ -9,6 +9,8 @@ import { ErrorCode } from "@/lib/errorCodes";
import { isServiceError } from "@/lib/utils";
import { githubSchema } from "@sourcebot/schemas/v3/github.schema";
import { gitlabSchema } from "@sourcebot/schemas/v3/gitlab.schema";
import { giteaSchema } from "@sourcebot/schemas/v3/gitea.schema";
import { gerritSchema } from "@sourcebot/schemas/v3/gerrit.schema";
import { ConnectionConfig } from "@sourcebot/schemas/v3/connection.type";
import { encrypt } from "@sourcebot/crypto"
import { getConnection } from "./data/connection";
@ -515,6 +517,10 @@ const parseConnectionConfig = (connectionType: string, config: string) => {
return githubSchema;
case "gitlab":
return gitlabSchema;
case 'gitea':
return giteaSchema;
case 'gerrit':
return gerritSchema;
}
})();

View file

@ -11,14 +11,18 @@ import { z } from "zod";
import { ConfigEditor, QuickAction } from "../../components/configEditor";
import { createZodConnectionConfigValidator } from "../../utils";
import { GithubConnectionConfig } from "@sourcebot/schemas/v3/github.type";
import { githubQuickActions, gitlabQuickActions } from "../../quickActions";
import { GiteaConnectionConfig } from "@sourcebot/schemas/v3/gitea.type";
import { GerritConnectionConfig } from "@sourcebot/schemas/v3/gerrit.type";
import { githubQuickActions, gitlabQuickActions, giteaQuickActions, gerritQuickActions } from "../../quickActions";
import { Schema } from "ajv";
import { GitLabConnectionConfig } from "@sourcebot/schemas/v3/gitlab.type";
import { GitlabConnectionConfig } from "@sourcebot/schemas/v3/gitlab.type";
import { gitlabSchema } from "@sourcebot/schemas/v3/gitlab.schema";
import { updateConnectionConfigAndScheduleSync } from "@/actions";
import { useToast } from "@/components/hooks/use-toast";
import { isServiceError } from "@/lib/utils";
import { useRouter } from "next/navigation";
import { giteaSchema } from "@sourcebot/schemas/v3/gitea.schema";
import { gerritSchema } from "@sourcebot/schemas/v3/gerrit.schema";
import { useDomain } from "@/hooks/useDomain";
@ -40,13 +44,29 @@ export const ConfigSetting = (props: ConfigSettingProps) => {
}
if (type === 'gitlab') {
return <ConfigSettingInternal<GitLabConnectionConfig>
return <ConfigSettingInternal<GitlabConnectionConfig>
{...props}
quickActions={gitlabQuickActions}
schema={gitlabSchema}
/>;
}
if (type === 'gitea') {
return <ConfigSettingInternal<GiteaConnectionConfig>
{...props}
quickActions={giteaQuickActions}
schema={giteaSchema}
/>;
}
if (type === 'gerrit') {
return <ConfigSettingInternal<GerritConnectionConfig>
{...props}
quickActions={gerritQuickActions}
schema={gerritSchema}
/>;
}
return null;
}

View file

@ -19,7 +19,7 @@ import { ConfigEditor, QuickActionFn } from "../../../components/configEditor";
import { useDomain } from "@/hooks/useDomain";
interface ConnectionCreationForm<T> {
type: 'github' | 'gitlab';
type: 'github' | 'gitlab' | 'gitea' | 'gerrit';
defaultValues: {
name: string;
config: string;

View file

@ -1,10 +1,14 @@
'use client';
import { githubQuickActions, gitlabQuickActions } from "../../quickActions";
import { gerritQuickActions, giteaQuickActions, githubQuickActions, gitlabQuickActions } from "../../quickActions";
import ConnectionCreationForm from "./components/connectionCreationForm";
import { GitLabConnectionConfig } from "@sourcebot/schemas/v3/gitlab.type";
import { GitlabConnectionConfig } from "@sourcebot/schemas/v3/gitlab.type";
import { GiteaConnectionConfig } from "@sourcebot/schemas/v3/gitea.type";
import { GerritConnectionConfig } from "@sourcebot/schemas/v3/gerrit.type";
import { gitlabSchema } from "@sourcebot/schemas/v3/gitlab.schema";
import { githubSchema } from "@sourcebot/schemas/v3/github.schema";
import { giteaSchema } from "@sourcebot/schemas/v3/gitea.schema";
import { gerritSchema } from "@sourcebot/schemas/v3/gerrit.schema";
import { GithubConnectionConfig } from "@sourcebot/schemas/v3/github.type";
import { useRouter } from "next/navigation";
@ -22,16 +26,24 @@ export default function NewConnectionPage({
return <GitLabCreationForm />;
}
if (type === 'gitea') {
return <GiteaCreationForm />;
}
if (type === 'gerrit') {
return <GerritCreationForm />;
}
router.push('/connections');
}
const GitLabCreationForm = () => {
const defaultConfig: GitLabConnectionConfig = {
const defaultConfig: GitlabConnectionConfig = {
type: 'gitlab',
}
return (
<ConnectionCreationForm<GitLabConnectionConfig>
<ConnectionCreationForm<GitlabConnectionConfig>
type="gitlab"
title="Create a GitLab connection"
defaultValues={{
@ -61,4 +73,43 @@ const GitHubCreationForm = () => {
quickActions={githubQuickActions}
/>
)
}
}
const GiteaCreationForm = () => {
const defaultConfig: GiteaConnectionConfig = {
type: 'gitea',
}
return (
<ConnectionCreationForm<GiteaConnectionConfig>
type="gitea"
title="Create a Gitea connection"
defaultValues={{
config: JSON.stringify(defaultConfig, null, 2),
name: 'my-gitea-connection',
}}
schema={giteaSchema}
quickActions={giteaQuickActions}
/>
)
}
const GerritCreationForm = () => {
const defaultConfig: GerritConnectionConfig = {
type: 'gerrit',
url: "https://gerrit.example.com"
}
return (
<ConnectionCreationForm<GerritConnectionConfig>
type="gerrit"
title="Create a Gerrit connection"
defaultValues={{
config: JSON.stringify(defaultConfig, null, 2),
name: 'my-gerrit-connection',
}}
schema={gerritSchema}
quickActions={gerritQuickActions}
/>
)
}

View file

@ -1,6 +1,8 @@
import { GithubConnectionConfig } from "@sourcebot/schemas/v3/github.type"
import { GitLabConnectionConfig } from "@sourcebot/schemas/v3/gitlab.type";
import { GitlabConnectionConfig } from "@sourcebot/schemas/v3/gitlab.type";
import { QuickAction } from "./components/configEditor";
import { GiteaConnectionConfig } from "@sourcebot/schemas/v3/connection.type";
import { GerritConnectionConfig } from "@sourcebot/schemas/v3/gerrit.type";
export const githubQuickActions: QuickAction<GithubConnectionConfig>[] = [
{
@ -41,9 +43,9 @@ export const githubQuickActions: QuickAction<GithubConnectionConfig>[] = [
}
];
export const gitlabQuickActions: QuickAction<GitLabConnectionConfig>[] = [
export const gitlabQuickActions: QuickAction<GitlabConnectionConfig>[] = [
{
fn: (previous: GitLabConnectionConfig) => ({
fn: (previous: GitlabConnectionConfig) => ({
...previous,
groups: [
...previous.groups ?? [],
@ -53,14 +55,14 @@ export const gitlabQuickActions: QuickAction<GitLabConnectionConfig>[] = [
name: "Add a group",
},
{
fn: (previous: GitLabConnectionConfig) => ({
fn: (previous: GitlabConnectionConfig) => ({
...previous,
url: previous.url ?? "",
}),
name: "Set a custom url",
},
{
fn: (previous: GitLabConnectionConfig) => ({
fn: (previous: GitlabConnectionConfig) => ({
...previous,
token: previous.token ?? {
secret: "",
@ -69,7 +71,7 @@ export const gitlabQuickActions: QuickAction<GitLabConnectionConfig>[] = [
name: "Add a secret",
},
{
fn: (previous: GitLabConnectionConfig) => ({
fn: (previous: GitlabConnectionConfig) => ({
...previous,
projects: [
...previous.projects ?? [],
@ -80,3 +82,68 @@ export const gitlabQuickActions: QuickAction<GitLabConnectionConfig>[] = [
}
]
export const giteaQuickActions: QuickAction<GiteaConnectionConfig>[] = [
{
fn: (previous: GiteaConnectionConfig) => ({
...previous,
orgs: [
...(previous.orgs ?? []),
""
]
}),
name: "Add an organization",
},
{
fn: (previous: GiteaConnectionConfig) => ({
...previous,
url: previous.url ?? "",
}),
name: "Set a custom url",
},
{
fn: (previous: GiteaConnectionConfig) => ({
...previous,
repos: [
...(previous.repos ?? []),
""
]
}),
name: "Add a repo",
},
{
fn: (previous: GiteaConnectionConfig) => ({
...previous,
token: previous.token ?? {
secret: "",
},
}),
name: "Add a secret",
}
]
export const gerritQuickActions: QuickAction<GerritConnectionConfig>[] = [
{
fn: (previous: GerritConnectionConfig) => ({
...previous,
projects: [
...(previous.projects ?? []),
""
]
}),
name: "Add a project",
},
{
fn: (previous: GerritConnectionConfig) => ({
...previous,
exclude: {
...previous.exclude,
projects: [
...(previous.exclude?.projects ?? []),
""
]
}
}),
name: "Exclude a project",
}
]

View file

@ -109,7 +109,7 @@ export function OrgCreateForm({ setOrgCreateData }: OrgCreateFormProps) {
<FormControl>
<div className="flex items-center">
<Input placeholder="aperature-labs" {...field} className="w-1/2" />
<span className="ml-2">.sourcebot.dev</span>
<span className="ml-2">.sourcebot.app</span>
</div>
</FormControl>
<FormMessage />

View file

@ -7,6 +7,18 @@
},
{
"$ref": "./gitlab.json"
},
{
"$ref": "./gitea.json"
},
{
"$ref": "./gerrit.json"
},
{
"$ref": "./git.json"
},
{
"$ref": "./local.json"
}
]
}

57
schemas/v3/gerrit.json Normal file
View file

@ -0,0 +1,57 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "GerritConnectionConfig",
"properties": {
"type": {
"const": "gerrit",
"description": "Gerrit Configuration"
},
"url": {
"type": "string",
"format": "url",
"description": "The URL of the Gerrit host.",
"examples": [
"https://gerrit.example.com"
],
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$"
},
"projects": {
"type": "array",
"items": {
"type": "string"
},
"description": "List of specific projects to sync. If not specified, all projects will be synced. Glob patterns are supported",
"examples": [
[
"project1/repo1",
"project2/**"
]
]
},
"exclude": {
"type": "object",
"properties": {
"projects": {
"type": "array",
"items": {
"type": "string"
},
"examples": [
[
"project1/repo1",
"project2/**"
]
],
"description": "List of specific projects to exclude from syncing."
}
},
"additionalProperties": false
}
},
"required": [
"type",
"url"
],
"additionalProperties": false
}

24
schemas/v3/git.json Normal file
View file

@ -0,0 +1,24 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "GitConnectionConfig",
"properties": {
"type": {
"const": "git",
"description": "Git Configuration"
},
"url": {
"type": "string",
"format": "url",
"description": "The URL to the git repository."
},
"revisions": {
"$ref": "./shared.json#/definitions/GitRevisions"
}
},
"required": [
"type",
"url"
],
"additionalProperties": false
}

96
schemas/v3/gitea.json Normal file
View file

@ -0,0 +1,96 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "GiteaConnectionConfig",
"properties": {
"type": {
"const": "gitea",
"description": "Gitea Configuration"
},
"token": {
"$ref": "./shared.json#/definitions/Token",
"description": "A Personal Access Token (PAT).",
"examples": [
"secret-token",
{
"env": "ENV_VAR_CONTAINING_TOKEN"
}
]
},
"url": {
"type": "string",
"format": "url",
"default": "https://gitea.com",
"description": "The URL of the Gitea host. Defaults to https://gitea.com",
"examples": [
"https://gitea.com",
"https://gitea.example.com"
],
"pattern": "^https?:\\/\\/[^\\s/$.?#].[^\\s]*$"
},
"orgs": {
"type": "array",
"items": {
"type": "string"
},
"examples": [
[
"my-org-name"
]
],
"description": "List of organizations to sync with. All repositories in the organization visible to the provided `token` (if any) will be synced, unless explicitly defined in the `exclude` property. If a `token` is provided, it must have the read:organization scope."
},
"repos": {
"type": "array",
"items": {
"type": "string",
"pattern": "^[\\w.-]+\\/[\\w.-]+$"
},
"description": "List of individual repositories to sync with. Expected to be formatted as '{orgName}/{repoName}' or '{userName}/{repoName}'."
},
"users": {
"type": "array",
"items": {
"type": "string"
},
"examples": [
[
"username-1",
"username-2"
]
],
"description": "List of users to sync with. All repositories that the user owns will be synced, unless explicitly defined in the `exclude` property. If a `token` is provided, it must have the read:user scope."
},
"exclude": {
"type": "object",
"properties": {
"forks": {
"type": "boolean",
"default": false,
"description": "Exclude forked repositories from syncing."
},
"archived": {
"type": "boolean",
"default": false,
"description": "Exclude archived repositories from syncing."
},
"repos": {
"type": "array",
"items": {
"type": "string"
},
"default": [],
"description": "List of individual repositories to exclude from syncing. Glob patterns are supported."
}
},
"additionalProperties": false
},
"revisions": {
"$ref": "./shared.json#/definitions/GitRevisions"
}
},
"required": [
"type"
],
"additionalProperties": false
}

View file

@ -1,7 +1,7 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "GitLabConnectionConfig",
"title": "GitlabConnectionConfig",
"properties": {
"type": {
"const": "gitlab",

50
schemas/v3/local.json Normal file
View file

@ -0,0 +1,50 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"title": "GerritConnectionConfig",
"properties": {
"type": {
"const": "local",
"description": "Local Configuration"
},
"path": {
"type": "string",
"description": "Path to the local directory to sync with. Relative paths are relative to the configuration file's directory.",
"pattern": ".+"
},
"watch": {
"type": "boolean",
"default": true,
"description": "Enables a file watcher that will automatically re-sync when changes are made within `path` (recursively). Defaults to true."
},
"exclude": {
"type": "object",
"properties": {
"paths": {
"type": "array",
"items": {
"type": "string",
"pattern": ".+"
},
"description": "List of paths relative to the provided `path` to exclude from the index. .git, .hg, and .svn are always exluded.",
"default": [],
"examples": [
[
"node_modules",
"bin",
"dist",
"build",
"out"
]
]
}
},
"additionalProperties": false
}
},
"required": [
"type",
"path"
],
"additionalProperties": false
}