use serde_json::{Value, json}; use std::{ ffi::{OsStr, OsString}, path::{Path, PathBuf}, }; use vamp::{ repository, review::{Review, Store}, ruby, }; pub struct Request { id: Option, delete_comment: Option, repo: Option, json: bool, all: bool, } impl Request { pub fn parse(args: &mut impl Iterator) -> Result { let usage = "--review requires list, show ID, or delete REVIEW_ID COMMENT_ID"; let command = args.next().ok_or(usage)?; let id = if command == "list" { None } else if command == "show" || command == "delete" { let error = if command == "show" { "`show` requires a positive review ID.\nUsage: vamp --review show ID [--all]\n`--all` includes resolved comments; it does not select all reviews." } else { "`delete` requires a positive review ID.\nUsage: vamp --review delete REVIEW_ID COMMENT_ID" }; let id = args.next().ok_or(error)?; Some( id.to_str() .and_then(|id| id.parse::().ok()) .filter(|id| *id > 0) .ok_or(error)?, ) } else { return Err(usage.into()); }; let delete_comment = if command == "delete" { let error = "delete requires a positive comment ID"; let id = args.next().ok_or(error)?; Some( id.to_str() .and_then(|id| id.parse::().ok()) .filter(|id| *id > 0) .ok_or(error)?, ) } else { None }; Ok(Self { id, delete_comment, repo: None, json: false, all: false, }) } pub fn option( &mut self, arg: &OsStr, args: &mut impl Iterator, ) -> Result<(), String> { if arg == "--repo" && self.repo.is_none() { self.repo = Some(PathBuf::from(args.next().ok_or("--repo requires a path")?)); } else if arg == "--json" && !self.json { self.json = true; } else if arg == "--all" && self.id.is_some() && self.delete_comment.is_none() && !self.all { self.all = true; } else { return Err(format!( "unexpected review argument: {}", arg.to_string_lossy() )); } Ok(()) } } fn envelope(review: &Review, all: bool, comments: bool) -> Value { let resolved = review .comments .iter() .filter(|comment| comment.resolved) .count(); let mut value = json!({ "repository": review.root, "id": review.storage_id, "revision": review.storage_revision, "summary": review.summary, "verdict": review.verdict, "counts": { "total": review.comments.len(), "resolved": resolved, "unresolved": review.comments.len() - resolved }, }); if comments { value["comments"] = review.comments.iter().filter(|comment| all || !comment.resolved).map(|comment| { json!({"id": comment.id, "body": comment.body, "resolved": comment.resolved, "anchor": { "revision": comment.anchor.revision, "path": comment.anchor.path, "side": comment.anchor.side, "start": comment.anchor.start, "end": comment.anchor.end }, "excerpt": comment.anchor.excerpt()}) }).collect(); } value } pub fn run(request: &Request, config: Option<&Path>) -> Result<(), String> { let directory = config .map(ruby::load_config) .transpose()? .and_then(|config| config.review_directory) .map_or_else(vamp::review::default_directory, Ok)?; let cwd = std::env::current_dir().map_err(|error| error.to_string())?; let root = repository::discover_root(request.repo.as_deref().unwrap_or(&cwd))?; let store = Store::open_read_only(&directory)?; let reviews = if let Some(id) = request.id { let mut review = store .as_ref() .ok_or_else(|| format!( "Review ID {id} not found in {}.\nRun `vamp --review list` to find review IDs for this repository.", root.display() ))? .load(&root, id)?; if let Some(comment_id) = request.delete_comment { let index = review .comments .iter() .position(|comment| comment.id == comment_id) .ok_or_else(|| format!("Comment {comment_id} does not exist in review {id}"))?; review.comments.remove(index); Store::open(&directory)?.save(&mut review)?; } vec![review] } else { match store { Some(store) => store.list(&root)?, None => Vec::new(), } }; if request.json { let output = if request.id.is_some() { envelope(&reviews[0], request.all, true) } else { let reviews = reviews .iter() .map(|review| envelope(review, true, false)) .collect::>(); json!({"repository": root, "reviews": reviews}) }; println!( "{}", serde_json::to_string_pretty(&output).map_err(|error| error.to_string())? ); } else if request.id.is_some() { print!("{}", reviews[0].format_text(request.all)); } else { println!("Repository: {}", root.display()); if reviews.is_empty() { println!("No stored reviews."); } for review in &reviews { print!("{}", review.summary_text()); println!( "\nShow comments: vamp --review show {}", review.storage_id.unwrap() ); } if !reviews.is_empty() { println!("Include resolved comments: add --all"); } } Ok(()) } #[cfg(test)] #[path = "../tests/unit/review_cli.rs"] mod tests;