#![forbid(unsafe_code)]
use git2::Branch;
use git2::ReferenceType;
use git2::Repository;
use git2::{ErrorClass, ErrorCode};
use git2::{Status, StatusOptions, StatusShow};
use std::fmt;
use std::io;
use std::path::Path;
use std::time::Instant;
mod shell_writer;
pub use shell_writer::*;
#[derive(Debug, Default)]
pub struct Reference {
pub name: String,
pub kind: String,
pub error: String,
}
impl Reference {
#[must_use]
pub fn new<N: fmt::Display, K: fmt::Display>(name: N, kind: K) -> Self {
Self {
name: name.to_string(),
kind: kind.to_string(),
error: "".to_owned(),
}
}
#[must_use]
pub fn new_with_error<N, K, E>(name: N, kind: K, error: E) -> Self
where
N: fmt::Display,
K: fmt::Display,
E: fmt::Debug,
{
Self {
name: name.to_string(),
kind: kind.to_string(),
error: format!("{error:?}"),
}
}
#[must_use]
pub fn symbolic<N: fmt::Display>(name: N) -> Self {
Self::new(name, "symbolic")
}
#[must_use]
pub fn direct<N: fmt::Display>(name: N) -> Self {
Self::new(name, "direct")
}
#[must_use]
pub fn short(&self) -> &str {
self.name
.strip_prefix("refs/heads/")
.or_else(|| self.name.strip_prefix("refs/tags/"))
.unwrap_or(&self.name)
}
}
impl ShellVars for Reference {
fn write_to_shell<W: io::Write>(&self, out: &ShellWriter<W>) {
out.write_var("name", &self.name);
out.write_var("short", self.short());
out.write_var("kind", &self.kind);
out.write_var("error", &self.error);
}
}
#[derive(Debug, Default)]
pub struct Head {
pub trail: Vec<Reference>,
pub hash: String,
pub ahead_of_upstream: Option<usize>,
pub behind_upstream: Option<usize>,
pub upstream_error: String,
}
impl ShellVars for Head {
fn write_to_shell<W: io::Write>(&self, out: &ShellWriter<W>) {
let trail = self.trail.get(1..).unwrap_or(&[]);
out.write_var("ref_length", trail.len());
for (i, reference) in trail.iter().enumerate() {
#[allow(clippy::arithmetic_side_effects)]
out.group_n("ref", i + 1).write_vars(reference);
}
out.write_var("hash", &self.hash);
out.write_var("ahead", display_option(self.ahead_of_upstream));
out.write_var("behind", display_option(self.behind_upstream));
out.write_var("upstream_error", &self.upstream_error);
}
}
pub fn summarize_repository<W: std::io::Write>(
out: &ShellWriter<W>,
opened: Result<Repository, git2::Error>,
) {
let result = match opened {
Ok(mut repository) => summarize_opened_repository(out, &mut repository),
Err(error)
if error.code() == ErrorCode::NotFound
&& error.class() == ErrorClass::Repository =>
{
out.write_var("repo_state", "NotFound");
Ok(())
}
Err(error) => Err(error),
};
if let Err(error) = result {
out.write_var("repo_state", "Error");
out.write_var_debug("repo_error", error);
}
}
pub fn summarize_opened_repository<W: std::io::Write>(
out: &ShellWriter<W>,
repository: &mut Repository,
) -> Result<(), git2::Error> {
out.write_var(
"repo_workdir",
display_option(
time("repository.workdir()", || repository.workdir())
.map(Path::display),
),
);
out.write_var(
"repo_empty",
time("repository.is_empty()", || repository.is_empty())?,
);
out.write_var(
"repo_bare",
time("repository.is_bare()", || repository.is_bare()),
);
out.group("head")
.write_vars(&time("head_info(repository)", || head_info(repository)));
out.write_var(
"stash_count",
time("count_stash(repository)", || count_stash(repository))?,
);
out.write_vars(&time("count_changes(repository)", || {
count_changes(repository)
})?);
out.write_var_debug(
"repo_state",
time("repository.state()", || repository.state()),
);
Ok(())
}
#[allow(clippy::similar_names)]
#[must_use]
pub fn head_info(repository: &Repository) -> Head {
let mut current = "HEAD".to_owned();
let mut head = Head::default();
loop {
match repository.find_reference(¤t) {
Ok(reference) => match reference.kind() {
Some(ReferenceType::Direct) => {
head.trail.push(Reference::direct(display_option(
reference.name(),
)));
head.hash = display_option(reference.target());
break;
}
Some(ReferenceType::Symbolic) => {
head.trail.push(Reference::symbolic(display_option(
reference.name(),
)));
let target = reference
.symbolic_target()
.expect("Symbolic ref should have symbolic target");
target.clone_into(&mut current);
}
None => {
head.trail.push(Reference::new(
display_option(reference.name()),
"unknown",
));
break;
}
},
Err(error) => {
head.trail
.push(Reference::new_with_error(current, "", error));
break;
}
};
}
match get_upstream_difference(repository) {
Ok(Some((ahead, behind))) => {
head.ahead_of_upstream = Some(ahead);
head.behind_upstream = Some(behind);
}
Ok(None) => {}
Err(error) => {
head.upstream_error = format!("{error:?}");
}
}
head
}
pub fn get_upstream_difference(
repository: &Repository,
) -> Result<Option<(usize, usize)>, git2::Error> {
let local_ref = repository.head()?.resolve()?;
if let Some(local_oid) = local_ref.target() {
Branch::wrap(local_ref)
.upstream()?
.get()
.target()
.map(|upstream_oid| {
repository.graph_ahead_behind(local_oid, upstream_oid)
})
.transpose()
} else {
Ok(None)
}
}
fn display_option<V: fmt::Display>(s: Option<V>) -> String {
s.map(|s| s.to_string()).unwrap_or_else(|| "".to_owned())
}
#[derive(Debug, Default)]
pub struct ChangeCounters {
pub untracked: usize,
pub unstaged: usize,
pub staged: usize,
pub conflicted: usize,
}
impl From<[usize; 4]> for ChangeCounters {
fn from(array: [usize; 4]) -> Self {
Self {
untracked: array[0],
unstaged: array[1],
staged: array[2],
conflicted: array[3],
}
}
}
impl ShellVars for ChangeCounters {
fn write_to_shell<W: io::Write>(&self, out: &ShellWriter<W>) {
out.write_var("untracked_count", self.untracked);
out.write_var("unstaged_count", self.unstaged);
out.write_var("staged_count", self.staged);
out.write_var("conflicted_count", self.conflicted);
}
}
pub fn count_changes(
repository: &Repository,
) -> Result<ChangeCounters, git2::Error> {
if repository.is_bare() {
return Ok(ChangeCounters::default());
}
let mut options = StatusOptions::new();
options
.show(StatusShow::IndexAndWorkdir)
.include_untracked(true)
.exclude_submodules(true)
.no_refresh(true)
.update_index(false);
let statuses = repository.statuses(Some(&mut options))?;
let mut counters: [usize; 4] = [0; 4];
let buckets = [
Status::WT_NEW,
Status::WT_MODIFIED
| Status::WT_DELETED
| Status::WT_TYPECHANGE
| Status::WT_RENAMED,
Status::INDEX_NEW
| Status::INDEX_MODIFIED
| Status::INDEX_DELETED
| Status::INDEX_RENAMED
| Status::INDEX_TYPECHANGE,
Status::CONFLICTED,
];
for status in statuses.iter() {
for (i, bits) in buckets.iter().enumerate() {
if status.status().intersects(*bits) {
counters[i] = counters[i].saturating_add(1);
}
}
}
Ok(ChangeCounters::from(counters))
}
pub fn count_stash(repository: &mut Repository) -> Result<usize, git2::Error> {
let mut count: usize = 0;
repository.stash_foreach(|_, _, _| {
count = count.saturating_add(1);
true
})?;
Ok(count)
}
fn time<D, F, R>(name: D, func: F) -> R
where
D: fmt::Display,
F: FnOnce() -> R,
{
let start = Instant::now();
let result = func();
tracing::debug!("{name}: {:?}", start.elapsed());
result
}