feat(git): README des Repositories holen
Erspart den Wechsel ins Git, nur um zu sehen, worum ein halbvergessenes Projekt eigentlich ging. GitHub hat einen eigenen Endpunkt, der jede Schreibweise findet - ein Aufruf genuegt. GitLab und Gitea kennen keinen; dort wird erst das Wurzelverzeichnis aufgelistet und dann die gefundene Datei geholt. Erkannt werden README.md und die ueblichen Varianten, Gross- und Kleinschreibung egal; Markdown hat Vorrang, weil nur das wirklich dargestellt werden kann. Geholt wird hoechstens einmal am Tag, wie der Kalender, und mit eigenem Zeitstempel: schlaegt das eine fehl, soll das andere nicht mit blockiert werden. Bei 60 000 Zeichen wird abgeschnitten, aber an einer Zeilengrenze - sonst bliebe womoeglich ein halber Codeblock stehen. Dekodiert wird ueber Buffer und nicht ueber atob, sonst kaemen Umlaute als Kauderwelsch heraus. Kein README ist eine Auskunft, kein Fehlschlag: der Zeitstempel wird dann trotzdem gesetzt, damit nicht jeder Abgleich erneut sucht. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "RepoSnapshot" ADD COLUMN "readme" TEXT,
|
||||||
|
ADD COLUMN "readmeAt" TIMESTAMP(3),
|
||||||
|
ADD COLUMN "readmeBaseUrl" TEXT,
|
||||||
|
ADD COLUMN "readmePath" TEXT;
|
||||||
@@ -612,6 +612,16 @@ model RepoSnapshot {
|
|||||||
commitCalendar Json @default("{}")
|
commitCalendar Json @default("{}")
|
||||||
commitCalendarAt DateTime?
|
commitCalendarAt DateTime?
|
||||||
|
|
||||||
|
/// Das README des Repositories, roh als Markdown. Wie der Kalender
|
||||||
|
/// hoechstens einmal am Tag geholt – es aendert sich selten, und der
|
||||||
|
/// Abgleich laeuft im Zweifel stuendlich.
|
||||||
|
readme String?
|
||||||
|
/// Dateiname, wie er im Repository steht ("README.md", "readme.rst" …).
|
||||||
|
readmePath String?
|
||||||
|
/// Wogegen relative Links im README aufzuloesen sind.
|
||||||
|
readmeBaseUrl String?
|
||||||
|
readmeAt DateTime?
|
||||||
|
|
||||||
// ── Letzter CI-Lauf auf dem Default-Branch ─────────────────
|
// ── Letzter CI-Lauf auf dem Default-Branch ─────────────────
|
||||||
/// success | failure | running | pending | cancelled
|
/// success | failure | running | pending | cancelled
|
||||||
ciState String?
|
ciState String?
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
calendarSince,
|
calendarSince,
|
||||||
type CommitCalendar,
|
type CommitCalendar,
|
||||||
} from "./calendar";
|
} from "./calendar";
|
||||||
|
import { capReadme, decodeBase64, pickReadme, type RepoReadme } from "./readme";
|
||||||
import type {
|
import type {
|
||||||
BranchInfo,
|
BranchInfo,
|
||||||
CiInfo,
|
CiInfo,
|
||||||
@@ -246,6 +247,43 @@ export class GiteaClient implements GitClient {
|
|||||||
return commits.map((commit) => toIso(commit.commit?.author?.date));
|
return commits.map((commit) => toIso(commit.commit?.author?.date));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gitea kennt keinen README-Endpunkt. Erst das Wurzelverzeichnis
|
||||||
|
* auflisten, dann die gefundene Datei holen – zwei Aufrufe.
|
||||||
|
*/
|
||||||
|
async fetchReadme(ref: RepoRef): Promise<RepoReadme | null> {
|
||||||
|
const base = `${this.apiBase}/repos/${encodeURIComponent(ref.owner)}/${encodeURIComponent(ref.name)}`;
|
||||||
|
const headers = this.headers();
|
||||||
|
|
||||||
|
const eintraege = await getJsonSafe<Array<{ name: string; type: string }>>(
|
||||||
|
`${base}/contents`,
|
||||||
|
[],
|
||||||
|
{ headers },
|
||||||
|
);
|
||||||
|
const name = pickReadme(
|
||||||
|
eintraege.filter((eintrag) => eintrag.type === "file").map((eintrag) => eintrag.name),
|
||||||
|
);
|
||||||
|
if (!name) return null;
|
||||||
|
|
||||||
|
const datei = await getJson<{
|
||||||
|
content?: string;
|
||||||
|
encoding?: string;
|
||||||
|
path?: string;
|
||||||
|
html_url?: string | null;
|
||||||
|
}>(`${base}/contents/${encodeURIComponent(name)}`, { headers });
|
||||||
|
if (!datei?.content || datei.encoding !== "base64") return null;
|
||||||
|
|
||||||
|
const { content, truncated } = capReadme(decodeBase64(datei.content));
|
||||||
|
return {
|
||||||
|
content,
|
||||||
|
path: datei.path ?? name,
|
||||||
|
// html_url zeigt auf die Datei; fuer relative Links braucht es das
|
||||||
|
// Verzeichnis darum herum.
|
||||||
|
baseUrl: datei.html_url ? datei.html_url.replace(/\/[^/]*$/, "/") : null,
|
||||||
|
truncated,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapCommitStatus(state: string): CiState {
|
function mapCommitStatus(state: string): CiState {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import type {
|
|||||||
TicketInfo,
|
TicketInfo,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { dayKey } from "./calendar";
|
import { dayKey } from "./calendar";
|
||||||
|
import { capReadme, decodeBase64, type RepoReadme } from "./readme";
|
||||||
import { GitApiError, bucketCommitActivity } from "./types";
|
import { GitApiError, bucketCommitActivity } from "./types";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -252,6 +253,43 @@ export class GitHubClient implements GitClient {
|
|||||||
|
|
||||||
return { from: von, to: heute, days, source: "stats", truncated: false };
|
return { from: von, to: heute, days, source: "stats", truncated: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GitHub hat einen eigenen Endpunkt, der jede Schreibweise findet – ein
|
||||||
|
* Aufruf genuegt. Ohne README antwortet er mit 404; das ist kein Fehler,
|
||||||
|
* sondern die Auskunft "gibt es nicht".
|
||||||
|
*/
|
||||||
|
async fetchReadme(ref: RepoRef): Promise<RepoReadme | null> {
|
||||||
|
const base = `${this.apiBase}/repos/${encodeURIComponent(ref.owner)}/${encodeURIComponent(ref.name)}`;
|
||||||
|
|
||||||
|
let datei: GitHubContent;
|
||||||
|
try {
|
||||||
|
datei = await getJson<GitHubContent>(`${base}/readme`, { headers: this.headers() });
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof GitApiError && error.status === 404) return null;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!datei?.content || datei.encoding !== "base64") return null;
|
||||||
|
|
||||||
|
const { content, truncated } = capReadme(decodeBase64(datei.content));
|
||||||
|
return {
|
||||||
|
content,
|
||||||
|
path: datei.path ?? datei.name ?? "README",
|
||||||
|
// html_url zeigt auf die Datei; fuer relative Links braucht es das
|
||||||
|
// Verzeichnis darum herum.
|
||||||
|
baseUrl: datei.html_url ? datei.html_url.replace(/\/[^/]*$/, "/") : null,
|
||||||
|
truncated,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GitHubContent {
|
||||||
|
name?: string;
|
||||||
|
path?: string;
|
||||||
|
content?: string;
|
||||||
|
encoding?: string;
|
||||||
|
html_url?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface GitHubWeek {
|
interface GitHubWeek {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
calendarSince,
|
calendarSince,
|
||||||
type CommitCalendar,
|
type CommitCalendar,
|
||||||
} from "./calendar";
|
} from "./calendar";
|
||||||
|
import { capReadme, decodeBase64, pickReadme, type RepoReadme } from "./readme";
|
||||||
import type {
|
import type {
|
||||||
BranchInfo,
|
BranchInfo,
|
||||||
CiInfo,
|
CiInfo,
|
||||||
@@ -227,6 +228,44 @@ export class GitLabClient implements GitClient {
|
|||||||
return commits.map((commit) => toIso(commit.committed_date ?? commit.created_at));
|
return commits.map((commit) => toIso(commit.committed_date ?? commit.created_at));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* GitLab kennt keinen README-Endpunkt. Erst das Wurzelverzeichnis
|
||||||
|
* auflisten, dann die gefundene Datei holen – zwei Aufrufe.
|
||||||
|
*/
|
||||||
|
async fetchReadme(ref: RepoRef): Promise<RepoReadme | null> {
|
||||||
|
const base = `${this.apiBase}/projects/${this.projectPath(ref)}`;
|
||||||
|
const headers = this.headers();
|
||||||
|
|
||||||
|
const eintraege = await getJsonSafe<Array<{ name: string; type: string }>>(
|
||||||
|
`${base}/repository/tree?per_page=100`,
|
||||||
|
[],
|
||||||
|
{ headers },
|
||||||
|
);
|
||||||
|
const name = pickReadme(
|
||||||
|
eintraege.filter((eintrag) => eintrag.type === "blob").map((eintrag) => eintrag.name),
|
||||||
|
);
|
||||||
|
if (!name) return null;
|
||||||
|
|
||||||
|
const datei = await getJson<{ content?: string; encoding?: string; file_path?: string }>(
|
||||||
|
`${base}/repository/files/${encodeURIComponent(name)}?ref=HEAD`,
|
||||||
|
{ headers },
|
||||||
|
);
|
||||||
|
if (!datei?.content || datei.encoding !== "base64") return null;
|
||||||
|
|
||||||
|
const { content, truncated } = capReadme(decodeBase64(datei.content));
|
||||||
|
const projekt = await getJsonSafe<GitLabProject>(base, {} as GitLabProject, { headers });
|
||||||
|
|
||||||
|
return {
|
||||||
|
content,
|
||||||
|
path: datei.file_path ?? name,
|
||||||
|
baseUrl:
|
||||||
|
projekt?.web_url && projekt.default_branch
|
||||||
|
? `${projekt.web_url}/-/blob/${encodeURIComponent(projekt.default_branch)}/`
|
||||||
|
: null,
|
||||||
|
truncated,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapPipelineState(status: string | null): CiState {
|
function mapPipelineState(status: string | null): CiState {
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { README_MAX_CHARS, capReadme, decodeBase64, pickReadme } from "./readme";
|
||||||
|
|
||||||
|
describe("pickReadme", () => {
|
||||||
|
it("findet die uebliche Schreibweise", () => {
|
||||||
|
expect(pickReadme(["src", "README.md", "package.json"])).toBe("README.md");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("ist bei der Gross- und Kleinschreibung nachsichtig", () => {
|
||||||
|
expect(pickReadme(["readme.md"])).toBe("readme.md");
|
||||||
|
expect(pickReadme(["ReadMe.MD"])).toBe("ReadMe.MD");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("zieht Markdown den uebrigen Formaten vor", () => {
|
||||||
|
// Beides kommt vor; nur das erste laesst sich wirklich darstellen.
|
||||||
|
expect(pickReadme(["README.rst", "README.md"])).toBe("README.md");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("nimmt auch ein README ohne Endung", () => {
|
||||||
|
expect(pickReadme(["LICENSE", "README"])).toBe("README");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("liefert null, wenn keines dabei ist", () => {
|
||||||
|
expect(pickReadme(["src", "package.json"])).toBeNull();
|
||||||
|
expect(pickReadme([])).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("verwechselt Aehnliches nicht mit dem README", () => {
|
||||||
|
expect(pickReadme(["README-CONTRIBUTING.md", "readme.md.bak"])).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("capReadme", () => {
|
||||||
|
it("laesst Kurzes unangetastet", () => {
|
||||||
|
const { content, truncated } = capReadme("# Titel\n\nEin Satz.");
|
||||||
|
expect(content).toBe("# Titel\n\nEin Satz.");
|
||||||
|
expect(truncated).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("kuerzt Langes an einer Zeilengrenze", () => {
|
||||||
|
// Sonst bliebe womoeglich ein halber Codeblock stehen.
|
||||||
|
const zeile = "x".repeat(80);
|
||||||
|
const lang = Array.from({ length: 2000 }, () => zeile).join("\n");
|
||||||
|
const { content, truncated } = capReadme(lang);
|
||||||
|
expect(truncated).toBe(true);
|
||||||
|
expect(content.length).toBeLessThanOrEqual(README_MAX_CHARS);
|
||||||
|
expect(content.endsWith("x")).toBe(true);
|
||||||
|
expect(content.split("\n").at(-1)).toBe(zeile);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("kuerzt notfalls hart, wenn es gar keine Zeilengrenze gibt", () => {
|
||||||
|
// Eine einzige riesige Zeile: hier waere der letzte Umbruch bei -1.
|
||||||
|
const { content, truncated } = capReadme("y".repeat(README_MAX_CHARS + 500));
|
||||||
|
expect(truncated).toBe(true);
|
||||||
|
expect(content).toHaveLength(README_MAX_CHARS);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("decodeBase64", () => {
|
||||||
|
it("dekodiert UTF-8 richtig", () => {
|
||||||
|
// Ueber Bytes gedacht kaeme hier Kauderwelsch heraus.
|
||||||
|
const kodiert = Buffer.from("# Nebula – Übersicht", "utf8").toString("base64");
|
||||||
|
expect(decodeBase64(kodiert)).toBe("# Nebula – Übersicht");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uebergeht Zeilenumbrueche in der Kodierung", () => {
|
||||||
|
// GitHub bricht den base64-Block alle 60 Zeichen um.
|
||||||
|
const roh = Buffer.from("Hallo Welt", "utf8").toString("base64");
|
||||||
|
const umgebrochen = `${roh.slice(0, 4)}\n${roh.slice(4)}`;
|
||||||
|
expect(decodeBase64(umgebrochen)).toBe("Hallo Welt");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/**
|
||||||
|
* Das README eines Repositories – gemeinsame Teile fuer alle drei Anbieter.
|
||||||
|
*
|
||||||
|
* Gezeigt wird es auf der Projektseite, damit man nicht ins Git wechseln
|
||||||
|
* muss, nur um zu sehen, worum ein halbvergessenes Projekt eigentlich ging.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface RepoReadme {
|
||||||
|
/** Roher Markdown-Text. */
|
||||||
|
content: string;
|
||||||
|
/** Dateiname im Repository, etwa "README.md". */
|
||||||
|
path: string;
|
||||||
|
/**
|
||||||
|
* Basis, gegen die relative Links im Text aufzuloesen sind – die
|
||||||
|
* Web-Ansicht des Repositories im Verzeichnis des READMEs.
|
||||||
|
*/
|
||||||
|
baseUrl: string | null;
|
||||||
|
/** Wurde am Deckel abgeschnitten? */
|
||||||
|
truncated: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hoechstlaenge. Ein README von einem Megabyte gibt es, und es waere in
|
||||||
|
* einer Vorschau ohnehin unlesbar – abgeschnitten wird an einer
|
||||||
|
* Zeilengrenze, damit kein halber Codeblock stehen bleibt.
|
||||||
|
*/
|
||||||
|
export const README_MAX_CHARS = 60_000;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dateinamen, die als README gelten – in dieser Reihenfolge. Markdown zuerst,
|
||||||
|
* weil nur das dargestellt werden kann; die uebrigen landen als Klartext in
|
||||||
|
* einem Codeblock, was immer noch besser ist als nichts.
|
||||||
|
*/
|
||||||
|
const KANDIDATEN = [
|
||||||
|
"readme.md",
|
||||||
|
"readme.markdown",
|
||||||
|
"readme.mdown",
|
||||||
|
"readme",
|
||||||
|
"readme.txt",
|
||||||
|
"readme.rst",
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sucht aus einer Verzeichnisauflistung die README-Datei heraus.
|
||||||
|
* Gross- und Kleinschreibung ist egal; die Reihenfolge oben entscheidet.
|
||||||
|
*/
|
||||||
|
export function pickReadme(namen: string[]): string | null {
|
||||||
|
const klein = new Map(namen.map((name) => [name.toLowerCase(), name]));
|
||||||
|
for (const kandidat of KANDIDATEN) {
|
||||||
|
const treffer = klein.get(kandidat);
|
||||||
|
if (treffer) return treffer;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Kuerzt am Deckel, aber an einer Zeilengrenze. */
|
||||||
|
export function capReadme(text: string): { content: string; truncated: boolean } {
|
||||||
|
if (text.length <= README_MAX_CHARS) return { content: text, truncated: false };
|
||||||
|
|
||||||
|
const roh = text.slice(0, README_MAX_CHARS);
|
||||||
|
const letzterUmbruch = roh.lastIndexOf("\n");
|
||||||
|
return {
|
||||||
|
// Nur wenn der Umbruch nicht ganz am Anfang liegt – sonst bliebe fast
|
||||||
|
// nichts uebrig.
|
||||||
|
content: letzterUmbruch > README_MAX_CHARS / 2 ? roh.slice(0, letzterUmbruch) : roh,
|
||||||
|
truncated: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dekodiert, was die Anbieter als base64 liefern.
|
||||||
|
*
|
||||||
|
* Bewusst ueber Buffer und nicht ueber atob: der Inhalt ist UTF-8, und atob
|
||||||
|
* gaebe Bytes zurueck, aus denen Umlaute als Kauderwelsch herauskaemen.
|
||||||
|
*/
|
||||||
|
export function decodeBase64(value: string): string {
|
||||||
|
return Buffer.from(value.replace(/\s/g, ""), "base64").toString("utf8");
|
||||||
|
}
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import type { GitProvider } from "@prisma/client";
|
import type { GitProvider } from "@prisma/client";
|
||||||
import type { CommitCalendar } from "./calendar";
|
import type { CommitCalendar } from "./calendar";
|
||||||
|
import type { RepoReadme } from "./readme";
|
||||||
|
|
||||||
export type { CommitCalendar };
|
export type { CommitCalendar, RepoReadme };
|
||||||
|
|
||||||
export type ProviderKind = GitProvider;
|
export type ProviderKind = GitProvider;
|
||||||
|
|
||||||
@@ -181,6 +182,12 @@ export interface GitClient {
|
|||||||
* hoechstens einmal am Tag noetig ist. null heisst: nichts zu holen.
|
* hoechstens einmal am Tag noetig ist. null heisst: nichts zu holen.
|
||||||
*/
|
*/
|
||||||
fetchCommitCalendar(ref: RepoRef): Promise<CommitCalendar | null>;
|
fetchCommitCalendar(ref: RepoRef): Promise<CommitCalendar | null>;
|
||||||
|
/**
|
||||||
|
* Das README des Repositories. Wie der Kalender getrennt, weil es
|
||||||
|
* zusaetzliche Aufrufe kostet und sich selten aendert. null heisst: es
|
||||||
|
* gibt keins.
|
||||||
|
*/
|
||||||
|
fetchReadme(ref: RepoRef): Promise<RepoReadme | null>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fehler mit HTTP-Kontext, damit die UI verstaendliche Meldungen zeigen kann. */
|
/** Fehler mit HTTP-Kontext, damit die UI verstaendliche Meldungen zeigen kann. */
|
||||||
|
|||||||
+44
-2
@@ -58,9 +58,10 @@ export async function syncRepository(repositoryId: string): Promise<SyncResult>
|
|||||||
try {
|
try {
|
||||||
const data = await client.fetchRepo(ref);
|
const data = await client.fetchRepo(ref);
|
||||||
await persistSnapshot(repository, data);
|
await persistSnapshot(repository, data);
|
||||||
// Erst nach dem Snapshot: die Zeile muss existieren, bevor der Kalender
|
// Erst nach dem Snapshot: die Zeile muss existieren, bevor Kalender und
|
||||||
// hineingeschrieben werden kann.
|
// README hineingeschrieben werden koennen.
|
||||||
await maybeRefreshCalendar(repository.id, client, ref);
|
await maybeRefreshCalendar(repository.id, client, ref);
|
||||||
|
await maybeRefreshReadme(repository.id, client, ref);
|
||||||
// Nach dem Speichern, damit die Meldung den neuen Stand sieht.
|
// Nach dem Speichern, damit die Meldung den neuen Stand sieht.
|
||||||
await notifyAfterSync(repository.id);
|
await notifyAfterSync(repository.id);
|
||||||
return {
|
return {
|
||||||
@@ -196,6 +197,47 @@ async function maybeRefreshCalendar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Holt das README – wie der Kalender hoechstens einmal am Tag.
|
||||||
|
*
|
||||||
|
* Ein eigener Zeitstempel und nicht der des Kalenders: schlaegt das eine
|
||||||
|
* fehl, soll das andere nicht mit blockiert werden.
|
||||||
|
*/
|
||||||
|
async function maybeRefreshReadme(
|
||||||
|
repositoryId: string,
|
||||||
|
client: GitClient,
|
||||||
|
ref: { owner: string; name: string },
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const snapshot = await prisma.repoSnapshot.findUnique({
|
||||||
|
where: { repositoryId },
|
||||||
|
select: { readmeAt: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
const zuletzt = snapshot?.readmeAt?.getTime() ?? 0;
|
||||||
|
if (Date.now() - zuletzt < CALENDAR_TTL_MS) return;
|
||||||
|
|
||||||
|
const readme = await client.fetchReadme(ref);
|
||||||
|
|
||||||
|
// Kein README im Repository: das ist eine Auskunft, kein Fehlschlag.
|
||||||
|
// Der Zeitstempel wird gesetzt, damit nicht jeder Abgleich erneut sucht,
|
||||||
|
// und ein etwaiger alter Stand verschwindet.
|
||||||
|
await prisma.repoSnapshot.update({
|
||||||
|
where: { repositoryId },
|
||||||
|
data: {
|
||||||
|
readme: readme?.content ?? null,
|
||||||
|
readmePath: readme?.path ?? null,
|
||||||
|
readmeBaseUrl: readme?.baseUrl ?? null,
|
||||||
|
readmeAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// Wie beim Kalender: Beiwerk. Ohne Zeitstempel versucht es der naechste
|
||||||
|
// Durchgang von selbst wieder.
|
||||||
|
console.error("[nebula] README konnte nicht geholt werden:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** Owner/Name aus den Feldern oder ersatzweise aus der URL bestimmen. */
|
/** Owner/Name aus den Feldern oder ersatzweise aus der URL bestimmen. */
|
||||||
export function resolveRepoRef(
|
export function resolveRepoRef(
|
||||||
repository: Pick<Repository, "repoOwner" | "repoName" | "repoUrl">,
|
repository: Pick<Repository, "repoOwner" | "repoName" | "repoUrl">,
|
||||||
|
|||||||
Reference in New Issue
Block a user