Files
vamp/src/review_cli.rs
T
2026-10-20 16:28:53 +02:00

181 lines
6.0 KiB
Rust

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<i64>,
delete_comment: Option<u64>,
repo: Option<PathBuf>,
json: bool,
all: bool,
}
impl Request {
pub fn parse(args: &mut impl Iterator<Item = OsString>) -> Result<Self, String> {
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::<i64>().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::<u64>().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<Item = OsString>,
) -> 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::<Vec<_>>();
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;