Radish alpha
r
rad:z39mP9rQAaGmERfUMPULfPUi473tY
Radicle terminal user interface
Radicle
Git
feat(patch): Introduce review mode (edit/show)
Erik Kundt committed 1 year ago
commit 5b44b2f8735cbc7a259bb7431c0a6c5bce3746d3
parent 9411ee3
3 files changed +120 -79
modified bin/commands/patch.rs
@@ -83,6 +83,7 @@ pub struct ListOptions {

#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct ReviewOptions {
+
    edit: bool,
    patch_id: Option<Rev>,
    revision_id: Option<Rev>,
}
@@ -121,6 +122,7 @@ impl Args for Options {
        let mut forward = None;
        let mut json = false;
        let mut help = false;
+
        let mut edit = false;
        let mut repo = None;
        let mut list_opts = ListOptions::default();
        let mut patch_id = None;
@@ -189,6 +191,9 @@ impl Args for Options {

                    revision_id = Some(rev_id);
                }
+
                Long("edit") => {
+
                    edit = true;
+
                }
                Value(val) if op == OperationName::List => match val.to_string_lossy().as_ref() {
                    "list" => op = OperationName::List,
                    "review" => op = OperationName::Review,
@@ -226,6 +231,7 @@ impl Args for Options {
        let op = match op {
            OperationName::Review if !forward => Operation::Review {
                opts: ReviewOptions {
+
                    edit,
                    patch_id,
                    revision_id,
                },
@@ -386,26 +392,30 @@ mod interface {
            return Ok(());
        };

-
        let mode = if let Some((id, _)) = patch::find_review(&patch, revision, &signer) {
-
            // Review already started, resume.
-
            log::info!("Resuming review {id}..");
+
        let mode = if opts.edit {
+
            if let Some((id, _)) = patch::find_review(&patch, revision, &signer) {
+
                // Review already started, resume.
+
                log::info!("Resuming review {id}..");

-
            ReviewMode::Resume
+
                ReviewMode::Edit { resume: true }
+
            } else {
+
                // No review to resume, start a new one.
+
                let id = patch.review(
+
                    revision.id(),
+
                    // This is amended before the review is finalized, if all hunks are
+
                    // accepted. We can't set this to `None`, as that will be invalid without
+
                    // a review summary.
+
                    Some(Verdict::Reject),
+
                    None,
+
                    vec![],
+
                    &signer,
+
                )?;
+
                log::info!("Starting new review {id}..");
+

+
                ReviewMode::Edit { resume: false }
+
            }
        } else {
-
            // No review to resume, start a new one.
-
            let id = patch.review(
-
                revision.id(),
-
                // This is amended before the review is finalized, if all hunks are
-
                // accepted. We can't set this to `None`, as that will be invalid without
-
                // a review summary.
-
                Some(Verdict::Reject),
-
                None,
-
                vec![],
-
                &signer,
-
            )?;
-
            log::info!("Starting new review {id}..");
-

-
            ReviewMode::Create
+
            ReviewMode::Show
        };

        loop {
modified bin/commands/patch/review.rs
@@ -11,7 +11,7 @@ use serde::{Deserialize, Serialize};
use termion::event::Key;

use ratatui::layout::{Constraint, Position};
-
use ratatui::style::Stylize;
+
use ratatui::style::{Style, Stylize};
use ratatui::text::Text;
use ratatui::{Frame, Viewport};

@@ -53,10 +53,10 @@ pub struct Response {
    pub action: Option<ReviewAction>,
}

-
#[derive(Clone)]
+
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
pub enum ReviewMode {
-
    Create,
-
    Resume,
+
    Show,
+
    Edit { resume: bool },
}

pub struct Tui {
@@ -102,24 +102,20 @@ impl Tui {
        let store = FileStore::new(identifier)?;

        let default = AppState::new(
+
            ReviewMode::Show,
            self.rid,
            self.patch,
            self.title,
            self.revision.id(),
            &self.hunks,
        );
-
        let state = match self.mode {
-
            ReviewMode::Resume => match store.read() {
-
                Ok(bytes) => state::from_json(&bytes)?,
-
                _ => {
-
                    log::warn!("Failed to load state. Falling back to default.");
-
                    default
-
                }
-
            },
-
            ReviewMode::Create => default,
-
        };

-
        let app = App::new(self.mode, self.storage, self.review, self.hunks, state)?;
+
        let state = store
+
            .read()
+
            .map(|bytes| state::from_json::<AppState>(&bytes).ok())?
+
            .unwrap_or(default);
+

+
        let app = App::new(self.storage, self.review, self.hunks, state, self.mode)?;
        let response = tui::im(app, viewport, channel).await?;

        if let Some(response) = response.as_ref() {
@@ -157,6 +153,8 @@ pub struct DiffViewState {

#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AppState {
+
    /// Review mode: edit or show.
+
    mode: ReviewMode,
    /// The repository to operate on.
    rid: RepoId,
    /// Patch this review belongs to.
@@ -179,6 +177,7 @@ pub struct AppState {

impl AppState {
    pub fn new(
+
        mode: ReviewMode,
        rid: RepoId,
        patch: PatchId,
        title: String,
@@ -186,6 +185,7 @@ impl AppState {
        hunks: &Hunks,
    ) -> Self {
        Self {
+
            mode,
            rid,
            patch,
            title,
@@ -242,19 +242,26 @@ pub struct App<'a> {
    hunks: Arc<Mutex<Vec<StatefulHunkItem<'a>>>>,
    /// The app state.
    state: Arc<Mutex<AppState>>,
-
    /// Review mode: create or resume.
-
    _mode: ReviewMode,
}

impl<'a> App<'a> {
    pub fn new(
-
        mode: ReviewMode,
        storage: Storage,
        review: Review,
        hunks: Hunks,
        state: AppState,
+
        mode: ReviewMode,
    ) -> Result<Self, anyhow::Error> {
        let repo = storage.repository(state.rid)?;
+
        // TODO: Check, if it's necessary to protect the app state.
+
        // let mode = match state.mode {
+
        //     ReviewMode::Edit { resume: _ } if mode == ReviewMode::Show => {
+
        //         // TODO: Ask user what to do.
+
        //         anyhow::bail!("Review not finalized, yet. Current state would be lost.")
+
        //     }
+
        //     _ => mode,
+
        // };
+

        let hunks = hunks
            .iter()
            .enumerate()
@@ -268,8 +275,7 @@ impl<'a> App<'a> {

        Ok(Self {
            hunks: Arc::new(Mutex::new(hunks)),
-
            state: Arc::new(Mutex::new(state)),
-
            _mode: mode,
+
            state: Arc::new(Mutex::new(AppState { mode, ..state })),
        })
    }

@@ -317,9 +323,13 @@ impl<'a> App<'a> {
        let hunks = self.hunks.lock().unwrap();
        let state = self.state.lock().unwrap();

+
        let state_column_width = match state.mode {
+
            ReviewMode::Show => 0,
+
            ReviewMode::Edit { resume: _ } => 2,
+
        };
        let header = [Column::new(" Hunks ", Constraint::Fill(1))].to_vec();
        let columns = [
-
            Column::new("", Constraint::Length(2)),
+
            Column::new("", Constraint::Length(state_column_width)),
            Column::new("", Constraint::Fill(1)),
            Column::new("", Constraint::Length(15)),
        ]
@@ -385,14 +395,25 @@ impl<'a> App<'a> {
            .collect::<Vec<_>>()
            .len();

-
        let accepted_stats = format!(" Accepted {hunks_accepted}/{hunks_total} ");
+
        let (mode, context, context_style) = match state.mode {
+
            ReviewMode::Show => (
+
                " Show ",
+
                "".into(),
+
                Style::default().cyan().dim().reversed(),
+
            ),
+
            ReviewMode::Edit { resume: _ } => (
+
                " Edit ",
+
                format!(" Accepted {hunks_accepted}/{hunks_total} "),
+
                Style::default().light_red().dim().reversed(),
+
            ),
+
        };

        ui.bar(
            frame,
            [
                Column::new(
-
                    span::default(" Review ").cyan().dim().reversed(),
-
                    Constraint::Length(8),
+
                    span::default(mode).style(context_style),
+
                    Constraint::Length(mode.chars().count() as u16),
                ),
                Column::new(
                    span::default(&id)
@@ -414,18 +435,55 @@ impl<'a> App<'a> {
                    Constraint::Fill(1),
                ),
                Column::new(
-
                    span::default(&accepted_stats)
+
                    span::default(&context)
                        .into_right_aligned_line()
-
                        .cyan()
-
                        .dim()
-
                        .reversed(),
-
                    Constraint::Length(accepted_stats.chars().count() as u16),
+
                        .style(context_style),
+
                    Constraint::Length(context.chars().count() as u16),
                ),
            ]
            .to_vec(),
            Some(Borders::None),
        );
    }
+

+
    fn show_footer(&self, ui: &mut Ui<Message>, frame: &mut Frame) {
+
        let state = self.state.lock().unwrap();
+
        match state.mode {
+
            ReviewMode::Edit { resume: _ } => {
+
                ui.shortcuts(
+
                    frame,
+
                    &[
+
                        ("c", "comment"),
+
                        ("a", "accept"),
+
                        ("r", "reject"),
+
                        ("?", "help"),
+
                        ("q", "quit"),
+
                    ],
+
                    '∙',
+
                );
+

+
                if ui.input_global(|key| key == Key::Char('?')) {
+
                    ui.send_message(Message::ShowHelp);
+
                }
+
                if ui.input_global(|key| key == Key::Char('c')) {
+
                    ui.send_message(Message::Comment);
+
                }
+
                if ui.input_global(|key| key == Key::Char('a')) {
+
                    ui.send_message(Message::Accept);
+
                }
+
                if ui.input_global(|key| key == Key::Char('r')) {
+
                    ui.send_message(Message::Reject);
+
                }
+
            }
+
            ReviewMode::Show => {
+
                ui.shortcuts(frame, &[("?", "help"), ("q", "quit")], '∙');
+

+
                if ui.input_global(|key| key == Key::Char('?')) {
+
                    ui.send_message(Message::ShowHelp);
+
                }
+
            }
+
        }
+
    }
}

impl<'a> Show<Message> for App<'a> {
@@ -455,31 +513,7 @@ impl<'a> Show<Message> for App<'a> {
                        }

                        self.show_context_bar(ui, frame);
-

-
                        ui.shortcuts(
-
                            frame,
-
                            &[
-
                                ("c", "comment"),
-
                                ("a", "accept"),
-
                                ("r", "reject"),
-
                                ("?", "help"),
-
                                ("q", "quit"),
-
                            ],
-
                            '∙',
-
                        );
-

-
                        if ui.input_global(|key| key == Key::Char('?')) {
-
                            ui.send_message(Message::ShowHelp);
-
                        }
-
                        if ui.input_global(|key| key == Key::Char('c')) {
-
                            ui.send_message(Message::Comment);
-
                        }
-
                        if ui.input_global(|key| key == Key::Char('a')) {
-
                            ui.send_message(Message::Accept);
-
                        }
-
                        if ui.input_global(|key| key == Key::Char('r')) {
-
                            ui.send_message(Message::Reject);
-
                        }
+
                        self.show_footer(ui, frame);
                    });
                }
                AppPage::Help => {
@@ -677,7 +711,9 @@ mod test {

            let hunks = ReviewBuilder::new(&node.repo).hunks(revision)?;

+
            let mode = ReviewMode::Edit { resume: false };
            let state = AppState::new(
+
                mode.clone(),
                node.repo.id,
                *patch.id(),
                patch.title().to_string(),
@@ -685,13 +721,7 @@ mod test {
                &hunks,
            );

-
            App::new(
-
                ReviewMode::Create,
-
                node.storage.clone(),
-
                review.clone(),
-
                hunks,
-
                state,
-
            )
+
            App::new(node.storage.clone(), review.clone(), hunks, state, mode)
        }

        pub fn draft_review<'a>(
modified bin/ui/items.rs
@@ -1108,6 +1108,7 @@ impl DiffLineIndex {
    }
}

+
/// Mention hunk header
impl From<&CodeLocation> for DiffLineIndex {
    fn from(location: &CodeLocation) -> Self {
        Self {