//! Branch querying operations. use git2::BranchType; use crate::branch::types::{BranchDiff, BranchInfo, BranchSummary}; use crate::commit::types::CommitOid; use crate::{GitDomain, GitError, GitResult}; impl GitDomain { pub fn branch_list(&self, remote_only: bool) -> GitResult> { let branch_type = if remote_only { BranchType::Remote } else { BranchType::Local }; let mut branches = Vec::with_capacity(16); let head_name = self.repo.head().ok().and_then(|r| { r.name() .map(|name| name.strip_prefix("refs/heads/").unwrap_or(name).to_string()) }); for branch_result in self .repo() .branches(Some(branch_type)) .map_err(|e| GitError::Internal(e.to_string()))? { let (branch, _) = branch_result.map_err(|e| GitError::Internal(e.to_string()))?; if let Some(name) = branch.name().ok().flatten() { let Some(target) = branch.get().target() else { continue; // skip branches without a target }; let name = name.to_string(); let oid = CommitOid::from_git2(target); let is_head = head_name.as_ref().map_or(false, |h| h == &name); let is_current = branch.is_head(); branches.push(BranchInfo { name, oid, is_head, is_remote: remote_only, is_current, upstream: None, }); } } Ok(branches) } pub fn branch_list_local(&self) -> GitResult> { self.branch_list(false) } pub fn branch_list_remote(&self) -> GitResult> { self.branch_list(true) } pub fn branch_list_all(&self) -> GitResult> { let mut all = self.branch_list(false)?; let remote = self.branch_list(true)?; for mut r in remote { r.is_remote = true; all.push(r); } Ok(all) } pub fn branch_summary(&self) -> GitResult { let local = self.branch_list(false)?; let remote = self.branch_list(true)?; Ok(BranchSummary { local_count: local.len(), remote_count: remote.len(), all_count: local.len() + remote.len(), }) } pub fn branch_get(&self, name: &str) -> GitResult { // Determine full ref name and branch type let full_name = if name.starts_with("refs/heads/") { name.to_string() } else if name.starts_with("refs/remotes/") { name.to_string() } else if name.contains('/') { // e.g. "origin/main" → remote branch format!("refs/remotes/{}", name) } else { format!("refs/heads/{}", name) }; let branch = self .repo() .find_branch(&full_name, git2::BranchType::Local) .or_else(|_| self.repo.find_branch(&full_name, git2::BranchType::Remote)) .map_err(|_e| GitError::RefNotFound(name.to_string()))?; let target = branch .get() .target() .ok_or_else(|| GitError::Internal("branch has no target".to_string()))?; let oid = CommitOid::from_git2(target); let head_name = self .repo() .head() .ok() .and_then(|r| r.name().map(String::from)); let branch_name = branch.name().ok().flatten().unwrap_or_default(); Ok(BranchInfo { name: branch_name.to_string(), oid, is_head: head_name.as_ref().map_or(false, |h| h == &full_name), is_remote: full_name.starts_with("refs/remotes/"), is_current: branch.is_head(), upstream: branch.upstream().ok().map(|u| { u.name() .ok() .and_then(|n| n) .unwrap_or_default() .to_string() }), }) } pub fn branch_exists(&self, name: &str) -> bool { let full_name = if name.starts_with("refs/heads/") || name.starts_with("refs/remotes/") { name.to_string() } else if name.contains('/') { format!("refs/remotes/{}", name) } else { format!("refs/heads/{}", name) }; self.repo.find_branch(&full_name, BranchType::Local).is_ok() || self .repo() .find_branch(&full_name, BranchType::Remote) .is_ok() } pub fn branch_is_head(&self, name: &str) -> GitResult { let info = self.branch_get(name)?; Ok(info.is_head) } pub fn branch_current(&self) -> GitResult> { let head = self .repo() .head() .map_err(|e| GitError::Internal(e.to_string()))?; if let Some(name) = head.name() { let name = name.to_string(); if name.starts_with("refs/heads/") { return Ok(Some(self.branch_get(&name)?)); } } Ok(None) } pub fn branch_target(&self, name: &str) -> GitResult> { let info = self.branch_get(name)?; if info.oid.0.is_empty() { Ok(None) } else { Ok(Some(info.oid)) } } pub fn branch_upstream(&self, name: &str) -> GitResult> { let full_name = if name.starts_with("refs/heads/") { name.to_string() } else { format!("refs/heads/{}", name) }; let branch = self .repo() .find_branch(&full_name, BranchType::Local) .map_err(|_e| GitError::RefNotFound(name.to_string()))?; match branch.upstream() { Ok(up) => { let up_target = up.get().target().ok_or_else(|| { GitError::Internal("upstream branch has no target".to_string()) })?; Ok(Some(BranchInfo { name: up .name() .ok() .and_then(|n| n) .unwrap_or_default() .to_string(), oid: CommitOid::from_git2(up_target), is_head: false, is_remote: true, is_current: false, upstream: None, })) } Err(e) if e.code() == git2::ErrorCode::NotFound => Ok(None), Err(e) => Err(GitError::Internal(e.to_string())), } } pub fn branch_upstream_name(&self, name: &str) -> GitResult> { let upstream = self.branch_upstream(name)?; Ok(upstream.map(|u| u.name)) } pub fn branch_has_upstream(&self, name: &str) -> GitResult { let full_name = if name.starts_with("refs/heads/") { name.to_string() } else { format!("refs/heads/{}", name) }; let branch = self .repo() .find_branch(&full_name, BranchType::Local) .map_err(|_e| GitError::RefNotFound(name.to_string()))?; Ok(branch.upstream().is_ok()) } pub fn branch_is_detached(&self) -> bool { self.repo.head().map_or(false, |h| !h.is_branch()) } pub fn branch_diff(&self, local: &str, remote: &str) -> GitResult { let local_oid = self.branch_target(local)?; let remote_oid = self.branch_target(remote)?; match (local_oid, remote_oid) { (Some(l), Some(r)) => { let l_oid = l.to_oid()?; let r_oid = r.to_oid()?; let (ahead, behind) = self .repo() .graph_ahead_behind(l_oid, r_oid) .map_err(|e| GitError::Internal(e.to_string()))?; Ok(BranchDiff { ahead, behind, diverged: ahead > 0 && behind > 0, }) } _ => Ok(BranchDiff { ahead: 0, behind: 0, diverged: false, }), } } pub fn branch_ahead_behind(&self, local: &str, upstream: &str) -> GitResult<(usize, usize)> { let diff = self.branch_diff(local, upstream)?; Ok((diff.ahead, diff.behind)) } }