You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

162 lines
6.9 KiB

/**
* API endpoint for getting and creating repository branches
*/
import { json, error } from '@sveltejs/kit';
// @ts-ignore - SvelteKit generates this type
import type { RequestHandler } from './$types';
import { fileManager, repoManager, 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, handleApiError, handleNotFoundError } from '$lib/utils/error-handler.js';
import { KIND } from '$lib/types/nostr.js';
import { join } from 'path';
import { existsSync } from 'fs';
import { repoCache, RepoCache } from '$lib/services/git/repo-cache.js';
import { DEFAULT_NOSTR_RELAYS, DEFAULT_NOSTR_SEARCH_RELAYS } from '$lib/config.js';
import { NostrClient } from '$lib/services/nostr/nostr-client.js';
import { eventCache } from '$lib/services/nostr/event-cache.js';
const repoRoot = typeof process !== 'undefined' && process.env?.GIT_REPO_ROOT
? process.env.GIT_REPO_ROOT
: '/repos';
export const GET: RequestHandler = createRepoGetHandler(
async (context: RepoRequestContext) => {
const repoPath = join(repoRoot, context.npub, `${context.repo}.git`);
// If repo doesn't exist, try to fetch it on-demand
if (!existsSync(repoPath)) {
try {
// Try cached client first (cache-first lookup)
const filters = [
{
kinds: [KIND.REPO_ANNOUNCEMENT],
authors: [context.repoOwnerPubkey],
'#d': [context.repo],
limit: 1
}
];
let events = await nostrClient.fetchEvents(filters);
// If no events found in cache/default relays, try all relays (default + search)
// But first invalidate the cache entry so we don't get the same cached empty result
if (events.length === 0) {
const allRelays = [...new Set([...DEFAULT_NOSTR_RELAYS, ...DEFAULT_NOSTR_SEARCH_RELAYS])];
// Only create new client if we have additional relays to try
if (allRelays.length > DEFAULT_NOSTR_RELAYS.length) {
// Invalidate the cache entry so we can try fresh with all relays
eventCache.invalidate(filters);
const allRelaysClient = new NostrClient(allRelays);
events = await allRelaysClient.fetchEvents(filters);
}
}
if (events.length > 0) {
// Try API-based fetching first (no cloning)
const { tryApiFetch } = await import('$lib/utils/api-repo-helper.js');
const apiData = await tryApiFetch(events[0], context.npub, context.repo);
if (apiData) {
// Return API data directly without cloning
return json(apiData.branches);
}
// API fetch failed - repo is not cloned and API fetch didn't work
// Return 404 with helpful message suggesting to clone
throw handleNotFoundError(
'Repository is not cloned locally and could not be fetched via API. Privileged users can clone this repository using the "Clone to Server" button.',
{ operation: 'getBranches', npub: context.npub, repo: context.repo }
);
} else {
// No events found - could be because:
// 1. Repository doesn't exist
// 2. Relays are unreachable
// 3. Repository is on different relays
throw handleNotFoundError(
'Repository announcement not found in Nostr. This could mean: (1) the repository does not exist, (2) the configured Nostr relays are unreachable, or (3) the repository is published on different relays. Try configuring additional relays via the NOSTR_RELAYS environment variable.',
{ operation: 'getBranches', npub: context.npub, repo: context.repo }
);
}
} catch (err) {
// Check if repo was created by another concurrent request
if (existsSync(repoPath)) {
// Repo exists now, clear cache and continue with normal flow
repoCache.delete(RepoCache.repoExistsKey(context.npub, context.repo));
} else {
// Log the error for debugging
console.error('[Branches] Error fetching repository:', err);
// If fetching fails, return 404 with more context
const errorMessage = err instanceof Error ? err.message : 'Repository not found';
throw handleNotFoundError(
errorMessage,
{ operation: 'getBranches', npub: context.npub, repo: context.repo }
);
}
}
}
// Double-check repo exists (should be true if we got here)
if (!existsSync(repoPath)) {
throw handleNotFoundError(
'Repository not found',
{ operation: 'getBranches', npub: context.npub, repo: context.repo }
);
}
try {
const branches = await fileManager.getBranches(context.npub, context.repo);
return json(branches);
} catch (err) {
// Log the actual error for debugging
console.error('[Branches] Error getting branches:', err);
// Check if it's a "not found" error
if (err instanceof Error && err.message.includes('not found')) {
throw handleNotFoundError(
err.message,
{ operation: 'getBranches', npub: context.npub, repo: context.repo }
);
}
// Otherwise, it's a server error
throw handleApiError(
err,
{ operation: 'getBranches', npub: context.npub, repo: context.repo },
'Failed to get branches'
);
}
},
{ operation: 'getBranches', requireRepoExists: false, requireRepoAccess: true } // Handle on-demand fetching, but check access for private repos
);
export const POST: RequestHandler = createRepoPostHandler(
async (context: RepoRequestContext, event: RequestEvent) => {
const body = await event.request.json();
const { branchName, fromBranch } = body;
if (!branchName) {
throw handleValidationError('Missing branchName parameter', { operation: 'createBranch', npub: context.npub, repo: context.repo });
}
// Get default branch if fromBranch not provided
const sourceBranch = fromBranch || await fileManager.getDefaultBranch(context.npub, context.repo);
await fileManager.createBranch(context.npub, context.repo, branchName, sourceBranch);
return json({ success: true, message: 'Branch created successfully' });
},
{ operation: 'createBranch' }
);
export const DELETE: RequestHandler = createRepoPostHandler(
async (context: RepoRequestContext, event: RequestEvent) => {
const body = await event.request.json();
const { branchName } = body;
if (!branchName) {
throw handleValidationError('Missing branchName parameter', { operation: 'deleteBranch', npub: context.npub, repo: context.repo });
}
await fileManager.deleteBranch(context.npub, context.repo, branchName);
return json({ success: true, message: 'Branch deleted successfully' });
},
{ operation: 'deleteBranch' }
);