Ein gemeinsames Interface (GitClient) mit drei Implementierungen. Jeder
Adapter liefert dieselbe normalisierte Struktur, sodass die Oberflaeche die
Unterschiede der APIs nicht kennen muss:
- GitHub – REST v3, Bearer-Token, funktioniert auch mit Enterprise (/api/v3).
open_issues_count enthaelt dort PRs, das wird herausgerechnet.
- GitLab – API v4, PRIVATE-TOKEN, Projekte ueber den URL-kodierten Pfad,
damit verschachtelte Gruppen funktionieren.
- Gitea – API v1, token-Header, deckt auch Forgejo ab.
Geholt werden Repo-Eckdaten, Branches, Tags, Commits, Releases, Sprachen und
offene PRs/MRs. Die Teilabfragen laufen parallel; eine fehlschlagende Liste
bricht den restlichen Sync nicht ab.
Weiteres:
- http.ts uebersetzt HTTP-Status in verstaendliche Meldungen (401 Token,
403 Scopes/Rate-Limit, 404 Pfad) und erzwingt ein Timeout.
- parseRepoUrl versteht https-, ssh- und GitLab-Weboberflaechen-URLs.
- meta.ts haelt die Provider-Metadaten frei von node:crypto, damit
Client-Komponenten sie importieren koennen.
- sync.ts schreibt das Ergebnis als Snapshot und vermerkt Fehler am Projekt,
statt eine Ausnahme bis in die Seite durchzureichen.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
106 lines
3.0 KiB
TypeScript
106 lines
3.0 KiB
TypeScript
import type { GitConnection } from "@prisma/client";
|
||
import { tryDecryptSecret } from "@/lib/crypto";
|
||
import { GiteaClient } from "./gitea";
|
||
import { GitHubClient } from "./github";
|
||
import { GitLabClient } from "./gitlab";
|
||
import { guessProvider } from "./meta";
|
||
import type { GitClient, ProviderConfig, ProviderKind, RepoRef } from "./types";
|
||
|
||
export * from "./types";
|
||
export { GiteaClient, GitHubClient, GitLabClient };
|
||
// PROVIDER_META liegt in ./meta, damit Client-Komponenten es ohne
|
||
// node:crypto-Abhaengigkeit importieren koennen.
|
||
export { PROVIDER_META, guessProvider } from "./meta";
|
||
|
||
export function createClient(config: ProviderConfig): GitClient {
|
||
switch (config.provider) {
|
||
case "GITHUB":
|
||
return new GitHubClient(config);
|
||
case "GITLAB":
|
||
return new GitLabClient(config);
|
||
case "GITEA":
|
||
return new GiteaClient(config);
|
||
default: {
|
||
const exhaustive: never = config.provider;
|
||
throw new Error(`Unbekannter Provider: ${String(exhaustive)}`);
|
||
}
|
||
}
|
||
}
|
||
|
||
/** Baut einen Client aus einer gespeicherten Verbindung (Token wird entschluesselt). */
|
||
export function clientForConnection(connection: GitConnection): GitClient {
|
||
return createClient({
|
||
provider: connection.provider,
|
||
baseUrl: connection.baseUrl,
|
||
token: tryDecryptSecret(connection.tokenCipher),
|
||
});
|
||
}
|
||
|
||
export interface ParsedRepoUrl {
|
||
host: string;
|
||
baseUrl: string;
|
||
owner: string;
|
||
name: string;
|
||
/** Aus dem Host geratener Provider – vom Nutzer ueberschreibbar. */
|
||
guessedProvider: ProviderKind;
|
||
}
|
||
|
||
/**
|
||
* Zerlegt eine Repository-URL. Unterstuetzt:
|
||
* https://github.com/user/repo(.git)
|
||
* git@gitlab.com:gruppe/subgruppe/repo.git
|
||
* https://git.firma.de/team/repo
|
||
*/
|
||
export function parseRepoUrl(input: string): ParsedRepoUrl | null {
|
||
const raw = input.trim();
|
||
if (!raw) return null;
|
||
|
||
let host = "";
|
||
let path = "";
|
||
let protocol = "https";
|
||
|
||
const sshMatch = raw.match(/^(?:ssh:\/\/)?(?:[\w.-]+@)([^/:]+)[:/](.+)$/);
|
||
if (sshMatch && !raw.startsWith("http")) {
|
||
host = sshMatch[1];
|
||
path = sshMatch[2];
|
||
} else {
|
||
try {
|
||
const url = new URL(raw.includes("://") ? raw : `https://${raw}`);
|
||
host = url.host;
|
||
path = url.pathname;
|
||
protocol = url.protocol.replace(":", "") || "https";
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
const segments = path
|
||
.replace(/\.git$/i, "")
|
||
.split("/")
|
||
.filter(Boolean)
|
||
// GitLab-Weboberflaeche haengt bei Unterseiten "/-/tree/main" an.
|
||
.reduce<string[]>((acc, segment) => {
|
||
if (segment === "-") return acc.length ? [...acc, "__STOP__"] : acc;
|
||
if (acc.includes("__STOP__")) return acc;
|
||
return [...acc, segment];
|
||
}, [])
|
||
.filter((segment) => segment !== "__STOP__");
|
||
|
||
if (segments.length < 2) return null;
|
||
|
||
const name = segments[segments.length - 1];
|
||
const owner = segments.slice(0, -1).join("/");
|
||
|
||
return {
|
||
host,
|
||
baseUrl: `${protocol}://${host}`,
|
||
owner,
|
||
name,
|
||
guessedProvider: guessProvider(host),
|
||
};
|
||
}
|
||
|
||
export function repoRefFrom(owner: string, name: string): RepoRef {
|
||
return { owner, name };
|
||
}
|