Created
March 14, 2019 20:29
-
-
Save autarch/b2712902574c51373126974e8985841a to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// For some reason I don't understand this needs to be loaded via "extern | |
// crate" and not "use". | |
#[macro_use] | |
extern crate failure_derive; | |
mod basepaths; | |
mod config; | |
mod gitignore; | |
mod tidier; | |
mod vcs; | |
use clap::{App, Arg, ArgGroup, SubCommand}; | |
use failure::Error; | |
use log::{debug, error, info}; | |
use std::env; | |
use std::fmt; | |
use std::path::{Path, PathBuf}; | |
fn main() { | |
let matches = make_app().get_matches(); | |
init_logger(&matches); | |
let main = Main::new(&matches); | |
let code = if main.is_ok() { | |
let exit = main.unwrap().run(); | |
match exit { | |
Ok(_) => 0, | |
Err(exit) => { | |
error!("{}", exit.error); | |
exit.code | |
} | |
} | |
} else { | |
error!("{}", main.unwrap_err()); | |
127 as i32 | |
}; | |
std::process::exit(code); | |
} | |
fn init_logger(matches: &clap::ArgMatches) { | |
let level: u64 = if matches.is_present("debug") { | |
2 // debug level | |
} else if matches.is_present("verbose") { | |
1 // info level | |
} else { | |
0 // warn level | |
}; | |
loggerv::init_with_verbosity(level).unwrap(); | |
} | |
fn make_app<'a>() -> App<'a, 'a> { | |
App::new("precious") | |
.version("0.0.1") | |
.author("Dave Rolsky <[email protected]>") | |
.about("One code quality tool to rule them all") | |
.arg( | |
Arg::with_name("config") | |
.short("c") | |
.long("config") | |
.takes_value(true) | |
.help("Path to config file"), | |
) | |
.arg( | |
Arg::with_name("verbose") | |
.short("v") | |
.long("verbose") | |
.help("Enable verbose output"), | |
) | |
.arg( | |
Arg::with_name("debug") | |
.short("d") | |
.long("debug") | |
.help("Enable debugging output"), | |
) | |
.arg( | |
Arg::with_name("quiet") | |
.short("q") | |
.long("quiet") | |
.help("Suppresses most output"), | |
) | |
.group(ArgGroup::with_name("log-level").args(&["verbose", "debug", "quiet"])) | |
.subcommand(common_subcommand( | |
"tidy", | |
"Tidies the specified files and/or directories", | |
)) | |
.subcommand(common_subcommand( | |
"lint", | |
"Lints the specified files and/or directories", | |
)) | |
.subcommand(common_subcommand( | |
"check", | |
"Checks the specified files and/or directories", | |
)) | |
} | |
fn common_subcommand<'a>(name: &'a str, about: &'a str) -> App<'a, 'a> { | |
SubCommand::with_name(name) | |
.about(about) | |
.arg( | |
Arg::with_name("all") | |
.short("a") | |
.long("all") | |
.help("Run against all files in the current directory and below"), | |
) | |
.arg( | |
Arg::with_name("git") | |
.short("g") | |
.long("git") | |
.help("Run against files that have been modified according to git"), | |
) | |
.arg( | |
Arg::with_name("staged") | |
.short("s") | |
.long("staged") | |
.help("Run against file content that is staged for a git commit"), | |
) | |
.arg( | |
Arg::with_name("paths") | |
.multiple(true) | |
.takes_value(true) | |
.help("A list of paths on which to operate"), | |
) | |
.group( | |
ArgGroup::with_name("operate-on") | |
.args(&["all", "git", "staged", "paths"]) | |
.required(true), | |
) | |
} | |
#[derive(Debug, Fail)] | |
enum RuntimeError { | |
CannotFindRoot { cwd: PathBuf }, | |
} | |
impl fmt::Display for RuntimeError { | |
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
match self { | |
RuntimeError::CannotFindRoot { cwd: cwd } => write!( | |
f, | |
"Could not find a VCS checkout root starting from {}", | |
cwd.to_string_lossy() | |
), | |
} | |
} | |
} | |
#[derive(Debug)] | |
struct Main<'a> { | |
matches: &'a clap::ArgMatches<'a>, | |
config: Option<config::Config>, | |
root: Option<PathBuf>, | |
} | |
#[derive(Debug)] | |
struct Exit { | |
code: i32, | |
error: String, | |
} | |
impl From<Error> for Exit { | |
fn from(err: Error) -> Exit { | |
Exit { | |
code: 1, | |
error: err.to_string(), | |
} | |
} | |
} | |
impl From<basepaths::ConstructorError> for Exit { | |
fn from(err: basepaths::ConstructorError) -> Exit { | |
Exit { | |
code: 2, | |
error: err.to_string(), | |
} | |
} | |
} | |
impl<'a> Main<'a> { | |
fn new(matches: &'a clap::ArgMatches) -> Result<Main<'a>, Error> { | |
let mut s = Main { | |
matches: matches, | |
config: None, | |
root: None, | |
}; | |
s.set_config()?; | |
Ok(s) | |
} | |
fn run(&mut self) -> Result<(), Exit> { | |
let subc_matches = self.subcommand_matches()?; | |
let mut paths: Vec<PathBuf> = vec![]; | |
let mode = if subc_matches.is_present("all") { | |
basepaths::Mode::All | |
} else if subc_matches.is_present("git") { | |
basepaths::Mode::GitModified | |
} else if subc_matches.is_present("staged") { | |
basepaths::Mode::GitStaged | |
} else { | |
if !subc_matches.is_present("paths") { | |
panic!("no paths - wtf"); | |
} | |
subc_matches.values_of("paths").unwrap().for_each(|p| { | |
let mut pb = PathBuf::new(); | |
pb.push(p); | |
paths.push(pb); | |
}); | |
basepaths::Mode::FromCLI | |
}; | |
let bp = basepaths::BasePaths::new( | |
mode, | |
paths, | |
self.root_dir(), | |
self.config().ignore_from.as_ref(), | |
self.config().exclude.as_ref(), | |
)?; | |
//debug!("{:#?}", bp); | |
debug!("{:#?}", bp.paths()); | |
Ok(()) | |
// if let Some(tidy) = matches.subcommand_matches("tidy") { | |
// let t = tidier::new( | |
} | |
fn subcommand_matches(&mut self) -> Result<&clap::ArgMatches<'a>, Exit> { | |
match self.matches.subcommand() { | |
("tidy", Some(m)) => Ok(m), | |
("lint", Some(m)) => Ok(m), | |
("check", Some(m)) => Ok(m), | |
_ => Err(Exit { | |
code: 3, | |
error: String::from("You must invoke one of the subcommand: tidy, lint, check"), | |
}), | |
} | |
} | |
fn set_config(&mut self) -> Result<(), Error> { | |
self.set_root()?; | |
let file = if self.matches.is_present("config") { | |
let conf_file = self.matches.value_of("config").unwrap(); | |
info!("Loading config from {} (set via flag)", conf_file); | |
let mut file = PathBuf::new(); | |
file.push(conf_file); | |
file | |
} else { | |
let default = self.default_config_file()?; | |
info!( | |
"Loading config from {} (default location)", | |
default.to_string_lossy() | |
); | |
default | |
}; | |
self.config = Some(config::Config::new_from_file(file)?); | |
Ok(()) | |
} | |
fn set_root(&mut self) -> Result<(), Error> { | |
let cwd = env::current_dir()?; | |
let mut root = PathBuf::new(); | |
for anc in cwd.ancestors() { | |
if self.is_checkout_root(&anc) { | |
root.push(anc); | |
self.root = Some(root); | |
return Ok(()); | |
} | |
} | |
Err(RuntimeError::CannotFindRoot { cwd: cwd }) | |
} | |
fn is_checkout_root(&self, path: &Path) -> bool { | |
for dir in vcs::VCS_DIRS { | |
let mut poss = PathBuf::new(); | |
poss.push(path); | |
poss.push(dir); | |
if poss.exists() { | |
return true; | |
} | |
} | |
false | |
} | |
fn default_config_file(&self) -> Result<PathBuf, Error> { | |
let mut file = self.root_dir(); | |
file.push("precious.toml"); | |
Ok(file) | |
} | |
fn root_dir(&self) -> PathBuf { | |
self.root.as_ref().unwrap().clone() | |
} | |
fn config(&self) -> &config::Config { | |
self.config.as_ref().unwrap() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment