Browse Source

bug-fixes

Nostr-Signature: 6aa4dcd1b3d8a933710a6eb43321aa4faaba56598c735a634069c882c83b4f03 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc 80ce253e890e8e84c8138e004bc2aaea402379d9aa67f62793ac7a4b344de6a7223f46fc733b240215a983a3a9b574ea8d0858a184f06df58ee66212ba58ee53
main
Silberengel 2 weeks ago
parent
commit
70873862a0
  1. 1
      nostr/commit-signatures.jsonl
  2. 35
      src/lib/services/git/file-manager.ts
  3. 76
      src/lib/services/git/file-manager/commit-operations.ts
  4. 276
      src/lib/services/git/repo-manager.ts
  5. 7
      src/routes/api/repos/[npub]/[repo]/readme/+server.ts
  6. 15
      src/routes/repos/[npub]/[repo]/components/DocsTab.svelte
  7. 20
      src/routes/repos/[npub]/[repo]/services/repo-operations.ts
  8. 19
      src/routes/repos/[npub]/[repo]/utils/safe-wrappers.ts

1
nostr/commit-signatures.jsonl

@ -122,3 +122,4 @@ @@ -122,3 +122,4 @@
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772271656,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","bug-fixes"]],"content":"Signed commit: bug-fixes","id":"f4a5e0d3e2aa7d0d99803f26008ab68e40551e36362bb6d04acf639c5b78d959","sig":"59da9e59a6fb5648f4c889e0045b571e0d2d66a555100d60dec373455309a640bea89e4bb3a42a0e502aa4d2091e4b698203721e79b346ff30e6b2bcdc5f48b3"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772274086,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","bug-fixes"]],"content":"Signed commit: bug-fixes","id":"32794e047f06902ad610f918834efb113f41eace26a53a3f0fad083b9d8323dc","sig":"3859f0de3de0f8a742b6fbe7709c5a5625f4d5612a936fd81f38a7e1231ee810b50a69c1ed5d23c8a6670b4cbc9ea3d4bd39d6fa9e6207802f45995689b924a9"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772293551,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","remove polling"]],"content":"Signed commit: remove polling","id":"40f01e84f96661bb7fea13aa63c7da428118061b0a1470a11890d4f9cd6d685b","sig":"dbb6947defac6c7f92a3cf6f72352a94ffe2c4b33e65f8410518a40406c93f1f5a3e13e81f2f04f676d826e6cf03ec802328f5228300f80a8114fa3fd26eaeff"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1772296288,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","administer the repos"]],"content":"Signed commit: administer the repos","id":"8825fb9bd01e099c1369f0c9ea1429dedd0a0116d103b4a640752c0a830fbc61","sig":"676f0817f817204ad910a70540399f71743a54453ae209535dcb30356d042b049138d9cfdeec08c4b7da03bb6bb51c71477bbf8d2f58bd4b602b9f69af4b3405"}

35
src/lib/services/git/file-manager.ts

@ -445,10 +445,37 @@ export class FileManager { @@ -445,10 +445,37 @@ export class FileManager {
logger.info({ npub, repoName, branchName }, '[FileManager.createBranch] Step 2: Creating empty commit');
// Create an empty commit pointing to the empty tree with author information
// Use --author flag to specify author identity (required when git config is not set)
const authorString = `${authorName} <${authorEmail}>`;
const commitHash = await git.raw(['commit-tree', '-m', `Initial commit on ${branchName}`, '--author', authorString, emptyTreeHash]);
const commit = commitHash.trim();
// git commit-tree doesn't support --author flag, so we use environment variables
const { spawn } = await import('child_process');
const commit = await new Promise<string>((resolve, reject) => {
const env = {
...process.env,
GIT_AUTHOR_NAME: authorName,
GIT_AUTHOR_EMAIL: authorEmail,
GIT_COMMITTER_NAME: authorName,
GIT_COMMITTER_EMAIL: authorEmail
};
const proc = spawn('git', ['commit-tree', '-m', `Initial commit on ${branchName}`, emptyTreeHash], {
cwd: repoPath,
env
});
let output = '';
proc.stdout.on('data', (data) => { output += data.toString(); });
proc.stderr.on('data', (data) => {
const error = data.toString();
if (error.trim()) {
logger.warn({ npub, repoName, branchName, error }, '[FileManager.createBranch] commit-tree stderr');
}
});
proc.on('close', (code) => {
if (code === 0) {
resolve(output.trim());
} else {
reject(new Error(`commit-tree failed with code ${code}: ${output || 'no output'}`));
}
});
proc.on('error', reject);
});
logger.info({ npub, repoName, branchName, commit }, '[FileManager.createBranch] Step 2 complete: empty commit created');
logger.info({ npub, repoName, branchName, commit }, '[FileManager.createBranch] Step 3: Creating branch ref pointing to empty commit');

76
src/lib/services/git/file-manager/commit-operations.ts

@ -85,17 +85,83 @@ export async function getCommitHistory(options: CommitHistoryOptions): Promise<C @@ -85,17 +85,83 @@ export async function getCommitHistory(options: CommitHistoryOptions): Promise<C
// First try with the specified branch
logOptions.from = branch;
log = await git.log(logOptions);
// If log.all is empty but we know there are commits, try --all as fallback
if (!log.all || log.all.length === 0) {
logger.debug({ npub, repoName, branch }, 'git.log() returned empty results, trying --all fallback');
delete logOptions.from;
log = await git.log(logOptions);
}
} catch (branchErr) {
// If branch doesn't exist or is ambiguous, try --all
const errorMsg = branchErr instanceof Error ? branchErr.message : String(branchErr);
if (errorMsg.includes('ambiguous') || errorMsg.includes('unknown') || errorMsg.includes('does not exist')) {
logger.debug({ npub, repoName, branch, error: errorMsg }, 'Branch does not exist or is ambiguous, trying --all');
logger.debug({ npub, repoName, branch, error: errorMsg }, 'git.log() failed, trying --all fallback');
try {
delete logOptions.from;
log = await git.log(logOptions);
} else {
// Re-throw if it's a different error
throw branchErr;
} catch (allErr) {
// If --all also fails, try using raw git command as last resort
logger.debug({ npub, repoName, branch, error: allErr }, 'git.log() with --all also failed, trying raw git command');
try {
const rawLog = await git.raw(['log', '--all', `--max-count=${limit}`, '--format=%H|%s|%an|%ae|%ai', ...(path ? ['--', path] : [])]);
if (rawLog && rawLog.trim()) {
// Parse raw log output
const lines = rawLog.trim().split('\n').filter(l => l.trim());
const commits = lines.map(line => {
const [hash, ...rest] = line.split('|');
const message = rest.slice(0, -3).join('|'); // Message might contain |
const authorName = rest[rest.length - 3];
const authorEmail = rest[rest.length - 2];
const date = rest[rest.length - 1];
return {
hash: hash || '',
message: message || '',
author: `${authorName || 'Unknown'} <${authorEmail || ''}>`,
date: date || new Date().toISOString(),
files: [] // Can't get files from raw log easily
};
}).filter(c => c.hash);
logger.operation('Commit history retrieved via raw git', { npub, repoName, count: commits.length });
return commits;
}
} catch (rawErr) {
logger.error({ error: rawErr, npub, repoName, branch }, 'All methods failed to get commit history');
throw branchErr; // Throw original error
}
}
}
// Ensure log.all exists and has data
if (!log || !log.all || log.all.length === 0) {
logger.warn({ npub, repoName, branch, logResult: log }, 'git.log() returned empty results despite commits existing');
// Try one more time with raw command
try {
const rawLog = await git.raw(['log', '--all', `--max-count=${limit}`, '--format=%H|%s|%an|%ae|%ai', ...(path ? ['--', path] : [])]);
if (rawLog && rawLog.trim()) {
const lines = rawLog.trim().split('\n').filter(l => l.trim());
const commits = lines.map(line => {
const [hash, ...rest] = line.split('|');
const message = rest.slice(0, -3).join('|');
const authorName = rest[rest.length - 3];
const authorEmail = rest[rest.length - 2];
const date = rest[rest.length - 1];
return {
hash: hash || '',
message: message || '',
author: `${authorName || 'Unknown'} <${authorEmail || ''}>`,
date: date || new Date().toISOString(),
files: []
};
}).filter(c => c.hash);
logger.operation('Commit history retrieved via raw git (fallback)', { npub, repoName, count: commits.length });
return commits;
}
} catch (rawErr) {
logger.error({ error: rawErr, npub, repoName, branch }, 'Raw git command also failed');
}
return [];
}
const commits = log.all.map(commit => ({

276
src/lib/services/git/repo-manager.ts

@ -10,7 +10,7 @@ @@ -10,7 +10,7 @@
*/
import { existsSync, mkdirSync, accessSync, constants } from 'fs';
import { join } from 'path';
import { join, resolve } from 'path';
import { spawn } from 'child_process';
import type { NostrEvent } from '../../types/nostr.js';
import { GIT_DOMAIN } from '../../config.js';
@ -213,6 +213,11 @@ export class RepoManager { @@ -213,6 +213,11 @@ export class RepoManager {
announcementEvent: NostrEvent,
preferredDefaultBranch?: string
): Promise<void> {
// Declare variables outside try block so they're accessible in finally
const { FileManager } = await import('./file-manager.js');
const fileManager = new FileManager(this.repoRoot);
let workDir: string | undefined;
try {
// Get default branch from preferred branch, git config, environment, or use 'master'
// Check preferred branch first (from user settings), then git's init.defaultBranch config
@ -291,23 +296,57 @@ You can use this read-me file to explain the purpose of this repo to everyone wh @@ -291,23 +296,57 @@ You can use this read-me file to explain the purpose of this repo to everyone wh
Your commits will all be signed by your Nostr keys and saved to the event files in the ./nostr folder.
`;
// Use FileManager to create the initial branch and files
const { FileManager } = await import('./file-manager.js');
const fileManager = new FileManager(this.repoRoot);
// Create both README.md and announcement in a single initial commit
// We'll use a worktree to write both files and commit them together
// If no branches exist, we'll create an orphan branch directly in the worktree
logger.info({ npub, repoName, defaultBranch }, 'Creating worktree for initial commit');
const { writeFile: writeFileFs, mkdir: mkdirFs } = await import('fs/promises');
const { join } = await import('path');
const { spawn } = await import('child_process');
// If no branches exist, create an orphan branch
// We already checked for existing branches above, so if existingBranches is empty, create one
if (existingBranches.length === 0) {
// Create orphan branch first (pass undefined for fromBranch to create orphan)
await fileManager.createBranch(npub, repoName, defaultBranch, undefined);
// No branches exist - create worktree with orphan branch
// We need to create the worktree manually with --orphan flag
logger.info({ npub, repoName, defaultBranch }, 'No branches exist, creating orphan branch in worktree');
// Create a temporary worktree directory
// Use absolute path to ensure git worktree can find it
const worktreeBase = resolve(this.repoRoot, 'worktrees', npub, repoName);
await mkdirFs(worktreeBase, { recursive: true });
workDir = resolve(worktreeBase, `worktree-${Date.now()}`);
// Create worktree with orphan branch
// Note: --orphan requires -b flag to specify the branch name
// git worktree add requires an absolute path
await new Promise<void>((resolvePromise, reject) => {
const proc = spawn('git', ['worktree', 'add', '--orphan', '-b', defaultBranch, workDir!], {
cwd: repoPath,
stdio: ['ignore', 'pipe', 'pipe']
});
let stdout = '';
let stderr = '';
proc.stdout?.on('data', (data: Buffer) => { stdout += data.toString(); });
proc.stderr?.on('data', (data: Buffer) => { stderr += data.toString(); });
proc.on('close', (code: number | null) => {
if (code === 0) {
resolvePromise();
} else {
reject(new Error(`git worktree add --orphan failed with code ${code}: ${stderr || stdout}`));
}
});
proc.on('error', reject);
});
logger.info({ npub, repoName, defaultBranch, workDir }, 'Orphan branch worktree created successfully');
} else {
// Branch exists - use normal worktree
workDir = await fileManager.getWorktree(repoPath, defaultBranch, npub, repoName);
logger.info({ npub, repoName, defaultBranch, workDir }, 'Worktree created successfully');
}
// Create both README.md and announcement in the initial commit
// We'll use a worktree to write both files and commit them together
const workDir = await fileManager.getWorktree(repoPath, defaultBranch, npub, repoName);
const { writeFile: writeFileFs } = await import('fs/promises');
const { join } = await import('path');
// Write README.md
const readmePath = join(workDir, 'README.md');
await writeFileFs(readmePath, readmeContent, 'utf-8');
@ -335,19 +374,35 @@ Your commits will all be signed by your Nostr keys and saved to the event files @@ -335,19 +374,35 @@ Your commits will all be signed by your Nostr keys and saved to the event files
logger.warn({ repoPath, npub, repoName, error: configError }, 'Failed to set git config, commit may fail');
}
// Commit files together
await workGit.commit('Initial commit', filesToAdd, {
// Commit files together with "Initial commit to GitRepublic" message
// This will be the first and only commit on the branch
await workGit.commit('Initial commit to GitRepublic', filesToAdd, {
'--author': `${authorName} <${authorEmail}>`
});
// Clean up worktree
await fileManager.removeWorktree(repoPath, workDir);
logger.info({ npub, repoName, branch: defaultBranch }, 'Created initial branch and README.md');
} catch (err) {
// Log but don't fail - initial README creation is nice-to-have
// This is a critical error - we need the initial branch and commit for the repo to be usable
const sanitizedErr = sanitizeError(err);
logger.warn({ error: sanitizedErr, repoPath, npub, repoName }, 'Failed to create initial branch and README, continuing anyway');
logger.error({
error: sanitizedErr,
repoPath,
npub,
repoName,
errorMessage: err instanceof Error ? err.message : String(err),
errorStack: err instanceof Error ? err.stack : undefined
}, 'CRITICAL: Failed to create initial branch and README - repository will be empty');
// Re-throw so caller can handle it appropriately
throw err;
} finally {
// Clean up worktree (always, even on error)
if (workDir) {
try {
await fileManager.removeWorktree(repoPath, workDir);
} catch (cleanupErr) {
logger.warn({ error: cleanupErr, workDir, repoPath }, 'Failed to clean up worktree (non-critical)');
}
}
}
}
@ -513,34 +568,70 @@ Your commits will all be signed by your Nostr keys and saved to the event files @@ -513,34 +568,70 @@ Your commits will all be signed by your Nostr keys and saved to the event files
): Promise<{ success: boolean; needsAnnouncement?: boolean; announcement?: NostrEvent; error?: string; cloneUrls?: string[]; remoteUrls?: string[] }> {
const repoPath = join(this.repoRoot, npub, `${repoName}.git`);
// If repo already exists, check if it has an announcement
// If repo already exists, check if it has commits (not just the directory)
if (existsSync(repoPath)) {
const hasAnnouncement = await this.announcementManager.hasAnnouncementInRepoFile(repoPath);
if (hasAnnouncement) {
return { success: true };
}
// Repo exists but no announcement - use provided announcement or try to fetch from relays
let announcementToUse: NostrEvent | null | undefined = announcementEvent;
if (!announcementToUse) {
const { requireNpubHex: requireNpubHexUtil } = await import('../../utils/npub-utils.js');
const repoOwnerPubkey = requireNpubHexUtil(npub);
announcementToUse = await this.announcementManager.fetchAnnouncementFromRelays(repoOwnerPubkey, repoName);
}
if (announcementToUse) {
// Save announcement to repo asynchronously (non-blocking)
// We have the announcement from relays, so this is just for offline papertrail
this.announcementManager.ensureAnnouncementInRepo(repoPath, announcementToUse)
.catch((err) => {
logger.warn({ error: err, repoPath, eventId: announcementToUse?.id },
'Failed to save announcement to repo (non-blocking, announcement available from relays)');
});
return { success: true, announcement: announcementToUse };
try {
const git = simpleGit(repoPath);
// Check if repo has any commits
const commitCountStr = await git.raw(['rev-list', '--count', '--all']).catch(() => '0');
const commitCount = parseInt(commitCountStr.trim(), 10);
const hasCommits = !isNaN(commitCount) && commitCount > 0;
if (hasCommits) {
// Repo has commits, check if it has an announcement
const hasAnnouncement = await this.announcementManager.hasAnnouncementInRepoFile(repoPath);
if (hasAnnouncement) {
return { success: true };
}
// Repo has commits but no announcement - use provided announcement or try to fetch from relays
let announcementToUse: NostrEvent | null | undefined = announcementEvent;
if (!announcementToUse) {
const { requireNpubHex: requireNpubHexUtil } = await import('../../utils/npub-utils.js');
const repoOwnerPubkey = requireNpubHexUtil(npub);
announcementToUse = await this.announcementManager.fetchAnnouncementFromRelays(repoOwnerPubkey, repoName);
}
if (announcementToUse) {
// Save announcement to repo asynchronously (non-blocking)
this.announcementManager.ensureAnnouncementInRepo(repoPath, announcementToUse)
.catch((err) => {
logger.warn({ error: err, repoPath, eventId: announcementToUse?.id },
'Failed to save announcement to repo (non-blocking, announcement available from relays)');
});
return { success: true, announcement: announcementToUse };
}
// Repo has commits but no announcement found - needs announcement
return { success: false, needsAnnouncement: true };
} else {
// Repo exists but is empty - remove it so we can clone fresh
logger.info({ npub, repoName }, 'Repository exists but is empty, removing to clone fresh');
try {
const { rmSync } = await import('fs');
rmSync(repoPath, { recursive: true, force: true });
logger.info({ npub, repoName }, 'Removed empty repository directory');
} catch (rmErr) {
logger.warn({ error: rmErr, npub, repoName }, 'Failed to remove empty repository, will try to fetch into it');
// Continue - might be able to fetch into existing empty repo
}
// Fall through to fetch from remotes below
}
} catch (err) {
// Error checking commits - assume empty and try to fetch
logger.warn({ error: err, npub, repoName }, 'Error checking if repo has commits, will try to fetch from remotes');
// Try to remove and clone fresh
try {
const { rmSync } = await import('fs');
rmSync(repoPath, { recursive: true, force: true });
logger.info({ npub, repoName }, 'Removed repository directory after error checking commits');
} catch (rmErr) {
logger.warn({ error: rmErr, npub, repoName }, 'Failed to remove repository after error');
}
// Fall through to fetch from remotes below
}
// Repo exists but no announcement found - needs announcement
return { success: false, needsAnnouncement: true };
// Repo exists but is empty - continue to fetch from remotes below
}
// If no announcement provided, try to fetch from relays
@ -778,13 +869,92 @@ Your commits will all be signed by your Nostr keys and saved to the event files @@ -778,13 +869,92 @@ Your commits will all be signed by your Nostr keys and saved to the event files
throw new Error('Repository clone completed but repository path does not exist');
}
// Ensure announcement is saved to nostr/repo-events.jsonl (non-blocking - repo is usable without it)
// Fire and forget - we have the announcement from relays, so this is just for offline papertrail
this.announcementManager.ensureAnnouncementInRepo(repoPath, announcementEvent)
.catch((verifyError) => {
// Announcement file creation is optional - log but don't fail
logger.warn({ error: verifyError, npub, repoName }, 'Failed to ensure announcement in repo, but repository is usable');
});
// After cloning, ensure default branch, README, and announcement are committed
try {
const repoGit = simpleGit(repoPath);
// Check if repo has any commits
let hasCommits = false;
try {
const commitCountStr = await repoGit.raw(['rev-list', '--count', '--all']).catch(() => '0');
const commitCount = parseInt(commitCountStr.trim(), 10);
hasCommits = !isNaN(commitCount) && commitCount > 0;
} catch {
hasCommits = false;
}
// Get default branch preference
let defaultBranch = preferredDefaultBranch || process.env.DEFAULT_BRANCH || 'master';
// Check existing branches
let existingBranches: string[] = [];
try {
const branches = await repoGit.branch(['-a']);
existingBranches = branches.all
.map(b => b.replace(/^remotes\/origin\//, '').replace(/^remotes\//, '').replace(/^refs\/heads\//, ''))
.filter(b => !b.includes('HEAD'));
existingBranches = [...new Set(existingBranches)];
// If we have a preferred branch and it exists, use it
if (preferredDefaultBranch && existingBranches.includes(preferredDefaultBranch)) {
defaultBranch = preferredDefaultBranch;
} else if (existingBranches.length > 0) {
// Prefer existing branches that match common defaults
const preferredBranches = preferredDefaultBranch
? [preferredDefaultBranch, defaultBranch, 'main', 'master', 'dev']
: [defaultBranch, 'main', 'master', 'dev'];
for (const preferred of preferredBranches) {
if (existingBranches.includes(preferred)) {
defaultBranch = preferred;
break;
}
}
// If no match, use the first existing branch
if (!existingBranches.includes(defaultBranch)) {
defaultBranch = existingBranches[0];
}
}
} catch {
// No branches exist yet
}
// If repo has no commits, create initial branch and commit README + announcement
if (!hasCommits) {
logger.info({ npub, repoName, defaultBranch }, 'Repository has no commits, creating initial branch and commit');
try {
await this.createInitialBranchAndReadme(repoPath, npub, repoName, announcementEvent, preferredDefaultBranch);
logger.info({ npub, repoName, defaultBranch }, 'Successfully created initial branch and commit');
} catch (createError) {
logger.error({
error: createError,
npub,
repoName,
defaultBranch,
errorMessage: createError instanceof Error ? createError.message : String(createError),
errorStack: createError instanceof Error ? createError.stack : undefined
}, 'Failed to create initial branch and commit - this is critical');
// Re-throw so the outer catch can handle it
throw createError;
}
} else {
// Repo has commits - ensure default branch exists and README/announcement are committed
logger.info({ npub, repoName, defaultBranch, hasCommits }, 'Repository has commits, ensuring default branch and files');
// Ensure announcement is committed (blocking - we want it in the repo)
// This will use worktrees to checkout the default branch and commit
await this.announcementManager.ensureAnnouncementInRepo(repoPath, announcementEvent, undefined, preferredDefaultBranch);
// Ensure README exists and is committed (also uses worktrees)
await this.ensureReadmeExists(repoPath, npub, repoName, announcementEvent, preferredDefaultBranch);
}
} catch (postCloneError) {
// Log but don't fail - repo is cloned and usable
logger.warn({
error: postCloneError,
npub,
repoName
}, 'Failed to set up default branch/README/announcement after clone, but repository is usable');
}
logger.info({ npub, repoName }, 'Successfully fetched repository on-demand');
return { success: true, announcement: announcementEvent };

7
src/routes/api/repos/[npub]/[repo]/readme/+server.ts

@ -120,11 +120,16 @@ export const GET: RequestHandler = createRepoGetHandler( @@ -120,11 +120,16 @@ export const GET: RequestHandler = createRepoGetHandler(
return json({ found: false });
}
// Determine content type
const isMarkdown = readmePath?.toLowerCase().endsWith('.md') || readmePath?.toLowerCase().endsWith('.markdown');
const isAsciiDoc = readmePath?.toLowerCase().endsWith('.adoc') || readmePath?.toLowerCase().endsWith('.asciidoc');
return json({
found: true,
content: readmeContent,
path: readmePath,
isMarkdown: readmePath?.toLowerCase().endsWith('.md') || readmePath?.toLowerCase().endsWith('.markdown')
type: isMarkdown ? 'markdown' : (isAsciiDoc ? 'asciidoc' : 'text'),
isMarkdown: isMarkdown // Keep for backward compatibility
});
},
{ operation: 'getReadme', requireRepoExists: false, requireRepoAccess: false } // README should be publicly accessible

15
src/routes/repos/[npub]/[repo]/components/DocsTab.svelte

@ -88,20 +88,29 @@ @@ -88,20 +88,29 @@
// ALWAYS load README FIRST and display immediately if available
// README is standard documentation and should always be shown
try {
const readmeResponse = await fetch(`/api/repos/${npub}/${repo}/readme?ref=${currentBranch || 'HEAD'}`);
const readmeUrl = `/api/repos/${npub}/${repo}/readme?ref=${currentBranch || 'HEAD'}`;
logger.debug({ npub, repo, branch: currentBranch, url: readmeUrl }, 'Fetching README');
const readmeResponse = await fetch(readmeUrl);
if (readmeResponse.ok) {
const readmeData = await readmeResponse.json();
logger.debug({ npub, repo, readmeData }, 'README API response');
if (readmeData.content) {
documentationContent = readmeData.content;
documentationKind = readmeData.type || 'markdown';
selectedDoc = 'README.md';
hasReadme = true;
loading = false; // Stop showing loading once README is loaded
logger.debug({ npub, repo }, 'README loaded and displayed');
logger.debug({ npub, repo, contentLength: readmeData.content.length }, 'README loaded and displayed');
} else if (readmeData.found === false) {
logger.debug({ npub, repo, branch: currentBranch }, 'README not found in repository');
} else {
logger.warn({ npub, repo, readmeData }, 'README API returned unexpected format');
}
} else {
logger.debug({ npub, repo, status: readmeResponse.status, statusText: readmeResponse.statusText }, 'README API request failed');
}
} catch (readmeErr) {
logger.debug({ error: readmeErr, npub, repo }, 'No README found');
logger.debug({ error: readmeErr, npub, repo, branch: currentBranch }, 'Error fetching README');
}
// Now check for docs folder in the background (but don't replace README)

20
src/routes/repos/[npub]/[repo]/services/repo-operations.ts

@ -215,7 +215,19 @@ export async function cloneRepository( @@ -215,7 +215,19 @@ export async function cloneRepository(
requestBody.defaultBranch = defaultBranch;
}
const data = await apiPost<{ alreadyExists?: boolean }>(`/api/repos/${state.npub}/${state.repo}/clone`, requestBody);
logger.debug({
npub: state.npub,
repo: state.repo,
hasProofEvent: !!proofEvent,
defaultBranch
}, '[Clone] Sending clone request to server');
const cloneUrl = `/api/repos/${state.npub}/${state.repo}/clone`;
logger.debug({ url: cloneUrl }, '[Clone] POST request URL');
const data = await apiPost<{ alreadyExists?: boolean }>(cloneUrl, requestBody);
logger.debug({ data }, '[Clone] Clone request successful');
if (data.alreadyExists) {
alert('Repository already exists locally.');
@ -238,6 +250,12 @@ export async function cloneRepository( @@ -238,6 +250,12 @@ export async function cloneRepository(
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to clone repository';
logger.error({
error: err,
npub: state.npub,
repo: state.repo,
errorMessage
}, '[Clone] Clone request failed');
alert(`Error: ${errorMessage}`);
console.error('Error cloning repository:', err);
} finally {

19
src/routes/repos/[npub]/[repo]/utils/safe-wrappers.ts

@ -5,15 +5,30 @@ @@ -5,15 +5,30 @@
/**
* Safely execute an async function, returning a resolved promise if window is undefined
*
* This function is designed to:
* 1. Prevent SSR errors by checking for window availability
* 2. Catch and log errors without crashing the app
* 3. Return resolved promises even on error to prevent unhandled rejections
*
* Note: Errors are logged but not re-thrown to prevent unhandled promise rejections
* in event handlers. The wrapped functions should handle their own errors (e.g., show alerts).
*/
export function safeAsync<T>(
fn: () => Promise<T>
): Promise<T | void> {
if (typeof window === 'undefined') return Promise.resolve();
try {
return fn();
return fn().catch((err) => {
// Log async errors but don't re-throw to prevent unhandled rejections
// The wrapped functions should handle their own errors (e.g., show alerts)
console.error('Error in safe async function:', err);
// Return resolved promise to prevent unhandled rejection
return Promise.resolve();
});
} catch (err) {
console.warn('Error in safe async function:', err);
// Synchronous errors - log and return resolved promise to prevent crashes
console.warn('Synchronous error in safe async function:', err);
return Promise.resolve();
}
}

Loading…
Cancel
Save