use clap::{Parser, Subcommand};
use std::io;
use thiserror::Error;
use vcsq_lib::plexer;
use vcsq_lib::repo::{Driver, DriverError, QueryDir};
#[derive(Parser, Debug)]
#[command(
name = "vcsq",
version,
about = "vcs queries in rust",
long_about = "vcsq is a rust CLI providing Version Control System (VCS) inspection, without you
needing to know each VCS's proprietary incantations."
)]
pub struct MainArgs {
#[arg(short, long)]
pub dir: Option<QueryDir>,
#[command(subcommand)]
pub query: Option<QueryCmd>,
}
#[derive(Error, Debug)]
enum CliError {
#[error("usage error: {0}")]
Usage(String),
#[error("vcs error: {0}")]
Plexing(#[from] DriverError),
#[error("{0}")]
Unknown(String),
}
impl MainArgs {
pub(self) fn reduce(&self) -> Result<QueryCmd, CliError> {
if let Some(q) = &self.query {
Ok(q.clone())
} else {
let dir = self
.dir
.clone()
.ok_or(CliError::Usage(
"require either subcmd with a query or a direct --dir".into(),
))?
.clone();
Ok(QueryCmd::Brand { dir })
}
}
}
#[derive(Debug, Subcommand, Clone)]
pub enum QueryCmd {
#[command(arg_required_else_help = true)]
Brand { dir: QueryDir },
#[command(arg_required_else_help = true)]
Root { dir: QueryDir },
#[command(arg_required_else_help = true)]
IsClean {
dir: QueryDir,
},
#[command(arg_required_else_help = true)]
CurrentId {
dir: QueryDir,
#[arg(long, default_value_t = false)]
dirty_ok: bool,
},
#[command(arg_required_else_help = true)]
#[cfg(debug_assertions)]
CurrentName {
dir: QueryDir,
#[arg(long, default_value_t = false)]
dirty_ok: bool,
},
#[command(arg_required_else_help = true)]
#[cfg(debug_assertions)]
ParentId { dir: QueryDir },
#[command(arg_required_else_help = true)]
#[cfg(debug_assertions)]
ParentName {
dir: QueryDir,
max: u64,
},
#[command(arg_required_else_help = true)]
TrackedFiles { dir: QueryDir },
#[command(arg_required_else_help = true)]
DirtyFiles {
dir: QueryDir,
#[arg(long, default_value_t = false)]
clean_ok: bool,
},
#[command(arg_required_else_help = true)]
#[cfg(debug_assertions)]
CurrentFiles {
dir: QueryDir,
dirty_ok: bool,
},
CheckHealth,
}
impl QueryCmd {
fn dir(&self) -> Option<QueryDir> {
self.dir_path().cloned()
}
fn dir_path(&self) -> Option<&QueryDir> {
match self {
QueryCmd::Brand { dir }
| QueryCmd::Root { dir }
| QueryCmd::IsClean { dir }
| QueryCmd::DirtyFiles { dir, clean_ok: _ }
| QueryCmd::TrackedFiles { dir }
| QueryCmd::CurrentId { dir, dirty_ok: _ } => Some(dir),
QueryCmd::CheckHealth => None,
#[cfg(debug_assertions)]
QueryCmd::CurrentName { dir, dirty_ok: _ }
| QueryCmd::ParentId { dir }
| QueryCmd::ParentName { dir, max: _ }
| QueryCmd::CurrentFiles { dir, dirty_ok: _ } => Some(dir),
}
}
}
struct PlexerQuery<'a> {
plexer: plexer::Repo,
cli: QueryCmd,
stdout: &'a mut dyn io::Write,
}
impl<'a> PlexerQuery<'a> {
fn new(
args: &'a MainArgs,
stdout: &'a mut dyn io::Write,
) -> Result<Option<PlexerQuery<'a>>, CliError> {
let query = args.reduce()?;
let Some(dir) = query.dir() else {
return Ok(None);
};
if !dir.is_dir() {
return Err(CliError::Usage(
"dir must be a readable directory".to_string(),
));
}
let plexer = plexer::Repo::new_driver(&dir)?;
Ok(Some(PlexerQuery {
plexer,
cli: query,
stdout,
}))
}
pub fn handle_query(&mut self) -> Result<u8, CliError> {
match self.cli {
QueryCmd::Brand { dir: _ } => {
writeln!(self.stdout, "{:?}", self.plexer.brand)
.unwrap_or_else(|_| panic!("failed stdout write of: {:?}", self.plexer.brand));
}
QueryCmd::Root { dir: _ } => {
let root_path = self.plexer.root()?;
let dir_path = root_path.as_path().to_str().ok_or_else(|| {
CliError::Unknown(format!("vcs generated invalid unicode: {root_path:?}"))
})?;
writeln!(self.stdout, "{dir_path}")
.unwrap_or_else(|_| panic!("failed stdout write of: {dir_path}"));
}
QueryCmd::IsClean { dir: _ } => {
let is_clean = self.plexer.is_clean().map_err(CliError::Plexing)?;
return Ok(u8::from(!is_clean));
}
QueryCmd::CheckHealth => panic!("bug: PlexerQuery() should not be constructed for the generalized CheckHealth query"),
QueryCmd::CurrentId {
dir: _,
dirty_ok,
} => {
let current_id = self.plexer.current_ref_id(dirty_ok)?;
writeln!(self.stdout, "{current_id}").unwrap_or_else(|_| {
panic!("failed stdout write of: {current_id}")
});
},
#[cfg(debug_assertions)]
QueryCmd::CurrentName {
dir: _,
dirty_ok: _,
}
| QueryCmd::ParentId { dir: _ }
| QueryCmd::ParentName { dir: _, max: _ }
| QueryCmd::CurrentFiles {
dir: _,
dirty_ok: _,
} => todo!(),
QueryCmd::DirtyFiles { dir: _, clean_ok } => {
let files = self
.plexer
.dirty_files(clean_ok)
.map_err(CliError::Plexing)?;
for file in files {
writeln!(self.stdout, "{}", file.display()).unwrap_or_else(|_| {
panic!("failed stdout write of: {}", file.display())
});
}
}
QueryCmd::TrackedFiles { dir: _ } => {
let files = self
.plexer
.tracked_files()
.map_err(CliError::Plexing)?;
for file in files {
writeln!(self.stdout, "{}", file.display()).unwrap_or_else(|_| {
panic!("failed stdout write of: {}", file.display())
});
}
}
}
Ok(0)
}
}
pub fn main_vcsquery(
args: &MainArgs,
stdout: &mut dyn io::Write,
stderr: &mut dyn io::Write,
) -> u8 {
let plexerq = match PlexerQuery::new(args, stdout) {
Ok(pq) => pq,
Err(e) => {
writeln!(stderr, "{e}").unwrap_or_else(|_| panic!("failed stderr write of: {e}"));
return 1;
}
};
if let Some(mut pq) = plexerq {
return match pq.handle_query() {
Ok(ret) => ret,
Err(e) => {
writeln!(stderr, "{e}").unwrap_or_else(|_| panic!("failed stderr write of: {e}"));
1
}
};
}
let mut has_fail = false;
for report in plexer::check_health() {
let message = match &report.health {
Ok(h) => h.stdout.clone(),
Err(e) => e.to_string(),
};
if report.health.is_err() {
writeln!(stderr, "FAIL: check for {:?}:\n{}", report.brand, message)
.unwrap_or_else(|e| panic!("failed stderr write: {e}"));
has_fail = true;
} else {
writeln!(stdout, "PASS: check for {:?}:\n{}", report.brand, message)
.unwrap_or_else(|e| panic!("failed stderr write: {e}"));
}
}
u8::from(has_fail)
}