181 lines
6.0 KiB
Rust
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;
|