//! Commit traversal and iteration. use crate::commit::types::*; use crate::{GitDomain, GitError, GitResult}; #[derive(Debug, Clone)] pub struct CommitWalkOptions { pub rev: Option, pub sort: CommitSort, pub limit: usize, pub first_parent_only: bool, } impl CommitWalkOptions { pub fn new() -> Self { Self { rev: None, sort: CommitSort(git2::Sort::TOPOLOGICAL.bits() | git2::Sort::TIME.bits()), limit: 0, first_parent_only: false, } } pub fn rev(mut self, rev: &str) -> Self { self.rev = Some(rev.to_string()); self } pub fn topological(mut self) -> Self { self.sort = CommitSort(git2::Sort::TOPOLOGICAL.bits()); self } pub fn time_order(mut self) -> Self { self.sort = CommitSort(git2::Sort::TIME.bits()); self } pub fn reverse(mut self) -> Self { self.sort = CommitSort(self.sort.0 | git2::Sort::REVERSE.bits()); self } pub fn limit(mut self, n: usize) -> Self { self.limit = n; self } pub fn first_parent(mut self) -> Self { self.first_parent_only = true; self } } impl Default for CommitWalkOptions { fn default() -> Self { Self::new() } } impl GitDomain { pub fn commit_walk(&self, opts: CommitWalkOptions) -> GitResult> { let mut revwalk = self .repo() .revwalk() .map_err(|e| GitError::Internal(e.to_string()))?; revwalk .set_sorting(opts.sort.to_git2()) .map_err(|e| GitError::Internal(e.to_string()))?; if let Some(ref r) = opts.rev { if r.contains("..") { revwalk .push_range(r) .map_err(|e| GitError::Internal(e.to_string()))?; } else { revwalk .push_ref(r) .map_err(|e| GitError::Internal(e.to_string()))?; } } else { revwalk .push_head() .map_err(|e| GitError::Internal(e.to_string()))?; } let mut commits = Vec::new(); if opts.first_parent_only { let mut prev_oid: Option = None; for oid_result in revwalk { let oid = oid_result.map_err(|e| GitError::Internal(e.to_string()))?; if let Some(prev) = prev_oid { if let Ok(commit) = self.repo.find_commit(oid) { if commit.parent_ids().next() == Some(prev) { if limit_check(&commits, opts.limit) { break; } commits.push(CommitMeta::from_git2(&commit)); prev_oid = Some(oid); } } } else { if let Ok(commit) = self.repo.find_commit(oid) { if limit_check(&commits, opts.limit) { break; } commits.push(CommitMeta::from_git2(&commit)); prev_oid = Some(oid); } } } } else { for oid_result in revwalk { let oid = oid_result.map_err(|e| GitError::Internal(e.to_string()))?; if limit_check(&commits, opts.limit) { break; } if let Ok(commit) = self.repo.find_commit(oid) { commits.push(CommitMeta::from_git2(&commit)); } } } Ok(commits) } pub fn commit_topo_walk(&self, rev: Option<&str>, limit: usize) -> GitResult> { self.commit_walk(CommitWalkOptions { rev: rev.map(String::from), sort: CommitSort(git2::Sort::TOPOLOGICAL.bits() | git2::Sort::TIME.bits()), limit, first_parent_only: false, }) } pub fn commit_reverse_walk( &self, rev: Option<&str>, limit: usize, ) -> GitResult> { self.commit_walk(CommitWalkOptions { rev: rev.map(String::from), sort: CommitSort(git2::Sort::TIME.bits() | git2::Sort::REVERSE.bits()), limit, first_parent_only: false, }) } pub fn commit_mainline(&self, rev: Option<&str>, limit: usize) -> GitResult> { self.commit_walk(CommitWalkOptions { rev: rev.map(String::from), sort: CommitSort(git2::Sort::TOPOLOGICAL.bits() | git2::Sort::TIME.bits()), limit, first_parent_only: true, }) } pub fn commit_ancestors(&self, oid: &CommitOid, limit: usize) -> GitResult> { let mut revwalk = self .repo() .revwalk() .map_err(|e| GitError::Internal(e.to_string()))?; revwalk .set_sorting(git2::Sort::TOPOLOGICAL | git2::Sort::TIME) .map_err(|e| GitError::Internal(e.to_string()))?; revwalk .push(oid.to_oid()?) .map_err(|e| GitError::Internal(e.to_string()))?; let mut commits = Vec::new(); for oid_result in revwalk { let oid = oid_result.map_err(|e| GitError::Internal(e.to_string()))?; if limit > 0 && commits.len() >= limit { break; } if let Ok(commit) = self.repo.find_commit(oid) { commits.push(CommitMeta::from_git2(&commit)); } } Ok(commits) } pub fn commit_descendants(&self, oid: &CommitOid, limit: usize) -> GitResult> { let range = format!("{}..", oid); self.commit_walk(CommitWalkOptions { rev: Some(range), sort: CommitSort(git2::Sort::TOPOLOGICAL.bits() | git2::Sort::TIME.bits()), limit, first_parent_only: false, }) } pub fn resolve_rev(&self, rev: &str) -> GitResult { if let Ok(oid) = git2::Oid::from_str(rev) { return Ok(CommitOid::from_git2(oid)); } // "HEAD" — use repo.head() to get the actual OID if rev == "HEAD" { if let Ok(reference) = self.repo.head() { if let Some(target) = reference.target() { return Ok(CommitOid::from_git2(target)); } } return Err(GitError::InvalidOid("cannot resolve: HEAD (detached or empty)".into())); } if let Ok(reference) = self.repo.find_reference(rev) { if let Some(target) = reference.target() { return Ok(CommitOid::from_git2(target)); } } if let Ok(commit) = self.repo.revparse_single(rev) { return Ok(CommitOid::from_git2(commit.id())); } Err(GitError::InvalidOid(format!("cannot resolve: {}", rev))) } } fn limit_check(commits: &[CommitMeta], limit: usize) -> bool { limit > 0 && commits.len() >= limit }