extern crate git2;
extern crate ordermap;
extern crate walkdir;
use std::path::{Path, PathBuf};
use ordermap::set::OrderSet as Set;
use walkdir::WalkDir;
use git2::{Branch, Delta, Error, Repository, StatusOptions};
pub struct Output {
pub path: PathBuf,
pub pending: Option<Set<&'static str>>,
pub error: Option<Error>,
}
pub struct Crawler<'a> {
pending: bool,
ignore_untracked: bool,
absolute_paths: bool,
untagged_heads: bool,
root_path: &'a Path,
iter: Box<Iterator<Item = Repository>>,
}
impl<'a> Crawler<'a> {
pub fn new(root: &'a Path) -> Self {
Crawler {
pending: false,
ignore_untracked: false,
absolute_paths: false,
untagged_heads: false,
root_path: root,
iter: Box::new(
WalkDir::new(root)
.into_iter()
.filter_map(|entry| entry.ok()) .filter(|entry| entry.file_type().is_dir()) .filter(|entry| entry.file_name() != ".git") .filter_map(|entry| Repository::open(entry.path()).ok())
),
}
}
pub fn pending(mut self, answer: bool) -> Self {
self.pending = answer;
self
}
pub fn ignore_untracked(mut self, answer: bool) -> Self {
self.ignore_untracked = answer;
self
}
pub fn absolute_paths(mut self, answer: bool) -> Self {
self.absolute_paths = answer;
self
}
pub fn untagged_heads(mut self, answer: bool) -> Self {
self.untagged_heads = answer;
self
}
fn repo_ops(&self, repo: &Repository) -> Option<Output> {
if let Some(path) = repo.workdir() {
if git2::Repository::discover(path).is_err() {
return None;
}
let mut path = path.to_path_buf();
if !self.absolute_paths {
path = self.make_relative(&path);
}
let mut opts = StatusOptions::new();
opts.include_ignored(false)
.include_untracked(true)
.renames_head_to_index(true)
.renames_index_to_workdir(true);
match repo.statuses(Some(&mut opts)) {
Ok(statuses) => {
let mut pending = Set::new();
for status in statuses.iter() {
if let Some(diff_delta) = status.index_to_workdir() {
match diff_delta.status() {
Delta::Untracked => {
if !self.ignore_untracked {
pending.insert("untracked files");
}
}
Delta::Modified => {
pending.insert("uncommitted changes");
}
Delta::Deleted => {
pending.insert("deleted files");
}
Delta::Renamed => {
pending.insert("renamed files");
}
_ => (),
}
}
if let Some(diff_delta) = status.head_to_index() {
match diff_delta.status() {
Delta::Added => {
pending.insert("added files");
}
Delta::Modified => {
pending.insert("uncommitted changes");
}
Delta::Deleted => {
pending.insert("deleted files");
}
Delta::Renamed => {
pending.insert("renamed files");
}
_ => (),
}
};
}
let local_ref = match repo.head() {
Ok(head) => head,
Err(why) => {
return Some(Output {
path: path,
pending: None,
error: Some(why),
});
}
};
if self.untagged_heads {
if let Ok(tags) = repo.tag_names(None) {
let mut untagged = true;
for tag in tags.iter() {
if let Some(tag) = tag {
let tag = format!("refs/tags/{}", tag);
if let Ok(reference) = repo.find_reference(&tag) {
if reference == local_ref {
untagged = false;
break;
}
}
}
}
if untagged {
pending.insert("untagged HEAD");
}
}
}
let branch = Branch::wrap(local_ref);
if let Ok(upstream_branch) = branch.upstream() {
let remote_ref = upstream_branch.into_reference();
let local_oid = branch.get().target().unwrap();
let remote_oid = remote_ref.target().unwrap();
if local_oid != remote_oid {
if let Ok((ahead, behind)) =
repo.graph_ahead_behind(local_oid, remote_oid)
{
if ahead > 0 {
pending.insert("unpushed commits");
}
if behind > 0 {
pending.insert("unpulled commits");
}
}
}
}
if !pending.is_empty() {
Some(Output {
path: path,
pending: Some(pending),
error: None,
})
} else if !self.pending {
Some(Output {
path: path,
pending: None,
error: None,
})
} else {
None
}
}
Err(why) => Some(Output {
path: path,
pending: None,
error: Some(why),
}),
}
} else {
None
}
}
fn make_relative(&self, target_dir: &Path) -> PathBuf {
if let Ok(path) = target_dir.strip_prefix(self.root_path) {
if path.to_string_lossy().is_empty() {
".".into()
} else {
path.into()
}
} else {
target_dir.into()
}
}
}
impl<'a> Iterator for Crawler<'a> {
type Item = Output;
fn next(&mut self) -> Option<Self::Item> {
loop {
match self.iter.next() {
None => return None,
Some(repo) => {
if let Some(output) = self.repo_ops(&repo) {
return Some(output);
}
}
}
}
}
}