diff --git a/nostr/commit-signatures.jsonl b/nostr/commit-signatures.jsonl
index 8b84cf9..3ac0dfb 100644
--- a/nostr/commit-signatures.jsonl
+++ b/nostr/commit-signatures.jsonl
@@ -68,3 +68,4 @@
{"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"}
+{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771949714,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","fallback to API if registered clone unavailble"]],"content":"Signed commit: fallback to API if registered clone unavailble","id":"4921a95aea13f6f72329ff8a278a8ff6321776973e8db327d59ea62b90d363cc","sig":"0efffc826cad23849bd311be582a70cb0a42f3958c742470e8488803c5882955184b9241bf77fcf65fa5ea38feef8bc82de4965de1c783adf53ed05e461dc5de"}
diff --git a/src/lib/components/RepoHeaderEnhanced.svelte b/src/lib/components/RepoHeaderEnhanced.svelte
index 5c7c298..e74d262 100644
--- a/src/lib/components/RepoHeaderEnhanced.svelte
+++ b/src/lib/components/RepoHeaderEnhanced.svelte
@@ -87,7 +87,6 @@
topics = []
}: Props = $props();
- let showCloneMenu = $state(false);
let showMoreMenu = $state(false);
let showBranchMenu = $state(false);
let showOwnerMenu = $state(false);
@@ -342,34 +341,6 @@
{/if}
-
- {#if cloneUrls.length > 0}
-
-
- {#if showCloneMenu}
-
- {/if}
-
- {/if}
{#if branches.length === 0}
diff --git a/src/lib/services/git/api-repo-fetcher.ts b/src/lib/services/git/api-repo-fetcher.ts
index 3434339..b01fac7 100644
--- a/src/lib/services/git/api-repo-fetcher.ts
+++ b/src/lib/services/git/api-repo-fetcher.ts
@@ -275,11 +275,55 @@ async function fetchFromGitHub(owner: string, repo: string): Promise
{
+
+ // Fetch all branches with pagination (GitHub API defaults to 30 per page, max 100)
+ const fetchAllBranches = async (): Promise => {
+ try {
+ let allBranches: any[] = [];
+ let page = 1;
+ let hasMore = true;
+ const perPage = 100; // Maximum per page for GitHub API
+
+ while (hasMore) {
+ const response = await fetch(`https://api.github.com/repos/${owner}/${repo}/branches?per_page=${perPage}&page=${page}`, { headers });
+ if (!response.ok) {
+ if (page === 1) {
+ // Only return null on first page failure
+ return null;
+ }
+ // If later pages fail, break and return what we have
+ break;
+ }
+
+ const pageBranches = await response.json();
+ allBranches = allBranches.concat(pageBranches);
+
+ // Check if there are more pages (GitHub API returns Link header)
+ const linkHeader = response.headers.get('Link');
+ hasMore = linkHeader?.includes('rel="next"') || pageBranches.length === perPage;
+ page++;
+
+ // Safety limit: don't fetch more than 10 pages (1000 branches)
+ if (page > 10) {
+ logger.warn({ owner, repo, branchCount: allBranches.length }, 'Reached pagination limit for branches (1000), some branches may be missing');
+ break;
+ }
+ }
+
+ // Return a mock Response object with the combined branches
+ return {
+ ok: true,
+ json: async () => allBranches,
+ headers: new Headers()
+ } as Response;
+ } catch (err) {
logger.debug({ error: err, owner, repo }, 'Failed to fetch branches from GitHub');
return null;
- }),
+ }
+ };
+
+ const [branchesResponse, commitsResponse, tagsResponse, treeResponse] = await Promise.all([
+ fetchAllBranches(),
fetch(`https://api.github.com/repos/${owner}/${repo}/commits?per_page=10`, { headers }).catch((err) => {
logger.debug({ error: err, owner, repo }, 'Failed to fetch commits from GitHub');
return null;
@@ -305,6 +349,15 @@ async function fetchFromGitHub(owner: string, repo: string): Promise 0 && defaultBranch) {
+ branches.sort((a, b) => {
+ if (a.name === defaultBranch) return -1;
+ if (b.name === defaultBranch) return 1;
+ return a.name.localeCompare(b.name);
+ });
+ }
const commits: ApiCommit[] = commitsResponse?.ok
? (await commitsResponse.json()).map((c: any) => ({
@@ -421,13 +474,61 @@ async function fetchFromGitLab(owner: string, repo: string, baseUrl: string): Pr
const repoData = await repoResponse.json();
const defaultBranch = repoData.default_branch || 'master';
+ // Fetch all branches with pagination (GitLab API defaults to 20 per page, max 100)
+ const fetchAllBranches = async (): Promise => {
+ try {
+ let allBranches: any[] = [];
+ let page = 1;
+ let hasMore = true;
+ const perPage = 100; // Maximum per page for GitLab API
+
+ while (hasMore) {
+ const response = await fetch(getApiBaseUrl(
+ `projects/${projectPath}/repository/branches`,
+ baseUrl,
+ new URLSearchParams({ per_page: String(perPage), page: String(page) })
+ ));
+
+ if (!response.ok) {
+ if (page === 1) {
+ return null;
+ }
+ break;
+ }
+
+ const pageBranches = await response.json();
+ if (!Array.isArray(pageBranches)) {
+ break;
+ }
+
+ allBranches = allBranches.concat(pageBranches);
+
+ // Check if there are more pages
+ const linkHeader = response.headers.get('Link');
+ hasMore = linkHeader?.includes('rel="next"') || pageBranches.length === perPage;
+ page++;
+
+ // Safety limit: don't fetch more than 10 pages (1000 branches)
+ if (page > 10) {
+ logger.warn({ owner, repo, branchCount: allBranches.length }, 'Reached pagination limit for branches (1000), some branches may be missing');
+ break;
+ }
+ }
+
+ return {
+ ok: true,
+ json: async () => allBranches,
+ headers: new Headers()
+ } as Response;
+ } catch (err) {
+ logger.debug({ error: err, owner, repo }, 'Failed to fetch branches from GitLab');
+ return null;
+ }
+ };
+
// Fetch branches, commits, and tags in parallel
const [branchesResponse, commitsResponse, tagsResponse] = await Promise.all([
- fetch(getApiBaseUrl(
- `projects/${projectPath}/repository/branches`,
- baseUrl,
- new URLSearchParams()
- )).catch(() => null),
+ fetchAllBranches(),
fetch(getApiBaseUrl(
`projects/${projectPath}/repository/commits`,
baseUrl,
@@ -468,6 +569,15 @@ async function fetchFromGitLab(owner: string, repo: string, baseUrl: string): Pr
date: b.commit.committed_date
}
}));
+
+ // Sort branches: default branch first, then alphabetically
+ if (branches.length > 0 && defaultBranch) {
+ branches.sort((a, b) => {
+ if (a.name === defaultBranch) return -1;
+ if (b.name === defaultBranch) return 1;
+ return a.name.localeCompare(b.name);
+ });
+ }
const commits: ApiCommit[] = commitsData.map((c: any) => ({
sha: c.id,
@@ -590,12 +700,59 @@ async function fetchFromGitea(owner: string, repo: string, baseUrl: string): Pro
const defaultBranch = repoData.default_branch || 'master';
+ // Fetch all branches with pagination (Gitea API defaults to 30 per page, max 50)
+ const fetchAllBranches = async (): Promise => {
+ try {
+ let allBranches: any[] = [];
+ let page = 1;
+ let hasMore = true;
+ const perPage = 50; // Maximum per page for Gitea API
+
+ while (hasMore) {
+ const response = await fetch(getApiBaseUrl(
+ `repos/${encodedOwner}/${encodedRepo}/branches`,
+ baseUrl,
+ new URLSearchParams({ limit: String(perPage), page: String(page) })
+ ));
+
+ if (!response.ok) {
+ if (page === 1) {
+ return null;
+ }
+ break;
+ }
+
+ const pageBranches = await response.json();
+ if (!Array.isArray(pageBranches)) {
+ break;
+ }
+
+ allBranches = allBranches.concat(pageBranches);
+
+ // Gitea doesn't use Link headers, check if we got a full page
+ hasMore = pageBranches.length === perPage;
+ page++;
+
+ // Safety limit: don't fetch more than 20 pages (1000 branches)
+ if (page > 20) {
+ logger.warn({ owner, repo, branchCount: allBranches.length }, 'Reached pagination limit for branches (1000), some branches may be missing');
+ break;
+ }
+ }
+
+ return {
+ ok: true,
+ json: async () => allBranches,
+ headers: new Headers()
+ } as Response;
+ } catch (err) {
+ logger.debug({ error: err, owner, repo }, 'Failed to fetch branches from Gitea');
+ return null;
+ }
+ };
+
const [branchesResponse, commitsResponse, tagsResponse] = await Promise.all([
- fetch(getApiBaseUrl(
- `repos/${encodedOwner}/${encodedRepo}/branches`,
- baseUrl,
- new URLSearchParams()
- )).catch(() => null),
+ fetchAllBranches(),
fetch(getApiBaseUrl(
`repos/${encodedOwner}/${encodedRepo}/commits`,
baseUrl,
@@ -652,6 +809,15 @@ async function fetchFromGitea(owner: string, repo: string, baseUrl: string): Pro
}
};
});
+
+ // Sort branches: default branch first, then alphabetically
+ if (branches.length > 0 && defaultBranch) {
+ branches.sort((a, b) => {
+ if (a.name === defaultBranch) return -1;
+ if (b.name === defaultBranch) return 1;
+ return a.name.localeCompare(b.name);
+ });
+ }
const commits: ApiCommit[] = commitsData.map((c: any) => {
const commitObj = c.commit || {};
diff --git a/src/lib/services/git/file-manager.ts b/src/lib/services/git/file-manager.ts
index 5d8afd8..a486dc4 100644
--- a/src/lib/services/git/file-manager.ts
+++ b/src/lib/services/git/file-manager.ts
@@ -1626,7 +1626,24 @@ export class FileManager {
}
}
- const branchList = Array.from(allBranches).sort();
+ // Sort branches: default branch first, then alphabetically
+ let branchList = Array.from(allBranches);
+ try {
+ const defaultBranch = await this.getDefaultBranch(npub, repoName);
+ if (defaultBranch) {
+ branchList.sort((a, b) => {
+ if (a === defaultBranch) return -1;
+ if (b === defaultBranch) return 1;
+ return a.localeCompare(b);
+ });
+ } else {
+ // No default branch found, just sort alphabetically
+ branchList.sort();
+ }
+ } catch {
+ // If we can't get default branch, just sort alphabetically
+ branchList.sort();
+ }
// Cache the result (cache for 2 minutes)
repoCache.set(cacheKey, branchList, 2 * 60 * 1000);
diff --git a/src/routes/api/repos/[npub]/[repo]/branches/+server.ts b/src/routes/api/repos/[npub]/[repo]/branches/+server.ts
index 292c9dc..137e252 100644
--- a/src/routes/api/repos/[npub]/[repo]/branches/+server.ts
+++ b/src/routes/api/repos/[npub]/[repo]/branches/+server.ts
@@ -84,8 +84,18 @@ export const GET: RequestHandler = createRepoGetHandler(
if (apiData && apiData.branches && apiData.branches.length > 0) {
logger.debug({ npub: context.npub, repo: context.repo, branchCount: apiData.branches.length }, 'Successfully fetched branches via API fallback');
- // Return API data directly without cloning
- return json(apiData.branches);
+ // Sort branches: default branch first, then alphabetically
+ const sortedBranches = [...apiData.branches];
+ if (apiData.defaultBranch) {
+ sortedBranches.sort((a: any, b: any) => {
+ const aName = typeof a === 'string' ? a : a.name;
+ const bName = typeof b === 'string' ? b : b.name;
+ if (aName === apiData.defaultBranch) return -1;
+ if (bName === apiData.defaultBranch) return 1;
+ return aName.localeCompare(bName);
+ });
+ }
+ return json(sortedBranches);
}
// API fetch failed - repo is not cloned and API fetch didn't work
@@ -183,7 +193,18 @@ export const GET: RequestHandler = createRepoGetHandler(
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);
+ // Sort branches: default branch first, then alphabetically
+ const sortedBranches = [...apiData.branches];
+ if (apiData.defaultBranch) {
+ sortedBranches.sort((a: any, b: any) => {
+ const aName = typeof a === 'string' ? a : a.name;
+ const bName = typeof b === 'string' ? b : b.name;
+ if (aName === apiData.defaultBranch) return -1;
+ if (bName === apiData.defaultBranch) return 1;
+ return aName.localeCompare(bName);
+ });
+ }
+ return json(sortedBranches);
}
}
} catch (apiErr) {
@@ -191,7 +212,36 @@ export const GET: RequestHandler = createRepoGetHandler(
}
}
- return json(branches);
+ // Sort branches: default branch first, then alphabetically
+ let sortedBranches = [...branches];
+ try {
+ const defaultBranch = await fileManager.getDefaultBranch(context.npub, context.repo);
+ if (defaultBranch) {
+ sortedBranches.sort((a: any, b: any) => {
+ const aName = typeof a === 'string' ? a : a.name;
+ const bName = typeof b === 'string' ? b : b.name;
+ if (aName === defaultBranch) return -1;
+ if (bName === defaultBranch) return 1;
+ return aName.localeCompare(bName);
+ });
+ } else {
+ // No default branch found, just sort alphabetically
+ sortedBranches.sort((a: any, b: any) => {
+ const aName = typeof a === 'string' ? a : a.name;
+ const bName = typeof b === 'string' ? b : b.name;
+ return aName.localeCompare(bName);
+ });
+ }
+ } catch {
+ // If we can't get default branch, just sort alphabetically
+ sortedBranches.sort((a: any, b: any) => {
+ const aName = typeof a === 'string' ? a : a.name;
+ const bName = typeof b === 'string' ? b : b.name;
+ return aName.localeCompare(bName);
+ });
+ }
+
+ return json(sortedBranches);
} catch (err) {
// Log the actual error for debugging
logger.error({ error: err, npub: context.npub, repo: context.repo }, '[Branches] Error getting branches');
diff --git a/src/routes/api/repos/[npub]/[repo]/diff/+server.ts b/src/routes/api/repos/[npub]/[repo]/diff/+server.ts
index bf3c21d..67f5d45 100644
--- a/src/routes/api/repos/[npub]/[repo]/diff/+server.ts
+++ b/src/routes/api/repos/[npub]/[repo]/diff/+server.ts
@@ -2,12 +2,21 @@
* API endpoint for getting diffs
*/
-import { json } from '@sveltejs/kit';
+import { json, error } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
-import { fileManager } from '$lib/services/service-registry.js';
+import { fileManager, nostrClient } from '$lib/services/service-registry.js';
import { createRepoGetHandler } from '$lib/utils/api-handlers.js';
import type { RepoRequestContext, RequestEvent } from '$lib/utils/api-context.js';
-import { handleValidationError } from '$lib/utils/error-handler.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
+ : '/repos';
export const GET: RequestHandler = createRepoGetHandler(
async (context: RepoRequestContext, event: RequestEvent) => {
@@ -19,8 +28,52 @@ export const GET: RequestHandler = createRepoGetHandler(
throw handleValidationError('Missing from parameter', { operation: 'getDiff', npub: context.npub, repo: context.repo });
}
- const diffs = await fileManager.getDiff(context.npub, context.repo, fromRef, toRef, filePath);
- return json(diffs);
+ const repoPath = join(repoRoot, context.npub, `${context.repo}.git`);
+
+ // Check if repo exists
+ if (!existsSync(repoPath)) {
+ // Repo doesn't exist - diffs are not available via API fallback
+ // GitHub/GitLab APIs don't provide easy diff endpoints
+ logger.debug({ npub: context.npub, repo: context.repo, fromRef, toRef }, 'Diff requested for non-existent repo');
+ throw handleNotFoundError(
+ 'Repository is not cloned locally. Diffs are only available for cloned repositories. Please clone the repository to view diffs.',
+ { operation: 'getDiff', npub: context.npub, repo: context.repo }
+ );
+ }
+
+ try {
+ const diffs = await fileManager.getDiff(context.npub, context.repo, fromRef, toRef, filePath);
+ return json(diffs);
+ } catch (err) {
+ // If error occurs, check if repo is empty
+ logger.debug({ error: err, npub: context.npub, repo: context.repo, fromRef, toRef }, 'Error getting diff, checking if repo is empty');
+
+ try {
+ // Check if repo has any branches
+ const branches = await fileManager.getBranches(context.npub, context.repo);
+ if (branches.length === 0) {
+ // Repo is empty - diffs not available
+ throw handleNotFoundError(
+ 'Repository is empty. Diffs are only available for repositories with commits.',
+ { operation: 'getDiff', npub: context.npub, repo: context.repo }
+ );
+ }
+ } catch (branchErr) {
+ // If we can't get branches, the repo might be empty or corrupted
+ logger.debug({ error: branchErr, npub: context.npub, repo: context.repo }, 'Failed to get branches, repo may be empty');
+ }
+
+ // Re-throw the original error with better context
+ const errorMessage = err instanceof Error ? err.message : 'Failed to get diff';
+ if (errorMessage.includes('not found') || errorMessage.includes('Invalid object name')) {
+ throw handleNotFoundError(
+ `Commit not found: ${errorMessage}. The commit hash may be invalid or the repository may not have the requested commits.`,
+ { operation: 'getDiff', npub: context.npub, repo: context.repo }
+ );
+ }
+
+ throw err;
+ }
},
- { operation: 'getDiff' }
+ { operation: 'getDiff', requireRepoExists: false, requireRepoAccess: true }
);
diff --git a/src/routes/api/repos/[npub]/[repo]/tree/+server.ts b/src/routes/api/repos/[npub]/[repo]/tree/+server.ts
index a4a461e..213e4d4 100644
--- a/src/routes/api/repos/[npub]/[repo]/tree/+server.ts
+++ b/src/routes/api/repos/[npub]/[repo]/tree/+server.ts
@@ -41,7 +41,13 @@ export const GET: RequestHandler = createRepoGetHandler(
const apiData = await tryApiFetch(announcement, context.npub, context.repo);
- if (apiData && apiData.files && apiData.files.length > 0) {
+ if (apiData && apiData.files !== undefined) {
+ // Return empty array if no files (legitimate for empty repos)
+ // Only proceed if we have files to filter
+ if (apiData.files.length === 0) {
+ logger.debug({ npub: context.npub, repo: context.repo, path: context.path }, 'API fallback returned empty files array (repo may be empty)');
+ return json([]);
+ }
logger.debug({ npub: context.npub, repo: context.repo, fileCount: apiData.files.length }, 'Successfully fetched files via API fallback');
// Return API data directly without cloning
const path = context.path || '';
diff --git a/src/routes/repos/[npub]/[repo]/+page.svelte b/src/routes/repos/[npub]/[repo]/+page.svelte
index b952d98..f24a7a9 100644
--- a/src/routes/repos/[npub]/[repo]/+page.svelte
+++ b/src/routes/repos/[npub]/[repo]/+page.svelte
@@ -3044,8 +3044,10 @@
: 'master');
}
- // Final validation: ensure branchName is a valid string and doesn't contain invalid characters
- if (!branchName || typeof branchName !== 'string' || branchName.includes('#') || branchName.trim() === '') {
+ // Final validation: ensure branchName is a valid string
+ // Note: We allow '#' in branch names for existing branches (they'll be URL-encoded)
+ // Only reject if it's empty or not a string
+ if (!branchName || typeof branchName !== 'string' || branchName.trim() === '') {
console.warn('[loadFile] Invalid branch name detected, using fallback:', branchName);
branchName = defaultBranch || (branches.length > 0
? (typeof branches[0] === 'string' ? branches[0] : branches[0].name)
@@ -3943,9 +3945,27 @@
if (response.ok) {
diffData = await response.json();
showDiff = true;
+ } else {
+ // Handle 404 or other errors
+ const errorText = await response.text().catch(() => response.statusText);
+ if (response.status === 404) {
+ // Check if this is an API fallback commit (repo not cloned or empty)
+ if (isRepoCloned === false || (isRepoCloned === true && apiFallbackAvailable)) {
+ error = 'Diffs are not available for commits viewed via API fallback. Please clone the repository to view diffs.';
+ } else {
+ error = `Commit not found: ${errorText || 'The commit may not exist in the repository'}`;
+ }
+ } else {
+ error = `Failed to load diff: ${errorText || response.statusText}`;
+ }
}
} catch (err) {
- error = err instanceof Error ? err.message : 'Failed to load diff';
+ // Handle network errors
+ if (err instanceof TypeError && err.message.includes('NetworkError')) {
+ error = 'Network error: Unable to fetch diff. Please check your connection and try again.';
+ } else {
+ error = err instanceof Error ? err.message : 'Failed to load diff';
+ }
} finally {
loadingCommits = false;
}