//! Copyright (c) 2022-2026 GitDataAi All rights reserved. use crate::bare::GitBare; use crate::error::GitResult; use crate::pb::*; impl GitBare { /// Search commits by message content. pub fn commits_by_message( &self, request: CommitsByMessageRequest, ) -> GitResult { let revision = if request.revision.is_empty() { "HEAD" } else { &request.revision }; crate::sanitize::validate_revision(revision)?; let limit = if request.limit == 0 { 20 } else { request.limit.min(200) }; let max_count = request.offset.saturating_add(limit).min(10_000); let mut args = vec![ "--git-dir".to_string(), self.bare_dir.to_string_lossy().into_owned(), "log".to_string(), format!("--max-count={max_count}"), "--format=%H".to_string(), ]; if request.case_insensitive { args.push(format!("--grep={}", request.query)); args.push("-i".to_string()); } else { args.push(format!("--grep={}", request.query)); } if !revision.is_empty() && revision != "HEAD" { args.push(revision.to_string()); } else { args.push("--all".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(), })?; if !output.status.success() { return Err(crate::error::GitError::CommandFailed { status_code: output.status.code(), stderr: String::from_utf8_lossy(&output.stderr).trim().to_string(), }); } let stdout = String::from_utf8_lossy(&output.stdout); let repo = self.gix_repo()?; let mut commits = Vec::new(); for line in stdout.lines().skip(request.offset as usize) { let hex = line.trim(); if let Ok(oid) = gix::ObjectId::from_hex(hex.as_bytes()) && let Ok(obj) = repo.find_object(oid) && let Ok(commit) = obj.try_into_commit() { commits.push(crate::commit::get_commit::commit_to_pb( self, &commit, false, )); } } Ok(CommitsByMessageResponse { commits }) } /// Batch check if objects/revisions exist. pub fn check_objects_exist( &self, request: CheckObjectsExistRequest, ) -> GitResult { const MAX_CHECK_REVISIONS: usize = 10_000; if request.revisions.len() > MAX_CHECK_REVISIONS { return Err(crate::error::GitError::InvalidArgument(format!( "too many revisions (max {MAX_CHECK_REVISIONS})" ))); } let repo = self.gix_repo()?; let mut revisions = Vec::new(); for rev in &request.revisions { crate::sanitize::validate_revision(rev)?; let exists = repo.rev_parse_single(rev.as_str()).is_ok(); revisions.push(RevisionExistence { revision: rev.clone(), exists, }); } Ok(CheckObjectsExistResponse { revisions }) } /// Get stats for a single commit. pub fn get_commit_stats(&self, request: GetCommitStatsRequest) -> GitResult { let revision = match request.revision.and_then(|s| s.selector) { Some(object_selector::Selector::Oid(oid)) => { crate::sanitize::validate_oid_hex(&oid.hex)?; oid.hex } Some(object_selector::Selector::Revision(name)) => name.revision, None => "HEAD".to_string(), }; crate::sanitize::validate_revision(&revision)?; let output = std::process::Command::new("git") .args([ "--git-dir", &self.bare_dir.to_string_lossy(), "diff-tree", "--numstat", &format!("{revision}^!"), ]) .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 Err(crate::error::GitError::CommandFailed { status_code: output.status.code(), stderr: String::from_utf8_lossy(&output.stderr).trim().to_string(), }); } let stdout = String::from_utf8_lossy(&output.stdout); let mut additions = 0u32; let mut deletions = 0u32; let mut changed_files = 0u32; for line in stdout.lines() { let parts: Vec<&str> = line.split('\t').collect(); if parts.len() >= 2 { if let Ok(add) = parts[0].parse::() { additions = additions.saturating_add(add); } if let Ok(del) = parts[1].parse::() { deletions = deletions.saturating_add(del); } changed_files = changed_files.saturating_add(1); } } Ok(CommitStats { additions, deletions, changed_files, }) } /// Get the last commit for a given path. pub fn last_commit_for_path( &self, request: LastCommitForPathRequest, ) -> GitResult { crate::sanitize::validate_file_path(&request.path)?; let revision = if request.revision.is_empty() { "HEAD" } else { &request.revision }; crate::sanitize::validate_revision(revision)?; let args = vec![ "--git-dir".to_string(), self.bare_dir.to_string_lossy().into_owned(), "log".to_string(), "-1".to_string(), "--format=%H".to_string(), revision.to_string(), "--".to_string(), request.path.clone(), ]; let _ = request.literal_pathspec; 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 Err(crate::error::GitError::CommandFailed { status_code: output.status.code(), stderr: String::from_utf8_lossy(&output.stderr).trim().to_string(), }); } let stdout = String::from_utf8_lossy(&output.stdout); let hex = stdout.lines().next().unwrap_or("").trim().to_string(); if hex.is_empty() { return Ok(LastCommitForPathResponse { commit: None, path: request.path, }); } let repo = self.gix_repo()?; let commit = if let Ok(oid) = gix::ObjectId::from_hex(hex.as_bytes()) { repo.find_object(oid).ok().and_then(|obj| { obj.try_into_commit() .ok() .map(|c| crate::commit::get_commit::commit_to_pb(self, &c, false)) }) } else { None }; Ok(LastCommitForPathResponse { commit, path: request.path, }) } }