use git2::{Branch, BranchType, DescribeOptions, Repository};
use nu_protocol::{IntoSpanned, LabeledError, Span, Spanned, Value, record};
use std::{fmt::Write, ops::BitAnd, path::Path};
#[derive(Default)]
pub struct GStat;
impl GStat {
pub fn new() -> Self {
Default::default()
}
pub fn gstat(
&self,
value: &Value,
current_dir: &str,
path: Option<Spanned<String>>,
calculate_tag: bool,
span: Span,
) -> Result<Value, LabeledError> {
let path = match path {
Some(path) => path,
None => {
if !value.is_nothing() {
value.coerce_string()?.into_spanned(value.span())
} else {
String::from(".").into_spanned(span)
}
}
};
let absolute_path = Path::new(current_dir).join(&path.item);
if !absolute_path.exists() {
return Err(LabeledError::new("error with path").with_label(
format!("path does not exist [{}]", absolute_path.display()),
path.span,
));
}
let metadata = std::fs::metadata(&absolute_path).map_err(|e| {
LabeledError::new("error with metadata").with_label(
format!(
"unable to get metadata for [{}], error: {}",
absolute_path.display(),
e
),
path.span,
)
})?;
if !metadata.is_dir() {
return Err(LabeledError::new("error with directory").with_label(
format!("path is not a directory [{}]", absolute_path.display()),
path.span,
));
}
let repo_path = match absolute_path.canonicalize() {
Ok(p) => p,
Err(e) => {
return Err(LabeledError::new(format!(
"error canonicalizing [{}]",
absolute_path.display()
))
.with_label(e.to_string(), path.span));
}
};
let (stats, repo) = if let Ok(mut repo) = Repository::discover(repo_path) {
(Stats::new(&mut repo), repo)
} else {
return Ok(self.create_empty_git_status(span));
};
let repo_name = repo
.path()
.parent()
.and_then(|p| p.file_name())
.map(|p| p.to_string_lossy().to_string())
.unwrap_or_else(|| "".to_string());
let tag = calculate_tag
.then(|| {
let mut desc_opts = DescribeOptions::new();
desc_opts.describe_tags();
repo.describe(&desc_opts).ok()?.format(None).ok()
})
.flatten()
.unwrap_or_else(|| "no_tag".to_string());
Ok(Value::record(
record! {
"idx_added_staged" => Value::int(stats.idx_added_staged as i64, span),
"idx_modified_staged" => Value::int(stats.idx_modified_staged as i64, span),
"idx_deleted_staged" => Value::int(stats.idx_deleted_staged as i64, span),
"idx_renamed" => Value::int(stats.idx_renamed as i64, span),
"idx_type_changed" => Value::int(stats.idx_type_changed as i64, span),
"wt_untracked" => Value::int(stats.wt_untracked as i64, span),
"wt_modified" => Value::int(stats.wt_modified as i64, span),
"wt_deleted" => Value::int(stats.wt_deleted as i64, span),
"wt_type_changed" => Value::int(stats.wt_type_changed as i64, span),
"wt_renamed" => Value::int(stats.wt_renamed as i64, span),
"ignored" => Value::int(stats.ignored as i64, span),
"conflicts" => Value::int(stats.conflicts as i64, span),
"ahead" => Value::int(stats.ahead as i64, span),
"behind" => Value::int(stats.behind as i64, span),
"stashes" => Value::int(stats.stashes as i64, span),
"repo_name" => Value::string(repo_name, span),
"tag" => Value::string(tag, span),
"branch" => Value::string(stats.branch, span),
"remote" => Value::string(stats.remote, span),
"state" => Value::string(stats.state, span),
},
span,
))
}
fn create_empty_git_status(&self, span: Span) -> Value {
Value::record(
record! {
"idx_added_staged" => Value::int(-1, span),
"idx_modified_staged" => Value::int(-1, span),
"idx_deleted_staged" => Value::int(-1, span),
"idx_renamed" => Value::int(-1, span),
"idx_type_changed" => Value::int(-1, span),
"wt_untracked" => Value::int(-1, span),
"wt_modified" => Value::int(-1, span),
"wt_deleted" => Value::int(-1, span),
"wt_type_changed" => Value::int(-1, span),
"wt_renamed" => Value::int(-1, span),
"ignored" => Value::int(-1, span),
"conflicts" => Value::int(-1, span),
"ahead" => Value::int(-1, span),
"behind" => Value::int(-1, span),
"stashes" => Value::int(-1, span),
"repo_name" => Value::string("no_repository", span),
"tag" => Value::string("no_tag", span),
"branch" => Value::string("no_branch", span),
"remote" => Value::string("no_remote", span),
"state" => Value::string("no_state", span),
},
span,
)
}
}
#[derive(Debug, PartialEq, Eq, Default, Clone)]
pub struct Stats {
pub idx_added_staged: u16,
pub idx_modified_staged: u16,
pub idx_deleted_staged: u16,
pub idx_renamed: u16,
pub idx_type_changed: u16,
pub wt_untracked: u16,
pub wt_modified: u16,
pub wt_deleted: u16,
pub wt_type_changed: u16,
pub wt_renamed: u16,
pub ignored: u16,
pub conflicts: u16,
pub ahead: u16,
pub behind: u16,
pub stashes: u16,
pub branch: String,
pub remote: String,
pub state: String,
}
impl Stats {
pub fn new(repo: &mut Repository) -> Stats {
let mut st: Stats = Default::default();
st.read_branch(repo);
st.state = format!("{:?}", repo.state()).to_lowercase();
let mut opts = git2::StatusOptions::new();
opts.include_untracked(true)
.recurse_untracked_dirs(true)
.renames_head_to_index(true);
if let Ok(statuses) = repo.statuses(Some(&mut opts)) {
for status in statuses.iter() {
let flags = status.status();
if check(flags, git2::Status::INDEX_NEW) {
st.idx_added_staged += 1;
}
if check(flags, git2::Status::INDEX_MODIFIED) {
st.idx_modified_staged += 1;
}
if check(flags, git2::Status::INDEX_DELETED) {
st.idx_deleted_staged += 1;
}
if check(flags, git2::Status::INDEX_RENAMED) {
st.idx_renamed += 1;
}
if check(flags, git2::Status::INDEX_TYPECHANGE) {
st.idx_type_changed += 1;
}
if check(flags, git2::Status::WT_NEW) {
st.wt_untracked += 1;
}
if check(flags, git2::Status::WT_MODIFIED) {
st.wt_modified += 1;
}
if check(flags, git2::Status::WT_DELETED) {
st.wt_deleted += 1;
}
if check(flags, git2::Status::WT_TYPECHANGE) {
st.wt_type_changed += 1;
}
if check(flags, git2::Status::WT_RENAMED) {
st.wt_renamed += 1;
}
if check(flags, git2::Status::IGNORED) {
st.ignored += 1;
}
if check(flags, git2::Status::CONFLICTED) {
st.conflicts += 1;
}
}
}
let _ = repo.stash_foreach(|_, &_, &_| {
st.stashes += 1;
true
});
st
}
fn read_branch(&mut self, repo: &Repository) {
self.branch = match repo.head() {
Ok(head) => {
if let Some(name) = head.shorthand() {
if name == "HEAD" {
if let Ok(commit) = head.peel_to_commit() {
let mut id = String::new();
for byte in &commit.id().as_bytes()[..4] {
write!(&mut id, "{byte:02x}").ok();
}
id
} else {
"HEAD".to_string()
}
} else {
let branch = name.to_string();
self.read_upstream_name(repo, &branch);
branch
}
} else {
"HEAD".to_string()
}
}
Err(ref err) if err.code() == git2::ErrorCode::BareRepo => "master".to_string(),
Err(_) if repo.is_empty().unwrap_or(false) => "master".to_string(),
Err(_) => "HEAD".to_string(),
};
}
fn read_upstream_name(&mut self, repo: &Repository, branch: &str) {
self.remote = if let Ok(branch) = repo.find_branch(branch, BranchType::Local) {
if let Ok(upstream) = branch.upstream() {
self.read_ahead_behind(repo, &branch, &upstream);
if let Ok(Some(name)) = upstream.name() {
name.to_string()
} else {
String::new()
}
} else {
String::new()
}
} else {
String::new()
};
}
fn read_ahead_behind(&mut self, repo: &Repository, local: &Branch, upstream: &Branch) {
if let (Some(local), Some(upstream)) = (local.get().target(), upstream.get().target())
&& let Ok((ahead, behind)) = repo.graph_ahead_behind(local, upstream)
{
self.ahead = ahead as u16;
self.behind = behind as u16;
}
}
}
#[inline]
fn check<B>(val: B, flag: B) -> bool
where
B: BitAnd<Output = B> + PartialEq + Copy,
{
val & flag == flag
}