use crate::bare::GitBare; use crate::error::{GitError, GitResult}; use crate::pb::GetDiffStatsRequest; impl GitBare { pub fn get_diff_stats(&self, request: GetDiffStatsRequest) -> GitResult { let base = match request.base.and_then(|s| s.selector) { Some(crate::pb::object_selector::Selector::Oid(oid)) => oid.hex, Some(crate::pb::object_selector::Selector::Revision(name)) => name.revision, None => "HEAD".into(), }; let head = match request.head.and_then(|s| s.selector) { Some(crate::pb::object_selector::Selector::Oid(oid)) => oid.hex, Some(crate::pb::object_selector::Selector::Revision(name)) => name.revision, None => "HEAD".into(), }; diff_stats_for_range(self, &base, &head, request.options.as_ref()) } } pub(crate) fn diff_stats_for_range( repo: &GitBare, base: &str, head: &str, options: Option<&crate::pb::DiffOptions>, ) -> GitResult { let mut args = vec![ "--git-dir".to_string(), repo.bare_dir.to_string_lossy().into_owned(), "diff".into(), "--shortstat".into(), ]; push_diff_options(&mut args, options); args.push(base.to_string()); args.push(head.to_string()); if let Some(options) = options && !options.pathspec.is_empty() { args.push("--".into()); args.extend(options.pathspec.iter().cloned()); } let result = duct::cmd("git", &args) .stdout_capture() .stderr_capture() .unchecked() .run()?; if !result.status.success() { return Err(GitError::CommandFailed { status_code: result.status.code(), stderr: String::from_utf8_lossy(&result.stderr).into_owned(), }); } Ok(parse_shortstat(&String::from_utf8_lossy(&result.stdout))) } pub(crate) fn parse_shortstat(output: &str) -> crate::pb::DiffStats { let mut stats = crate::pb::DiffStats::default(); for part in output.trim().split(',') { let part = part.trim(); if let Some(n) = part .strip_suffix(" insertion(+)") .or_else(|| part.strip_suffix(" insertions(+)")) { stats.additions = n.trim().parse().unwrap_or(0); } else if let Some(n) = part .strip_suffix(" deletion(-)") .or_else(|| part.strip_suffix(" deletions(-)")) { stats.deletions = n.trim().parse().unwrap_or(0); } else if let Some(n) = part .strip_suffix(" file changed") .or_else(|| part.strip_suffix(" files changed")) { stats.changed_files = n.trim().parse().unwrap_or(0); } } stats } pub(crate) fn push_diff_options(args: &mut Vec, options: Option<&crate::pb::DiffOptions>) { let Some(options) = options else { return; }; if options.rename_detection { args.push("-M".into()); } if options.copy_detection { args.push("-C".into()); } match crate::pb::diff_options::WhitespaceMode::try_from(options.whitespace_mode) .unwrap_or(crate::pb::diff_options::WhitespaceMode::DiffWhitespaceModeDefault) { crate::pb::diff_options::WhitespaceMode::DiffWhitespaceModeIgnoreAll => { args.push("-w".into()) } crate::pb::diff_options::WhitespaceMode::DiffWhitespaceModeIgnoreChange => { args.push("-b".into()) } crate::pb::diff_options::WhitespaceMode::DiffWhitespaceModeIgnoreEol => { args.push("--ignore-space-at-eol".into()); } _ => {} } }