Files
nebula/src/lib/git
florian.wilde abd9f73405
CI / pruefen (pull_request) Canceled after 0s
fix(git): Seitengroesse an der Antwort messen, nicht an der Anfrage
Behebt florian.wilde/nebula#22.

Gitea deckelt jede Listenantwort auf MAX_RESPONSE_ITEMS (Voreinstellung
50), egal was limit sagt. calendarByPaging verglich gegen die angefragten
100 und hielt damit bei jedem Gitea-Repository die erste Seite fuer die
letzte. Die Heatmap kannte nur die juengsten 50 Commits - und gab deren
aeltesten als Anfang des Zeitraums aus, ohne truncated zu setzen. Sie
behauptete also Vollstaendigkeit, die sie nicht hatte.

Maßstab ist jetzt die erste tatsaechlich gelieferte Seite. Kuerzer als
die heisst letzte Seite, leer heisst Ende, und der Deckel gilt weiterhin
nach CALENDAR_MAX_PAGES - dann aber ehrlich mit truncated.

Am echten Repository nachgesehen, dem aus dem Issue:

  vorher   Seite 1: 50                       -> 50 Commits
  jetzt    Seite 1: 50 | Seite 2: 26         -> 76 Commits

Gitea selbst weist 76 aus.

WAS DAS KOSTET

Bei Gitea macht der Kalender jetzt bis zu fuenf Aufrufe statt einem, und
ein Repository mit kurzer Historie fragt eine Seite mehr, um zu erfahren,
dass nichts mehr kommt. Das laeuft hoechstens einmal am Tag; der Preis
dafuer, dass die Zahl stimmt, ist ihn wert.

VIER BESTEHENDE TESTS ANGEFASST

Ihr Fixture lieferte fuer jede Seite dieselben Daten und hielt nur
deshalb an, weil die alte Regel eine kurze Seite fuer die letzte hielt -
also wegen genau des Fehlers, der hier behoben wird. Kein Server
verhaelt sich so. Sie holen ihre Daten jetzt ueber eineSeite() und
pruefen unveraendert das, worum es ihnen geht: Zaehlen und Filtern.

Vier neue Tests stellen eine deckelnde Gegenseite nach. Gegengeprobt:
mit der alten Zeile fallen genau diese vier.

Refs #22
2026-09-09 22:39:26 +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");
}