sourcebot/packages/backend/src/gerrit.ts
Konrad Staniszewski b452fd2983
Gerrit sync (#104)
* Basic gerrit sync with working gitiles web-links functionality

This adds basic support for gerrit repo code host syncing. Gerrit uses
gitiles plugin for code browsing (in most cases).
It may be usefull to allow users to provide their own web code-browsing
url templates in the future.

* Add gerrit readme update

* Remove config arg from gerrit fetchAllProjects

* Remove example urls

* Resolve comments
2024-12-02 16:07:02 -08:00

109 lines
3.5 KiB
TypeScript

import fetch from 'cross-fetch';
import { GerritConfig } from './schemas/v2.js';
import { AppContext, GitRepository } from './types.js';
import { createLogger } from './logger.js';
import path from 'path';
import { measure, marshalBool, excludeReposByName, includeReposByName } from './utils.js';
// https://gerrit-review.googlesource.com/Documentation/rest-api.html
interface GerritProjects {
[projectName: string]: GerritProjectInfo;
}
interface GerritProjectInfo {
id: string;
state?: string;
web_links?: GerritWebLink[];
}
interface GerritWebLink {
name: string;
url: string;
}
const logger = createLogger('Gerrit');
export const getGerritReposFromConfig = async (config: GerritConfig, ctx: AppContext) => {
const url = config.url.endsWith('/') ? config.url : `${config.url}/`;
const hostname = new URL(config.url).hostname;
const { durationMs, data: projects } = await measure(() =>
fetchAllProjects(url)
);
// exclude "All-Projects" and "All-Users" projects
delete projects['All-Projects'];
delete projects['All-Users'];
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;
});
// include repos by glob if specified in config
if (config.projects) {
repos = includeReposByName(repos, config.projects);
}
if (config.exclude && config.exclude.projects) {
repos = excludeReposByName(repos, config.exclude.projects);
}
return repos;
};
const fetchAllProjects = async (url: string): Promise<GerritProjects> => {
const projectsEndpoint = `${url}projects/`;
logger.debug(`Fetching projects from Gerrit at ${projectsEndpoint}...`);
const response = await fetch(projectsEndpoint);
if (!response.ok) {
throw new Error(`Failed to fetch projects from Gerrit: ${response.statusText}`);
}
const text = await response.text();
// Gerrit prepends ")]}'\n" to prevent XSSI attacks; remove it
// https://gerrit-review.googlesource.com/Documentation/rest-api.html
const jsonText = text.replace(")]}'\n", '');
const data = JSON.parse(jsonText);
return data;
};