use anyhow::Result;
use git2::{ErrorCode, Reference, Remote, Repository, StatusOptions};
use log::debug;
use serde::{Deserialize, Serialize};
#[remain::sorted]
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub enum Status {
Bare,
Clean,
Unclean,
Unknown,
Unpushed,
}
impl Status {
pub fn as_str(&self) -> &'static str {
match self {
Self::Bare => "bare",
Self::Clean => "clean",
Self::Unclean => "unclean",
Self::Unknown => "unknown",
Self::Unpushed => "unpushed",
}
}
pub fn find(repo: &Repository) -> Result<(Status, Option<Reference<'_>>, Option<Remote<'_>>)> {
let head = match repo.head() {
Ok(head) => Some(head),
Err(ref e)
if e.code() == ErrorCode::UnbornBranch || e.code() == ErrorCode::NotFound =>
{
None
}
Err(e) => return Err(e.into()),
};
let (remote, remote_name) = match repo.find_remote("origin") {
Ok(origin) => (Some(origin), Some("origin".to_string())),
Err(e) if e.code() == ErrorCode::NotFound => Self::choose_remote_greedily(repo)?,
Err(e) => return Err(e.into()),
};
let mut opts = StatusOptions::new();
opts.include_untracked(true).recurse_untracked_dirs(true);
let status = match repo.statuses(Some(&mut opts)) {
Ok(v) if v.is_empty() => match &head {
Some(head) => match remote_name {
Some(remote_name) => match Self::is_unpushed(repo, head, &remote_name)? {
true => Status::Unpushed,
false => Status::Clean,
},
None => Status::Clean,
},
None => Status::Clean,
},
Ok(_) => Status::Unclean,
Err(e) if e.code() == ErrorCode::BareRepo => Status::Bare,
Err(e) => return Err(e.into()),
};
Ok((status, head, remote))
}
fn is_unpushed(
repo: &Repository,
head: &Reference<'_>,
remote_name: &str,
) -> Result<bool, git2::Error> {
let local_head = head.peel_to_commit()?;
let remote = format!(
"{}/{}",
remote_name,
match head.shorthand() {
Some(v) => v,
None => {
debug!("assuming unpushed; could not determine shorthand for head");
return Ok(true);
}
}
);
let remote_head = match repo.resolve_reference_from_short_name(&remote) {
Ok(reference) => reference.peel_to_commit()?,
Err(e) => {
debug!(
"assuming unpushed; could not resolve remote reference from short name (ignored error: {e})"
);
return Ok(true);
}
};
Ok(
matches!(repo.graph_ahead_behind(local_head.id(), remote_head.id()), Ok(number_unique_commits) if number_unique_commits.0 > 0),
)
}
fn choose_remote_greedily(
repository: &Repository,
) -> Result<(Option<Remote<'_>>, Option<String>), git2::Error> {
let remotes = repository.remotes()?;
Ok(match remotes.get(0) {
Some(remote_name) => (
Some(repository.find_remote(remote_name)?),
Some(remote_name.to_string()),
),
None => (None, None),
})
}
}