Radish alpha
r
Radicle terminal user interface
Radicle
Git (anonymous pull)
Log in to clone via SSH
lib: Add patch filter
Erik Kundt committed 2 years ago
commit 5e9e6140edc8cce6c35830158fb5cf75b87b9b92
parent 86b0d4f0c27a11ecd85a805ca2b40907ecc74f88
11 files changed +140 -43
modified bin/commands/patch.rs
@@ -9,7 +9,9 @@ use std::ffi::OsString;

use anyhow::anyhow;

-
use radicle_tui::{context, log, Window};
+
use radicle_tui as tui;
+
use tui::cob::patch;
+
use tui::{context, log, Window};

use crate::terminal;
use crate::terminal::args::{Args, Error, Help};
@@ -53,14 +55,15 @@ pub enum OperationName {
    Select,
}

-
#[derive(Debug, Default, Copy, Clone, PartialEq, Eq)]
+
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct SelectOptions {
    subject: select::Subject,
+
    filter: patch::Filter,
}

impl SelectOptions {
-
    pub fn new(subject: select::Subject) -> Self {
-
        Self { subject }
+
    pub fn new(subject: select::Subject, filter: patch::Filter) -> Self {
+
        Self { subject, filter }
    }
}

@@ -70,6 +73,7 @@ impl Args for Options {

        let mut parser = lexopt::Parser::from_args(args);
        let mut op: Option<OperationName> = None;
+
        let filter = patch::Filter::default();
        let mut json = false;
        let mut select_opts: Option<SelectOptions> = None;

@@ -88,13 +92,16 @@ impl Args for Options {
                    if select_opts.is_some() {
                        anyhow::bail!("select option already given")
                    }
-
                    select_opts = Some(SelectOptions::new(select::Subject::Operation));
+
                    select_opts = Some(SelectOptions::new(
+
                        select::Subject::Operation,
+
                        filter.clone(),
+
                    ));
                }
                Long("id") | Short('i') if op == Some(OperationName::Select) => {
                    if select_opts.is_some() {
                        anyhow::bail!("select option already given")
                    }
-
                    select_opts = Some(SelectOptions::new(select::Subject::Id));
+
                    select_opts = Some(SelectOptions::new(select::Subject::Id, filter.clone()));
                }

                Value(val) if op.is_none() => match val.to_string_lossy().as_ref() {
@@ -119,13 +126,13 @@ pub fn run(options: Options, _ctx: impl terminal::Context) -> anyhow::Result<()>
        .map_err(|_| anyhow!("this command must be run in the context of a project"))?;

    match options.op {
-
        Operation::Select { opts } => {
+
        Operation::Select { ref opts } => {
            let profile = terminal::profile()?;
            let context = context::Context::new(profile, id)?.with_patches();

            log::enable(context.profile(), "patch", "select")?;

-
            let mut app = select::App::new(context, opts.subject);
+
            let mut app = select::App::new(context, opts.subject.clone(), opts.filter.clone());
            let output = Window::default().run(&mut app, 1000 / FPS)?;

            let output = if options.json {
@@ -134,7 +141,7 @@ pub fn run(options: Options, _ctx: impl terminal::Context) -> anyhow::Result<()>
                    .unwrap_or_default()
            } else {
                match options.op {
-
                    Operation::Select { opts } => match opts.subject {
+
                    Operation::Select { ref opts } => match &opts.subject {
                        select::Subject::Id => output.map(|o| format!("{}", o)).unwrap_or_default(),
                        select::Subject::Operation => output
                            .map(|o| format!("rad patch {}", o))
modified bin/commands/patch/common/ui.rs
@@ -7,6 +7,7 @@ use tuirealm::{AttrValue, Attribute, Frame, MockComponent, Props, State};

use radicle_tui as tui;

+
use tui::cob::patch::Filter;
use tui::context::Context;
use tui::ui::cob::PatchItem;
use tui::ui::theme::{style, Theme};
@@ -21,7 +22,12 @@ pub struct PatchBrowser {
}

impl PatchBrowser {
-
    pub fn new(context: &Context, theme: &Theme, selected: Option<(PatchId, Patch)>) -> Self {
+
    pub fn new(
+
        theme: &Theme,
+
        context: &Context,
+
        filter: Filter,
+
        selected: Option<(PatchId, Patch)>,
+
    ) -> Self {
        let header = [
            label::header(" ● "),
            label::header("ID"),
@@ -45,9 +51,14 @@ impl PatchBrowser {
        ];

        let repo = context.repository();
-
        let patches = context.patches().as_ref().unwrap();
-
        let mut items = vec![];
+
        let patches = context
+
            .patches()
+
            .as_ref()
+
            .unwrap()
+
            .iter()
+
            .filter(|(_, patch)| filter.matches(patch));

+
        let mut items = vec![];
        for (id, patch) in patches {
            if let Ok(item) = PatchItem::try_from((context.profile(), repo, *id, patch.clone())) {
                items.push(item);
modified bin/commands/patch/select.rs
@@ -17,7 +17,9 @@ use tuirealm::{Application, Frame, NoUserEvent, Sub, SubClause};

use radicle_tui as tui;

+
use tui::cob::patch::Filter;
use tui::context::Context;
+

use tui::ui::subscription;
use tui::ui::theme::Theme;
use tui::{Exit, PageStack, Tui};
@@ -54,7 +56,7 @@ impl Serialize for PatchId {
/// which widgets to render and which output to produce.
///
/// Depends on CLI arguments given by the user.
-
#[derive(Clone, Default, Copy, Debug, Eq, PartialEq)]
+
#[derive(Clone, Default, Debug, Eq, PartialEq)]
pub enum Subject {
    #[default]
    Operation,
@@ -151,19 +153,21 @@ pub struct App {
    theme: Theme,
    quit: bool,
    subject: Subject,
+
    filter: Filter,
    output: Option<Output>,
}

/// Creates a new application using a tui-realm-application, mounts all
/// components and sets focus to a default one.
impl App {
-
    pub fn new(context: Context, subject: Subject) -> Self {
+
    pub fn new(context: Context, subject: Subject, filter: Filter) -> Self {
        Self {
            context,
            pages: PageStack::default(),
            theme: Theme::default(),
            quit: false,
            subject,
+
            filter,
            output: None,
        }
    }
@@ -173,7 +177,7 @@ impl App {
        app: &mut Application<Cid, Message, NoUserEvent>,
        theme: &Theme,
    ) -> Result<()> {
-
        let home = Box::new(ListView::new(self.subject));
+
        let home = Box::new(ListView::new(self.subject.clone(), self.filter.clone()));
        self.pages.push(home, app, &self.context, theme)?;

        Ok(())
modified bin/commands/patch/select/page.rs
@@ -6,6 +6,7 @@ use tuirealm::{AttrValue, Attribute, Frame, NoUserEvent, State, StateValue, Sub,

use radicle_tui as tui;

+
use tui::cob::patch::Filter;
use tui::context::Context;
use tui::ui::theme::Theme;
use tui::ui::widget::context::{Progress, Shortcuts};
@@ -22,14 +23,16 @@ use super::{ui, Application, Cid, ListCid, Message, Subject};
pub struct ListView {
    active_component: ListCid,
    subject: Subject,
+
    filter: Filter,
    shortcuts: HashMap<ListCid, Widget<Shortcuts>>,
}

impl ListView {
-
    pub fn new(subject: Subject) -> Self {
+
    pub fn new(subject: Subject, filter: Filter) -> Self {
        Self {
            active_component: ListCid::PatchBrowser,
            subject,
+
            filter,
            shortcuts: HashMap::default(),
        }
    }
@@ -85,13 +88,15 @@ impl ViewPage<Cid, Message> for ListView {

        match self.subject {
            Subject::Id => {
-
                let patch_browser = ui::id_select(context, theme, None).to_boxed();
+
                let patch_browser =
+
                    ui::id_select(theme, context, self.filter.clone(), None).to_boxed();
                self.shortcuts = patch_browser.as_ref().shortcuts();

                app.remount(Cid::List(ListCid::PatchBrowser), patch_browser, vec![])?;
            }
            Subject::Operation => {
-
                let patch_browser = ui::operation_select(context, theme, None).to_boxed();
+
                let patch_browser =
+
                    ui::operation_select(theme, context, self.filter.clone(), None).to_boxed();
                self.shortcuts = patch_browser.as_ref().shortcuts();

                app.remount(Cid::List(ListCid::PatchBrowser), patch_browser, vec![])?;
modified bin/commands/patch/select/ui.rs
@@ -4,6 +4,7 @@ use radicle::cob::patch::{Patch, PatchId};

use radicle_tui as tui;

+
use tui::cob::patch::Filter;
use tui::context::Context;
use tui::ui::cob::PatchItem;
use tui::ui::theme::{style, Theme};
@@ -131,21 +132,27 @@ pub fn list_navigation(theme: &Theme) -> Widget<Tabs> {
}

pub fn id_select(
-
    context: &Context,
    theme: &Theme,
+
    context: &Context,
+
    filter: Filter,
    selected: Option<(PatchId, Patch)>,
) -> Widget<IdSelect> {
-
    let browser = Widget::new(common::ui::PatchBrowser::new(context, theme, selected));
+
    let browser = Widget::new(common::ui::PatchBrowser::new(
+
        theme, context, filter, selected,
+
    ));

    Widget::new(IdSelect::new(theme.clone(), browser))
}

pub fn operation_select(
-
    context: &Context,
    theme: &Theme,
+
    context: &Context,
+
    filter: Filter,
    selected: Option<(PatchId, Patch)>,
) -> Widget<OperationSelect> {
-
    let browser = Widget::new(common::ui::PatchBrowser::new(context, theme, selected));
+
    let browser = Widget::new(common::ui::PatchBrowser::new(
+
        theme, context, filter, selected,
+
    ));

    Widget::new(OperationSelect::new(theme.clone(), browser))
}
modified bin/commands/patch/suite.rs
@@ -18,6 +18,7 @@ use tuirealm::{Application, Frame, NoUserEvent, Sub, SubClause};
use radicle_tui as tui;

use tui::cob;
+
use tui::cob::patch::Filter;
use tui::context::Context;
use tui::ui::subscription;
use tui::ui::theme::Theme;
@@ -83,6 +84,7 @@ pub struct App {
    context: Context,
    pages: PageStack<Cid, Message>,
    theme: Theme,
+
    filter: Filter,
    quit: bool,
}

@@ -90,11 +92,12 @@ pub struct App {
/// components and sets focus to a default one.
#[allow(dead_code)]
impl App {
-
    pub fn new(context: Context) -> Self {
+
    pub fn new(context: Context, filter: Filter) -> Self {
        Self {
            context,
            pages: PageStack::default(),
            theme: Theme::default(),
+
            filter,
            quit: false,
        }
    }
@@ -104,7 +107,7 @@ impl App {
        app: &mut Application<Cid, Message, NoUserEvent>,
        theme: &Theme,
    ) -> Result<()> {
-
        let home = Box::new(ListView::new(theme.clone()));
+
        let home = Box::new(ListView::new(theme.clone(), self.filter.clone()));
        self.pages.push(home, app, &self.context, theme)?;

        Ok(())
modified bin/commands/patch/suite/page.rs
@@ -8,6 +8,7 @@ use tuirealm::{AttrValue, Attribute, Frame, NoUserEvent, State, StateValue, Sub,

use radicle_tui as tui;

+
use tui::cob::patch::Filter;
use tui::context::Context;
use tui::ui::theme::Theme;
use tui::ui::widget::context::{Progress, Shortcuts};
@@ -23,14 +24,16 @@ use super::{ui, Application, Cid, ListCid, Message, PatchCid};
pub struct ListView {
    active_component: ListCid,
    shortcuts: HashMap<ListCid, Widget<Shortcuts>>,
+
    filter: Filter,
}

impl ListView {
-
    pub fn new(theme: Theme) -> Self {
+
    pub fn new(theme: Theme, filter: Filter) -> Self {
        let shortcuts = Self::build_shortcuts(&theme);
        Self {
            active_component: ListCid::PatchBrowser,
            shortcuts,
+
            filter,
        }
    }

@@ -97,7 +100,7 @@ impl ViewPage<Cid, Message> for ListView {
    ) -> Result<()> {
        let navigation = ui::list_navigation(theme);
        let header = tui::ui::app_header(context, theme, Some(navigation)).to_boxed();
-
        let patch_browser = ui::patches(context, theme, None).to_boxed();
+
        let patch_browser = ui::patches(theme, context, self.filter.clone(), None).to_boxed();

        app.remount(Cid::List(ListCid::Header), header, vec![])?;
        app.remount(Cid::List(ListCid::PatchBrowser), patch_browser, vec![])?;
modified bin/commands/patch/suite/ui.rs
@@ -7,6 +7,7 @@ use tuirealm::{AttrValue, Attribute, Frame, MockComponent, Props, State};

use radicle_tui as tui;

+
use tui::cob::patch::Filter;
use tui::context::Context;
use tui::ui::cob;
use tui::ui::layout;
@@ -99,11 +100,14 @@ pub fn navigation(theme: &Theme) -> Widget<Tabs> {
}

pub fn patches(
-
    context: &Context,
    theme: &Theme,
+
    context: &Context,
+
    filter: Filter,
    selected: Option<(PatchId, Patch)>,
) -> Widget<common::ui::PatchBrowser> {
-
    Widget::new(common::ui::PatchBrowser::new(context, theme, selected))
+
    Widget::new(common::ui::PatchBrowser::new(
+
        theme, context, filter, selected,
+
    ))
}

pub fn activity(_theme: &Theme) -> Widget<Activity> {
modified bin/terminal/args.rs
@@ -4,8 +4,10 @@ use std::str::FromStr;
use anyhow::anyhow;

use radicle::cob::{issue, patch};
+
use radicle::crypto;
+
use radicle::identity::Did;

-
/// Git revision parameter. Supports extended SHA-1 syntax.
+
/// Git revision parameter. Supports     extended SHA-1 syntax.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Rev(String);

@@ -98,6 +100,19 @@ pub fn rev(val: &OsString) -> anyhow::Result<Rev> {
}

#[allow(dead_code)]
+
pub fn did(val: &OsString) -> anyhow::Result<Did> {
+
    let val = val.to_string_lossy();
+
    let Ok(peer) = Did::from_str(&val) else {
+
        if crypto::PublicKey::from_str(&val).is_ok() {
+
            return Err(anyhow!("expected DID, did you mean 'did:key:{val}'?"));
+
        } else {
+
            return Err(anyhow!("invalid DID '{}', expected 'did:key'", val));
+
        }
+
    };
+
    Ok(peer)
+
}
+

+
#[allow(dead_code)]
pub fn issue(val: &OsString) -> anyhow::Result<issue::IssueId> {
    let val = val.to_string_lossy();
    issue::IssueId::from_str(&val).map_err(|_| anyhow!("invalid Issue ID '{}'", val))
modified src/cob/patch.rs
@@ -1,8 +1,46 @@
use anyhow::Result;

use radicle::cob::patch::{Patch, PatchId, Patches};
+
use radicle::identity::Did;
use radicle::storage::git::Repository;

+
#[derive(Clone, Default, Debug, Eq, PartialEq)]
+
pub enum State {
+
    Draft,
+
    #[default]
+
    Open,
+
    Merged,
+
    Archived,
+
}
+

+
#[derive(Clone, Default, Debug, Eq, PartialEq)]
+
pub struct Filter {
+
    state: Option<State>,
+
    authored: bool,
+
    authors: Vec<Did>,
+
}
+

+
impl Filter {
+
    pub fn with_state(mut self, state: State) -> Self {
+
        self.state = Some(state);
+
        self
+
    }
+

+
    pub fn with_authored(mut self, authored: bool) -> Self {
+
        self.authored = authored;
+
        self
+
    }
+

+
    pub fn with_author(mut self, author: Did) -> Self {
+
        self.authors.push(author);
+
        self
+
    }
+

+
    pub fn matches(&self, _patch: &Patch) -> bool {
+
        true
+
    }
+
}
+

pub fn all(repository: &Repository) -> Result<Vec<(PatchId, Patch)>> {
    let patches = Patches::open(repository)?
        .all()
modified src/ui/cob.rs
@@ -13,8 +13,8 @@ use radicle::storage::git::Repository;
use radicle::storage::{Oid, ReadRepository};
use radicle::Profile;

-
use radicle::cob::issue::{Issue, IssueId, State as IssueState};
-
use radicle::cob::patch::{Patch, PatchId, State as PatchState};
+
use radicle::cob::issue::{self, Issue, IssueId};
+
use radicle::cob::patch::{self, Patch, PatchId};
use radicle::cob::{Label, Timestamp};

use crate::ui::theme::Theme;
@@ -59,7 +59,7 @@ pub struct PatchItem {
    /// Patch OID.
    id: PatchId,
    /// Patch state.
-
    state: PatchState,
+
    state: patch::State,
    /// Patch title.
    title: String,
    /// Author of the latest revision.
@@ -79,7 +79,7 @@ impl PatchItem {
        &self.id
    }

-
    pub fn state(&self) -> &PatchState {
+
    pub fn state(&self) -> &patch::State {
        &self.state
    }

@@ -206,7 +206,7 @@ pub struct IssueItem {
    /// Issue OID.
    id: IssueId,
    /// Issue state.
-
    state: IssueState,
+
    state: issue::State,
    /// Issue title.
    title: String,
    /// Issue author.
@@ -224,7 +224,7 @@ impl IssueItem {
        &self.id
    }

-
    pub fn state(&self) -> &IssueState {
+
    pub fn state(&self) -> &issue::State {
        &self.state
    }

@@ -356,12 +356,12 @@ impl PartialEq for IssueItem {
    }
}

-
pub fn format_patch_state(state: &PatchState) -> (String, Color) {
+
pub fn format_patch_state(state: &patch::State) -> (String, Color) {
    match state {
-
        PatchState::Open { conflicts: _ } => (" ● ".into(), Color::Green),
-
        PatchState::Archived => (" ● ".into(), Color::Yellow),
-
        PatchState::Draft => (" ● ".into(), Color::Gray),
-
        PatchState::Merged {
+
        patch::State::Open { conflicts: _ } => (" ● ".into(), Color::Green),
+
        patch::State::Archived => (" ● ".into(), Color::Yellow),
+
        patch::State::Draft => (" ● ".into(), Color::Gray),
+
        patch::State::Merged {
            revision: _,
            commit: _,
        } => (" ● ".into(), Color::Cyan),
@@ -381,10 +381,10 @@ pub fn format_author(did: &Did, alias: &Option<Alias>, is_you: bool) -> String {
    }
}

-
pub fn format_issue_state(state: &IssueState) -> (String, Color) {
+
pub fn format_issue_state(state: &issue::State) -> (String, Color) {
    match state {
-
        IssueState::Open => (" ● ".into(), Color::Green),
-
        IssueState::Closed { reason: _ } => (" ● ".into(), Color::Red),
+
        issue::State::Open => (" ● ".into(), Color::Green),
+
        issue::State::Closed { reason: _ } => (" ● ".into(), Color::Red),
    }
}