Files
nebula/src/lib/git
florian.wilde e9f01ba8fa
CI / pruefen (pull_request) Canceled after 0s
docs(git): jeden ausgehenden Aufruf einzeln mit seinem Recht belegen
Die Faelle A und B beantworteten die Frage auf Ebene der Scopes, nicht
auf Ebene der Aufrufe. Wer nachpruefen wollte, ob ein Recht wirklich
noetig ist, musste die drei Clients selbst lesen.

Jetzt steht in der README je System eine vollstaendige Tabelle: Aufruf,
Zweck, benoetigtes Recht. Die Zuordnung bei GitHub folgt der offiziellen
Berechtigungsuebersicht - dass /tags und /stats/commit_activity unter
Metadata fallen und nicht unter Contents, ist nicht zu erraten. Fuer
GitLab ist mit Quellenangabe belegt, dass es unterhalb von 'api' keinen
Schreibzugriff auf Issues gibt.

Ausserdem war offen, woher die Kommentare an den Issues stammen: Nebula
schreibt keine. Der Schreibzugriff besteht aus genau zwei Aufrufen. Ein
Test klopft dafuer die Clients ab - kaeme ein dritter dazu, wuerde die
Empfehlung 'nimm ein lesendes Token' still falsch, ohne dass jemand
einen Fehler saehe.

Der Hinweis am Eingabefeld nannte Actions nicht, obwohl der CI-Zustand
bei GitHub genau daran haengt.

Refs #10
2026-08-18 12:57:02 +02:00
..

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
 * 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");
}