diff --git a/packages/backend/src/connectionManager.ts b/packages/backend/src/connectionManager.ts index 3fe5c0a7..a112b49b 100644 --- a/packages/backend/src/connectionManager.ts +++ b/packages/backend/src/connectionManager.ts @@ -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; @@ -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; - 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 diff --git a/packages/backend/src/gerrit.ts b/packages/backend/src/gerrit.ts index 21bd8e8b..9c4896da 100644 --- a/packages/backend/src/gerrit.ts +++ b/packages/backend/src/gerrit.ts @@ -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 => { +export const getGerritReposFromConfig = async (config: GerritConfig): Promise => { 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 => { +const fetchAllProjects = async (url: string): Promise => { 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 => { 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( diff --git a/packages/backend/src/gitea.ts b/packages/backend/src/gitea.ts index 8701b523..73bbd369 100644 --- a/packages/backend/src/gitea.ts +++ b/packages/backend/src/gitea.ts @@ -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); - } - - if (!!config.exclude.archived) { - repos = excludeArchivedRepos(repos, logger); - } - - if (config.exclude.repos) { - repos = excludeReposByName(repos, config.exclude.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; } - } - - logger.debug(`Found ${repos.length} total repositories.`); + return true; + }); + 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 (owner: string, repo: string, api: Api) => { try { logger.debug(`Fetching tags for repo ${owner}/${repo}...`); diff --git a/packages/backend/src/gitlab.ts b/packages/backend/src/gitlab.ts index 73ed5e92..dc607a80 100644 --- a/packages/backend/src/gitlab.ts +++ b/packages/backend/src/gitlab.ts @@ -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 = ''; diff --git a/packages/backend/src/repoCompileUtils.ts b/packages/backend/src/repoCompileUtils.ts new file mode 100644 index 00000000..55e9461a --- /dev/null +++ b/packages/backend/src/repoCompileUtils.ts @@ -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; + +export const compileGithubConfig = async ( + config: GithubConnectionConfig, + connectionId: number, + orgId: number, + db: PrismaClient, + abortController: AbortController): Promise => { + 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; + }) +} \ No newline at end of file diff --git a/packages/backend/src/repoManager.ts b/packages/backend/src/repoManager.ts index 0012b426..f576d2dd 100644 --- a/packages/backend/src/repoManager.ts +++ b/packages/backend/src/repoManager.ts @@ -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) { diff --git a/packages/schemas/src/v3/connection.schema.ts b/packages/schemas/src/v3/connection.schema.ts index dbef6edf..9a2d3587 100644 --- a/packages/schemas/src/v3/connection.schema.ts +++ b/packages/schemas/src/v3/connection.schema.ts @@ -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; diff --git a/packages/schemas/src/v3/connection.type.ts b/packages/schemas/src/v3/connection.type.ts index 4ef6d4b7..74a70d8f 100644 --- a/packages/schemas/src/v3/connection.type.ts +++ b/packages/schemas/src/v3/connection.type.ts @@ -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[]; + }; +} diff --git a/packages/schemas/src/v3/gerrit.schema.ts b/packages/schemas/src/v3/gerrit.schema.ts new file mode 100644 index 00000000..9ecca34a --- /dev/null +++ b/packages/schemas/src/v3/gerrit.schema.ts @@ -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 }; \ No newline at end of file diff --git a/packages/schemas/src/v3/gerrit.type.ts b/packages/schemas/src/v3/gerrit.type.ts new file mode 100644 index 00000000..735f87f6 --- /dev/null +++ b/packages/schemas/src/v3/gerrit.type.ts @@ -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[]; + }; +} diff --git a/packages/schemas/src/v3/git.schema.ts b/packages/schemas/src/v3/git.schema.ts new file mode 100644 index 00000000..3d8e889f --- /dev/null +++ b/packages/schemas/src/v3/git.schema.ts @@ -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 }; \ No newline at end of file diff --git a/packages/schemas/src/v3/git.type.ts b/packages/schemas/src/v3/git.type.ts new file mode 100644 index 00000000..b60d5832 --- /dev/null +++ b/packages/schemas/src/v3/git.type.ts @@ -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[]; +} diff --git a/packages/schemas/src/v3/gitea.schema.ts b/packages/schemas/src/v3/gitea.schema.ts new file mode 100644 index 00000000..57341345 --- /dev/null +++ b/packages/schemas/src/v3/gitea.schema.ts @@ -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 }; \ No newline at end of file diff --git a/packages/schemas/src/v3/gitea.type.ts b/packages/schemas/src/v3/gitea.type.ts new file mode 100644 index 00000000..09e1df19 --- /dev/null +++ b/packages/schemas/src/v3/gitea.type.ts @@ -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[]; +} diff --git a/packages/schemas/src/v3/gitlab.schema.ts b/packages/schemas/src/v3/gitlab.schema.ts index 8ed238ad..eeb90658 100644 --- a/packages/schemas/src/v3/gitlab.schema.ts +++ b/packages/schemas/src/v3/gitlab.schema.ts @@ -2,7 +2,7 @@ const schema = { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", - "title": "GitLabConnectionConfig", + "title": "GitlabConnectionConfig", "properties": { "type": { "const": "gitlab", diff --git a/packages/schemas/src/v3/gitlab.type.ts b/packages/schemas/src/v3/gitlab.type.ts index aa30c206..7fcbbbc8 100644 --- a/packages/schemas/src/v3/gitlab.type.ts +++ b/packages/schemas/src/v3/gitlab.type.ts @@ -1,6 +1,6 @@ // THIS IS A AUTO-GENERATED FILE. DO NOT MODIFY MANUALLY! -export interface GitLabConnectionConfig { +export interface GitlabConnectionConfig { /** * GitLab Configuration */ diff --git a/packages/schemas/src/v3/local.schema.ts b/packages/schemas/src/v3/local.schema.ts new file mode 100644 index 00000000..fce2c51c --- /dev/null +++ b/packages/schemas/src/v3/local.schema.ts @@ -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 }; \ No newline at end of file diff --git a/packages/schemas/src/v3/local.type.ts b/packages/schemas/src/v3/local.type.ts new file mode 100644 index 00000000..4551e3f9 --- /dev/null +++ b/packages/schemas/src/v3/local.type.ts @@ -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[]; + }; +} diff --git a/packages/web/src/actions.ts b/packages/web/src/actions.ts index 8fad44bc..351a30d7 100644 --- a/packages/web/src/actions.ts +++ b/packages/web/src/actions.ts @@ -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; } })(); diff --git a/packages/web/src/app/[domain]/connections/[id]/components/configSetting.tsx b/packages/web/src/app/[domain]/connections/[id]/components/configSetting.tsx index b81b233f..8d11f00c 100644 --- a/packages/web/src/app/[domain]/connections/[id]/components/configSetting.tsx +++ b/packages/web/src/app/[domain]/connections/[id]/components/configSetting.tsx @@ -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 + return {...props} quickActions={gitlabQuickActions} schema={gitlabSchema} />; } + if (type === 'gitea') { + return + {...props} + quickActions={giteaQuickActions} + schema={giteaSchema} + />; + } + + if (type === 'gerrit') { + return + {...props} + quickActions={gerritQuickActions} + schema={gerritSchema} + />; + } + return null; } diff --git a/packages/web/src/app/[domain]/connections/new/[type]/components/connectionCreationForm.tsx b/packages/web/src/app/[domain]/connections/new/[type]/components/connectionCreationForm.tsx index 02612fea..1fd27ee3 100644 --- a/packages/web/src/app/[domain]/connections/new/[type]/components/connectionCreationForm.tsx +++ b/packages/web/src/app/[domain]/connections/new/[type]/components/connectionCreationForm.tsx @@ -19,7 +19,7 @@ import { ConfigEditor, QuickActionFn } from "../../../components/configEditor"; import { useDomain } from "@/hooks/useDomain"; interface ConnectionCreationForm { - type: 'github' | 'gitlab'; + type: 'github' | 'gitlab' | 'gitea' | 'gerrit'; defaultValues: { name: string; config: string; diff --git a/packages/web/src/app/[domain]/connections/new/[type]/page.tsx b/packages/web/src/app/[domain]/connections/new/[type]/page.tsx index 6c16d113..b785ce55 100644 --- a/packages/web/src/app/[domain]/connections/new/[type]/page.tsx +++ b/packages/web/src/app/[domain]/connections/new/[type]/page.tsx @@ -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 ; } + if (type === 'gitea') { + return ; + } + + if (type === 'gerrit') { + return ; + } + router.push('/connections'); } const GitLabCreationForm = () => { - const defaultConfig: GitLabConnectionConfig = { + const defaultConfig: GitlabConnectionConfig = { type: 'gitlab', } return ( - + type="gitlab" title="Create a GitLab connection" defaultValues={{ @@ -61,4 +73,43 @@ const GitHubCreationForm = () => { quickActions={githubQuickActions} /> ) -} \ No newline at end of file +} + +const GiteaCreationForm = () => { + const defaultConfig: GiteaConnectionConfig = { + type: 'gitea', + } + + return ( + + 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 ( + + type="gerrit" + title="Create a Gerrit connection" + defaultValues={{ + config: JSON.stringify(defaultConfig, null, 2), + name: 'my-gerrit-connection', + }} + schema={gerritSchema} + quickActions={gerritQuickActions} + /> + ) +} diff --git a/packages/web/src/app/[domain]/connections/quickActions.ts b/packages/web/src/app/[domain]/connections/quickActions.ts index 5051dd07..21d20cb7 100644 --- a/packages/web/src/app/[domain]/connections/quickActions.ts +++ b/packages/web/src/app/[domain]/connections/quickActions.ts @@ -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[] = [ { @@ -41,9 +43,9 @@ export const githubQuickActions: QuickAction[] = [ } ]; -export const gitlabQuickActions: QuickAction[] = [ +export const gitlabQuickActions: QuickAction[] = [ { - fn: (previous: GitLabConnectionConfig) => ({ + fn: (previous: GitlabConnectionConfig) => ({ ...previous, groups: [ ...previous.groups ?? [], @@ -53,14 +55,14 @@ export const gitlabQuickActions: QuickAction[] = [ 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[] = [ name: "Add a secret", }, { - fn: (previous: GitLabConnectionConfig) => ({ + fn: (previous: GitlabConnectionConfig) => ({ ...previous, projects: [ ...previous.projects ?? [], @@ -80,3 +82,68 @@ export const gitlabQuickActions: QuickAction[] = [ } ] +export const giteaQuickActions: QuickAction[] = [ + { + 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[] = [ + { + 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", + } +] + diff --git a/packages/web/src/app/onboard/components/orgCreateForm.tsx b/packages/web/src/app/onboard/components/orgCreateForm.tsx index dcd689f9..eccfcc6d 100644 --- a/packages/web/src/app/onboard/components/orgCreateForm.tsx +++ b/packages/web/src/app/onboard/components/orgCreateForm.tsx @@ -109,7 +109,7 @@ export function OrgCreateForm({ setOrgCreateData }: OrgCreateFormProps) {
- .sourcebot.dev + .sourcebot.app
diff --git a/schemas/v3/connection.json b/schemas/v3/connection.json index 0e720d29..67700fb6 100644 --- a/schemas/v3/connection.json +++ b/schemas/v3/connection.json @@ -7,6 +7,18 @@ }, { "$ref": "./gitlab.json" + }, + { + "$ref": "./gitea.json" + }, + { + "$ref": "./gerrit.json" + }, + { + "$ref": "./git.json" + }, + { + "$ref": "./local.json" } ] } \ No newline at end of file diff --git a/schemas/v3/gerrit.json b/schemas/v3/gerrit.json new file mode 100644 index 00000000..fe18a97f --- /dev/null +++ b/schemas/v3/gerrit.json @@ -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 +} \ No newline at end of file diff --git a/schemas/v3/git.json b/schemas/v3/git.json new file mode 100644 index 00000000..f6c030ff --- /dev/null +++ b/schemas/v3/git.json @@ -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 +} \ No newline at end of file diff --git a/schemas/v3/gitea.json b/schemas/v3/gitea.json new file mode 100644 index 00000000..99fdeaf7 --- /dev/null +++ b/schemas/v3/gitea.json @@ -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 +} \ No newline at end of file diff --git a/schemas/v3/gitlab.json b/schemas/v3/gitlab.json index 1690929c..1d3f2918 100644 --- a/schemas/v3/gitlab.json +++ b/schemas/v3/gitlab.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", - "title": "GitLabConnectionConfig", + "title": "GitlabConnectionConfig", "properties": { "type": { "const": "gitlab", diff --git a/schemas/v3/local.json b/schemas/v3/local.json new file mode 100644 index 00000000..5be97d31 --- /dev/null +++ b/schemas/v3/local.json @@ -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 +} \ No newline at end of file