feat(api): extend commit and diff services with new functionality
- Add FindCommit, ListCommitsByOid, CommitIsAncestor RPCs to CommitService - Add CheckObjectsExist, CommitsByMessage, GetCommitStats RPCs to CommitService - Add LastCommitForPath, CountCommits, CountDivergingCommits RPCs to CommitService - Add RawDiff, RawPatch, FindChangedPaths RPCs to DiffService - Add FindMergeBase, WriteRef, SearchFilesByContent RPCs to RepositoryService - Add SearchFilesByName, ObjectsSize, RepositorySize RPCs to RepositoryService - Add FindLicense, OptimizeRepository, GetRawChanges RPCs to RepositoryService - Add FetchRemote, CreateRepositoryFromURL RPCs to RepositoryService - Implement server handlers for all new RPC methods - Add new modules for commit counting, finding, and querying features - Add new modules for diff changed paths and raw operations - Add new modules for refs and remote operations - Remove unnecessary comments from various source files - Update proto definitions with new message types and service methods
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
use crate::bare::GitBare;
|
||||
use crate::error::GitResult;
|
||||
use crate::pb::*;
|
||||
|
||||
impl GitBare {
|
||||
/// Detect license by reading LICENSE/COPYING files and doing basic matching.
|
||||
pub fn find_license(&self) -> GitResult<FindLicenseResponse> {
|
||||
let possible_paths = [
|
||||
"LICENSE", "LICENSE.md", "LICENSE.txt",
|
||||
"LICENCE", "LICENCE.md", "LICENCE.txt",
|
||||
"COPYING", "COPYING.md", "COPYING.txt",
|
||||
"UNLICENSE",
|
||||
];
|
||||
|
||||
for path in &possible_paths {
|
||||
let output = std::process::Command::new("git")
|
||||
.args([
|
||||
"--git-dir",
|
||||
&self.bare_dir.to_string_lossy(),
|
||||
"show",
|
||||
&format!("HEAD:{path}"),
|
||||
])
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.output()
|
||||
.map_err(|e| crate::error::GitError::CommandFailed {
|
||||
status_code: None,
|
||||
stderr: e.to_string(),
|
||||
})?;
|
||||
|
||||
if output.status.success() {
|
||||
let content = String::from_utf8_lossy(&output.stdout);
|
||||
let (spdx, name, conf) = detect_license(&content);
|
||||
if conf > 0.0 {
|
||||
return Ok(FindLicenseResponse {
|
||||
license_spdx: spdx.to_string(),
|
||||
license_name: name.to_string(),
|
||||
confidence: conf,
|
||||
license_path: path.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(FindLicenseResponse::default())
|
||||
}
|
||||
}
|
||||
|
||||
/// Very basic license detection by keyword matching.
|
||||
/// Returns (SPDX identifier, human-readable name, confidence).
|
||||
fn detect_license(content: &str) -> (&'static str, &'static str, f64) {
|
||||
let lower = content.to_lowercase();
|
||||
|
||||
// MIT
|
||||
if lower.contains("permission is hereby granted, free of charge") && lower.contains("mit") {
|
||||
return ("MIT", "MIT License", 0.95);
|
||||
}
|
||||
|
||||
// Apache 2.0
|
||||
if lower.contains("apache license, version 2.0") || lower.contains("apache-2.0") {
|
||||
return ("Apache-2.0", "Apache License 2.0", 0.95);
|
||||
}
|
||||
|
||||
// GPL 3.0
|
||||
if lower.contains("gnu general public license") && lower.contains("version 3") {
|
||||
return ("GPL-3.0", "GNU General Public License v3.0", 0.90);
|
||||
}
|
||||
// GPL 2.0
|
||||
if lower.contains("gnu general public license") && lower.contains("version 2") {
|
||||
return ("GPL-2.0", "GNU General Public License v2.0", 0.90);
|
||||
}
|
||||
|
||||
// BSD 3
|
||||
if lower.contains("redistribution and use in source and binary forms")
|
||||
&& lower.contains("neither the name of")
|
||||
{
|
||||
return ("BSD-3-Clause", "BSD 3-Clause License", 0.85);
|
||||
}
|
||||
// BSD 2
|
||||
if lower.contains("redistribution and use in source and binary forms") {
|
||||
return ("BSD-2-Clause", "BSD 2-Clause License", 0.80);
|
||||
}
|
||||
|
||||
// AGPL
|
||||
if lower.contains("gnu affero general public license") {
|
||||
return ("AGPL-3.0", "GNU Affero General Public License v3.0", 0.90);
|
||||
}
|
||||
|
||||
// LGPL
|
||||
if lower.contains("gnu lesser general public license") {
|
||||
return ("LGPL-3.0", "GNU Lesser General Public License v3.0", 0.85);
|
||||
}
|
||||
|
||||
// MPL
|
||||
if lower.contains("mozilla public license") {
|
||||
return ("MPL-2.0", "Mozilla Public License 2.0", 0.90);
|
||||
}
|
||||
|
||||
// Unlicense
|
||||
if lower.contains("this is free and unencumbered software released into the public domain") {
|
||||
return ("Unlicense", "The Unlicense", 0.95);
|
||||
}
|
||||
|
||||
// ISC
|
||||
if lower.contains("permission to use, copy, modify, and/or distribute")
|
||||
&& lower.contains("isc")
|
||||
{
|
||||
return ("ISC", "ISC License", 0.80);
|
||||
}
|
||||
|
||||
("", "", 0.0)
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
use crate::bare::GitBare;
|
||||
use crate::error::GitResult;
|
||||
use crate::pb::*;
|
||||
|
||||
impl GitBare {
|
||||
/// Find the best merge base for a set of revisions (OIDs).
|
||||
pub fn find_merge_base(&self, request: FindMergeBaseRequest) -> GitResult<FindMergeBaseResponse> {
|
||||
if request.revisions.is_empty() {
|
||||
return Ok(FindMergeBaseResponse::default());
|
||||
}
|
||||
|
||||
let revisions: Vec<String> = request
|
||||
.revisions
|
||||
.iter()
|
||||
.map(|b| String::from_utf8_lossy(b).to_string())
|
||||
.collect();
|
||||
|
||||
if revisions.len() < 2 {
|
||||
return Ok(FindMergeBaseResponse {
|
||||
base_oid: revisions.first().cloned().unwrap_or_default(),
|
||||
});
|
||||
}
|
||||
|
||||
let mut args = vec![
|
||||
"--git-dir".to_string(),
|
||||
self.bare_dir.to_string_lossy().into_owned(),
|
||||
"merge-base".to_string(),
|
||||
];
|
||||
args.extend(revisions.iter().cloned());
|
||||
|
||||
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(),
|
||||
})?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Ok(FindMergeBaseResponse {
|
||||
base_oid: String::new(),
|
||||
});
|
||||
}
|
||||
|
||||
let base_oid = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||
Ok(FindMergeBaseResponse { base_oid })
|
||||
}
|
||||
|
||||
/// Check if one commit is an ancestor of another.
|
||||
pub fn commit_is_ancestor(&self, request: CommitIsAncestorRequest) -> GitResult<CommitIsAncestorResponse> {
|
||||
crate::sanitize::validate_revision(&request.ancestor_oid)?;
|
||||
crate::sanitize::validate_revision(&request.descendant_oid)?;
|
||||
|
||||
let result = std::process::Command::new("git")
|
||||
.args([
|
||||
"--git-dir",
|
||||
&self.bare_dir.to_string_lossy(),
|
||||
"merge-base",
|
||||
"--is-ancestor",
|
||||
&request.ancestor_oid,
|
||||
&request.descendant_oid,
|
||||
])
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null())
|
||||
.status()
|
||||
.map(|s| s.success())
|
||||
.unwrap_or(false);
|
||||
|
||||
Ok(CommitIsAncestorResponse { is_ancestor: result })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
pub mod find_license;
|
||||
pub mod find_merge_base;
|
||||
pub mod objects_size;
|
||||
pub mod optimize;
|
||||
pub mod raw_changes;
|
||||
pub mod search_files;
|
||||
@@ -0,0 +1,93 @@
|
||||
use crate::bare::GitBare;
|
||||
use crate::error::GitResult;
|
||||
use crate::pb::*;
|
||||
|
||||
impl GitBare {
|
||||
/// Get sizes for a list of objects by OID.
|
||||
pub fn objects_size(&self, request: ObjectsSizeRequest) -> GitResult<ObjectsSizeResponse> {
|
||||
if request.oids.is_empty() {
|
||||
return Ok(ObjectsSizeResponse::default());
|
||||
}
|
||||
|
||||
let mut input = String::new();
|
||||
for oid in &request.oids {
|
||||
crate::sanitize::validate_revision(oid)?;
|
||||
input.push_str(oid);
|
||||
input.push('\n');
|
||||
}
|
||||
|
||||
let mut child = std::process::Command::new("git")
|
||||
.args([
|
||||
"--git-dir",
|
||||
&self.bare_dir.to_string_lossy(),
|
||||
"cat-file",
|
||||
"--batch-check=%(objectname) %(objectsize)",
|
||||
])
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| crate::error::GitError::CommandFailed {
|
||||
status_code: None,
|
||||
stderr: e.to_string(),
|
||||
})?;
|
||||
|
||||
use std::io::Write;
|
||||
if let Some(ref mut stdin) = child.stdin {
|
||||
stdin.write_all(input.as_bytes()).map_err(|e| {
|
||||
crate::error::GitError::CommandFailed {
|
||||
status_code: None,
|
||||
stderr: e.to_string(),
|
||||
}
|
||||
})?;
|
||||
}
|
||||
|
||||
let output = child.wait_with_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 sizes = Vec::new();
|
||||
|
||||
for line in stdout.lines() {
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() >= 2 {
|
||||
let oid = parts[0];
|
||||
let found = parts.get(1).map_or(true, |&s| s != "missing");
|
||||
let size = parts.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
|
||||
sizes.push(ObjectSize {
|
||||
oid: oid.to_string(),
|
||||
size,
|
||||
found,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(ObjectsSizeResponse { sizes })
|
||||
}
|
||||
|
||||
/// Get total repository size on disk.
|
||||
pub fn repository_size(&self) -> GitResult<RepositorySizeResponse> {
|
||||
let output = std::process::Command::new("du")
|
||||
.args(["-sb", &self.bare_dir.to_string_lossy()])
|
||||
.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 size = stdout
|
||||
.split_whitespace()
|
||||
.next()
|
||||
.and_then(|s| s.parse().ok())
|
||||
.unwrap_or(0);
|
||||
|
||||
Ok(RepositorySizeResponse { size_bytes: size })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
use crate::bare::GitBare;
|
||||
use crate::error::GitResult;
|
||||
use crate::pb::*;
|
||||
|
||||
impl GitBare {
|
||||
/// Run heuristic optimization based on repo state.
|
||||
pub fn optimize_repository(&self, request: OptimizeRepositoryRequest) -> GitResult<OptimizeRepositoryResponse> {
|
||||
let strategy = OptimizeStrategy::try_from(request.strategy).unwrap_or(OptimizeStrategy::Heuristic);
|
||||
|
||||
let mut stdout_all = String::new();
|
||||
let mut stderr_all = String::new();
|
||||
|
||||
match strategy {
|
||||
OptimizeStrategy::Heuristic | OptimizeStrategy::Aggressive => {
|
||||
let stats = self.get_repository_statistics()?;
|
||||
|
||||
// Run commit-graph write if needed
|
||||
if stats.commit_graph_size_bytes == 0 || strategy == OptimizeStrategy::Aggressive {
|
||||
if let Ok(resp) = write_commit_graph(self, false, false) {
|
||||
if !resp.ok { stderr_all.push_str(&resp.stderr); }
|
||||
stdout_all.push_str(&resp.stdout);
|
||||
}
|
||||
}
|
||||
|
||||
// Repack if many loose objects or packfiles
|
||||
let repack_needed = stats.loose_object_count > 1000 || stats.packfile_count > 10;
|
||||
|
||||
if repack_needed || strategy == OptimizeStrategy::Aggressive {
|
||||
let full = strategy == OptimizeStrategy::Aggressive;
|
||||
if let Ok(resp) = run_repack(self, full, true, true) {
|
||||
if !resp.ok { stderr_all.push_str(&resp.stderr); }
|
||||
stdout_all.push_str(&resp.stdout);
|
||||
}
|
||||
}
|
||||
|
||||
// Prune if aggressive
|
||||
if strategy == OptimizeStrategy::Aggressive {
|
||||
if let Ok(resp) = run_gc(self, true, true) {
|
||||
if !resp.ok { stderr_all.push_str(&resp.stderr); }
|
||||
stdout_all.push_str(&resp.stdout);
|
||||
}
|
||||
}
|
||||
}
|
||||
OptimizeStrategy::Incremental => {
|
||||
// Just run commit-graph write incrementally
|
||||
if let Ok(resp) = write_commit_graph(self, false, false) {
|
||||
if !resp.ok { stderr_all.push_str(&resp.stderr); }
|
||||
stdout_all.push_str(&resp.stdout);
|
||||
}
|
||||
}
|
||||
OptimizeStrategy::Unspecified => {}
|
||||
}
|
||||
|
||||
Ok(OptimizeRepositoryResponse {
|
||||
ok: stderr_all.is_empty(),
|
||||
stdout: stdout_all,
|
||||
stderr: stderr_all,
|
||||
})
|
||||
}
|
||||
|
||||
fn get_repository_statistics(&self) -> GitResult<RepositoryStatistics> {
|
||||
// Count loose objects
|
||||
let loose = std::fs::read_dir(self.bare_dir.join("objects"))
|
||||
.map(|d| {
|
||||
d.filter_map(|e| e.ok())
|
||||
.filter(|e| {
|
||||
e.file_type().map(|t| t.is_dir()).unwrap_or(false)
|
||||
&& e.file_name().to_string_lossy().len() == 2
|
||||
})
|
||||
.count() as u64
|
||||
})
|
||||
.unwrap_or(0);
|
||||
|
||||
// Count packfiles
|
||||
let pack_dir = self.bare_dir.join("objects").join("pack");
|
||||
let pack_count = std::fs::read_dir(&pack_dir)
|
||||
.map(|d| d.filter_map(|e| e.ok()).count() as u64)
|
||||
.unwrap_or(0);
|
||||
|
||||
// Check commit-graph
|
||||
let cg_size = std::fs::metadata(
|
||||
self.bare_dir.join("objects").join("info").join("commit-graph")
|
||||
)
|
||||
.map(|m| m.len())
|
||||
.unwrap_or(0);
|
||||
|
||||
Ok(RepositoryStatistics {
|
||||
size_bytes: 0,
|
||||
loose_object_count: loose,
|
||||
packed_object_count: 0,
|
||||
packfile_count: pack_count,
|
||||
reference_count: 0,
|
||||
commit_graph_size_bytes: cg_size,
|
||||
multi_pack_index_size_bytes: 0,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn write_commit_graph(gb: &GitBare, _split: bool, _replace: bool) -> GitResult<RepositoryMaintenanceResponse> {
|
||||
let out = std::process::Command::new("git")
|
||||
.args([
|
||||
"--git-dir", &gb.bare_dir.to_string_lossy(),
|
||||
"commit-graph", "write", "--reachable",
|
||||
])
|
||||
.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(),
|
||||
})?;
|
||||
|
||||
Ok(RepositoryMaintenanceResponse {
|
||||
ok: out.status.success(),
|
||||
stdout: String::from_utf8_lossy(&out.stdout).into_owned(),
|
||||
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
fn run_repack(gb: &GitBare, full: bool, bitmaps: bool, _midx: bool) -> GitResult<RepositoryMaintenanceResponse> {
|
||||
let mut args = vec![
|
||||
"--git-dir".to_string(), gb.bare_dir.to_string_lossy().into_owned(),
|
||||
"repack".to_string(),
|
||||
];
|
||||
if full { args.push("-ad".to_string()); } else { args.push("-d".to_string()); }
|
||||
if bitmaps { args.push("--write-bitmap-index".to_string()); }
|
||||
|
||||
let out = 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(),
|
||||
})?;
|
||||
|
||||
Ok(RepositoryMaintenanceResponse {
|
||||
ok: out.status.success(),
|
||||
stdout: String::from_utf8_lossy(&out.stdout).into_owned(),
|
||||
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
fn run_gc(gb: &GitBare, prune: bool, aggressive: bool) -> GitResult<RepositoryMaintenanceResponse> {
|
||||
let mut args = vec![
|
||||
"--git-dir".to_string(), gb.bare_dir.to_string_lossy().into_owned(),
|
||||
"gc".to_string(),
|
||||
];
|
||||
if prune { args.push("--prune=now".to_string()); }
|
||||
if aggressive { args.push("--aggressive".to_string()); }
|
||||
|
||||
let out = 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(),
|
||||
})?;
|
||||
|
||||
Ok(RepositoryMaintenanceResponse {
|
||||
ok: out.status.success(),
|
||||
stdout: String::from_utf8_lossy(&out.stdout).into_owned(),
|
||||
stderr: String::from_utf8_lossy(&out.stderr).into_owned(),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
use crate::bare::GitBare;
|
||||
use crate::error::GitResult;
|
||||
use crate::pb::*;
|
||||
|
||||
impl GitBare {
|
||||
/// Get raw changes between two revisions (file-level changes only, no diff content).
|
||||
pub fn get_raw_changes(&self, request: GetRawChangesRequest) -> GitResult<GetRawChangesResponse> {
|
||||
crate::sanitize::validate_revision(&request.base)?;
|
||||
crate::sanitize::validate_revision(&request.head)?;
|
||||
|
||||
let output = std::process::Command::new("git")
|
||||
.args([
|
||||
"--git-dir",
|
||||
&self.bare_dir.to_string_lossy(),
|
||||
"diff-tree",
|
||||
"--raw",
|
||||
"-r",
|
||||
"--root",
|
||||
&request.base,
|
||||
&request.head,
|
||||
])
|
||||
.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 changes = Vec::new();
|
||||
|
||||
for line in stdout.lines() {
|
||||
let line = line.trim();
|
||||
if !line.starts_with(':') { continue; }
|
||||
let line = &line[1..];
|
||||
|
||||
let parts: Vec<&str> = line.split_whitespace().collect();
|
||||
if parts.len() < 5 { continue; }
|
||||
|
||||
let old_mode = u32::from_str_radix(parts[0], 8).unwrap_or(0);
|
||||
let new_mode = u32::from_str_radix(parts[1], 8).unwrap_or(0);
|
||||
let old_oid = parts[2].to_string();
|
||||
let new_oid = parts[3].to_string();
|
||||
let status_str = parts[4];
|
||||
let status_letter = status_str.chars().next().unwrap_or('M');
|
||||
|
||||
let operation = match status_letter {
|
||||
'A' => raw_change::Operation::RawChangeOperationAdded as i32,
|
||||
'D' => raw_change::Operation::RawChangeOperationDeleted as i32,
|
||||
'R' => raw_change::Operation::RawChangeOperationRenamed as i32,
|
||||
'C' => raw_change::Operation::RawChangeOperationCopied as i32,
|
||||
'M' | 'T' => raw_change::Operation::RawChangeOperationModified as i32,
|
||||
_ => raw_change::Operation::RawChangeOperationUnspecified as i32,
|
||||
};
|
||||
|
||||
let (old_path, new_path) = if parts.len() >= 6 {
|
||||
(parts[5].to_string(), if status_letter == 'R' || status_letter == 'C' {
|
||||
parts.get(6).map(|s| s.to_string()).unwrap_or_default()
|
||||
} else {
|
||||
String::new()
|
||||
})
|
||||
} else {
|
||||
(String::new(), String::new())
|
||||
};
|
||||
|
||||
changes.push(RawChange {
|
||||
operation,
|
||||
old_path,
|
||||
new_path,
|
||||
old_mode,
|
||||
new_mode,
|
||||
old_oid,
|
||||
new_oid,
|
||||
similarity: 0.0,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(GetRawChangesResponse { changes })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
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<SearchFilesByContentResponse> {
|
||||
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(),
|
||||
"-I".to_string(), // don't match binary files
|
||||
"--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::<u32>() {
|
||||
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<SearchFilesByNameResponse> {
|
||||
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 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user