pub use builder::Builder;
use documentation::Documentation;
use generated::Generated;
use git2::{AttrCheckFlags, AttrValue, Blob, Commit, ObjectType, Repository, Tree};
use indexmap::IndexMap;
pub use languages::analyzer::Analyzers;
use languages::Category;
pub use languages::Language;
use std::error::Error;
use std::ffi::OsStr;
use std::path::Path;
use vendored::Vendored;
mod builder;
mod documentation;
mod generated;
pub mod languages;
mod vendored;
pub struct Gengo {
repository: Repository,
analyzers: Analyzers,
read_limit: usize,
}
impl Gengo {
const ATTR_CHECK_FLAGS: [AttrCheckFlags; 2] =
[AttrCheckFlags::NO_SYSTEM, AttrCheckFlags::INDEX_THEN_FILE];
fn rev(&self, rev: &str) -> Result<Commit, Box<dyn Error>> {
let reference = self.repository.revparse_single(rev)?;
let commit = reference.peel_to_commit()?;
Ok(commit)
}
pub fn analyze(&self, rev: &str) -> Result<IndexMap<String, Entry>, Box<dyn Error>> {
let mut results = IndexMap::new();
let commit = self.rev(rev)?;
let tree = commit.tree()?;
self.analyze_tree("", &tree, &mut results)?;
Ok(results)
}
fn analyze_tree(
&self,
root: &str,
tree: &Tree,
results: &mut IndexMap<String, Entry>,
) -> Result<(), Box<dyn Error>> {
for entry in tree.iter() {
let object = entry.to_object(&self.repository)?;
match entry.kind() {
Some(ObjectType::Tree) => {
let path = entry.name().ok_or("invalid path")?;
let tree = object.as_tree().expect("object to be a tree");
let path = Path::new(root).join(path);
let path = path.to_str().ok_or("invalid path")?;
self.analyze_tree(path, tree, results)?;
}
Some(ObjectType::Blob) => {
let path = entry.name().ok_or("invalid path").unwrap();
let filepath = Path::new(root).join(path);
let filepath = filepath.as_os_str();
let blob = object.as_blob().expect("object to be a blob");
self.analyze_blob(filepath, blob, results)?;
}
_ => continue,
}
}
Ok(())
}
fn analyze_blob(
&self,
filepath: &OsStr,
blob: &Blob,
results: &mut IndexMap<String, Entry>,
) -> Result<(), Box<dyn Error>> {
let path = Path::new(filepath);
let contents = blob.content();
let lang_override = self
.get_str_attr(path, "gengo-language")?
.map(|s| s.replace('-', " "))
.and_then(|s| self.analyzers.get(&s));
let language =
lang_override.or_else(|| self.analyzers.pick(filepath, contents, self.read_limit));
let language = if let Some(language) = language {
language.clone()
} else {
return Ok(());
};
let size = contents.len();
let generated = self
.get_boolean_attr(path, "gengo-generated")?
.unwrap_or_else(|| self.is_generated(filepath, contents));
let documentation = self
.get_boolean_attr(path, "gengo-documentation")?
.unwrap_or_else(|| self.is_documentation(filepath, contents));
let vendored = self
.get_boolean_attr(path, "gengo-vendored")?
.unwrap_or_else(|| self.is_vendored(filepath, contents));
let detectable = match language.category() {
Category::Data | Category::Prose => false,
Category::Programming | Category::Markup | Category::Query => {
!(generated || documentation || vendored)
}
};
let detectable = self
.get_boolean_attr(path, "gengo-detectable")?
.unwrap_or(detectable);
let path = String::from(filepath.to_str().ok_or("invalid path")?);
let entry = Entry {
language,
size,
detectable,
generated,
documentation,
vendored,
};
results.insert(path, entry);
Ok(())
}
pub fn is_generated(&self, filepath: &OsStr, contents: &[u8]) -> bool {
Generated::is_generated(filepath, contents)
}
pub fn is_documentation(&self, filepath: &OsStr, contents: &[u8]) -> bool {
Documentation::is_documentation(filepath, contents)
}
pub fn is_vendored(&self, filepath: &OsStr, contents: &[u8]) -> bool {
Vendored::is_vendored(filepath, contents)
}
fn get_attr(&self, path: &Path, attr: &str) -> Result<AttrValue, Box<dyn Error>> {
let flags = Self::ATTR_CHECK_FLAGS
.into_iter()
.reduce(|a, b| a | b)
.unwrap();
let attr = self.repository.get_attr(path, attr, flags)?;
let attr = AttrValue::from_string(attr);
Ok(attr)
}
fn get_boolean_attr(&self, path: &Path, attr: &str) -> Result<Option<bool>, Box<dyn Error>> {
let attr = self.get_attr(path, attr)?;
let attr = match attr {
AttrValue::True => Some(true),
AttrValue::False => Some(false),
AttrValue::Unspecified => None,
_ => None,
};
Ok(attr)
}
fn get_str_attr(&self, path: &Path, attr: &str) -> Result<Option<String>, Box<dyn Error>> {
let attr = self.get_attr(path, attr)?;
let attr = match attr {
AttrValue::String(s) => Some(s),
AttrValue::Unspecified => None,
_ => None,
};
Ok(attr.map(String::from))
}
}
#[derive(Debug)]
pub struct Entry {
language: Language,
size: usize,
detectable: bool,
generated: bool,
documentation: bool,
vendored: bool,
}
impl Entry {
pub fn language(&self) -> &Language {
&self.language
}
pub fn size(&self) -> usize {
self.size
}
pub fn detectable(&self) -> bool {
self.detectable
}
pub fn generated(&self) -> bool {
self.generated
}
pub fn documentation(&self) -> bool {
self.documentation
}
pub fn vendored(&self) -> bool {
self.vendored
}
}