use std::path::Path;
use anyhow::Result;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GitStatusSnapshot {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub branch: Option<String>,
pub dirty: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ahead: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub behind: Option<usize>,
}
pub fn capture(repo_root: &Path) -> Result<GitStatusSnapshot> {
let repo = gix::open(repo_root)?;
let branch = head_branch(&repo);
let dirty = is_dirty(&repo)?;
let (ahead, behind) = ahead_behind(&repo, branch.as_deref());
Ok(GitStatusSnapshot {
branch,
dirty,
ahead,
behind,
})
}
fn head_branch(repo: &gix::Repository) -> Option<String> {
let head = repo.head().ok()?;
match head.referent_name() {
Some(name) => Some(name.shorten().to_string()),
None => Some("HEAD".to_string()),
}
}
fn is_dirty(repo: &gix::Repository) -> Result<bool> {
Ok(repo.is_dirty()?)
}
fn ahead_behind(repo: &gix::Repository, branch: Option<&str>) -> (Option<usize>, Option<usize>) {
let inner = || -> Option<(usize, usize)> {
let branch = branch?;
if branch == "HEAD" {
return None;
}
let head_id = repo.head_id().ok()?.detach();
let upstream_ref = format!("refs/remotes/origin/{branch}");
let upstream_id = repo
.find_reference(upstream_ref.as_str())
.ok()?
.id()
.detach();
if head_id == upstream_id {
return Some((0, 0));
}
let ahead = count_reachable_excluding(repo, head_id, upstream_id).ok()?;
let behind = count_reachable_excluding(repo, upstream_id, head_id).ok()?;
Some((ahead, behind))
};
match inner() {
Some((a, b)) => (Some(a), Some(b)),
None => (None, None),
}
}
fn count_reachable_excluding(
repo: &gix::Repository,
tip: gix::ObjectId,
hide: gix::ObjectId,
) -> Result<usize> {
let walk = repo.rev_walk([tip]).with_hidden([hide]).all()?;
let mut count = 0usize;
for item in walk {
item?;
count += 1;
}
Ok(count)
}