diff --git a/nostr/commit-signatures.jsonl b/nostr/commit-signatures.jsonl index 0ea2f32..dc8fe52 100644 --- a/nostr/commit-signatures.jsonl +++ b/nostr/commit-signatures.jsonl @@ -73,3 +73,4 @@ {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771956701,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","implemented releases and code serach\nadd contributors to private repos\napply/merge buttons for patches and PRs\nhighlgihts and comments on patches and prs\nadded tagged downloads"]],"content":"Signed commit: implemented releases and code serach\nadd contributors to private repos\napply/merge buttons for patches and PRs\nhighlgihts and comments on patches and prs\nadded tagged downloads","id":"e822be2b0fbf3285bbedf9d8f9d1692b5503080af17a4d28941a1dc81c96187c","sig":"70c8b6e499551ce43478116cf694992102a29572d5380cbe3b070a3026bc2c9e35177587712c7414f25d1ca50038c9614479f7758bbdc48f69cc44cd52bf4842"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771958124,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","fix crash on download"]],"content":"Signed commit: fix crash on download","id":"3fdcc681cdda4b523f9c3752309b8cf740b58178ca02dcff4ef97ec714bf394c","sig":"e405612a5aafeef66818f0a3c683e322f862d1fc3c662c32f618f516fd8c11ece5f4539b94893583301d31fd2ecd3de3b6d7a953505e2696915afe10710a16d7"} {"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771964922,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","fix page crash on download"]],"content":"Signed commit: fix page crash on download","id":"eafa232557affbacb430b467507febc201f0a8f54f4b9ecf57e315c32e51a589","sig":"53c58aabe0bfad6e432a8bb980c2046fc14bc8163825fde2ac766a449ce4418adb1049ac732c7fc7ecc7ad050539fb68c023d54f2b6c390e478616b5c0b91a31"} +{"kind":1640,"pubkey":"573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc","created_at":1771967413,"tags":[["author","Silberengel","silberengel7@protonmail.com"],["message","get rid of light theme"]],"content":"Signed commit: get rid of light theme","id":"16cc720587afa7994fdf4d1951934298d731f79d8fe4a3c5d4b9143e3b41abfd","sig":"125b3afa090a8a2679d6e2614163c8c95a42ba6d3323e9682ce94ecff387da8d1abbfffcc61d59646c6925d8e845527570387b012c194deed032fa7d43bceac0"} diff --git a/src/lib/styles/repo.css b/src/lib/styles/repo.css index 365b164..a3a258d 100644 --- a/src/lib/styles/repo.css +++ b/src/lib/styles/repo.css @@ -934,7 +934,8 @@ .discussions-sidebar, .docs-sidebar, .history-sidebar, - .tags-sidebar { + .tags-sidebar, + .code-search-sidebar { width: 100% !important; max-width: 100% !important; height: 100%; @@ -955,6 +956,7 @@ .docs-header, .history-header, .tags-header, + .code-search-header, .file-tree-header { width: 100%; max-width: 100%; @@ -987,7 +989,8 @@ .discussions-sidebar.hide-on-mobile, .docs-sidebar.hide-on-mobile, .history-sidebar.hide-on-mobile, - .tags-sidebar.hide-on-mobile { + .tags-sidebar.hide-on-mobile, + .code-search-sidebar.hide-on-mobile { display: none !important; } @@ -995,7 +998,8 @@ .prs-content.hide-on-mobile, .patches-content.hide-on-mobile, .discussions-content.hide-on-mobile, - .docs-content.hide-on-mobile { + .docs-content.hide-on-mobile, + .code-search-content.hide-on-mobile { display: none !important; } @@ -1438,7 +1442,8 @@ span.clone-more { .prs-sidebar, .patches-sidebar, .discussions-sidebar, -.docs-sidebar { +.docs-sidebar, +.code-search-sidebar { width: 300px; border-right: 1px solid var(--border-color); background: var(--bg-secondary); @@ -1460,7 +1465,8 @@ span.clone-more { .prs-header, .docs-header, .patches-header, -.discussions-header { +.discussions-header, +.code-search-header { padding: 1rem; border-bottom: 1px solid var(--border-color); display: flex; @@ -1487,10 +1493,12 @@ span.clone-more { .prs-header h2, .issues-header h2, .history-header h2, -.tags-header h2 { +.tags-header h2, +.code-search-header h2 { margin: 0; white-space: nowrap; flex-shrink: 0; + color: var(--text-primary); /* Ensure proper contrast in dark themes */ } .issues-content, @@ -1499,12 +1507,149 @@ span.clone-more { .discussions-content, .tags-content, .docs-content, -.commits-content { +.commits-content, +.code-search-content { padding: 1rem; flex: 1; overflow-y: auto; } +/* Code Search Form */ +.code-search-form { + margin-bottom: 1.5rem; +} + +.search-input-group { + display: flex; + gap: 0.75rem; + align-items: center; + flex-wrap: wrap; +} + +.code-search-input { + flex: 1; + min-width: 200px; + padding: 0.5rem 0.75rem; + border: 1px solid var(--border-color); + border-radius: 0.375rem; + background: var(--input-bg); + color: var(--text-primary); + font-size: 0.875rem; + font-family: 'IBM Plex Mono', monospace; +} + +.code-search-input:focus { + outline: none; + border-color: var(--input-focus); + box-shadow: 0 0 0 2px var(--focus-ring); +} + +.code-search-scope { + padding: 0.5rem 0.75rem; + border: 1px solid var(--border-color); + border-radius: 0.375rem; + background: var(--input-bg); + color: var(--text-primary); + font-size: 0.875rem; + cursor: pointer; +} + +.code-search-scope:focus { + outline: none; + border-color: var(--input-focus); + box-shadow: 0 0 0 2px var(--focus-ring); +} + +.search-button { + padding: 0.5rem 1rem; + background: var(--button-primary); + color: var(--accent-text, #ffffff); + border: none; + border-radius: 0.375rem; + font-size: 0.875rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; +} + +.search-button:hover:not(:disabled) { + background: var(--button-primary-hover); + transform: translateY(-1px); +} + +.search-button:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* Code Search Results */ +.code-search-results { + margin-top: 1rem; +} + +.code-search-results h3 { + margin: 0 0 1rem 0; + color: var(--text-primary); + font-size: 1.1rem; + font-weight: 600; +} + +.code-search-result-item { + margin-bottom: 1.5rem; + padding: 1rem; + background: var(--bg-secondary); + border: 1px solid var(--border-color); + border-radius: 0.375rem; + transition: all 0.2s ease; +} + +.code-search-result-item:hover { + border-color: var(--accent); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.result-header { + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 0.75rem; + flex-wrap: wrap; +} + +.result-file { + font-weight: 600; + color: var(--text-primary); + font-family: 'IBM Plex Mono', monospace; + font-size: 0.9rem; +} + +.result-line { + color: var(--text-secondary); + font-size: 0.85rem; + font-family: 'IBM Plex Mono', monospace; +} + +.result-repo { + color: var(--text-muted); + font-size: 0.85rem; + font-family: 'IBM Plex Mono', monospace; +} + +.result-content { + margin: 0; + padding: 0.75rem; + background: var(--bg-tertiary); + border: 1px solid var(--border-color); + border-radius: 0.25rem; + overflow-x: auto; + font-family: 'IBM Plex Mono', monospace; + font-size: 0.85rem; + line-height: 1.5; + color: var(--text-primary); + white-space: pre-wrap; + word-wrap: break-word; +} + .patch-header, .issue-header { display: flex; diff --git a/src/routes/api/code-search/+server.ts b/src/routes/api/code-search/+server.ts index f5a6be7..72ba6be 100644 --- a/src/routes/api/code-search/+server.ts +++ b/src/routes/api/code-search/+server.ts @@ -17,6 +17,7 @@ import { readdir, stat } from 'fs/promises'; import { join } from 'path'; import { existsSync } from 'fs'; import { simpleGit } from 'simple-git'; +import { fileManager } from '$lib/services/service-registry.js'; const repoRoot = typeof process !== 'undefined' && process.env?.GIT_REPO_ROOT ? process.env.GIT_REPO_ROOT @@ -168,60 +169,156 @@ async function searchInRepo( try { const branches = await git.branchLocal(); branch = branches.current || 'HEAD'; + // If no current branch, try common defaults + if (!branch || branch === 'HEAD') { + const allBranches = branches.all.map(b => b.replace(/^remotes\/origin\//, '').replace(/^remotes\//, '')); + branch = allBranches.find(b => b === 'main') || allBranches.find(b => b === 'master') || allBranches[0] || 'main'; + } } catch { - // Use HEAD if we can't get branch + branch = 'main'; } - const searchQuery = query.trim(); - const gitArgs = ['grep', '-n', '-I', '--break', '--heading', searchQuery, branch]; - + // For bare repositories, we need to use a worktree or search the index + let worktreePath: string | null = null; try { - const grepOutput = await git.raw(gitArgs); - - if (!grepOutput || !grepOutput.trim()) { - return []; + // Get the actual branch name (resolve HEAD if needed) + let actualBranch = branch; + if (branch === 'HEAD') { + actualBranch = 'main'; } - const lines = grepOutput.split('\n'); - let currentFile = ''; - - for (const line of lines) { - if (!line.trim()) { - continue; - } + // Get or create worktree + worktreePath = await fileManager.getWorktree(repoPath, actualBranch, npub, repo); + } catch (worktreeError) { + logger.debug({ error: worktreeError, npub, repo, branch }, 'Could not create worktree, trying git grep with tree reference'); + // Fall back to searching the index + } + + const searchQuery = query.trim(); + + // If we have a worktree, search in the worktree + if (worktreePath && existsSync(worktreePath)) { + try { + const worktreeGit = simpleGit(worktreePath); + const gitArgs = ['grep', '-n', '-I', '--break', '--heading', searchQuery]; + const grepOutput = await worktreeGit.raw(gitArgs); - if (!line.includes(':')) { - currentFile = line.trim(); - continue; + if (!grepOutput || !grepOutput.trim()) { + return []; } + + // Parse git grep output + const lines = grepOutput.split('\n'); + let currentFile = ''; - const colonIndex = line.indexOf(':'); - if (colonIndex > 0 && currentFile) { - const lineNumber = parseInt(line.substring(0, colonIndex), 10); - const content = line.substring(colonIndex + 1); + for (const line of lines) { + if (!line.trim()) { + continue; + } - if (!isNaN(lineNumber) && content) { - results.push({ - repo, - npub, - file: currentFile, - line: lineNumber, - content: content.trim(), - branch: branch === 'HEAD' ? 'HEAD' : branch - }); + // Check if this is a filename (no colon) + if (!line.includes(':')) { + currentFile = line.trim(); + continue; + } + + // Parse line:content format + const colonIndex = line.indexOf(':'); + if (colonIndex > 0 && currentFile) { + const lineNumber = parseInt(line.substring(0, colonIndex), 10); + const content = line.substring(colonIndex + 1); - if (results.length >= limit) { - break; + if (!isNaN(lineNumber) && content) { + // Make file path relative to repo root + const relativeFile = currentFile.replace(worktreePath + '/', '').replace(/^\.\//, ''); + results.push({ + repo, + npub, + file: relativeFile, + line: lineNumber, + content: content.trim(), + branch: branch === 'HEAD' ? 'HEAD' : branch + }); + + if (results.length >= limit) { + break; + } } } } + } catch (grepError: any) { + // git grep returns exit code 1 when no matches found + if (grepError.message && grepError.message.includes('exit code 1')) { + return []; + } + throw grepError; } - } catch (grepError: any) { - // git grep returns exit code 1 when no matches found - if (grepError.message && grepError.message.includes('exit code 1')) { - return []; + } else { + // Fallback: search in the index using git grep with tree reference + try { + // Get the tree for the branch + let treeRef = branch; + if (branch === 'HEAD') { + try { + const branchInfo = await git.branch(['-a']); + treeRef = branchInfo.current || 'HEAD'; + } catch { + treeRef = 'HEAD'; + } + } + + // Use git grep with tree reference for bare repos + const gitArgs = ['grep', '-n', '-I', '--break', '--heading', searchQuery, treeRef]; + const grepOutput = await git.raw(gitArgs); + + if (!grepOutput || !grepOutput.trim()) { + return []; + } + + // Parse git grep output + const lines = grepOutput.split('\n'); + let currentFile = ''; + + for (const line of lines) { + if (!line.trim()) { + continue; + } + + // Check if this is a filename (no colon) + if (!line.includes(':')) { + currentFile = line.trim(); + continue; + } + + // Parse line:content format + const colonIndex = line.indexOf(':'); + if (colonIndex > 0 && currentFile) { + const lineNumber = parseInt(line.substring(0, colonIndex), 10); + const content = line.substring(colonIndex + 1); + + if (!isNaN(lineNumber) && content) { + results.push({ + repo, + npub, + file: currentFile, + line: lineNumber, + content: content.trim(), + branch: branch === 'HEAD' ? 'HEAD' : branch + }); + + if (results.length >= limit) { + break; + } + } + } + } + } catch (grepError: any) { + // git grep returns exit code 1 when no matches found + if (grepError.message && grepError.message.includes('exit code 1')) { + return []; + } + throw grepError; } - throw grepError; } } catch (err) { logger.debug({ error: err, npub, repo, query }, 'Error searching in repo'); diff --git a/src/routes/api/repos/[npub]/[repo]/code-search/+server.ts b/src/routes/api/repos/[npub]/[repo]/code-search/+server.ts index f2f21e9..e443c39 100644 --- a/src/routes/api/repos/[npub]/[repo]/code-search/+server.ts +++ b/src/routes/api/repos/[npub]/[repo]/code-search/+server.ts @@ -13,6 +13,7 @@ import { join } from 'path'; import { existsSync } from 'fs'; import logger from '$lib/services/logger.js'; import { simpleGit } from 'simple-git'; +import { readFile } from 'fs/promises'; const repoRoot = typeof process !== 'undefined' && process.env?.GIT_REPO_ROOT ? process.env.GIT_REPO_ROOT @@ -48,80 +49,155 @@ export const GET: RequestHandler = createRepoGetHandler( const git = simpleGit(repoPath); const results: CodeSearchResult[] = []; - // Use git grep to search file contents - // git grep -n -I --break --heading -i "query" branch - // -n: show line numbers - // -I: ignore binary files - // --break: add blank line between matches from different files - // --heading: show filename before matches - // -i: case-insensitive (optional, we'll make it configurable) - - const searchQuery = query.trim(); - const gitArgs = ['grep', '-n', '-I', '--break', '--heading', searchQuery, branch]; - + // For bare repositories, we need to use a worktree or search the index + // First, try to get or create a worktree for the branch + let worktreePath: string | null = null; try { - const grepOutput = await git.raw(gitArgs); - - if (!grepOutput || !grepOutput.trim()) { - return json([]); + // Get the actual branch name (resolve HEAD if needed) + let actualBranch = branch; + if (branch === 'HEAD') { + try { + const branchInfo = await git.branch(['-a']); + actualBranch = branchInfo.current || 'main'; + // If no current branch, try common defaults + if (!actualBranch || actualBranch === 'HEAD') { + const allBranches = branchInfo.all.map(b => b.replace(/^remotes\/origin\//, '').replace(/^remotes\//, '')); + actualBranch = allBranches.find(b => b === 'main') || allBranches.find(b => b === 'master') || allBranches[0] || 'main'; + } + } catch { + actualBranch = 'main'; + } } - // Parse git grep output - // Format: - // filename - // line:content - // line:content - // - // filename2 - // line:content - - const lines = grepOutput.split('\n'); - let currentFile = ''; - - for (const line of lines) { - if (!line.trim()) { - continue; // Skip empty lines + // Get or create worktree + worktreePath = await fileManager.getWorktree(repoPath, actualBranch, context.npub, context.repo); + } catch (worktreeError) { + logger.debug({ error: worktreeError, npub: context.npub, repo: context.repo, branch }, 'Could not create worktree, trying git grep with --cached'); + // Fall back to searching the index + } + + const searchQuery = query.trim(); + + // If we have a worktree, search in the worktree + if (worktreePath && existsSync(worktreePath)) { + try { + const worktreeGit = simpleGit(worktreePath); + const gitArgs = ['grep', '-n', '-I', '--break', '--heading', searchQuery]; + const grepOutput = await worktreeGit.raw(gitArgs); + + if (!grepOutput || !grepOutput.trim()) { + return json([]); } + + // Parse git grep output + const lines = grepOutput.split('\n'); + let currentFile = ''; - // Check if this is a filename (no colon, or starts with a path) - if (!line.includes(':') || line.startsWith('/') || line.match(/^[a-zA-Z0-9_\-./]+$/)) { - // This might be a filename - // Git grep with --heading shows filename on its own line - // But we need to be careful - it could also be content with a colon - // If it doesn't have a colon and looks like a path, it's a filename + for (const line of lines) { + if (!line.trim()) { + continue; + } + + // Check if this is a filename (no colon) if (!line.includes(':')) { currentFile = line.trim(); continue; } + + // Parse line:content format + const colonIndex = line.indexOf(':'); + if (colonIndex > 0 && currentFile) { + const lineNumber = parseInt(line.substring(0, colonIndex), 10); + const content = line.substring(colonIndex + 1); + + if (!isNaN(lineNumber) && content) { + // Make file path relative to repo root + const relativeFile = currentFile.replace(worktreePath + '/', '').replace(/^\.\//, ''); + results.push({ + file: relativeFile, + line: lineNumber, + content: content.trim(), + branch: branch === 'HEAD' ? 'HEAD' : branch + }); + + if (results.length >= limit) { + break; + } + } + } } + } catch (grepError: any) { + // git grep returns exit code 1 when no matches found + if (grepError.message && grepError.message.includes('exit code 1')) { + return json([]); + } + throw grepError; + } + } else { + // Fallback: search in the index using git grep --cached + try { + // Get the tree for the branch + let treeRef = branch; + if (branch === 'HEAD') { + try { + const branchInfo = await git.branch(['-a']); + treeRef = branchInfo.current || 'HEAD'; + } catch { + treeRef = 'HEAD'; + } + } + + // Use git grep with --cached to search the index + // For bare repos, we can search a specific tree + const gitArgs = ['grep', '-n', '-I', '--break', '--heading', searchQuery, treeRef]; + const grepOutput = await git.raw(gitArgs); - // Parse line:content format - const colonIndex = line.indexOf(':'); - if (colonIndex > 0 && currentFile) { - const lineNumber = parseInt(line.substring(0, colonIndex), 10); - const content = line.substring(colonIndex + 1); + if (!grepOutput || !grepOutput.trim()) { + return json([]); + } + + // Parse git grep output + const lines = grepOutput.split('\n'); + let currentFile = ''; + + for (const line of lines) { + if (!line.trim()) { + continue; + } + + // Check if this is a filename (no colon) + if (!line.includes(':')) { + currentFile = line.trim(); + continue; + } - if (!isNaN(lineNumber) && content) { - results.push({ - file: currentFile, - line: lineNumber, - content: content.trim(), - branch: branch === 'HEAD' ? 'HEAD' : branch - }); + // Parse line:content format + const colonIndex = line.indexOf(':'); + if (colonIndex > 0 && currentFile) { + const lineNumber = parseInt(line.substring(0, colonIndex), 10); + const content = line.substring(colonIndex + 1); - if (results.length >= limit) { - break; + if (!isNaN(lineNumber) && content) { + results.push({ + file: currentFile, + line: lineNumber, + content: content.trim(), + branch: branch === 'HEAD' ? 'HEAD' : branch + }); + + if (results.length >= limit) { + break; + } } } } + } catch (grepError: any) { + // git grep returns exit code 1 when no matches found + if (grepError.message && grepError.message.includes('exit code 1')) { + return json([]); + } + throw grepError; } - } catch (grepError: any) { - // git grep returns exit code 1 when no matches found, which is not an error - if (grepError.message && grepError.message.includes('exit code 1')) { - // No matches found, return empty array - return json([]); - } - throw grepError; } return json(results); diff --git a/src/routes/repos/[npub]/[repo]/+page.svelte b/src/routes/repos/[npub]/[repo]/+page.svelte index f44c5ff..72d7a0d 100644 --- a/src/routes/repos/[npub]/[repo]/+page.svelte +++ b/src/routes/repos/[npub]/[repo]/+page.svelte @@ -4565,22 +4565,37 @@ error = null; try { + // Get current branch for repo-specific search + const branchParam = codeSearchScope === 'repo' && currentBranch + ? `&branch=${encodeURIComponent(currentBranch)}` + : ''; + + // For "All Repositories", don't pass repo filter - let it search all repos const url = codeSearchScope === 'repo' - ? `/api/repos/${npub}/${repo}/code-search?q=${encodeURIComponent(codeSearchQuery.trim())}` - : `/api/code-search?q=${encodeURIComponent(codeSearchQuery.trim())}&repo=${encodeURIComponent(`${npub}/${repo}`)}`; + ? `/api/repos/${npub}/${repo}/code-search?q=${encodeURIComponent(codeSearchQuery.trim())}${branchParam}` + : `/api/code-search?q=${encodeURIComponent(codeSearchQuery.trim())}`; const response = await fetch(url, { headers: buildApiHeaders() }); if (response.ok) { - codeSearchResults = await response.json(); + const data = await response.json(); + codeSearchResults = Array.isArray(data) ? data : []; } else { - const errorData = await response.json(); - throw new Error(errorData.message || 'Failed to search code'); + let errorMessage = 'Failed to search code'; + try { + const errorData = await response.json(); + errorMessage = errorData.message || errorMessage; + } catch { + errorMessage = `Search failed: ${response.status} ${response.statusText}`; + } + throw new Error(errorMessage); } } catch (err) { - error = err instanceof Error ? err.message : 'Failed to search code'; + const errorMessage = err instanceof Error ? err.message : 'Failed to search code'; + console.error('[Code Search] Error:', err); + error = errorMessage; codeSearchResults = []; } finally { loadingCodeSearch = false; @@ -6097,7 +6112,11 @@ {:else if codeSearchQuery.trim() && !loadingCodeSearch}
-

No results found

+ {#if error} +

Error: {error}

+ {:else} +

No results found

+ {/if}
{/if}