Radish alpha
r
rad:z39mP9rQAaGmERfUMPULfPUi473tY
Radicle terminal user interface
Radicle
Git
bin/ui: Reorganize patch types
Erik Kundt committed 4 months ago
commit eac471462b998abb8b45da9db6e75b8918a59205
parent b0744f8
4 files changed +225 -231
modified bin/cob/patch.rs
@@ -1,79 +1,12 @@
-
use std::fmt;
-
use std::fmt::Write as _;
-

use anyhow::Result;

use radicle::cob::patch::{Patch, PatchId};
-
use radicle::identity::Did;
use radicle::node::device::Device;
use radicle::patch::cache::Patches;
-
use radicle::patch::{Review, ReviewId, Revision, Status};
+
use radicle::patch::{Review, ReviewId, Revision};
use radicle::storage::git::Repository;
use radicle::Profile;

-
#[derive(Clone, Debug, Eq, PartialEq)]
-
pub struct Filter {
-
    status: Option<Status>,
-
    authored: bool,
-
    authors: Vec<Did>,
-
}
-

-
impl Default for Filter {
-
    fn default() -> Self {
-
        Self {
-
            status: Some(Status::default()),
-
            authored: false,
-
            authors: vec![],
-
        }
-
    }
-
}
-

-
impl Filter {
-
    pub fn with_status(mut self, status: Option<Status>) -> Self {
-
        self.status = status;
-
        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
-
    }
-
}
-

-
impl fmt::Display for Filter {
-
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-
        if let Some(state) = &self.status {
-
            write!(f, "is:{state}")?;
-
            f.write_char(' ')?;
-
        }
-
        if self.authored {
-
            f.write_str("is:authored")?;
-
            f.write_char(' ')?;
-
        }
-
        if !self.authors.is_empty() {
-
            f.write_str("authors:")?;
-
            f.write_char('[')?;
-

-
            let mut authors = self.authors.iter().peekable();
-
            while let Some(author) = authors.next() {
-
                f.write_str(&author.encode())?;
-

-
                if authors.peek().is_some() {
-
                    f.write_char(',')?;
-
                }
-
            }
-
            f.write_char(']')?;
-
        }
-

-
        Ok(())
-
    }
-
}
-

pub fn all(profile: &Profile, repository: &Repository) -> Result<Vec<(PatchId, Patch)>> {
    let cache = profile.patches(repository)?;
    let patches = cache.list()?;
@@ -96,51 +29,3 @@ pub fn find_review<'a, G>(
        .find(|(_, review)| review.author().public_key() == signer.public_key())
        .map(|(id, review)| (*id, review))
}
-

-
#[cfg(test)]
-
mod tests {
-
    use std::str::FromStr;
-

-
    use anyhow::Result;
-
    use radicle::patch;
-

-
    use super::*;
-

-
    #[test]
-
    fn patch_filter_display_with_status_should_succeed() -> Result<()> {
-
        let actual = Filter::default().with_status(Some(patch::Status::Open));
-

-
        assert_eq!(String::from("is:open "), actual.to_string());
-

-
        Ok(())
-
    }
-

-
    #[test]
-
    fn patch_filter_display_with_status_and_authored_should_succeed() -> Result<()> {
-
        let actual = Filter::default()
-
            .with_status(Some(patch::Status::Open))
-
            .with_authored(true);
-

-
        assert_eq!(String::from("is:open is:authored "), actual.to_string());
-

-
        Ok(())
-
    }
-

-
    #[test]
-
    fn patch_filter_display_with_status_and_author_should_succeed() -> Result<()> {
-
        let actual = Filter::default()
-
            .with_status(Some(patch::Status::Open))
-
            .with_author(Did::from_str(
-
                "did:key:z6MkswQE8gwZw924amKatxnNCXA55BMupMmRg7LvJuim2C1V",
-
            )?);
-

-
        assert_eq!(
-
            String::from(
-
                "is:open authors:[did:key:z6MkswQE8gwZw924amKatxnNCXA55BMupMmRg7LvJuim2C1V]"
-
            ),
-
            actual.to_string()
-
        );
-

-
        Ok(())
-
    }
-
}
modified bin/commands/patch.rs
@@ -12,15 +12,16 @@ use anyhow::anyhow;
use radicle::cob::ObjectId;
use radicle::identity::RepoId;
use radicle::patch::{Patch, Revision, RevisionId, Status};
-

use radicle::storage::git::Repository;
+

use radicle_cli::git::Rev;
-
use radicle_cli::terminal;
+
use radicle_cli::terminal::args;
use radicle_cli::terminal::args::{string, Args, Error, Help};

-
use crate::cob::patch;
use crate::commands::tui_patch::common::PatchOperation;
+
use crate::terminal;
use crate::terminal::Quiet;
+
use crate::ui::items::patch::filter::PatchFilter;

pub const HELP: Help = Help {
    name: "patch",
@@ -75,7 +76,7 @@ pub enum OperationName {

#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct ListOptions {
-
    filter: patch::Filter,
+
    filter: PatchFilter,
    json: bool,
}

@@ -162,19 +163,17 @@ impl Args for Options {
                    list_opts.filter = list_opts.filter.with_authored(true);
                }
                Long("author") if op == OperationName::List => {
-
                    list_opts.filter = list_opts
-
                        .filter
-
                        .with_author(terminal::args::did(&parser.value()?)?);
+
                    list_opts.filter = list_opts.filter.with_author(args::did(&parser.value()?)?);
                }
                Long("repo") => {
                    let val = parser.value()?;
-
                    let rid = terminal::args::rid(&val)?;
+
                    let rid = args::rid(&val)?;

                    repo = Some(rid);
                }
                Long("revision") => {
                    let val = parser.value()?;
-
                    let rev_id = terminal::args::rev(&val)?;
+
                    let rev_id = args::rev(&val)?;

                    revision_id = Some(rev_id);
                }
@@ -236,7 +235,7 @@ impl Args for Options {
}

#[tokio::main]
-
pub async fn run(options: Options, ctx: impl terminal::Context) -> anyhow::Result<()> {
+
pub async fn run(options: Options, ctx: impl radicle_cli::terminal::Context) -> anyhow::Result<()> {
    use radicle::storage::ReadStorage;

    let (_, rid) = radicle::rad::cwd()
@@ -267,21 +266,21 @@ pub async fn run(options: Options, ctx: impl terminal::Context) -> anyhow::Resul
                if let Some(operation) = selection.operation.clone() {
                    match operation {
                        PatchOperation::Show { id } => {
-
                            crate::terminal::run_rad(
+
                            terminal::run_rad(
                                Some("patch"),
                                &["show".into(), id.to_string().into()],
                                Quiet::No,
                            )?;
                        }
                        PatchOperation::Diff { id } => {
-
                            crate::terminal::run_rad(
+
                            terminal::run_rad(
                                Some("patch"),
                                &["diff".into(), id.to_string().into()],
                                Quiet::No,
                            )?;
                        }
                        PatchOperation::Checkout { id } => {
-
                            crate::terminal::run_rad(
+
                            terminal::run_rad(
                                Some("patch"),
                                &["checkout".into(), id.to_string().into()],
                                Quiet::No,
@@ -312,7 +311,7 @@ pub async fn run(options: Options, ctx: impl terminal::Context) -> anyhow::Resul
            interface::review(opts.clone(), profile, rid, patch_id).await?;
        }
        Operation::Other { args } => {
-
            crate::terminal::run_rad(Some("patch"), &args, Quiet::No)?;
+
            terminal::run_rad(Some("patch"), &args, Quiet::No)?;
        }
        Operation::Unknown { .. } => {
            anyhow::bail!("unknown operation provided");
modified bin/commands/patch/list.rs
@@ -27,7 +27,8 @@ use super::common::PatchOperation;

use crate::cob::patch;
use crate::ui::items::filter::Filter;
-
use crate::ui::items::patch::{Patch, PatchFilter};
+
use crate::ui::items::patch::filter::PatchFilter;
+
use crate::ui::items::patch::Patch;

const HELP: &str = r#"# Generic keybindings

@@ -58,7 +59,7 @@ type Selection = tui::Selection<PatchOperation>;
pub struct Context {
    pub profile: Profile,
    pub repository: Repository,
-
    pub filter: patch::Filter,
+
    pub filter: PatchFilter,
}

pub struct Tui {
modified bin/ui/items/patch.rs
@@ -2,19 +2,12 @@ use std::collections::HashMap;
use std::fmt;
use std::fmt::Debug;
use std::ops::Range;
-
use std::str::FromStr;

use ansi_to_tui::IntoText;

-
use nom::bytes::complete::{tag, take};
-
use nom::multi::separated_list0;
-
use nom::sequence::{delimited, preceded};
-
use nom::{IResult, Parser};
-

use radicle::cob::thread::Comment;
use radicle::cob::{CodeLocation, CodeRange, EntryId, Timestamp};
use radicle::git::Oid;
-
use radicle::patch;
use radicle::patch::{PatchId, Review};
use radicle::prelude::Did;
use radicle::storage::git::Repository;
@@ -42,7 +35,6 @@ use tui::ui::{Column, ToRow};
use crate::git::{self, Blobs, DiffStats, HunkDiff, HunkState, HunkStats};
use crate::ui;

-
use super::filter::Filter;
use super::format;
use super::AuthorItem;

@@ -135,114 +127,189 @@ impl ToRow<9> for Patch {
    }
}

-
#[derive(Clone, Default, Debug, Eq, PartialEq)]
-
pub struct PatchFilter {
-
    status: Option<patch::Status>,
-
    authored: bool,
-
    authors: Vec<Did>,
-
    search: Option<String>,
-
}
+
pub mod filter {
+
    use std::fmt;
+
    use std::fmt::Debug;
+
    use std::fmt::Write as _;
+
    use std::str::FromStr;

-
impl PatchFilter {
-
    pub fn is_default(&self) -> bool {
-
        *self == PatchFilter::default()
-
    }
-
}
+
    use nom::bytes::complete::{tag, take};
+
    use nom::multi::separated_list0;
+
    use nom::sequence::{delimited, preceded};
+
    use nom::{IResult, Parser};

-
impl Filter<Patch> for PatchFilter {
-
    fn matches(&self, patch: &Patch) -> bool {
-
        use fuzzy_matcher::skim::SkimMatcherV2;
-
        use fuzzy_matcher::FuzzyMatcher;
+
    use radicle::patch;
+
    use radicle::patch::Status;
+
    use radicle::prelude::Did;

-
        let matcher = SkimMatcherV2::default();
+
    use crate::ui::items::filter::Filter;

-
        let matches_state = match self.status {
-
            Some(patch::Status::Draft) => matches!(patch.state, patch::State::Draft),
-
            Some(patch::Status::Open) => matches!(patch.state, patch::State::Open { .. }),
-
            Some(patch::Status::Merged) => matches!(patch.state, patch::State::Merged { .. }),
-
            Some(patch::Status::Archived) => matches!(patch.state, patch::State::Archived),
-
            None => true,
-
        };
+
    use super::Patch;

-
        let matches_authored = if self.authored {
-
            patch.author.you
-
        } else {
-
            true
-
        };
+
    #[derive(Clone, Debug, Eq, PartialEq)]
+
    pub struct PatchFilter {
+
        pub status: Option<patch::Status>,
+
        pub authored: bool,
+
        pub authors: Vec<Did>,
+
        pub search: Option<String>,
+
    }

-
        let matches_authors = if !self.authors.is_empty() {
-
            {
-
                self.authors
-
                    .iter()
-
                    .any(|other| patch.author.nid == Some(**other))
+
    impl Default for PatchFilter {
+
        fn default() -> Self {
+
            Self {
+
                status: Some(Status::Open),
+
                authored: false,
+
                authors: vec![],
+
                search: None,
            }
-
        } else {
-
            true
-
        };
+
        }
+
    }

-
        let matches_search = match &self.search {
-
            Some(search) => match matcher.fuzzy_match(&patch.title, search) {
-
                Some(score) => score == 0 || score > 60,
-
                _ => false,
-
            },
-
            None => true,
-
        };
+
    impl PatchFilter {
+
        pub fn is_default(&self) -> bool {
+
            *self == PatchFilter::default()
+
        }
+

+
        pub fn with_status(mut self, status: Option<Status>) -> Self {
+
            self.status = status;
+
            self
+
        }
+

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

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

-
impl FromStr for PatchFilter {
-
    type Err = anyhow::Error;
-

-
    fn from_str(value: &str) -> Result<Self, Self::Err> {
-
        let mut status = None;
-
        let mut search = String::new();
-
        let mut authored = false;
-
        let mut authors = vec![];
-

-
        let mut authors_parser = |input| -> IResult<&str, Vec<&str>> {
-
            preceded(
-
                tag("authors:"),
-
                delimited(
-
                    tag("["),
-
                    separated_list0(tag(","), take(56_usize)),
-
                    tag("]"),
-
                ),
-
            )(input)
-
        };
+
    impl fmt::Display for PatchFilter {
+
        fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
            if let Some(state) = &self.status {
+
                write!(f, "is:{state}")?;
+
                f.write_char(' ')?;
+
            }
+
            if self.authored {
+
                f.write_str("is:authored")?;
+
                f.write_char(' ')?;
+
            }
+
            if !self.authors.is_empty() {
+
                f.write_str("authors:")?;
+
                f.write_char('[')?;

-
        let parts = value.split(' ');
-
        for part in parts {
-
            match part {
-
                "is:open" => status = Some(patch::Status::Open),
-
                "is:merged" => status = Some(patch::Status::Merged),
-
                "is:archived" => status = Some(patch::Status::Archived),
-
                "is:draft" => status = Some(patch::Status::Draft),
-
                "is:authored" => authored = true,
-
                other => match authors_parser.parse(other) {
-
                    Ok((_, dids)) => {
-
                        for did in dids {
-
                            authors.push(Did::from_str(did)?);
-
                        }
+
                let mut authors = self.authors.iter().peekable();
+
                while let Some(author) = authors.next() {
+
                    f.write_str(&author.encode())?;
+

+
                    if authors.peek().is_some() {
+
                        f.write_char(',')?;
                    }
-
                    _ => search.push_str(other),
-
                },
+
                }
+
                f.write_char(']')?;
            }
+

+
            Ok(())
        }
+
    }

-
        let search = if search.is_empty() {
-
            None
-
        } else {
-
            Some(search)
-
        };
+
    impl Filter<Patch> for PatchFilter {
+
        fn matches(&self, patch: &Patch) -> bool {
+
            use fuzzy_matcher::skim::SkimMatcherV2;
+
            use fuzzy_matcher::FuzzyMatcher;

-
        Ok(Self {
-
            status,
-
            authored,
-
            authors,
-
            search,
-
        })
+
            let matcher = SkimMatcherV2::default();
+

+
            let matches_state = match self.status {
+
                Some(patch::Status::Draft) => matches!(patch.state, patch::State::Draft),
+
                Some(patch::Status::Open) => matches!(patch.state, patch::State::Open { .. }),
+
                Some(patch::Status::Merged) => matches!(patch.state, patch::State::Merged { .. }),
+
                Some(patch::Status::Archived) => matches!(patch.state, patch::State::Archived),
+
                None => true,
+
            };
+

+
            let matches_authored = if self.authored {
+
                patch.author.you
+
            } else {
+
                true
+
            };
+

+
            let matches_authors = if !self.authors.is_empty() {
+
                {
+
                    self.authors
+
                        .iter()
+
                        .any(|other| patch.author.nid == Some(**other))
+
                }
+
            } else {
+
                true
+
            };
+

+
            let matches_search = match &self.search {
+
                Some(search) => match matcher.fuzzy_match(&patch.title, search) {
+
                    Some(score) => score == 0 || score > 60,
+
                    _ => false,
+
                },
+
                None => true,
+
            };
+

+
            matches_state && matches_authored && matches_authors && matches_search
+
        }
+
    }
+

+
    impl FromStr for PatchFilter {
+
        type Err = anyhow::Error;
+

+
        fn from_str(value: &str) -> Result<Self, Self::Err> {
+
            let mut status = None;
+
            let mut search = String::new();
+
            let mut authored = false;
+
            let mut authors = vec![];
+

+
            let mut authors_parser = |input| -> IResult<&str, Vec<&str>> {
+
                preceded(
+
                    tag("authors:"),
+
                    delimited(
+
                        tag("["),
+
                        separated_list0(tag(","), take(56_usize)),
+
                        tag("]"),
+
                    ),
+
                )(input)
+
            };
+

+
            let parts = value.split(' ');
+
            for part in parts {
+
                match part {
+
                    "is:open" => status = Some(patch::Status::Open),
+
                    "is:merged" => status = Some(patch::Status::Merged),
+
                    "is:archived" => status = Some(patch::Status::Archived),
+
                    "is:draft" => status = Some(patch::Status::Draft),
+
                    "is:authored" => authored = true,
+
                    other => match authors_parser.parse(other) {
+
                        Ok((_, dids)) => {
+
                            for did in dids {
+
                                authors.push(Did::from_str(did)?);
+
                            }
+
                        }
+
                        _ => search.push_str(other),
+
                    },
+
                }
+
            }
+

+
            let search = if search.is_empty() {
+
                None
+
            } else {
+
                Some(search)
+
            };
+

+
            Ok(Self {
+
                status,
+
                authored,
+
                authors,
+
                search,
+
            })
+
        }
    }
}

@@ -1167,10 +1234,14 @@ impl<'a> ToText<'a> for Hunk<Modification> {
#[cfg(test)]
mod tests {
    use std::path::PathBuf;
+
    use std::str::FromStr;

    use anyhow::Result;

+
    use radicle::cob::patch;
+

    use crate::test;
+
    use crate::ui::items::patch::filter::PatchFilter;

    use super::*;

@@ -1195,6 +1266,44 @@ mod tests {
    }

    #[test]
+
    fn patch_filter_display_with_status_should_succeed() -> Result<()> {
+
        let actual = PatchFilter::default().with_status(Some(patch::Status::Open));
+

+
        assert_eq!(String::from("is:open "), actual.to_string());
+

+
        Ok(())
+
    }
+

+
    #[test]
+
    fn patch_filter_display_with_status_and_authored_should_succeed() -> Result<()> {
+
        let actual = PatchFilter::default()
+
            .with_status(Some(patch::Status::Open))
+
            .with_authored(true);
+

+
        assert_eq!(String::from("is:open is:authored "), actual.to_string());
+

+
        Ok(())
+
    }
+

+
    #[test]
+
    fn patch_filter_display_with_status_and_author_should_succeed() -> Result<()> {
+
        let actual = PatchFilter::default()
+
            .with_status(Some(patch::Status::Open))
+
            .with_author(Did::from_str(
+
                "did:key:z6MkswQE8gwZw924amKatxnNCXA55BMupMmRg7LvJuim2C1V",
+
            )?);
+

+
        assert_eq!(
+
            String::from(
+
                "is:open authors:[did:key:z6MkswQE8gwZw924amKatxnNCXA55BMupMmRg7LvJuim2C1V]"
+
            ),
+
            actual.to_string()
+
        );
+

+
        Ok(())
+
    }
+

+
    #[test]
    fn diff_line_index_checks_ranges_correctly() -> Result<()> {
        let commit = Oid::from_str("a32c4b93e2573fd83b15ac1ad6bf1317dc8fd760").unwrap();
        let path = PathBuf::from_str("main.rs").unwrap();