+server.ts

  1import { json } from '@sveltejs/kit';
  2import { getDiffByMode, parseDiff, getFileContent, getOldRef, getNewRef, getRepoDir } from '$lib/server/vcs';
  3import type { DiffMode } from '$lib/types/index';
  4import type { DiffFile, DiffScope } from '$lib/types/index';
  5import { highlightLines } from '$lib/server/highlighter';
  6import { readFile } from 'fs/promises';
  7import { join } from 'path';
  8import type { RequestHandler } from './$types.ts';
  9
 10async function applyHighlighting(files: DiffFile[], oldRef: string = 'HEAD', newRef: string | null = null): Promise<void> {
 11	for (const file of files) {
 12		// Get the full new file content for accurate highlighting
 13		let newContent = '';
 14		let oldContent = '';
 15
 16		try {
 17			if (newRef) {
 18				// Viewing a specific revision (not the working tree)
 19				newContent = await getFileContent(file.path, newRef);
 20			} else {
 21				// Reading the working tree version (new content)
 22				newContent = await readFile(join(getRepoDir(), file.path), 'utf-8');
 23			}
 24		} catch {
 25			// File might be deleted
 26		}
 27
 28		try {
 29			// Get the old version (HEAD or merge-base for worktree scope)
 30			oldContent = await getFileContent(file.path, oldRef);
 31		} catch {
 32			// File might be new
 33		}
 34
 35		// Highlight both versions
 36		const [newHighlighted, oldHighlighted] = await Promise.all([
 37			newContent ? highlightLines(newContent, file.language) : Promise.resolve(new Map<number, string>()),
 38			oldContent ? highlightLines(oldContent, file.language) : Promise.resolve(new Map<number, string>())
 39		]);
 40
 41		// Apply highlighted HTML to diff lines
 42		for (const hunk of file.hunks) {
 43			for (const line of hunk.lines) {
 44				if (line.type === 'add' && line.newNumber !== undefined) {
 45					line.html = newHighlighted.get(line.newNumber);
 46				} else if (line.type === 'del' && line.oldNumber !== undefined) {
 47					line.html = oldHighlighted.get(line.oldNumber);
 48				} else if (line.type === 'context' && line.newNumber !== undefined) {
 49					line.html = newHighlighted.get(line.newNumber);
 50				}
 51			}
 52		}
 53	}
 54}
 55
 56const MAX_LINES_PER_FILE = 1500;
 57const MAX_LINES_FOR_HIGHLIGHTING = 5000;
 58
 59function truncateLargeFiles(files: DiffFile[]): void {
 60	for (const file of files) {
 61		let totalLines = 0;
 62		for (const hunk of file.hunks) {
 63			totalLines += hunk.lines.length;
 64		}
 65		file.totalLines = totalLines;
 66
 67		if (totalLines > MAX_LINES_PER_FILE) {
 68			file.truncated = true;
 69			// Keep only first N lines across hunks
 70			let remaining = MAX_LINES_PER_FILE;
 71			for (const hunk of file.hunks) {
 72				if (remaining <= 0) {
 73					hunk.lines = [];
 74				} else if (hunk.lines.length > remaining) {
 75					hunk.lines = hunk.lines.slice(0, remaining);
 76				}
 77				remaining -= hunk.lines.length;
 78			}
 79			// Remove empty hunks
 80			file.hunks = file.hunks.filter((h) => h.lines.length > 0);
 81		}
 82	}
 83}
 84
 85export const GET: RequestHandler = async ({ url }) => {
 86	try {
 87		const filePath = url.searchParams.get('file') || undefined;
 88		const mode = (url.searchParams.get('mode') || 'full') as DiffMode;
 89		const scope = (url.searchParams.get('scope') || 'uncommitted') as DiffScope;
 90		const skipTruncation = url.searchParams.get('full') === '1';
 91		const raw = await getDiffByMode(mode, filePath, scope);
 92		const parsed = parseDiff(raw);
 93
 94		// Truncate large files before highlighting (unless full load requested)
 95		if (!skipTruncation) {
 96			truncateLargeFiles(parsed);
 97		} else {
 98			// Still count total lines for display
 99			for (const file of parsed) {
100				let totalLines = 0;
101				for (const hunk of file.hunks) totalLines += hunk.lines.length;
102				file.totalLines = totalLines;
103			}
104		}
105
106		// Determine the old/new refs for syntax highlighting
107		const oldRef = await getOldRef(scope);
108		const newRef = await getNewRef();
109
110		// Skip highlighting for very large files
111		const filesToHighlight = parsed.filter((f) => (f.totalLines || 0) <= MAX_LINES_FOR_HIGHLIGHTING);
112		await applyHighlighting(filesToHighlight, oldRef, newRef);
113
114		return json({ files: parsed, raw });
115	} catch (err) {
116		return json({ error: String(err) }, { status: 500 });
117	}
118};