use crate::bare::GitBare; use crate::error::GitResult; use crate::pb::*; impl GitBare { /// Search file contents with a regex pattern. pub fn search_files_by_content( &self, request: SearchFilesByContentRequest, ) -> GitResult { crate::sanitize::validate_revision(&request.revision)?; let revision = if request.revision.is_empty() { "HEAD" } else { &request.revision }; let max_results = if request.max_results == 0 { 100 } else { request.max_results }; let mut args = vec![ "--git-dir".to_string(), self.bare_dir.to_string_lossy().into_owned(), "grep".to_string(), "-F".to_string(), "-I".to_string(), "--line-number".to_string(), "--column".to_string(), ]; if !request.case_sensitive { args.push("-i".to_string()); } args.push(format!("--max-count={}", max_results)); args.push("-e".to_string()); args.push(request.query.clone()); args.push(revision.to_string()); let output = std::process::Command::new("git") .args(&args) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .output() .map_err(|e| crate::error::GitError::CommandFailed { status_code: None, stderr: e.to_string(), })?; // git grep returns exit code 1 when no matches found — that's not an error let stdout = String::from_utf8_lossy(&output.stdout); let mut results = Vec::new(); for line in stdout.lines() { // Format: path:line:col:matched_text if let Some((path_and_rest, matched)) = line.rsplit_once(':') { let prefix_parts: Vec<&str> = path_and_rest.rsplitn(3, ':').collect(); if prefix_parts.len() >= 3 { if let Ok(line_num) = prefix_parts[0].parse::() { results.push(SearchResult { path: prefix_parts[2].to_string(), line: line_num, matched_text: matched.to_string(), }); } } } } Ok(SearchFilesByContentResponse { results }) } /// Search file names matching a pattern. pub fn search_files_by_name( &self, request: SearchFilesByNameRequest, ) -> GitResult { let revision = if request.revision.is_empty() { "HEAD" } else { &request.revision }; crate::sanitize::validate_revision(revision)?; let max_results = if request.max_results == 0 { 100 } else { request.max_results }; let mut args = vec![ "--git-dir".to_string(), self.bare_dir.to_string_lossy().into_owned(), "ls-tree".to_string(), ]; if request.recursive { args.push("-r".to_string()); } args.push("--name-only".to_string()); args.push(revision.to_string()); let output = std::process::Command::new("git") .args(&args) .stdout(std::process::Stdio::piped()) .stderr(std::process::Stdio::piped()) .output() .map_err(|e| crate::error::GitError::CommandFailed { status_code: None, stderr: e.to_string(), })?; let stdout = String::from_utf8_lossy(&output.stdout); let mut results = Vec::new(); for line in stdout.lines() { let path = line.trim(); if path.is_empty() || crate::sanitize::validate_file_path(path).is_err() { continue; } // Simple substring/case-insensitive matching for file names let query = &request.query; let matched = if query.is_empty() { true } else { path.to_lowercase().contains(&query.to_lowercase()) }; if matched { results.push(SearchResult { path: path.to_string(), line: 0, matched_text: String::new(), }); if results.len() >= max_results as usize { break; } } } Ok(SearchFilesByNameResponse { results }) } }