Browse Source

fallback to API if registered clone unavailble

Nostr-Signature: 4921a95aea13f6f72329ff8a278a8ff6321776973e8db327d59ea62b90d363cc 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc 0efffc826cad23849bd311be582a70cb0a42f3958c742470e8488803c5882955184b9241bf77fcf65fa5ea38feef8bc82de4965de1c783adf53ed05e461dc5de
main
Silberengel 3 weeks ago
parent
commit
5f602ce3c6
  1. 1
      nostr/commit-signatures.jsonl
  2. 75
      src/lib/services/git/api-repo-fetcher.ts
  3. 2
      src/lib/styles/repo.css
  4. 4
      src/lib/utils/api-repo-helper.ts
  5. 59
      src/routes/api/repos/[npub]/[repo]/branches/+server.ts
  6. 43
      src/routes/api/repos/[npub]/[repo]/commits/+server.ts
  7. 61
      src/routes/api/repos/[npub]/[repo]/file/+server.ts
  8. 93
      src/routes/api/repos/[npub]/[repo]/tags/+server.ts
  9. 113
      src/routes/api/repos/[npub]/[repo]/tree/+server.ts

1
nostr/commit-signatures.jsonl

@ -67,3 +67,4 @@ @@ -67,3 +67,4 @@
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771850840,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","rearrange repo pages"]],"content":"Signed commit: rearrange repo pages","id":"9f8b68f36189073807510a2dac268b466629ecbc6b8dca66ba809cbf3a36dab5","sig":"911debb546c23038bbf77a57bee089130c7cce3a51f2cfb385c3904ec39bc76b90dc9bef2e8e501824ecff13925523d802b6c916d07fef2718554f4f65e6f4d2"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771923126,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","fix new branch creation"]],"content":"Signed commit: fix new branch creation","id":"7802c9afbf005e2637282f9d06ac8130fe27bfe3a94cc67c211da51d2e9e8350","sig":"30978d6a71b4935c88ff9cd1412294d850a752977943e1aa65bcfc2290d2f2e8bbce809556849a14f0923da33b12cb53d3339741cdabab3ba949dfbb48e9cc4c"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771923236,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","clean up build warning"]],"content":"Signed commit: clean up build warning","id":"297f43968ae4bcfc8b054037b914a728eaec805770ba0c02e33aab3009c1c046","sig":"91177b6f9c4cd0d69455d5e1c109912588f05c2ddbf287d606a9687ec522ba259ed83750dfbb4b77f20e3cb82a266f251983a14405babc28c0d83eb19bf3da70"}
{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771924650,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","pass announcement"]],"content":"Signed commit: pass announcement","id":"57e1440848e4b322a9b10a6dff49973f29c8dd20b85f6cc75fd40d32eb04f0e4","sig":"3866152051a42592e83a1850bf9f3fd49af597f7dcdb523ef39374d528f6c46df6118682cac3202c29ce89a90fec8b4284c68a57101c6c590d8d1a184cac9731"}

75
src/lib/services/git/api-repo-fetcher.ts

@ -45,6 +45,7 @@ export interface ApiRepoInfo { @@ -45,6 +45,7 @@ export interface ApiRepoInfo {
branches: ApiBranch[];
commits: ApiCommit[];
files: ApiFile[];
tags?: ApiTag[];
readme?: {
path: string;
content: string;
@ -54,6 +55,13 @@ export interface ApiRepoInfo { @@ -54,6 +55,13 @@ export interface ApiRepoInfo {
isCloned: boolean; // Whether repo exists locally
}
export interface ApiTag {
name: string;
sha: string;
message?: string;
date?: string;
}
export interface ApiBranch {
name: string;
commit: {
@ -264,10 +272,10 @@ async function fetchFromGitHub(owner: string, repo: string): Promise<Partial<Api @@ -264,10 +272,10 @@ async function fetchFromGitHub(owner: string, repo: string): Promise<Partial<Api
logger.debug({ error: err, owner, repo, branch: defaultBranch }, 'Failed to get default branch SHA, will try branch name');
}
// Fetch branches, commits, and tree in parallel
// Fetch branches, commits, tags, and tree in parallel
// Use SHA if available, otherwise fall back to branch name
const treeRef = defaultBranchSha || defaultBranch;
const [branchesResponse, commitsResponse, treeResponse] = await Promise.all([
const [branchesResponse, commitsResponse, tagsResponse, treeResponse] = await Promise.all([
fetch(`https://api.github.com/repos/${owner}/${repo}/branches`, { headers }).catch((err) => {
logger.debug({ error: err, owner, repo }, 'Failed to fetch branches from GitHub');
return null;
@ -276,6 +284,10 @@ async function fetchFromGitHub(owner: string, repo: string): Promise<Partial<Api @@ -276,6 +284,10 @@ async function fetchFromGitHub(owner: string, repo: string): Promise<Partial<Api
logger.debug({ error: err, owner, repo }, 'Failed to fetch commits from GitHub');
return null;
}),
fetch(`https://api.github.com/repos/${owner}/${repo}/tags?per_page=100`, { headers }).catch((err) => {
logger.debug({ error: err, owner, repo }, 'Failed to fetch tags from GitHub');
return null;
}),
fetch(`https://api.github.com/repos/${owner}/${repo}/git/trees/${treeRef}?recursive=1`, { headers }).catch((err) => {
logger.debug({ error: err, owner, repo, treeRef }, 'Failed to fetch tree from GitHub');
return null;
@ -303,6 +315,15 @@ async function fetchFromGitHub(owner: string, repo: string): Promise<Partial<Api @@ -303,6 +315,15 @@ async function fetchFromGitHub(owner: string, repo: string): Promise<Partial<Api
}))
: [];
const tags: ApiTag[] = tagsResponse?.ok
? (await tagsResponse.json()).map((t: any) => ({
name: t.name,
sha: t.commit?.sha || '',
message: t.commit?.commit?.message?.split('\n')[0],
date: t.commit?.commit?.author?.date
}))
: [];
let files: ApiFile[] = [];
if (treeResponse?.ok) {
try {
@ -364,6 +385,7 @@ async function fetchFromGitHub(owner: string, repo: string): Promise<Partial<Api @@ -364,6 +385,7 @@ async function fetchFromGitHub(owner: string, repo: string): Promise<Partial<Api
branches,
commits,
files,
tags,
readme,
platform: 'github'
};
@ -399,8 +421,8 @@ async function fetchFromGitLab(owner: string, repo: string, baseUrl: string): Pr @@ -399,8 +421,8 @@ async function fetchFromGitLab(owner: string, repo: string, baseUrl: string): Pr
const repoData = await repoResponse.json();
const defaultBranch = repoData.default_branch || 'master';
// Fetch branches and commits in parallel
const [branchesResponse, commitsResponse] = await Promise.all([
// Fetch branches, commits, and tags in parallel
const [branchesResponse, commitsResponse, tagsResponse] = await Promise.all([
fetch(getApiBaseUrl(
`projects/${projectPath}/repository/branches`,
baseUrl,
@ -410,6 +432,11 @@ async function fetchFromGitLab(owner: string, repo: string, baseUrl: string): Pr @@ -410,6 +432,11 @@ async function fetchFromGitLab(owner: string, repo: string, baseUrl: string): Pr
`projects/${projectPath}/repository/commits`,
baseUrl,
new URLSearchParams({ per_page: '10' })
)).catch(() => null),
fetch(getApiBaseUrl(
`projects/${projectPath}/repository/tags`,
baseUrl,
new URLSearchParams({ per_page: '100' })
)).catch(() => null)
]);
@ -449,6 +476,22 @@ async function fetchFromGitLab(owner: string, repo: string, baseUrl: string): Pr @@ -449,6 +476,22 @@ async function fetchFromGitLab(owner: string, repo: string, baseUrl: string): Pr
date: c.committed_date
}));
let tagsData: any[] = [];
if (tagsResponse && tagsResponse.ok) {
tagsData = await tagsResponse.json();
if (!Array.isArray(tagsData)) {
logger.warn({ owner, repo }, 'GitLab tags response is not an array');
tagsData = [];
}
}
const tags: ApiTag[] = tagsData.map((t: any) => ({
name: t.name,
sha: t.commit?.id || '',
message: t.message,
date: t.commit?.created_at
}));
// Fetch file tree (simplified - GitLab tree API is more complex)
let files: ApiFile[] = [];
try {
@ -507,6 +550,7 @@ async function fetchFromGitLab(owner: string, repo: string, baseUrl: string): Pr @@ -507,6 +550,7 @@ async function fetchFromGitLab(owner: string, repo: string, baseUrl: string): Pr
branches,
commits,
files,
tags,
readme,
platform: 'gitlab'
};
@ -546,7 +590,7 @@ async function fetchFromGitea(owner: string, repo: string, baseUrl: string): Pro @@ -546,7 +590,7 @@ async function fetchFromGitea(owner: string, repo: string, baseUrl: string): Pro
const defaultBranch = repoData.default_branch || 'master';
const [branchesResponse, commitsResponse] = await Promise.all([
const [branchesResponse, commitsResponse, tagsResponse] = await Promise.all([
fetch(getApiBaseUrl(
`repos/${encodedOwner}/${encodedRepo}/branches`,
baseUrl,
@ -556,6 +600,11 @@ async function fetchFromGitea(owner: string, repo: string, baseUrl: string): Pro @@ -556,6 +600,11 @@ async function fetchFromGitea(owner: string, repo: string, baseUrl: string): Pro
`repos/${encodedOwner}/${encodedRepo}/commits`,
baseUrl,
new URLSearchParams({ limit: '10' })
)).catch(() => null),
fetch(getApiBaseUrl(
`repos/${encodedOwner}/${encodedRepo}/tags`,
baseUrl,
new URLSearchParams({ limit: '100' })
)).catch(() => null)
]);
@ -582,6 +631,15 @@ async function fetchFromGitea(owner: string, repo: string, baseUrl: string): Pro @@ -582,6 +631,15 @@ async function fetchFromGitea(owner: string, repo: string, baseUrl: string): Pro
logger.warn({ status: commitsResponse?.status, owner, repo }, 'Gitea API error for commits');
}
let tagsData: any[] = [];
if (tagsResponse && tagsResponse.ok) {
tagsData = await tagsResponse.json();
if (!Array.isArray(tagsData)) {
logger.warn({ owner, repo }, 'Gitea tags response is not an array');
tagsData = [];
}
}
const branches: ApiBranch[] = branchesData.map((b: any) => {
const commitObj = b.commit || {};
return {
@ -605,6 +663,13 @@ async function fetchFromGitea(owner: string, repo: string, baseUrl: string): Pro @@ -605,6 +663,13 @@ async function fetchFromGitea(owner: string, repo: string, baseUrl: string): Pro
};
});
const tags: ApiTag[] = tagsData.map((t: any) => ({
name: t.name || '',
sha: t.commit?.sha || t.sha || '',
message: t.message,
date: t.commit?.created || t.created
}));
// Fetch file tree - Gitea uses /git/trees API endpoint
let files: ApiFile[] = [];
const encodedBranch = encodeURIComponent(defaultBranch);

2
src/lib/styles/repo.css

@ -1678,7 +1678,7 @@ span.clone-more { @@ -1678,7 +1678,7 @@ span.clone-more {
.commit-button {
width: 100%;
padding: 0;
padding: 0.75rem 1rem;
text-align: left;
background: none;
border: none;

4
src/lib/utils/api-repo-helper.ts

@ -49,6 +49,7 @@ export async function tryApiFetch( @@ -49,6 +49,7 @@ export async function tryApiFetch(
defaultBranch: string;
files?: Array<{ name: string; path: string; type: 'file' | 'dir'; size?: number }>;
commits?: Array<{ sha: string; message: string; author: string; date: string }>;
tags?: Array<{ name: string; sha: string; message?: string; date?: string }>;
} | null> {
try {
const cloneUrls = extractCloneUrls(announcementEvent);
@ -126,7 +127,8 @@ export async function tryApiFetch( @@ -126,7 +127,8 @@ export async function tryApiFetch(
branches: metadata.branches || [],
defaultBranch: metadata.defaultBranch || 'main',
files: metadata.files || [],
commits: metadata.commits || []
commits: metadata.commits || [],
tags: metadata.tags || []
};
} else {
logger.warn({ url, npub, repoName, attempt: i + 1, total: sortedUrls.length }, `[${i + 1}/${sortedUrls.length}] fetchRepoMetadata returned null, trying next URL`);

59
src/routes/api/repos/[npub]/[repo]/branches/+server.ts

@ -17,6 +17,7 @@ import { DEFAULT_NOSTR_RELAYS, DEFAULT_NOSTR_SEARCH_RELAYS } from '$lib/config.j @@ -17,6 +17,7 @@ import { DEFAULT_NOSTR_RELAYS, DEFAULT_NOSTR_SEARCH_RELAYS } from '$lib/config.j
import { NostrClient } from '$lib/services/nostr/nostr-client.js';
import { eventCache } from '$lib/services/nostr/event-cache.js';
import { fetchRepoAnnouncementsWithCache, findRepoAnnouncement } from '$lib/utils/nostr-utils.js';
import { isGraspUrl } from '$lib/services/git/api-repo-fetcher.js';
import logger from '$lib/services/logger.js';
/**
@ -90,11 +91,26 @@ export const GET: RequestHandler = createRepoGetHandler( @@ -90,11 +91,26 @@ export const GET: RequestHandler = createRepoGetHandler(
// API fetch failed - repo is not cloned and API fetch didn't work
// Check if we have clone URLs to provide better error message
const hasCloneUrls = cloneUrls.length > 0;
logger.warn({ npub: context.npub, repo: context.repo, hasCloneUrls, cloneUrlCount: cloneUrls.length }, 'API fallback failed for branches');
logger.warn({
npub: context.npub,
repo: context.repo,
hasCloneUrls,
cloneUrlCount: cloneUrls.length,
cloneUrls: cloneUrls.slice(0, 3) // Log first 3 URLs for debugging
}, 'API fallback failed for branches - repo not cloned and API fetch unsuccessful');
// Provide more detailed error message
const cloneUrlTypes = cloneUrls.map(url => {
if (url.includes('github.com')) return 'GitHub';
if (url.includes('gitlab.com') || url.includes('gitlab')) return 'GitLab';
if (url.includes('gitea')) return 'Gitea';
if (isGraspUrl(url)) return 'GRASP';
return 'Unknown';
});
throw handleNotFoundError(
hasCloneUrls
? 'Repository is not cloned locally and could not be fetched via API from external clone URLs. Privileged users can clone this repository using the "Clone to Server" button.'
? `Repository is not cloned locally and could not be fetched via API from external clone URLs (${cloneUrlTypes.join(', ')}). This may be due to API rate limits, network issues, or the repository being private. Privileged users can clone this repository using the "Clone to Server" button.`
: 'Repository is not cloned locally and has no external clone URLs for API fallback. Privileged users can clone this repository using the "Clone to Server" button.',
{ operation: 'getBranches', npub: context.npub, repo: context.repo }
);
@ -136,6 +152,45 @@ export const GET: RequestHandler = createRepoGetHandler( @@ -136,6 +152,45 @@ export const GET: RequestHandler = createRepoGetHandler(
try {
const branches = await fileManager.getBranches(context.npub, context.repo);
// If repo exists but has no branches (empty repo), try API fallback
if (branches.length === 0) {
logger.debug({ npub: context.npub, repo: context.repo }, 'Repo exists but is empty, attempting API fallback');
try {
// Fetch repository announcement for API fallback
let allEvents = await fetchRepoAnnouncementsWithCache(nostrClient, context.repoOwnerPubkey, eventCache);
let announcement = findRepoAnnouncement(allEvents, context.repo);
// If no events found in cache/default relays, try all relays (default + search)
if (!announcement) {
const allRelays = [...new Set([...DEFAULT_NOSTR_RELAYS, ...DEFAULT_NOSTR_SEARCH_RELAYS])];
if (allRelays.length > DEFAULT_NOSTR_RELAYS.length) {
const allRelaysClient = new NostrClient(allRelays);
allEvents = await fetchRepoAnnouncementsWithCache(allRelaysClient, context.repoOwnerPubkey, eventCache);
announcement = findRepoAnnouncement(allEvents, context.repo);
}
}
if (announcement) {
const { tryApiFetch } = await import('$lib/utils/api-repo-helper.js');
const { extractCloneUrls } = await import('$lib/utils/nostr-utils.js');
const cloneUrls = extractCloneUrls(announcement);
logger.debug({ npub: context.npub, repo: context.repo, cloneUrlCount: cloneUrls.length }, 'Attempting API fallback for empty repo');
const apiData = await tryApiFetch(announcement, context.npub, context.repo);
if (apiData && apiData.branches && apiData.branches.length > 0) {
logger.info({ npub: context.npub, repo: context.repo, branchCount: apiData.branches.length }, 'Successfully fetched branches via API fallback for empty repo');
return json(apiData.branches);
}
}
} catch (apiErr) {
logger.debug({ error: apiErr, npub: context.npub, repo: context.repo }, 'API fallback failed for empty repo, returning empty branches');
}
}
return json(branches);
} catch (err) {
// Log the actual error for debugging

43
src/routes/api/repos/[npub]/[repo]/commits/+server.ts

@ -90,8 +90,51 @@ export const GET: RequestHandler = createRepoGetHandler( @@ -90,8 +90,51 @@ export const GET: RequestHandler = createRepoGetHandler(
try {
const commits = await fileManager.getCommitHistory(context.npub, context.repo, branch, limit, path);
// If repo exists but has no commits (empty repo), try API fallback
if (commits.length === 0) {
logger.debug({ npub: context.npub, repo: context.repo, branch }, 'Repo exists but is empty, attempting API fallback for commits');
try {
const allEvents = await fetchRepoAnnouncementsWithCache(nostrClient, context.repoOwnerPubkey, eventCache);
const announcement = findRepoAnnouncement(allEvents, context.repo);
if (announcement) {
const { tryApiFetch } = await import('$lib/utils/api-repo-helper.js');
const apiData = await tryApiFetch(announcement, context.npub, context.repo);
if (apiData && apiData.commits && apiData.commits.length > 0) {
logger.info({ npub: context.npub, repo: context.repo, commitCount: apiData.commits.length }, 'Successfully fetched commits via API fallback for empty repo');
return json(apiData.commits.slice(0, limit));
}
}
} catch (apiErr) {
logger.debug({ error: apiErr, npub: context.npub, repo: context.repo }, 'API fallback failed for empty repo, returning empty commits');
}
}
return json(commits);
} catch (err) {
// If error occurs, try API fallback before giving up
logger.debug({ error: err, npub: context.npub, repo: context.repo }, '[Commits] Error getting commit history, attempting API fallback');
try {
const allEvents = await fetchRepoAnnouncementsWithCache(nostrClient, context.repoOwnerPubkey, eventCache);
const announcement = findRepoAnnouncement(allEvents, context.repo);
if (announcement) {
const { tryApiFetch } = await import('$lib/utils/api-repo-helper.js');
const apiData = await tryApiFetch(announcement, context.npub, context.repo);
if (apiData && apiData.commits && apiData.commits.length > 0) {
logger.info({ npub: context.npub, repo: context.repo, commitCount: apiData.commits.length }, 'Successfully fetched commits via API fallback after error');
return json(apiData.commits.slice(0, limit));
}
}
} catch (apiErr) {
logger.debug({ error: apiErr, npub: context.npub, repo: context.repo }, 'API fallback failed after error');
}
// Log the actual error for debugging
logger.error({ error: err, npub: context.npub, repo: context.repo }, '[Commits] Error getting commit history');
// Check if it's a "not found" error

61
src/routes/api/repos/[npub]/[repo]/file/+server.ts

@ -198,10 +198,69 @@ export const GET: RequestHandler = async (event) => { @@ -198,10 +198,69 @@ export const GET: RequestHandler = async (event) => {
fileContent = await fileManager.getFileContent(npub, repo, filePath, 'HEAD');
ref = 'HEAD'; // Update ref for logging
} catch (headErr) {
// If HEAD also fails, throw the original error
// If HEAD also fails, try API fallback before throwing
logger.debug({ error: headErr, npub, repo, filePath }, 'Failed to read file from local repo, attempting API fallback');
try {
const allEvents = await fetchRepoAnnouncementsWithCache(nostrClient, repoOwnerPubkey, eventCache);
const announcement = findRepoAnnouncement(allEvents, repo);
if (announcement) {
const { tryApiFetchFile } = await import('$lib/utils/api-repo-helper.js');
// Use the original ref, or 'main' as fallback
const apiRef = url.searchParams.get('ref') || 'main';
const apiFileContent = await tryApiFetchFile(announcement, npub, repo, filePath, apiRef);
if (apiFileContent && apiFileContent.content) {
logger.info({ npub, repo, filePath, ref: apiRef }, 'Successfully fetched file via API fallback for empty repo');
auditLogger.logFileOperation(
userPubkeyHex || null,
requestContext.clientIp,
'read',
`${npub}/${repo}`,
filePath,
'success'
);
return json(apiFileContent);
}
}
} catch (apiErr) {
logger.debug({ error: apiErr, npub, repo, filePath }, 'API fallback failed for file');
}
// If API fallback also fails, throw the original error
throw firstErr;
}
} else {
// Try API fallback before throwing
logger.debug({ error: firstErr, npub, repo, filePath }, 'Failed to read file from local repo, attempting API fallback');
try {
const allEvents = await fetchRepoAnnouncementsWithCache(nostrClient, repoOwnerPubkey, eventCache);
const announcement = findRepoAnnouncement(allEvents, repo);
if (announcement) {
const { tryApiFetchFile } = await import('$lib/utils/api-repo-helper.js');
const apiRef = ref === 'HEAD' ? 'main' : ref;
const apiFileContent = await tryApiFetchFile(announcement, npub, repo, filePath, apiRef);
if (apiFileContent && apiFileContent.content) {
logger.info({ npub, repo, filePath, ref: apiRef }, 'Successfully fetched file via API fallback for empty repo');
auditLogger.logFileOperation(
userPubkeyHex || null,
requestContext.clientIp,
'read',
`${npub}/${repo}`,
filePath,
'success'
);
return json(apiFileContent);
}
}
} catch (apiErr) {
logger.debug({ error: apiErr, npub, repo, filePath }, 'API fallback failed for file');
}
throw firstErr;
}
}

93
src/routes/api/repos/[npub]/[repo]/tags/+server.ts

@ -5,12 +5,15 @@ @@ -5,12 +5,15 @@
import { json } from '@sveltejs/kit';
// @ts-ignore - SvelteKit generates this type
import type { RequestHandler } from './$types';
import { fileManager } from '$lib/services/service-registry.js';
import { fileManager, nostrClient } from '$lib/services/service-registry.js';
import { createRepoGetHandler, createRepoPostHandler } from '$lib/utils/api-handlers.js';
import type { RepoRequestContext, RequestEvent } from '$lib/utils/api-context.js';
import { handleValidationError, handleNotFoundError } from '$lib/utils/error-handler.js';
import { join } from 'path';
import { existsSync } from 'fs';
import { eventCache } from '$lib/services/nostr/event-cache.js';
import { fetchRepoAnnouncementsWithCache, findRepoAnnouncement } from '$lib/utils/nostr-utils.js';
import logger from '$lib/services/logger.js';
const repoRoot = typeof process !== 'undefined' && process.env?.GIT_REPO_ROOT
? process.env.GIT_REPO_ROOT
@ -20,14 +23,98 @@ export const GET: RequestHandler = createRepoGetHandler( @@ -20,14 +23,98 @@ export const GET: RequestHandler = createRepoGetHandler(
async (context: RepoRequestContext) => {
const repoPath = join(repoRoot, context.npub, `${context.repo}.git`);
// If repo doesn't exist locally, return empty tags array
// Tags are only available for locally cloned repositories
// If repo doesn't exist locally, try API fallback
if (!existsSync(repoPath)) {
try {
const allEvents = await fetchRepoAnnouncementsWithCache(nostrClient, context.repoOwnerPubkey, eventCache);
const announcement = findRepoAnnouncement(allEvents, context.repo);
if (announcement) {
const { tryApiFetch } = await import('$lib/utils/api-repo-helper.js');
const apiData = await tryApiFetch(announcement, context.npub, context.repo);
if (apiData && apiData.tags && apiData.tags.length > 0) {
logger.debug({ npub: context.npub, repo: context.repo, tagCount: apiData.tags.length }, 'Successfully fetched tags via API fallback');
// Convert API tags to FileManager.Tag format
const tags = apiData.tags.map(t => ({
name: t.name,
hash: t.sha,
message: t.message
}));
return json(tags);
}
}
} catch (apiErr) {
logger.debug({ error: apiErr, npub: context.npub, repo: context.repo }, 'API fallback failed for tags');
}
// No tags found via API fallback, return empty array
return json([]);
}
try {
const tags = await fileManager.getTags(context.npub, context.repo);
// If repo exists but has no tags (empty repo), try API fallback
if (tags.length === 0) {
logger.debug({ npub: context.npub, repo: context.repo }, 'Repo exists but is empty, attempting API fallback for tags');
try {
const allEvents = await fetchRepoAnnouncementsWithCache(nostrClient, context.repoOwnerPubkey, eventCache);
const announcement = findRepoAnnouncement(allEvents, context.repo);
if (announcement) {
const { tryApiFetch } = await import('$lib/utils/api-repo-helper.js');
const apiData = await tryApiFetch(announcement, context.npub, context.repo);
if (apiData && apiData.tags && apiData.tags.length > 0) {
logger.info({ npub: context.npub, repo: context.repo, tagCount: apiData.tags.length }, 'Successfully fetched tags via API fallback for empty repo');
// Convert API tags to FileManager.Tag format
const apiTags = apiData.tags.map(t => ({
name: t.name,
hash: t.sha,
message: t.message
}));
return json(apiTags);
}
}
} catch (apiErr) {
logger.debug({ error: apiErr, npub: context.npub, repo: context.repo }, 'API fallback failed for empty repo, returning empty tags');
}
}
return json(tags);
} catch (err) {
// If error occurs, try API fallback before giving up
logger.debug({ error: err, npub: context.npub, repo: context.repo }, '[Tags] Error getting tags, attempting API fallback');
try {
const allEvents = await fetchRepoAnnouncementsWithCache(nostrClient, context.repoOwnerPubkey, eventCache);
const announcement = findRepoAnnouncement(allEvents, context.repo);
if (announcement) {
const { tryApiFetch } = await import('$lib/utils/api-repo-helper.js');
const apiData = await tryApiFetch(announcement, context.npub, context.repo);
if (apiData && apiData.tags && apiData.tags.length > 0) {
logger.info({ npub: context.npub, repo: context.repo, tagCount: apiData.tags.length }, 'Successfully fetched tags via API fallback after error');
// Convert API tags to FileManager.Tag format
const apiTags = apiData.tags.map(t => ({
name: t.name,
hash: t.sha,
message: t.message
}));
return json(apiTags);
}
}
} catch (apiErr) {
logger.debug({ error: apiErr, npub: context.npub, repo: context.repo }, 'API fallback failed after error');
}
// If all else fails, return empty array
logger.warn({ error: err, npub: context.npub, repo: context.repo }, '[Tags] Error getting tags, returning empty array');
return json([]);
}
},
{ operation: 'getTags', requireRepoExists: false, requireRepoAccess: true } // Handle on-demand fetching, but check access for private repos
);

113
src/routes/api/repos/[npub]/[repo]/tree/+server.ts

@ -159,6 +159,67 @@ export const GET: RequestHandler = createRepoGetHandler( @@ -159,6 +159,67 @@ export const GET: RequestHandler = createRepoGetHandler(
try {
const files = await fileManager.listFiles(context.npub, context.repo, ref, path);
// If repo exists but has no files (empty repo), try API fallback
if (files.length === 0) {
logger.debug({ npub: context.npub, repo: context.repo, path, ref }, 'Repo exists but is empty, attempting API fallback for tree');
try {
// Fetch repository announcement for API fallback
const allEvents = await fetchRepoAnnouncementsWithCache(nostrClient, context.repoOwnerPubkey, eventCache);
const announcement = findRepoAnnouncement(allEvents, context.repo);
if (announcement) {
const { tryApiFetch } = await import('$lib/utils/api-repo-helper.js');
const apiData = await tryApiFetch(announcement, context.npub, context.repo);
if (apiData && apiData.files && apiData.files.length > 0) {
logger.info({ npub: context.npub, repo: context.repo, fileCount: apiData.files.length }, 'Successfully fetched files via API fallback for empty repo');
// Filter files by path if specified (same logic as above)
let filteredFiles: typeof apiData.files;
if (path) {
const normalizedPath = path.endsWith('/') ? path : `${path}/`;
filteredFiles = apiData.files.filter(f => {
if (!f.path.startsWith(normalizedPath)) {
return false;
}
const relativePath = f.path.slice(normalizedPath.length);
if (!relativePath) {
return false;
}
const cleanRelativePath = relativePath.endsWith('/') ? relativePath.slice(0, -1) : relativePath;
return !cleanRelativePath.includes('/');
});
} else {
filteredFiles = apiData.files.filter(f => {
const cleanPath = f.path.endsWith('/') ? f.path.slice(0, -1) : f.path;
const pathParts = cleanPath.split('/');
return pathParts.length === 1;
});
}
// Normalize type and name
const normalizedFiles = filteredFiles.map(f => {
const cleanPath = f.path.endsWith('/') ? f.path.slice(0, -1) : f.path;
const pathParts = cleanPath.split('/');
const displayName = pathParts[pathParts.length - 1] || f.name;
return {
name: displayName,
path: f.path,
type: (f.type === 'dir' ? 'directory' : 'file') as 'file' | 'directory',
size: f.size
};
});
return json(normalizedFiles);
}
}
} catch (apiErr) {
logger.debug({ error: apiErr, npub: context.npub, repo: context.repo }, 'API fallback failed for empty repo, returning empty files');
}
}
// Debug logging to help diagnose missing files
logger.debug({
npub: context.npub,
@ -170,6 +231,58 @@ export const GET: RequestHandler = createRepoGetHandler( @@ -170,6 +231,58 @@ export const GET: RequestHandler = createRepoGetHandler(
}, '[Tree] Returning files from fileManager.listFiles');
return json(files);
} catch (err) {
// If error occurs, try API fallback before giving up
logger.debug({ error: err, npub: context.npub, repo: context.repo }, '[Tree] Error listing files, attempting API fallback');
try {
const allEvents = await fetchRepoAnnouncementsWithCache(nostrClient, context.repoOwnerPubkey, eventCache);
const announcement = findRepoAnnouncement(allEvents, context.repo);
if (announcement) {
const { tryApiFetch } = await import('$lib/utils/api-repo-helper.js');
const apiData = await tryApiFetch(announcement, context.npub, context.repo);
if (apiData && apiData.files && apiData.files.length > 0) {
logger.info({ npub: context.npub, repo: context.repo, fileCount: apiData.files.length }, 'Successfully fetched files via API fallback after error');
// Filter and normalize files (same logic as above)
const path = context.path || '';
let filteredFiles: typeof apiData.files;
if (path) {
const normalizedPath = path.endsWith('/') ? path : `${path}/`;
filteredFiles = apiData.files.filter(f => {
if (!f.path.startsWith(normalizedPath)) return false;
const relativePath = f.path.slice(normalizedPath.length);
if (!relativePath) return false;
const cleanRelativePath = relativePath.endsWith('/') ? relativePath.slice(0, -1) : relativePath;
return !cleanRelativePath.includes('/');
});
} else {
filteredFiles = apiData.files.filter(f => {
const cleanPath = f.path.endsWith('/') ? f.path.slice(0, -1) : f.path;
return cleanPath.split('/').length === 1;
});
}
const normalizedFiles = filteredFiles.map(f => {
const cleanPath = f.path.endsWith('/') ? f.path.slice(0, -1) : f.path;
const pathParts = cleanPath.split('/');
const displayName = pathParts[pathParts.length - 1] || f.name;
return {
name: displayName,
path: f.path,
type: (f.type === 'dir' ? 'directory' : 'file') as 'file' | 'directory',
size: f.size
};
});
return json(normalizedFiles);
}
}
} catch (apiErr) {
logger.debug({ error: apiErr, npub: context.npub, repo: context.repo }, 'API fallback failed after error');
}
// Log the actual error for debugging
logger.error({ error: err, npub: context.npub, repo: context.repo }, '[Tree] Error listing files');
// Check if it's a "not found" error

Loading…
Cancel
Save