266 lines
8.5 KiB
Rust
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))
|
|
}
|
|
}
|