//! Copyright (c) 2022-2026 GitDataAi All rights reserved. use crate::bare::GitBare; use crate::commit::list_commits::read_commit_from_repo; use crate::diff::get_diff_stats::diff_stats_for_range; use crate::error::{GitError, GitResult}; use crate::pb::{CommitStats, CompareCommitsRequest, CompareCommitsResponse}; use crate::resolve_revision; impl GitBare { pub fn compare_commits( &self, request: CompareCommitsRequest, ) -> GitResult { let repo = self.gix_repo()?; let base = resolve_revision!(request.base.clone()); let head = resolve_revision!(request.head.clone()); let base_id = repo.rev_parse_single(base.as_str())?; let head_id = repo.rev_parse_single(head.as_str())?; let merge_base = repo .merge_base(base_id.detach(), head_id.detach()) .ok() .map(|id| self.oid_to_pb(id.to_string())); let range = if request.straight { format!("{base}..{head}") } else { format!("{base}...{head}") }; let mut base_args = vec![ "--git-dir".to_string(), self.bare_dir.to_string_lossy().into_owned(), "rev-list".into(), ]; if request.first_parent { base_args.push("--first-parent".into()); } base_args.push(range); let total = { let mut args = base_args.clone(); args.insert(3, "--count".into()); 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(), }); } String::from_utf8_lossy(&result.stdout) .trim() .parse::() .unwrap_or(0) }; let page_size = request .pagination .as_ref() .map(|p| p.page_size as usize) .unwrap_or(total.max(1)) .max(1); let start_offset = request .pagination .as_ref() .and_then(|p| { if p.page_token.is_empty() { None } else { p.page_token.parse::().ok() } }) .unwrap_or(0) .min(total); let mut fetch_args = base_args; fetch_args.insert(3, format!("--skip={start_offset}")); fetch_args.insert(4, format!("-n{page_size}")); let rev_list = duct::cmd("git", &fetch_args) .stdout_capture() .stderr_capture() .unchecked() .run()?; if !rev_list.status.success() { return Err(GitError::CommandFailed { status_code: rev_list.status.code(), stderr: String::from_utf8_lossy(&rev_list.stderr).into_owned(), }); } let page_ids: Vec = String::from_utf8_lossy(&rev_list.stdout) .lines() .map(str::trim) .filter(|line| !line.is_empty()) .map(ToOwned::to_owned) .collect(); let mut commits = Vec::with_capacity(page_ids.len()); for id in &page_ids { commits.push(read_commit_from_repo(self, &repo, id)?); } let end = start_offset.saturating_add(page_ids.len()); let has_next = end < total; let page_info = crate::pb::PageInfo { next_page_token: if has_next { end.to_string() } else { String::new() }, has_next_page: has_next, total_count: total as u64, }; let diff_stats = diff_stats_for_range(self, &base, &head, None)?; Ok(CompareCommitsResponse { commits, stats: Some(CommitStats { additions: diff_stats.additions, deletions: diff_stats.deletions, changed_files: diff_stats.changed_files, }), page_info: Some(page_info), merge_base, }) } }