mirror of
https://seed.radicle.dev/rad:z39mP9rQAaGmERfUMPULfPUi473tY.git
synced 2026-11-16 00:10:38 +08:00
patch-select: Add support for patch filter option
This commit is contained in:
+54
-36
@@ -10,7 +10,8 @@ use std::ffi::OsString;
|
||||
use anyhow::anyhow;
|
||||
|
||||
use radicle_tui as tui;
|
||||
use tui::cob::patch;
|
||||
|
||||
use tui::cob::patch::{self, State};
|
||||
use tui::{context, log, Window};
|
||||
|
||||
use crate::terminal;
|
||||
@@ -24,15 +25,23 @@ pub const HELP: Help = Help {
|
||||
usage: r#"
|
||||
Usage
|
||||
|
||||
rad-tui patch [<option>...]
|
||||
rad-tui patch select [--operation | --id] [<option>...]
|
||||
rad-tui patch select [<option>...]
|
||||
|
||||
Select options
|
||||
|
||||
--operation Select patch id and operation (default)
|
||||
--id Select patch id only
|
||||
|
||||
--mode <MODE> Set selection mode; see MODE below (default: operation)
|
||||
--all Show all patches, including merged and archived patches
|
||||
--archived Show only archived patches
|
||||
--merged Show only merged patches
|
||||
--open Show only open patches (default)
|
||||
--draft Show only draft patches
|
||||
--authored Show only patches that you have authored
|
||||
--author <did> Show only patched where the given user is an author
|
||||
(may be specified multiple times)
|
||||
|
||||
The MODE argument can be 'operation' or 'id'. 'operation' selects a patch id and
|
||||
an operation, whereas 'id' selects a patch id only.
|
||||
|
||||
|
||||
Other options
|
||||
|
||||
@@ -57,25 +66,18 @@ pub enum OperationName {
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct SelectOptions {
|
||||
subject: select::Subject,
|
||||
mode: select::Mode,
|
||||
filter: patch::Filter,
|
||||
}
|
||||
|
||||
impl SelectOptions {
|
||||
pub fn new(subject: select::Subject, filter: patch::Filter) -> Self {
|
||||
Self { subject, filter }
|
||||
}
|
||||
}
|
||||
|
||||
impl Args for Options {
|
||||
fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
|
||||
use lexopt::prelude::*;
|
||||
|
||||
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;
|
||||
let mut select_opts = SelectOptions::default();
|
||||
|
||||
#[allow(clippy::never_loop)]
|
||||
while let Some(arg) = parser.next()? {
|
||||
@@ -87,21 +89,39 @@ impl Args for Options {
|
||||
json = true;
|
||||
}
|
||||
|
||||
// Select options.
|
||||
Long("operation") | Short('o') if op == Some(OperationName::Select) => {
|
||||
if select_opts.is_some() {
|
||||
anyhow::bail!("select option already given")
|
||||
}
|
||||
select_opts = Some(SelectOptions::new(
|
||||
select::Subject::Operation,
|
||||
filter.clone(),
|
||||
));
|
||||
// select options.
|
||||
Long("mode") | Short('m') if op == Some(OperationName::Select) => {
|
||||
let val = parser.value()?;
|
||||
let val = val.to_str().unwrap_or_default();
|
||||
|
||||
select_opts.mode = match val {
|
||||
"operation" => select::Mode::Operation,
|
||||
"id" => select::Mode::Id,
|
||||
unknown => anyhow::bail!("unknown mode '{}'", unknown),
|
||||
};
|
||||
}
|
||||
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, filter.clone()));
|
||||
Long("all") if op == Some(OperationName::Select) => {
|
||||
select_opts.filter = select_opts.filter.with_state(None);
|
||||
}
|
||||
Long("draft") if op == Some(OperationName::Select) => {
|
||||
select_opts.filter = select_opts.filter.with_state(Some(State::Draft));
|
||||
}
|
||||
Long("archived") if op == Some(OperationName::Select) => {
|
||||
select_opts.filter = select_opts.filter.with_state(Some(State::Archived));
|
||||
}
|
||||
Long("merged") if op == Some(OperationName::Select) => {
|
||||
select_opts.filter = select_opts.filter.with_state(Some(State::Merged));
|
||||
}
|
||||
Long("open") if op == Some(OperationName::Select) => {
|
||||
select_opts.filter = select_opts.filter.with_state(Some(State::Open));
|
||||
}
|
||||
Long("authored") if op == Some(OperationName::Select) => {
|
||||
select_opts.filter = select_opts.filter.with_authored(true);
|
||||
}
|
||||
Long("author") if op == Some(OperationName::Select) => {
|
||||
select_opts.filter = select_opts
|
||||
.filter
|
||||
.with_author(terminal::args::did(&parser.value()?)?);
|
||||
}
|
||||
|
||||
Value(val) if op.is_none() => match val.to_string_lossy().as_ref() {
|
||||
@@ -113,9 +133,7 @@ impl Args for Options {
|
||||
}
|
||||
|
||||
let op = match op.ok_or_else(|| anyhow!("an operation must be provided"))? {
|
||||
OperationName::Select => Operation::Select {
|
||||
opts: select_opts.unwrap_or_default(),
|
||||
},
|
||||
OperationName::Select => Operation::Select { opts: select_opts },
|
||||
};
|
||||
Ok((Options { op, json }, vec![]))
|
||||
}
|
||||
@@ -132,7 +150,7 @@ pub fn run(options: Options, _ctx: impl terminal::Context) -> anyhow::Result<()>
|
||||
|
||||
log::enable(context.profile(), "patch", "select")?;
|
||||
|
||||
let mut app = select::App::new(context, opts.subject.clone(), opts.filter.clone());
|
||||
let mut app = select::App::new(context, opts.mode.clone(), opts.filter.clone());
|
||||
let output = Window::default().run(&mut app, 1000 / FPS)?;
|
||||
|
||||
let output = if options.json {
|
||||
@@ -141,9 +159,9 @@ pub fn run(options: Options, _ctx: impl terminal::Context) -> anyhow::Result<()>
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
match options.op {
|
||||
Operation::Select { ref opts } => match &opts.subject {
|
||||
select::Subject::Id => output.map(|o| format!("{}", o)).unwrap_or_default(),
|
||||
select::Subject::Operation => output
|
||||
Operation::Select { ref opts } => match &opts.mode {
|
||||
select::Mode::Id => output.map(|o| format!("{}", o)).unwrap_or_default(),
|
||||
select::Mode::Operation => output
|
||||
.map(|o| format!("rad patch {}", o))
|
||||
.unwrap_or_default(),
|
||||
},
|
||||
|
||||
@@ -56,7 +56,7 @@ impl PatchBrowser {
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|(_, patch)| filter.matches(patch));
|
||||
.filter(|(_, patch)| filter.matches(context.profile(), patch));
|
||||
|
||||
let mut items = vec![];
|
||||
for (id, patch) in patches {
|
||||
@@ -104,7 +104,12 @@ impl WidgetComponent for PatchBrowser {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn browse_context(context: &Context, _theme: &Theme, progress: Progress) -> Widget<ContextBar> {
|
||||
pub fn browse_context(
|
||||
context: &Context,
|
||||
_theme: &Theme,
|
||||
filter: Filter,
|
||||
progress: Progress,
|
||||
) -> Widget<ContextBar> {
|
||||
use radicle::cob::patch::State;
|
||||
|
||||
let mut draft = 0;
|
||||
@@ -112,7 +117,13 @@ pub fn browse_context(context: &Context, _theme: &Theme, progress: Progress) ->
|
||||
let mut archived = 0;
|
||||
let mut merged = 0;
|
||||
|
||||
let patches = context.patches().as_ref().unwrap();
|
||||
let patches = context
|
||||
.patches()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.iter()
|
||||
.filter(|(_, patch)| filter.matches(context.profile(), patch));
|
||||
|
||||
for (_, patch) in patches {
|
||||
match patch.state() {
|
||||
State::Draft => draft += 1,
|
||||
@@ -125,8 +136,8 @@ pub fn browse_context(context: &Context, _theme: &Theme, progress: Progress) ->
|
||||
}
|
||||
}
|
||||
|
||||
let context = label::default("");
|
||||
let divider = label::default(" | ");
|
||||
let context = label::reversable("/").style(style::magenta_reversed());
|
||||
let filter = label::default(&filter.to_string()).style(style::magenta_dim());
|
||||
|
||||
let draft_n = label::default(&format!("{draft}")).style(style::gray_dim());
|
||||
let draft = label::default(" Draft");
|
||||
@@ -141,11 +152,13 @@ pub fn browse_context(context: &Context, _theme: &Theme, progress: Progress) ->
|
||||
let merged = label::default(" Merged ");
|
||||
|
||||
let progress = label::reversable(&progress.to_string()).style(style::magenta_reversed());
|
||||
|
||||
let spacer = label::default("");
|
||||
let divider = label::default(" | ");
|
||||
|
||||
let context_bar = ContextBar::new(
|
||||
label::group(&[context]),
|
||||
label::group(&[spacer.clone()]),
|
||||
label::group(&[filter]),
|
||||
label::group(&[spacer]),
|
||||
label::group(&[
|
||||
draft_n,
|
||||
|
||||
@@ -57,7 +57,7 @@ impl Serialize for PatchId {
|
||||
///
|
||||
/// Depends on CLI arguments given by the user.
|
||||
#[derive(Clone, Default, Debug, Eq, PartialEq)]
|
||||
pub enum Subject {
|
||||
pub enum Mode {
|
||||
#[default]
|
||||
Operation,
|
||||
Id,
|
||||
@@ -152,7 +152,7 @@ pub struct App {
|
||||
pages: PageStack<Cid, Message>,
|
||||
theme: Theme,
|
||||
quit: bool,
|
||||
subject: Subject,
|
||||
mode: Mode,
|
||||
filter: Filter,
|
||||
output: Option<Output>,
|
||||
}
|
||||
@@ -160,13 +160,13 @@ pub struct App {
|
||||
/// 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, filter: Filter) -> Self {
|
||||
pub fn new(context: Context, mode: Mode, filter: Filter) -> Self {
|
||||
Self {
|
||||
context,
|
||||
pages: PageStack::default(),
|
||||
theme: Theme::default(),
|
||||
quit: false,
|
||||
subject,
|
||||
mode,
|
||||
filter,
|
||||
output: None,
|
||||
}
|
||||
@@ -177,7 +177,7 @@ impl App {
|
||||
app: &mut Application<Cid, Message, NoUserEvent>,
|
||||
theme: &Theme,
|
||||
) -> Result<()> {
|
||||
let home = Box::new(ListView::new(self.subject.clone(), self.filter.clone()));
|
||||
let home = Box::new(ListView::new(self.mode.clone(), self.filter.clone()));
|
||||
self.pages.push(home, app, &self.context, theme)?;
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -15,20 +15,20 @@ use tui::ui::{layout, subscription};
|
||||
use tui::ViewPage;
|
||||
|
||||
use super::super::common;
|
||||
use super::{ui, Application, Cid, ListCid, Message, Subject};
|
||||
use super::{ui, Application, Cid, ListCid, Message, Mode};
|
||||
|
||||
///
|
||||
/// Home
|
||||
///
|
||||
pub struct ListView {
|
||||
active_component: ListCid,
|
||||
subject: Subject,
|
||||
subject: Mode,
|
||||
filter: Filter,
|
||||
shortcuts: HashMap<ListCid, Widget<Shortcuts>>,
|
||||
}
|
||||
|
||||
impl ListView {
|
||||
pub fn new(subject: Subject, filter: Filter) -> Self {
|
||||
pub fn new(subject: Mode, filter: Filter) -> Self {
|
||||
Self {
|
||||
active_component: ListCid::PatchBrowser,
|
||||
subject,
|
||||
@@ -50,7 +50,7 @@ impl ListView {
|
||||
}
|
||||
_ => Progress::None,
|
||||
};
|
||||
let context = common::ui::browse_context(context, theme, progress);
|
||||
let context = common::ui::browse_context(context, theme, self.filter.clone(), progress);
|
||||
|
||||
app.remount(Cid::List(ListCid::Context), context.to_boxed(), vec![])?;
|
||||
|
||||
@@ -87,14 +87,14 @@ impl ViewPage<Cid, Message> for ListView {
|
||||
app.remount(Cid::List(ListCid::Header), header, vec![])?;
|
||||
|
||||
match self.subject {
|
||||
Subject::Id => {
|
||||
Mode::Id => {
|
||||
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 => {
|
||||
Mode::Operation => {
|
||||
let patch_browser =
|
||||
ui::operation_select(theme, context, self.filter.clone(), None).to_boxed();
|
||||
self.shortcuts = patch_browser.as_ref().shortcuts();
|
||||
|
||||
@@ -5,6 +5,7 @@ use anyhow::Result;
|
||||
use radicle::cob::Label;
|
||||
use radicle::prelude::Did;
|
||||
|
||||
pub mod format;
|
||||
pub mod issue;
|
||||
pub mod patch;
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
use radicle::identity::Did;
|
||||
|
||||
/// Format a DID.
|
||||
pub fn did(did: &Did) -> String {
|
||||
let nid = did.as_key().to_human();
|
||||
format!("{}…{}", &nid[..7], &nid[nid.len() - 7..])
|
||||
}
|
||||
+74
-4
@@ -1,8 +1,13 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
use radicle::cob::patch::{Patch, PatchId, Patches};
|
||||
use radicle::identity::Did;
|
||||
use radicle::storage::git::Repository;
|
||||
use radicle::{patch, Profile};
|
||||
|
||||
use super::format;
|
||||
|
||||
#[derive(Clone, Default, Debug, Eq, PartialEq)]
|
||||
pub enum State {
|
||||
@@ -13,6 +18,18 @@ pub enum State {
|
||||
Archived,
|
||||
}
|
||||
|
||||
impl Display for State {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let state = match self {
|
||||
State::Draft => "draft",
|
||||
State::Open => "open",
|
||||
State::Merged => "merged",
|
||||
State::Archived => "archived",
|
||||
};
|
||||
f.write_str(state)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, Eq, PartialEq)]
|
||||
pub struct Filter {
|
||||
state: Option<State>,
|
||||
@@ -21,8 +38,8 @@ pub struct Filter {
|
||||
}
|
||||
|
||||
impl Filter {
|
||||
pub fn with_state(mut self, state: State) -> Self {
|
||||
self.state = Some(state);
|
||||
pub fn with_state(mut self, state: Option<State>) -> Self {
|
||||
self.state = state;
|
||||
self
|
||||
}
|
||||
|
||||
@@ -36,8 +53,61 @@ impl Filter {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn matches(&self, _patch: &Patch) -> bool {
|
||||
true
|
||||
pub fn matches(&self, profile: &Profile, patch: &Patch) -> bool {
|
||||
let matches_state = match self.state {
|
||||
Some(State::Draft) => matches!(patch.state(), patch::State::Draft),
|
||||
Some(State::Open) => matches!(patch.state(), patch::State::Open { .. }),
|
||||
Some(State::Merged) => matches!(patch.state(), patch::State::Merged { .. }),
|
||||
Some(State::Archived) => matches!(patch.state(), patch::State::Archived),
|
||||
None => true,
|
||||
};
|
||||
|
||||
let matches_authored = self
|
||||
.authored
|
||||
.then(|| *patch.author().id() == profile.did())
|
||||
.unwrap_or(true);
|
||||
|
||||
let matches_authors = (!self.authors.is_empty())
|
||||
.then(|| {
|
||||
self.authors
|
||||
.iter()
|
||||
.any(|other| *patch.author().id() == *other)
|
||||
})
|
||||
.unwrap_or(true);
|
||||
|
||||
matches_state && matches_authored && matches_authors
|
||||
}
|
||||
}
|
||||
|
||||
impl ToString for Filter {
|
||||
fn to_string(&self) -> String {
|
||||
let mut filter = String::new();
|
||||
filter.push(' ');
|
||||
|
||||
if let Some(state) = &self.state {
|
||||
filter.push_str(&format!("is:{}", state));
|
||||
filter.push(' ');
|
||||
}
|
||||
if self.authored {
|
||||
filter.push_str("is:authored");
|
||||
filter.push(' ');
|
||||
}
|
||||
if !self.authors.is_empty() {
|
||||
filter.push_str("authors:");
|
||||
filter.push('[');
|
||||
|
||||
let mut authors = self.authors.iter().peekable();
|
||||
while let Some(author) = authors.next() {
|
||||
filter.push_str(&format::did(author));
|
||||
|
||||
if authors.peek().is_some() {
|
||||
filter.push(',');
|
||||
}
|
||||
}
|
||||
filter.push(']');
|
||||
}
|
||||
|
||||
filter
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ use radicle::cob::issue::{Issue, IssueId};
|
||||
use radicle::cob::patch::{Patch, PatchId};
|
||||
use radicle::crypto::ssh::keystore::{Keystore, MemorySigner};
|
||||
use radicle::crypto::Signer;
|
||||
use radicle::prelude::{Id, Project};
|
||||
use radicle::identity::{Id, Project};
|
||||
use radicle::profile::env::RAD_PASSPHRASE;
|
||||
use radicle::storage::git::Repository;
|
||||
use radicle::storage::{ReadRepository, ReadStorage};
|
||||
|
||||
+10
-1
@@ -66,6 +66,14 @@ pub mod style {
|
||||
Style::default().fg(Color::Yellow)
|
||||
}
|
||||
|
||||
pub fn yellow_dim() -> Style {
|
||||
yellow().add_modifier(TextModifiers::DIM)
|
||||
}
|
||||
|
||||
pub fn yellow_dim_reversed() -> Style {
|
||||
yellow_dim().add_modifier(TextModifiers::REVERSED)
|
||||
}
|
||||
|
||||
pub fn blue() -> Style {
|
||||
Style::default().fg(Color::Blue)
|
||||
}
|
||||
@@ -108,7 +116,8 @@ pub mod style {
|
||||
|
||||
pub fn default_reversed() -> Style {
|
||||
Style::default()
|
||||
.add_modifier(TextModifiers::DIM)
|
||||
.fg(Color::DarkGray)
|
||||
// .add_modifier(TextModifiers::DIM)
|
||||
.add_modifier(TextModifiers::REVERSED)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user