//! Copyright (c) 2022-2026 GitDataAi All rights reserved. use crate::bare::GitBare; use crate::error::{GitError, GitResult}; use crate::pb::{Commit, ListCommitsRequest, ListCommitsResponse}; use crate::resolve_revision; impl GitBare { pub fn list_commits(&self, request: ListCommitsRequest) -> GitResult { let revision = resolve_revision!(request.revision.clone()); let base_args = build_rev_list_args(self, &request, &revision)?; 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 result = duct::cmd("git", &fetch_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(), }); } let page_ids: Vec = String::from_utf8_lossy(&result.stdout) .lines() .map(str::trim) .filter(|line| !line.is_empty()) .map(ToOwned::to_owned) .collect(); let commits = if page_ids.is_empty() { Vec::new() } else { let repo = self.gix_repo()?; let mut commits = Vec::with_capacity(page_ids.len()); for id in &page_ids { commits.push(read_commit_from_repo(self, &repo, id)?); } commits }; 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, }; Ok(ListCommitsResponse { commits, page_info: Some(page_info), }) } } fn build_rev_list_args( gb: &GitBare, request: &ListCommitsRequest, revision: &str, ) -> GitResult> { let mut args = vec![ "--git-dir".to_string(), gb.bare_dir.to_string_lossy().into_owned(), "rev-list".into(), ]; if request.first_parent { args.push("--first-parent".into()); } if request.reverse { args.push("--reverse".into()); } if request.max_parents > 0 { args.push(format!("--max-parents={}", request.max_parents)); } if request.min_parents > 0 { args.push(format!("--min-parents={}", request.min_parents)); } if let Some(since) = request.since.as_ref() { args.push(format!("--since=@{}", since.seconds)); } if let Some(until) = request.until.as_ref() { args.push(format!("--until=@{}", until.seconds)); } if request.all { args.push("--all".into()); } else { args.push(revision.to_string()); } if !request.path.is_empty() { crate::sanitize::validate_file_path(&request.path)?; args.push("--".into()); args.push(request.path.clone()); } Ok(args) } /// Read a single commit from an already-opened gix repo (no subprocess). pub(crate) fn read_commit_from_repo( gb: &GitBare, repo: &gix::Repository, hex: &str, ) -> GitResult { let id = gix::hash::ObjectId::from_hex(hex.as_bytes()) .map_err(|e| crate::error::GitError::InvalidOid(e.to_string()))?; let commit = repo .find_object(id)? .try_into_commit() .map_err(|e| crate::error::GitError::Gix(e.to_string()))?; let tree_hex = commit.tree_id()?.to_string(); let message = commit.message_raw()?.to_string(); let (subject, body) = message .split_once('\n') .map(|(s, b)| (s.to_string(), b.trim_start_matches('\n').to_string())) .unwrap_or_else(|| (message.clone(), String::new())); let author_sig = commit.author().ok(); let committer_sig = commit.committer().ok(); Ok(Commit { oid: Some(gb.oid_to_pb(hex.to_string())), abbreviated_oid: commit .short_id() .map(|s| s.to_string()) .unwrap_or_else(|_| hex.chars().take(7).collect()), parent_oids: commit .parent_ids() .map(|p| gb.oid_to_pb(p.to_string())) .collect(), tree_oid: Some(gb.oid_to_pb(tree_hex)), author: author_sig .as_ref() .map(crate::commit::get_commit::gix_sig_to_pb), committer: committer_sig .as_ref() .map(crate::commit::get_commit::gix_sig_to_pb), subject, body, message, trailers: Vec::new(), signature: None, stats: None, authored_at: author_sig.as_ref().map(|s| prost_types::Timestamp { seconds: s.seconds(), nanos: 0, }), committed_at: committer_sig.as_ref().map(|s| prost_types::Timestamp { seconds: s.seconds(), nanos: 0, }), raw: Vec::new(), }) }