1use clap::{Parser, Subcommand};
8use std::io;
9use thiserror::Error;
10use vcsq_lib::plexer;
11use vcsq_lib::repo::{Driver, DriverError, QueryDir};
12
13#[derive(Parser, Debug)]
15#[command(
16 name = "vcsq",
17 version,
18 about = "vcs queries in rust",
19 long_about = "vcsq is a rust CLI providing Version Control System (VCS) inspection, without you
20needing to know each VCS's proprietary incantations."
21)]
22pub struct MainArgs {
23 #[arg(short, long)]
25 pub dir: Option<QueryDir>,
26
27 #[command(subcommand)]
28 pub query: Option<QueryCmd>,
29}
30
31#[derive(Error, Debug)]
32enum CliError {
33 #[error("usage error: {0}")]
34 Usage(String),
35
36 #[error("vcs error: {0}")]
37 Plexing(#[from] DriverError),
38
39 #[error("{0}")]
40 Unknown(String),
41}
42
43impl MainArgs {
44 pub(self) fn reduce(&self) -> Result<QueryCmd, CliError> {
49 if let Some(q) = &self.query {
50 Ok(q.clone())
51 } else {
52 let dir = self
53 .dir
54 .clone()
55 .ok_or(CliError::Usage(
56 "require either subcmd with a query or a direct --dir".into(),
57 ))?
58 .clone();
59 Ok(QueryCmd::Brand { dir })
60 }
61 }
62}
63
64#[derive(Debug, Subcommand, Clone)]
70pub enum QueryCmd {
71 #[command(arg_required_else_help = true)]
73 Brand { dir: QueryDir },
74
75 #[command(arg_required_else_help = true)]
77 Root { dir: QueryDir },
78
79 #[command(arg_required_else_help = true)]
81 IsClean {
82 dir: QueryDir,
84 },
85
86 #[command(arg_required_else_help = true)]
88 CurrentId {
89 dir: QueryDir,
90
91 #[arg(long, default_value_t = false)]
93 dirty_ok: bool,
94 },
95
96 #[command(arg_required_else_help = true)]
99 #[cfg(debug_assertions)]
100 CurrentName {
101 dir: QueryDir,
102
103 #[arg(long, default_value_t = false)]
105 dirty_ok: bool,
106 },
107
108 #[command(arg_required_else_help = true)]
111 #[cfg(debug_assertions)]
112 ParentId { dir: QueryDir },
113
114 #[command(arg_required_else_help = true)]
118 #[cfg(debug_assertions)]
119 ParentName {
120 dir: QueryDir,
121
122 max: u64,
125 },
126
127 #[command(arg_required_else_help = true)]
131 TrackedFiles { dir: QueryDir },
132
133 #[command(arg_required_else_help = true)]
136 DirtyFiles {
137 dir: QueryDir,
138 #[arg(long, default_value_t = false)]
139 clean_ok: bool,
140 },
143
144 #[command(arg_required_else_help = true)]
146 #[cfg(debug_assertions)]
147 CurrentFiles {
148 dir: QueryDir,
149
150 dirty_ok: bool,
152 },
158
159 CheckHealth,
162}
163
164impl QueryCmd {
165 fn dir(&self) -> Option<QueryDir> {
166 self.dir_path().cloned()
167 }
168
169 fn dir_path(&self) -> Option<&QueryDir> {
172 match self {
173 QueryCmd::Brand { dir }
174 | QueryCmd::Root { dir }
175 | QueryCmd::IsClean { dir }
176 | QueryCmd::DirtyFiles { dir, clean_ok: _ }
177 | QueryCmd::TrackedFiles { dir }
178 | QueryCmd::CurrentId { dir, dirty_ok: _ } => Some(dir),
179 QueryCmd::CheckHealth => None,
180 #[cfg(debug_assertions)]
181 QueryCmd::CurrentName { dir, dirty_ok: _ }
182 | QueryCmd::ParentId { dir }
183 | QueryCmd::ParentName { dir, max: _ }
184 | QueryCmd::CurrentFiles { dir, dirty_ok: _ } => Some(dir),
185 }
186 }
187}
188
189struct PlexerQuery<'a> {
190 plexer: plexer::Repo,
191 cli: QueryCmd,
192 stdout: &'a mut dyn io::Write,
193}
194
195impl<'a> PlexerQuery<'a> {
196 fn new(
197 args: &'a MainArgs,
198 stdout: &'a mut dyn io::Write,
199 ) -> Result<Option<PlexerQuery<'a>>, CliError> {
200 let query = args.reduce()?;
201 let Some(dir) = query.dir() else {
202 return Ok(None);
203 };
204 if !dir.is_dir() {
205 return Err(CliError::Usage(
206 "dir must be a readable directory".to_string(),
207 ));
208 }
209 let plexer = plexer::Repo::new_driver(&dir)?;
210 Ok(Some(PlexerQuery {
211 plexer,
212 cli: query,
213 stdout,
214 }))
215 }
216
217 pub fn handle_query(&mut self) -> Result<u8, CliError> {
218 match self.cli {
219 QueryCmd::Brand { dir: _ } => {
220 writeln!(self.stdout, "{:?}", self.plexer.brand)
221 .unwrap_or_else(|_| panic!("failed stdout write of: {:?}", self.plexer.brand));
222 }
223 QueryCmd::Root { dir: _ } => {
224 let root_path = self.plexer.root()?;
225 let dir_path = root_path.as_path().to_str().ok_or_else(|| {
226 CliError::Unknown(format!("vcs generated invalid unicode: {root_path:?}"))
227 })?;
228 writeln!(self.stdout, "{dir_path}")
229 .unwrap_or_else(|_| panic!("failed stdout write of: {dir_path}"));
230 }
231 QueryCmd::IsClean { dir: _ } => {
232 let is_clean = self.plexer.is_clean().map_err(CliError::Plexing)?;
233 return Ok(u8::from(!is_clean));
234 }
235 QueryCmd::CheckHealth => panic!("bug: PlexerQuery() should not be constructed for the generalized CheckHealth query"),
236 QueryCmd::CurrentId {
237 dir: _,
238 dirty_ok,
239 } => {
240 let current_id = self.plexer.current_ref_id(dirty_ok)?;
241 writeln!(self.stdout, "{current_id}").unwrap_or_else(|_| {
242 panic!("failed stdout write of: {current_id}")
243 });
244 },
245 #[cfg(debug_assertions)]
246 QueryCmd::CurrentName {
247 dir: _,
248 dirty_ok: _,
249 }
250 | QueryCmd::ParentId { dir: _ }
251 | QueryCmd::ParentName { dir: _, max: _ }
252 | QueryCmd::CurrentFiles {
253 dir: _,
254 dirty_ok: _,
255 } => todo!(),
256 QueryCmd::DirtyFiles { dir: _, clean_ok } => {
257 let files = self
258 .plexer
259 .dirty_files(clean_ok)
260 .map_err(CliError::Plexing)?;
261 for file in files {
262 writeln!(self.stdout, "{}", file.display()).unwrap_or_else(|_| {
263 panic!("failed stdout write of: {}", file.display())
264 });
265 }
266 }
267 QueryCmd::TrackedFiles { dir: _ } => {
268 let files = self
269 .plexer
270 .tracked_files()
271 .map_err(CliError::Plexing)?;
272 for file in files {
273 writeln!(self.stdout, "{}", file.display()).unwrap_or_else(|_| {
274 panic!("failed stdout write of: {}", file.display())
275 });
276 }
277 }
278 }
279 Ok(0)
280 }
281}
282
283pub fn main_vcsquery(
293 args: &MainArgs,
294 stdout: &mut dyn io::Write,
295 stderr: &mut dyn io::Write,
296) -> u8 {
297 let plexerq = match PlexerQuery::new(args, stdout) {
298 Ok(pq) => pq,
299 Err(e) => {
300 writeln!(stderr, "{e}").unwrap_or_else(|_| panic!("failed stderr write of: {e}"));
301 return 1;
302 }
303 };
304 if let Some(mut pq) = plexerq {
305 return match pq.handle_query() {
306 Ok(ret) => ret,
307 Err(e) => {
308 writeln!(stderr, "{e}").unwrap_or_else(|_| panic!("failed stderr write of: {e}"));
309 1
310 }
311 };
312 }
313
314 let mut has_fail = false;
315 for report in plexer::check_health() {
316 let message = match &report.health {
317 Ok(h) => h.stdout.clone(),
318 Err(e) => e.to_string(),
319 };
320 if report.health.is_err() {
321 writeln!(stderr, "FAIL: check for {:?}:\n{}", report.brand, message)
322 .unwrap_or_else(|e| panic!("failed stderr write: {e}"));
323 has_fail = true;
324 } else {
325 writeln!(stdout, "PASS: check for {:?}:\n{}", report.brand, message)
326 .unwrap_or_else(|e| panic!("failed stderr write: {e}"));
327 }
328 }
329 u8::from(has_fail)
330}
331
332