Radish alpha
h
rad:z3gqcJUoA1n9HaHKufZs5FCSGazv5
Radicle Heartwood Protocol & Stack
Radicle
Git
This patchset implements two new flags:
Draft did:key:z6MkwcUR...q1kL opened 8 months ago

rad issue list –sort=

and

rad issue list --rsort=<mode>

to sort the issue listing.

2 files changed +141 -10 f00d1d67 cb1120d9
modified crates/radicle-cli/src/commands/issue.rs
@@ -8,7 +8,7 @@ use std::str::FromStr;
use anyhow::{anyhow, Context as _};

use radicle::cob::common::{Label, Reaction};
-
use radicle::cob::issue::{CloseReason, State};
+
use radicle::cob::issue::{CloseReason, Sort, SortBy, SortDirection, State};
use radicle::cob::{issue, thread, Title};
use radicle::crypto;
use radicle::git::Oid;
@@ -65,6 +65,17 @@ Label options

    Note: --add takes precedence over --delete

+
List options
+

+
    --sort=<sort mode>     Sort issues
+
    --rsort=<sort mode>    Sort issues (reversed)
+

+
    Possible sort modes are:
+

+
        * "id" - Their ID
+
        * "title" - Alphabetical order of titles
+
        * "comments" - Number of comments
+

Show options

    -v, --verbose          Show additional information about the issue
@@ -154,6 +165,7 @@ pub enum Operation {
    List {
        assigned: Option<Assigned>,
        state: Option<State>,
+
        sort: Option<Sort>,
    },
    Cache {
        id: Option<Rev>,
@@ -194,6 +206,7 @@ impl Args for Options {
        let mut comment_id: Option<thread::CommentId> = None;
        let mut description: Option<String> = None;
        let mut state: Option<State> = Some(State::Open);
+
        let mut sort: Option<Sort> = None;
        let mut labels = Vec::new();
        let mut assignees = Vec::new();
        let mut format = Format::default();
@@ -231,6 +244,20 @@ impl Args for Options {
                        reason: CloseReason::Solved,
                    });
                }
+
                Long("sort") if op.is_none() || op == Some(OperationName::List) => {
+
                    let by = SortBy::from_str(parser.value()?.to_string_lossy().as_ref())?;
+
                    sort = Some(Sort {
+
                        sort_by: by,
+
                        direction: SortDirection::Asc,
+
                    });
+
                }
+
                Long("rsort") if op.is_none() || op == Some(OperationName::List) => {
+
                    let by = SortBy::from_str(parser.value()?.to_string_lossy().as_ref())?;
+
                    sort = Some(Sort {
+
                        sort_by: by,
+
                        direction: SortDirection::Desc,
+
                    });
+
                }

                // Open/Edit options.
                Long("title")
@@ -453,7 +480,11 @@ impl Args for Options {
                id: id.ok_or_else(|| anyhow!("an issue to label must be provided"))?,
                opts: label_opts,
            },
-
            OperationName::List => Operation::List { assigned, state },
+
            OperationName::List => Operation::List {
+
                assigned,
+
                state,
+
                sort,
+
            },
            OperationName::Cache => Operation::Cache {
                id,
                storage: cache_storage,
@@ -677,8 +708,12 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
                .collect::<Vec<_>>();
            issue.label(labels, &signer)?;
        }
-
        Operation::List { assigned, state } => {
-
            list(issues, &assigned, &state, &profile)?;
+
        Operation::List {
+
            assigned,
+
            state,
+
            sort,
+
        } => {
+
            list(issues, &assigned, &state, &profile, sort)?;
        }
        Operation::Delete { id } => {
            let signer = term::signer(&profile)?;
@@ -720,6 +755,7 @@ fn list<C>(
    assigned: &Option<Assigned>,
    state: &Option<State>,
    profile: &profile::Profile,
+
    sort: Option<Sort>,
) -> anyhow::Result<()>
where
    C: issue::cache::Issues,
@@ -763,12 +799,7 @@ where
        })
        .collect::<Vec<_>>();

-
    all.sort_by(|(id1, i1), (id2, i2)| {
-
        let by_timestamp = i2.timestamp().cmp(&i1.timestamp());
-
        let by_id = id1.cmp(id2);
-

-
        by_timestamp.then(by_id)
-
    });
+
    sort_issues(&mut all, sort);

    let mut table = term::Table::new(term::table::TableOptions::bordered());
    table.header([
@@ -828,6 +859,34 @@ where
    Ok(())
}

+
fn sort_issues(issues: &mut [(cob::issue::IssueId, cob::issue::Issue)], sort: Option<Sort>) {
+
    match sort {
+
        None => issues.sort_by(|(id1, i1), (id2, i2)| {
+
            let by_timestamp = i2.timestamp().cmp(&i1.timestamp());
+
            let by_id = id1.cmp(id2);
+

+
            by_timestamp.then(by_id)
+
        }),
+

+
        Some(Sort {
+
            direction,
+
            sort_by: SortBy::Id,
+
        }) => issues.sort_by(|(id1, _), (id2, _)| direction.apply_to(id1.cmp(id2))),
+

+
        Some(Sort {
+
            direction,
+
            sort_by: SortBy::Title,
+
        }) => issues.sort_by(|(_, i1), (_, i2)| direction.apply_to(i1.title().cmp(i2.title()))),
+

+
        Some(Sort {
+
            direction,
+
            sort_by: SortBy::Comments,
+
        }) => issues.sort_by(|(_, i1), (_, i2)| {
+
            direction.apply_to(i1.comments().count().cmp(&i2.comments().count()))
+
        }),
+
    }
+
}
+

fn open<R, G>(
    title: Option<Title>,
    description: Option<String>,
modified crates/radicle/src/cob/issue.rs
@@ -139,6 +139,78 @@ impl State {
    }
}

+
#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase", tag = "sort")]
+
pub struct Sort {
+
    pub direction: SortDirection,
+
    pub sort_by: SortBy,
+
}
+

+
#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase", tag = "sort-direction")]
+
pub enum SortDirection {
+
    Asc,
+
    Desc,
+
}
+

+
impl SortDirection {
+
    #[inline]
+
    pub fn apply_to(&self, ordering: std::cmp::Ordering) -> std::cmp::Ordering {
+
        match self {
+
            SortDirection::Asc => ordering,
+
            SortDirection::Desc => ordering.reverse(),
+
        }
+
    }
+
}
+

+
impl std::fmt::Display for Sort {
+
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+
        match self.direction {
+
            SortDirection::Asc => write!(f, "sort={}", self.sort_by),
+
            SortDirection::Desc => write!(f, "rsort={}", self.sort_by),
+
        }
+
    }
+
}
+

+
#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, Serialize, Deserialize)]
+
#[serde(rename_all = "camelCase", tag = "sort-by")]
+
pub enum SortBy {
+
    Id,
+
    Title,
+
    Comments,
+
}
+

+
impl std::fmt::Display for SortBy {
+
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+
        write!(
+
            f,
+
            "{}",
+
            match self {
+
                SortBy::Id => "id",
+
                SortBy::Title => "title",
+
                SortBy::Comments => "comments",
+
            }
+
        )
+
    }
+
}
+

+
#[derive(Debug, thiserror::Error)]
+
#[error("Unknown sorting method '{}'", .0)]
+
pub struct SortByFromStrError(String);
+

+
impl FromStr for SortBy {
+
    type Err = SortByFromStrError;
+

+
    fn from_str(s: &str) -> Result<Self, Self::Err> {
+
        Ok(match s {
+
            "id" => SortBy::Id,
+
            "title" => SortBy::Title,
+
            "comments" => SortBy::Comments,
+
            _ => return Err(SortByFromStrError(s.to_string())),
+
        })
+
    }
+
}
+

/// Issue state. Accumulates [`Action`].
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]