diff --git a/bin/cob/patch.rs b/bin/cob/patch.rs index 7a0fde31..9b445e77 100644 --- a/bin/cob/patch.rs +++ b/bin/cob/patch.rs @@ -80,7 +80,6 @@ pub fn all(profile: &Profile, repository: &Repository) -> Result Result> { let cache = profile.patches(repository)?; Ok(cache.get(id)?) diff --git a/bin/commands/patch.rs b/bin/commands/patch.rs index b9bb1496..29c17ed4 100644 --- a/bin/commands/patch.rs +++ b/bin/commands/patch.rs @@ -9,16 +9,21 @@ use std::ffi::OsString; use anyhow::anyhow; +use radicle::crypto::Signer; use radicle::identity::RepoId; use radicle::patch::Status; +use radicle::storage::WriteRepository; +use radicle_cli::git::Rev; use radicle_cli::terminal; -use radicle_cli::terminal::args::{Args, Error, Help}; +use radicle_cli::terminal::args::{string, Args, Error, Help}; use crate::cob::patch; use crate::cob::patch::Filter; use crate::commands::tui_patch::review::ReviewAction; +use crate::tui_patch::review::builder::{Brain, ReviewBuilder}; + pub const HELP: Help = Help { name: "patch", description: "Terminal interfaces for patches", @@ -56,14 +61,14 @@ pub struct Options { } pub enum Operation { - Review, Select { opts: SelectOptions }, + Review { opts: ReviewOptions }, } #[derive(PartialEq, Eq)] pub enum OperationName { - Review, Select, + Review, } #[derive(Debug, Default, Clone, PartialEq, Eq)] @@ -72,6 +77,12 @@ pub struct SelectOptions { filter: patch::Filter, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ReviewOptions { + patch_id: Rev, + revision_id: Option, +} + impl Args for Options { fn from_args(args: Vec) -> anyhow::Result<(Self, Vec)> { use lexopt::prelude::*; @@ -80,6 +91,8 @@ impl Args for Options { let mut op: Option = None; let mut repo = None; let mut select_opts = SelectOptions::default(); + let mut patch_id = None; + let mut revision_id = None; while let Some(arg) = parser.next()? { match arg { @@ -121,19 +134,27 @@ impl Args for Options { .filter .with_author(terminal::args::did(&parser.value()?)?); } - Long("repo") => { let val = parser.value()?; let rid = terminal::args::rid(&val)?; repo = Some(rid); } + Long("revision") => { + let val = parser.value()?; + let rev_id = terminal::args::rev(&val)?; + revision_id = Some(rev_id); + } Value(val) if op.is_none() => match val.to_string_lossy().as_ref() { "select" => op = Some(OperationName::Select), "review" => op = Some(OperationName::Review), unknown => anyhow::bail!("unknown operation '{}'", unknown), }, + Value(val) if patch_id.is_none() => { + let val = string(&val); + patch_id = Some(Rev::from(val)); + } _ => return Err(anyhow!(arg.unexpected())), } } @@ -143,7 +164,12 @@ impl Args for Options { } let op = match op.ok_or_else(|| anyhow!("an operation must be provided"))? { - OperationName::Review => Operation::Review, + OperationName::Review => Operation::Review { + opts: ReviewOptions { + patch_id: patch_id.ok_or_else(|| anyhow!("a patch must be provided"))?, + revision_id: revision_id, + }, + }, OperationName::Select => Operation::Select { opts: select_opts }, }; Ok((Options { op, repo }, vec![])) @@ -185,38 +211,68 @@ pub async fn run(options: Options, ctx: impl terminal::Context) -> anyhow::Resul eprint!("{output}"); } - Operation::Review => { - let profile = ctx.profile()?; - let rid = options.repo.unwrap_or(rid); - let repository = profile.storage.repository(rid).unwrap(); - + Operation::Review { opts } => { if let Err(err) = crate::log::enable() { println!("{}", err); } - log::info!("Starting patch review interface in project {}..", rid); + log::info!("Starting patch review interface in project {rid}.."); - let mut queue = vec![0, 1, 2]; + let profile = ctx.profile()?; + let signer = terminal::signer(&profile)?; + let rid = options.repo.unwrap_or(rid); + let repo = profile.storage.repository(rid).unwrap(); + + // Load patch + let patch_id = opts.patch_id.resolve(&repo.backend)?; + let patch = patch::find(&profile, &repo, &patch_id)? + .ok_or_else(|| anyhow!("Patch `{patch_id}` not found"))?; + + // Load revision + let revision_id = opts + .revision_id + .map(|rev| rev.resolve::(&repo.backend)) + .transpose()? + .map(radicle::cob::patch::RevisionId::from); + let (_revision_id, revision) = match revision_id { + Some(id) => ( + id, + patch + .revision(&id) + .ok_or_else(|| anyhow!("Patch revision `{id}` not found"))?, + ), + None => patch.latest(), + }; + + let brain = if let Ok(b) = Brain::load(patch_id, signer.public_key(), repo.raw()) { + log::info!( + "Loaded existing review {} for patch {}", + b.head().id(), + &patch_id + ); + b + } else { + let base = repo.raw().find_commit((*revision.base()).into())?; + Brain::new(patch_id, signer.public_key(), base, repo.raw())? + }; + + let queue = ReviewBuilder::new(patch_id, signer, &repo).queue(&brain, &revision)?; while !queue.is_empty() { - let selection = review::Tui::new(&profile, &repository).run().await?; + let selection = review::Tui::new(&profile, &repo, &queue).run().await?; log::info!("Received selection from TUI: {:?}", selection); if let Some(selection) = selection.as_ref() { match ReviewAction::try_from(selection.action)? { ReviewAction::Accept => { // brain accept - queue.pop(); } ReviewAction::Ignore => { // next hunk - queue.pop(); } ReviewAction::Comment => { radicle_cli::terminal::Editor::new() .extension("diff") .edit(String::new())?; - - queue.pop(); } } } else { diff --git a/bin/commands/patch/review.rs b/bin/commands/patch/review.rs index ad55b9af..d3ba1b65 100644 --- a/bin/commands/patch/review.rs +++ b/bin/commands/patch/review.rs @@ -1,5 +1,5 @@ #[path = "review/builder.rs"] -mod builder; +pub mod builder; use anyhow::Result; @@ -18,6 +18,8 @@ use tui::ui::im::widget::{TextViewState, Window}; use tui::ui::im::{Borders, Context, Show}; use tui::{Channel, Exit}; +use self::builder::ReviewQueue; + /// The actions that a user can carry out on a review item. #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum ReviewAction { @@ -62,13 +64,15 @@ pub struct Selection { pub struct Tui<'a> { pub _profile: &'a Profile, pub _repository: &'a Repository, + pub queue: &'a ReviewQueue, } impl<'a> Tui<'a> { - pub fn new(profile: &'a Profile, repository: &'a Repository) -> Self { + pub fn new(profile: &'a Profile, repository: &'a Repository, queue: &'a ReviewQueue) -> Self { Self { _profile: profile, _repository: repository, + queue, } } diff --git a/bin/commands/patch/review/builder.rs b/bin/commands/patch/review/builder.rs index 20ca4319..3fa8f4d7 100644 --- a/bin/commands/patch/review/builder.rs +++ b/bin/commands/patch/review/builder.rs @@ -486,7 +486,7 @@ pub struct Brain<'a> { impl<'a> Brain<'a> { /// Create a new brain in the repository. - fn new( + pub fn new( patch: PatchId, remote: &NodeId, base: git::raw::Commit, @@ -515,12 +515,12 @@ impl<'a> Brain<'a> { /// Return the content identifier of this brain. This represents the state of the /// accepted hunks, ie. the git tree. - fn cid(&self) -> Oid { + pub fn cid(&self) -> Oid { self.accepted.id().into() } /// Load an existing brain from the repository. - fn load( + pub fn load( patch: PatchId, remote: &NodeId, repo: &'a git::raw::Repository, @@ -539,7 +539,7 @@ impl<'a> Brain<'a> { } /// Accept changes to the brain. - fn accept( + pub fn accept( &mut self, diff: git::raw::Diff, repo: &'a git::raw::Repository, @@ -563,9 +563,17 @@ impl<'a> Brain<'a> { } /// Get the brain's refname given the patch and remote. - fn refname(patch: &PatchId, remote: &NodeId) -> git::Namespaced<'a> { + pub fn refname(patch: &PatchId, remote: &NodeId) -> git::Namespaced<'a> { git::refs::storage::draft::review(remote, patch) } + + pub fn head(&self) -> &git::raw::Commit<'a> { + &self.head + } + + pub fn accepted(&self) -> &git::raw::Tree<'a> { + &self.accepted + } } /// Builds a patch review interactively, across multiple files. @@ -606,6 +614,32 @@ impl<'a, G: Signer> ReviewBuilder<'a, G> { self } + /// Assemble the review for the given revision. + pub fn queue(&self, brain: &'a Brain<'a>, revision: &Revision) -> anyhow::Result { + let repo = self.repo.raw(); + let signer = &self.signer; + let patch_id = self.patch_id; + let base = repo.find_commit((*revision.base()).into())?; + let tree = { + let commit = repo.find_commit(revision.head().into())?; + commit.tree()? + }; + + let mut opts = git::raw::DiffOptions::new(); + opts.patience(true).minimal(true).context_lines(3_u32); + + let diff = self.diff(&brain.accepted(), &tree, repo, &mut opts)?; + let drafts = DraftStore::new(*signer.public_key(), self.repo).with( + signer.public_key(), + &cob::patch::TYPENAME, + &patch_id, + )?; + let mut patches = cob::patch::Cache::no_cache(&drafts)?; + let mut patch = patches.get_mut(&patch_id)?; + + Ok(ReviewQueue::from(diff)) + } + /// Run the review builder for the given revision. pub fn run(self, revision: &Revision, opts: &mut git::raw::DiffOptions) -> anyhow::Result<()> { let repo = self.repo.raw(); @@ -774,7 +808,7 @@ impl<'a, G: Signer> ReviewBuilder<'a, G> { Ok(()) } - fn diff( + pub fn diff( &self, brain: &git::raw::Tree<'_>, tree: &git::raw::Tree<'_>,