Radish alpha
r
Radicle terminal user interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
bin: Layout basic queue logic for patch review
Erik Kundt committed 1 year ago
commit d4e1c3a0b58fd75a09ef79e041af1415c9213fa5
parent f461bbbd48f4f691ae17d9673135ed15a6026abc
4 files changed +119 -26
modified bin/cob/patch.rs
@@ -80,7 +80,6 @@ pub fn all(profile: &Profile, repository: &Repository) -> Result<Vec<(PatchId, P
    Ok(patches.flatten().collect())
}

-
#[allow(dead_code)]
pub fn find(profile: &Profile, repository: &Repository, id: &PatchId) -> Result<Option<Patch>> {
    let cache = profile.patches(repository)?;
    Ok(cache.get(id)?)
modified 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<Rev>,
+
}
+

impl Args for Options {
    fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
        use lexopt::prelude::*;
@@ -80,6 +91,8 @@ impl Args for Options {
        let mut op: Option<OperationName> = 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::<radicle::git::Oid>(&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 {
modified 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,
        }
    }

modified 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<ReviewQueue> {
+
        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<'_>,