use crate::{GitError, GitResult, RepoHandle};
use gix::bstr::ByteSlice;
#[derive(Debug, Clone)]
pub struct BranchInfo {
pub name: String,
pub is_current: bool,
pub commit_hash: String,
pub upstream: Option<String>,
pub ahead_count: Option<usize>,
pub behind_count: Option<usize>,
}
#[derive(Debug, Clone)]
pub struct RemoteInfo {
pub name: String,
pub fetch_url: String,
pub push_url: String,
}
pub async fn is_clean(repo: &RepoHandle) -> GitResult<bool> {
let repo_clone = repo.clone_inner();
tokio::task::spawn_blocking(move || {
let is_dirty = repo_clone
.is_dirty()
.map_err(|e| GitError::Gix(Box::new(e)))?;
Ok(!is_dirty)
})
.await
.map_err(|e| GitError::Gix(Box::new(e)))?
}
pub async fn current_branch(repo: &RepoHandle) -> GitResult<BranchInfo> {
let repo_clone = repo.clone_inner();
tokio::task::spawn_blocking(move || {
let mut head = repo_clone.head().map_err(|e| GitError::Gix(Box::new(e)))?;
let branch_name = head
.referent_name()
.and_then(|name| {
name.shorten()
.to_str()
.ok()
.map(std::string::ToString::to_string)
})
.unwrap_or_else(|| "detached HEAD".to_string());
let commit = head
.peel_to_commit()
.map_err(|e| GitError::Gix(Box::new(e)))?;
let commit_hash = commit.id().to_string();
let (upstream, ahead_count, behind_count) = get_upstream_info(&repo_clone, &mut head)?;
Ok(BranchInfo {
name: branch_name,
is_current: true,
commit_hash,
upstream,
ahead_count,
behind_count,
})
})
.await
.map_err(|e| GitError::Gix(Box::new(e)))?
}
fn calculate_ahead_behind(
repo: &gix::Repository,
local_commit_id: gix::ObjectId,
upstream_ref: &str,
) -> GitResult<(Option<usize>, Option<usize>)> {
use gix::bstr::ByteSlice;
let upstream_ref_path = if upstream_ref.starts_with("refs/") {
upstream_ref.to_string()
} else {
format!("refs/remotes/{upstream_ref}")
};
let mut upstream_reference =
match repo.try_find_reference(upstream_ref_path.as_bytes().as_bstr()) {
Ok(Some(r)) => r,
Ok(None) => return Ok((None, None)), Err(e) => return Err(GitError::Gix(Box::new(e))),
};
let upstream_commit_id = match upstream_reference.peel_to_id() {
Ok(id) => id.detach(),
Err(e) => return Err(GitError::Gix(Box::new(e))),
};
if local_commit_id == upstream_commit_id {
return Ok((Some(0), Some(0)));
}
let mut graph = repo.revision_graph(None);
let merge_base_id =
match repo.merge_base_with_graph(local_commit_id, upstream_commit_id, &mut graph) {
Ok(base_id) => base_id.detach(),
Err(e) => {
return Err(GitError::Gix(Box::new(e)));
}
};
let ahead_count = count_commits_between(repo, merge_base_id, local_commit_id)?;
let behind_count = count_commits_between(repo, merge_base_id, upstream_commit_id)?;
Ok((Some(ahead_count), Some(behind_count)))
}
fn count_commits_between(
repo: &gix::Repository,
from: gix::ObjectId,
to: gix::ObjectId,
) -> GitResult<usize> {
if from == to {
return Ok(0);
}
let mut from_commits = std::collections::HashSet::new();
let from_walker = repo
.rev_walk([from])
.all()
.map_err(|e| GitError::Gix(Box::new(e)))?;
for commit_result in from_walker {
match commit_result {
Ok(info) => {
from_commits.insert(info.id);
}
Err(e) => return Err(GitError::Gix(Box::new(e))),
}
}
let mut count = 0;
let to_walker = repo
.rev_walk([to])
.all()
.map_err(|e| GitError::Gix(Box::new(e)))?;
for commit_result in to_walker {
match commit_result {
Ok(info) => {
if !from_commits.contains(&info.id) {
count += 1;
}
}
Err(e) => return Err(GitError::Gix(Box::new(e))),
}
}
Ok(count)
}
fn get_upstream_info(
repo: &gix::Repository,
head: &mut gix::Head,
) -> GitResult<(Option<String>, Option<usize>, Option<usize>)> {
let upstream = if let Some(branch_ref) = head.referent_name() {
let branch_name = branch_ref.shorten();
let config = repo.config_snapshot();
let branch_section = format!("branch.{branch_name}");
let remote_name = config
.string(format!("{branch_section}.remote"))
.map(|s| s.to_string());
let merge_ref = config
.string(format!("{branch_section}.merge"))
.map(|s| s.to_string());
if let (Some(remote), Some(merge)) = (remote_name, merge_ref) {
Some(format!(
"{}/{}",
remote,
merge.trim_start_matches("refs/heads/")
))
} else {
None
}
} else {
None
};
let (ahead_count, behind_count) = if let Some(ref upstream_ref) = upstream {
let local_commit_id = match head.peel_to_commit() {
Ok(commit) => commit.id().detach(),
Err(_) => {
return Ok((upstream, None, None));
}
};
calculate_ahead_behind(repo, local_commit_id, upstream_ref).unwrap_or_default()
} else {
(None, None)
};
Ok((upstream, ahead_count, behind_count))
}
pub async fn list_remotes(repo: &RepoHandle) -> GitResult<Vec<RemoteInfo>> {
let repo_clone = repo.clone_inner();
tokio::task::spawn_blocking(move || {
let mut remotes = Vec::new();
for remote_name in repo_clone.remote_names() {
if let Ok(remote) = repo_clone.find_remote(remote_name.as_ref()) {
let fetch_url = remote
.url(gix::remote::Direction::Fetch)
.map_or_else(|| "unknown".to_string(), std::string::ToString::to_string);
let push_url = remote
.url(gix::remote::Direction::Push)
.map_or_else(|| fetch_url.clone(), std::string::ToString::to_string);
remotes.push(RemoteInfo {
name: remote_name.to_string(),
fetch_url,
push_url,
});
}
}
Ok(remotes)
})
.await
.map_err(|e| GitError::Gix(Box::new(e)))?
}
pub async fn remote_exists(repo: &RepoHandle, remote_name: &str) -> GitResult<bool> {
let repo_clone = repo.clone_inner();
let remote_name = remote_name.to_string();
tokio::task::spawn_blocking(move || {
use gix::bstr::ByteSlice;
Ok(repo_clone
.find_remote(remote_name.as_bytes().as_bstr())
.is_ok())
})
.await
.map_err(|e| GitError::Gix(Box::new(e)))?
}
pub async fn head_commit(repo: &RepoHandle) -> GitResult<String> {
let repo_clone = repo.clone_inner();
tokio::task::spawn_blocking(move || {
let mut head = repo_clone.head().map_err(|e| GitError::Gix(Box::new(e)))?;
let commit = head
.peel_to_commit()
.map_err(|e| GitError::Gix(Box::new(e)))?;
Ok(commit.id().to_string())
})
.await
.map_err(|e| GitError::Gix(Box::new(e)))?
}
pub async fn is_detached(repo: &RepoHandle) -> GitResult<bool> {
let repo_clone = repo.clone_inner();
tokio::task::spawn_blocking(move || {
let head = repo_clone.head().map_err(|e| GitError::Gix(Box::new(e)))?;
Ok(head.referent_name().is_none())
})
.await
.map_err(|e| GitError::Gix(Box::new(e)))?
}