Skip to content

Instantly share code, notes, and snippets.

@autarch
Created March 14, 2019 20:29
Show Gist options
  • Save autarch/b2712902574c51373126974e8985841a to your computer and use it in GitHub Desktop.
Save autarch/b2712902574c51373126974e8985841a to your computer and use it in GitHub Desktop.
// 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