gitdataai/libs/git/branch/query.rs
2026-04-14 19:02:01 +08:00

266 lines
8.5 KiB
Rust

//! 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<Vec<BranchInfo>> {
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<Vec<BranchInfo>> {
self.branch_list(false)
}
pub fn branch_list_remote(&self) -> GitResult<Vec<BranchInfo>> {
self.branch_list(true)
}
pub fn branch_list_all(&self) -> GitResult<Vec<BranchInfo>> {
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<BranchSummary> {
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<BranchInfo> {
// 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<bool> {
let info = self.branch_get(name)?;
Ok(info.is_head)
}
pub fn branch_current(&self) -> GitResult<Option<BranchInfo>> {
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<Option<CommitOid>> {
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<Option<BranchInfo>> {
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<Option<String>> {
let upstream = self.branch_upstream(name)?;
Ok(upstream.map(|u| u.name))
}
pub fn branch_has_upstream(&self, name: &str) -> GitResult<bool> {
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<BranchDiff> {
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))
}
}