Browse Source

fix repo search

Nostr-Signature: e9eff432fe83e0b629e217fe4c00b19858a797127ff0dad28e248e02629d938c 573634b648634cbad10f2451776089ea21090d9407f715e83c577b4611ae6edc 47c81818e91fa1b2760ceee78da38a82698c9609df0acc7f4dfaae7c6e7025c870cf2a8c34f3ea9c09dc9110be74f4605762a903d722f8bc15d2a0a2fd7edd04
main
Silberengel 3 weeks ago
parent
commit
4428fd4f8c
  1. 1
      nostr/commit-signatures.jsonl
  2. 159
      src/lib/styles/repo.css
  3. 173
      src/routes/api/code-search/+server.ts
  4. 190
      src/routes/api/repos/[npub]/[repo]/code-search/+server.ts
  5. 33
      src/routes/repos/[npub]/[repo]/+page.svelte

1
nostr/commit-signatures.jsonl

@ -73,3 +73,4 @@ @@ -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"}

159
src/lib/styles/repo.css

@ -934,7 +934,8 @@ @@ -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 @@ @@ -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 @@ @@ -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 @@ @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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;

173
src/routes/api/code-search/+server.ts

@ -17,6 +17,7 @@ import { readdir, stat } from 'fs/promises'; @@ -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( @@ -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';
}
// For bare repositories, we need to use a worktree or search the index
let worktreePath: string | null = null;
try {
// Get the actual branch name (resolve HEAD if needed)
let actualBranch = branch;
if (branch === 'HEAD') {
actualBranch = 'main';
}
// 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();
const gitArgs = ['grep', '-n', '-I', '--break', '--heading', searchQuery, branch];
try {
const grepOutput = await git.raw(gitArgs);
// 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 [];
}
if (!grepOutput || !grepOutput.trim()) {
return [];
}
const lines = grepOutput.split('\n');
let currentFile = '';
// Parse git grep output
const lines = grepOutput.split('\n');
let currentFile = '';
for (const line of lines) {
if (!line.trim()) {
continue;
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({
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;
}
} 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';
}
}
if (!line.includes(':')) {
currentFile = line.trim();
continue;
// 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 [];
}
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;
// 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;
}
} 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 (err) {
logger.debug({ error: err, npub, repo, query }, 'Error searching in repo');

190
src/routes/api/repos/[npub]/[repo]/code-search/+server.ts

@ -13,6 +13,7 @@ import { join } from 'path'; @@ -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( @@ -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)
// 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 {
// 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';
}
}
// 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();
const gitArgs = ['grep', '-n', '-I', '--break', '--heading', searchQuery, branch];
try {
const grepOutput = await git.raw(gitArgs);
// 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 = '';
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 (!grepOutput || !grepOutput.trim()) {
return json([]);
// 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';
}
}
// 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
// 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);
if (!grepOutput || !grepOutput.trim()) {
return json([]);
}
// 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
// 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({
file: currentFile,
line: lineNumber,
content: content.trim(),
branch: branch === 'HEAD' ? 'HEAD' : branch
});
if (results.length >= limit) {
break;
// 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({
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);

33
src/routes/repos/[npub]/[repo]/+page.svelte

@ -4565,22 +4565,37 @@ @@ -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 @@ @@ -6097,7 +6112,11 @@
</div>
{:else if codeSearchQuery.trim() && !loadingCodeSearch}
<div class="empty-state">
<p>No results found</p>
{#if error}
<p class="error-message">Error: {error}</p>
{:else}
<p>No results found</p>
{/if}
</div>
{/if}
</div>

Loading…
Cancel
Save