feat(git): Adapter fuer GitHub, GitLab und Gitea
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>
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
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 };
|
||||
}
|
||||
Reference in New Issue
Block a user