Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cli/issue: reorganise comment actions
✗ CI failure Fintan Halpenny committed 7 months ago
commit c403100d580a776ed516471de660e5efe6d4130e
parent bc2ffc082e603fd4077cf76b2777b98384ed3c23
1 passed 1 failed (2 total) View logs
3 files changed +316 -189
modified crates/radicle-cli/src/commands/issue.rs
@@ -1,14 +1,14 @@
mod args;
mod cache;
+
mod comment;

use anyhow::Context as _;

use radicle::cob::common::Label;
use radicle::cob::issue::{CloseReason, State};
-
use radicle::cob::{issue, thread, Title};
+
use radicle::cob::{issue, Title};

use radicle::crypto;
-
use radicle::git::Oid;
use radicle::issue::cache::Issues as _;
use radicle::node::device::Device;
use radicle::node::NodeId;
@@ -20,7 +20,7 @@ use radicle::Profile;
use radicle::{cob, Node};

pub use args::Args;
-
use args::{Assigned, Command, StateArg};
+
use args::{Assigned, Command, CommentAction, StateArg};

use crate::git::Rev;
use crate::node;
@@ -28,7 +28,6 @@ use crate::terminal as term;
use crate::terminal::args::Error;
use crate::terminal::format::Author;
use crate::terminal::issue::Format;
-
use crate::terminal::patch::Message;
use crate::terminal::Element;

pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
@@ -42,21 +41,7 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {

    let should_announce = args.should_announce();
    let command = args.command.unwrap_or_default();
-
    let announce = should_announce
-
        && matches!(
-
            &command,
-
            Command::Open { .. }
-
                | Command::React { .. }
-
                | Command::State { .. }
-
                | Command::Delete { .. }
-
                | Command::Assign { .. }
-
                | Command::Label { .. }
-
                | Command::Edit { .. }
-
                // TODO(erikli): Remove special handling for `--edit` and
-
                // make it also announce.
-
                | Command::Comment { edit: None, .. }
-
        );
-

+
    let announce = should_announce && command.should_announce_for();
    let mut issues = term::cob::issues_mut(&profile, &repo)?;

    match command {
@@ -90,66 +75,37 @@ pub fn run(args: Args, ctx: impl term::Context) -> anyhow::Result<()> {
                &profile,
            )?;
        }
-
        Command::Comment {
-
            id,
-
            message,
-
            reply_to,
-
            edit: None,
-
        } => {
-
            let reply_to = reply_to
-
                .map(|rev| rev.resolve::<radicle::git::Oid>(repo.raw()))
-
                .transpose()?;
-

-
            let signer = term::signer(&profile)?;
-
            let issue_id = id.resolve::<cob::ObjectId>(&repo.backend)?;
-
            let mut issue = issues.get_mut(&issue_id)?;
-
            let (root_comment_id, _) = issue.root();
-
            let body = prompt_comment(message, issue.thread(), reply_to, None)?;
-
            let comment_id =
-
                issue.comment(body, reply_to.unwrap_or(*root_comment_id), vec![], &signer)?;
-

-
            if args.quiet {
-
                term::print(comment_id);
-
            } else {
-
                let comment = issue.thread().comment(&comment_id).unwrap();
-
                term::comment::widget(&comment_id, comment, &profile).print();
+
        Command::Comment(c) => match CommentAction::from(c) {
+
            CommentAction::Comment { id, message } => {
+
                comment::comment(&profile, &repo, &mut issues, id, message, None, args.quiet)?;
            }
-
        }
-
        Command::Comment {
-
            id,
-
            message,
-
            reply_to: None,
-
            edit: Some(comment_id),
-
        } => {
-
            let signer = term::signer(&profile)?;
-
            let issue_id = id.resolve::<cob::ObjectId>(&repo.backend)?;
-
            let comment_id = comment_id.resolve(&repo.backend)?;
-
            let mut issue = issues.get_mut(&issue_id)?;
-

-
            let comment = issue
-
                .thread()
-
                .comment(&comment_id)
-
                .ok_or(anyhow::anyhow!("comment '{comment_id}' not found"))?;
-

-
            let body = prompt_comment(
+
            CommentAction::Reply {
+
                id,
                message,
-
                issue.thread(),
-
                comment.reply_to(),
-
                Some(comment.body()),
-
            )?;
-

-
            issue.edit_comment(comment_id, body, vec![], &signer)?;
-

-
            if args.quiet {
-
                term::print(comment_id);
-
            } else {
-
                let comment = issue.thread().comment(&comment_id).unwrap();
-
                term::comment::widget(&comment_id, comment, &profile).print();
-
            }
-
        }
-
        Command::Comment { .. } => {
-
            unreachable!("the argument '--reply-to' cannot be used with '--edit'");
-
        }
+
                reply_to,
+
            } => comment::comment(
+
                &profile,
+
                &repo,
+
                &mut issues,
+
                id,
+
                message,
+
                Some(reply_to),
+
                args.quiet,
+
            )?,
+
            CommentAction::Edit {
+
                id,
+
                message,
+
                to_edit,
+
            } => comment::edit(
+
                &profile,
+
                &repo,
+
                &mut issues,
+
                id,
+
                message,
+
                to_edit,
+
                args.quiet,
+
            )?,
+
        },
        Command::Show { id } => {
            let format = if args.header {
                term::issue::Format::Header
@@ -486,94 +442,3 @@ where

    Ok(issue)
}
-

-
/// Get a comment from the user, by prompting.
-
pub fn prompt_comment(
-
    message: Message,
-
    thread: &thread::Thread,
-
    mut reply_to: Option<Oid>,
-
    edit: Option<&str>,
-
) -> anyhow::Result<String> {
-
    let (chase, missing) = {
-
        let mut chase = Vec::with_capacity(thread.len());
-
        let mut missing = None;
-
        while let Some(id) = reply_to {
-
            if let Some(comment) = thread.comment(&id) {
-
                chase.push(comment);
-
                reply_to = comment.reply_to();
-
            } else {
-
                missing = reply_to;
-
                break;
-
            }
-
        }
-

-
        (chase, missing)
-
    };
-

-
    let quotes = if chase.is_empty() {
-
        ""
-
    } else {
-
        "Quotes (lines starting with '>') will be preserved. Please remove those that you do not intend to keep.\n"
-
    };
-

-
    let mut buffer = term::format::html::commented(format!("HTML comments, such as this one, are deleted before posting.\n{quotes}Saving an empty file aborts the operation.").as_str());
-
    buffer.push('\n');
-

-
    for comment in chase.iter().rev() {
-
        buffer.reserve(2);
-
        buffer.push('\n');
-
        comment_quoted(comment, &mut buffer);
-
    }
-

-
    if let Some(id) = missing {
-
        buffer.push('\n');
-
        buffer.push_str(
-
            term::format::html::commented(
-
                format!("The comment with ID {id} that was replied to could not be found.")
-
                    .as_str(),
-
            )
-
            .as_str(),
-
        );
-
    }
-

-
    if let Some(edit) = edit {
-
        if !chase.is_empty() {
-
            buffer.push_str(
-
                "\n<!-- The contents of the comment you are editing follow below this line. -->\n",
-
            );
-
        }
-

-
        buffer.reserve(2 + edit.len());
-
        buffer.push('\n');
-
        buffer.push_str(edit);
-
    }
-

-
    let body = message.get(&buffer)?;
-
    if body.is_empty() {
-
        anyhow::bail!("aborting operation due to empty comment");
-
    }
-

-
    Ok(body)
-
}
-

-
fn comment_quoted(comment: &thread::Comment, buffer: &mut String) {
-
    let body = comment.body();
-
    let lines = body.lines();
-
    let hint = {
-
        let (lower, upper) = lines.size_hint();
-
        upper.unwrap_or(lower)
-
    };
-

-
    buffer.push_str(format!("{} wrote:\n", comment.author()).as_str());
-
    buffer.reserve(body.len() + hint * 2);
-

-
    for line in lines {
-
        buffer.push('>');
-
        if !line.is_empty() {
-
            buffer.push(' ');
-
        }
-

-
        buffer.push_str(line);
-
        buffer.push('\n');
-
    }
-
}
modified crates/radicle-cli/src/commands/issue/args.rs
@@ -44,6 +44,7 @@ pub struct Args {
    #[clap(global = true)]
    pub(crate) no_announce: bool,
    #[arg(long)]
+
    /// Announce issue changes to the network
    #[arg(value_name = "announce", overrides_with("no_announce"), hide(true))]
    #[clap(global = true)]
    pub(crate) announce: bool,
@@ -66,7 +67,12 @@ pub struct Args {
}

impl Args {
-
    pub fn should_announce(&self) -> bool {
+
    /// Resolve the `--announce` and `--no-announce` flags. If both flags are
+
    /// equal then `true` is returned, since it is the default.
+
    ///
+
    /// Note that `clap` should make it impossible to have both flags set to
+
    /// `true.`
+
    pub(crate) fn should_announce(&self) -> bool {
        match (self.announce, self.no_announce) {
            (true, false) => true,
            (false, true) => false,
@@ -108,26 +114,7 @@ pub(crate) enum Command {
        storage: bool,
    },
    /// Add a comment to an issue
-
    Comment {
-
        /// ID of the issue
-
        id: Rev,
-

-
        /// The body of the comment
-
        #[arg(long, short)]
-
        #[arg(value_name = "MESSAGE")]
-
        message: Message,
-

-
        /// Optionally, the comment to reply to. If not specified, the comment
-
        /// will be in reply to the issue itself
-
        #[arg(long, value_name = "COMMENT_ID")]
-
        #[arg(conflicts_with = "edit")]
-
        reply_to: Option<Rev>,
-

-
        /// Edit a comment by specifying its ID
-
        #[arg(long, value_name = "COMMENT_ID")]
-
        #[arg(conflicts_with = "reply_to")]
-
        edit: Option<Rev>,
-
    },
+
    Comment(CommentArgs),
    /// Edit the title and description of an issue
    Edit {
        /// ID of the issue
@@ -223,6 +210,26 @@ pub(crate) enum Command {
    },
}

+
impl Command {
+
    /// Returns `true` if the changes made by the command should announce to the
+
    /// network.
+
    pub(crate) fn should_announce_for(&self) -> bool {
+
        match self {
+
            Command::Open { .. }
+
            | Command::React { .. }
+
            | Command::State { .. }
+
            | Command::Delete { .. }
+
            | Command::Assign { .. }
+
            | Command::Label { .. }
+
            | Command::Edit { .. } => true,
+
            // TODO(erikli): Remove special handling for `--edit` and
+
            // make it also announce.
+
            Command::Comment(args) => !args.is_edit(),
+
            _ => false,
+
        }
+
    }
+
}
+

impl Default for Command {
    fn default() -> Self {
        Self::List(ListArgs::default())
@@ -286,6 +293,95 @@ impl From<ListArgs> for Option<State> {
    }
}

+
/// Arguments for the [`Command::Comment`] subcommand.
+
#[derive(Parser, Debug)]
+
pub(crate) struct CommentArgs {
+
    /// ID of the issue
+
    id: Rev,
+

+
    /// The body of the comment
+
    #[arg(long, short)]
+
    #[arg(value_name = "MESSAGE")]
+
    message: Message,
+

+
    /// Optionally, the comment to reply to. If not specified, the comment
+
    /// will be in reply to the issue itself
+
    #[arg(long, value_name = "COMMENT_ID")]
+
    #[arg(conflicts_with = "edit")]
+
    reply_to: Option<Rev>,
+

+
    /// Edit a comment by specifying its ID
+
    #[arg(long, value_name = "COMMENT_ID")]
+
    #[arg(conflicts_with = "reply_to")]
+
    edit: Option<Rev>,
+
}
+

+
impl CommentArgs {
+
    // TODO(finto): this is only needed to avoid announcing edits for the time
+
    // being
+
    /// If the comment is editing an existing comment
+
    pub(crate) fn is_edit(&self) -> bool {
+
        self.edit.is_some()
+
    }
+
}
+

+
/// The action that should be performed based on the supplied [`CommentArgs`].
+
pub(crate) enum CommentAction {
+
    /// Comment to the main issue thread.
+
    Comment {
+
        /// The issue ID
+
        id: Rev,
+
        /// The message of the comment.
+
        message: Message,
+
    },
+
    /// Reply to a specific comment in the issue.
+
    Reply {
+
        /// The issue ID
+
        id: Rev,
+
        /// The message that is being used to reply to the comment.
+
        message: Message,
+
        /// The comment ID that is being replied to.
+
        reply_to: Rev,
+
    },
+
    /// Edit a specific comment in the issue.
+
    Edit {
+
        /// The issue ID
+
        id: Rev,
+
        /// The message that is being used to edit the comment.
+
        message: Message,
+
        /// The comment ID that is being edited.
+
        to_edit: Rev,
+
    },
+
}
+

+
impl From<CommentArgs> for CommentAction {
+
    fn from(
+
        CommentArgs {
+
            id,
+
            message,
+
            reply_to,
+
            edit,
+
        }: CommentArgs,
+
    ) -> Self {
+
        match (reply_to, edit) {
+
            (Some(_), Some(_)) => {
+
                unreachable!("the argument '--reply-to' cannot be used with '--edit'")
+
            }
+
            (Some(reply_to), None) => Self::Reply {
+
                id,
+
                message,
+
                reply_to,
+
            },
+
            (None, Some(to_edit)) => Self::Edit {
+
                id,
+
                message,
+
                to_edit,
+
            },
+
            (None, None) => Self::Comment { id, message },
+
        }
+
    }
+
}
+

/// Arguments for the [`Command::State`] subcommand.
#[derive(Parser, Debug)]
#[group(id = "state", required = true, multiple = false)]
added crates/radicle-cli/src/commands/issue/comment.rs
@@ -0,0 +1,166 @@
+
use radicle::cob::thread;
+
use radicle::storage::WriteRepository;
+
use radicle::Profile;
+
use radicle::{cob, git, issue, storage};
+

+
use crate::git::Rev;
+
use crate::terminal as term;
+
use crate::terminal::patch::Message;
+
use crate::terminal::Element as _;
+

+
pub(super) fn comment(
+
    profile: &Profile,
+
    repo: &storage::git::Repository,
+
    issues: &mut issue::Cache<
+
        issue::Issues<'_, storage::git::Repository>,
+
        cob::cache::Store<cob::cache::Write>,
+
    >,
+
    id: Rev,
+
    message: Message,
+
    reply_to: Option<Rev>,
+
    quiet: bool,
+
) -> Result<(), anyhow::Error> {
+
    let reply_to = reply_to
+
        .map(|rev| rev.resolve::<git::Oid>(repo.raw()))
+
        .transpose()?;
+
    let signer = term::signer(profile)?;
+
    let issue_id = id.resolve::<cob::ObjectId>(&repo.backend)?;
+
    let mut issue = issues.get_mut(&issue_id)?;
+
    let (root_comment_id, _) = issue.root();
+
    let body = prompt_comment(message, issue.thread(), reply_to, None)?;
+
    let comment_id = issue.comment(body, reply_to.unwrap_or(*root_comment_id), vec![], &signer)?;
+
    if quiet {
+
        term::print(comment_id);
+
    } else {
+
        let comment = issue.thread().comment(&comment_id).unwrap();
+
        term::comment::widget(&comment_id, comment, profile).print();
+
    }
+
    Ok(())
+
}
+

+
pub(super) fn edit(
+
    profile: &Profile,
+
    repo: &storage::git::Repository,
+
    issues: &mut issue::Cache<
+
        issue::Issues<'_, storage::git::Repository>,
+
        cob::cache::Store<cob::cache::Write>,
+
    >,
+
    id: Rev,
+
    message: Message,
+
    comment_id: Rev,
+
    quiet: bool,
+
) -> Result<(), anyhow::Error> {
+
    let signer = term::signer(profile)?;
+
    let issue_id = id.resolve::<cob::ObjectId>(&repo.backend)?;
+
    let comment_id = comment_id.resolve(&repo.backend)?;
+
    let mut issue = issues.get_mut(&issue_id)?;
+
    let comment = issue
+
        .thread()
+
        .comment(&comment_id)
+
        .ok_or(anyhow::anyhow!("comment '{comment_id}' not found"))?;
+
    let body = prompt_comment(
+
        message,
+
        issue.thread(),
+
        comment.reply_to(),
+
        Some(comment.body()),
+
    )?;
+
    issue.edit_comment(comment_id, body, vec![], &signer)?;
+
    if quiet {
+
        term::print(comment_id);
+
    } else {
+
        let comment = issue.thread().comment(&comment_id).unwrap();
+
        term::comment::widget(&comment_id, comment, profile).print();
+
    }
+
    Ok(())
+
}
+

+
/// Get a comment from the user, by prompting.
+
fn prompt_comment(
+
    message: Message,
+
    thread: &thread::Thread,
+
    mut reply_to: Option<git::Oid>,
+
    edit: Option<&str>,
+
) -> anyhow::Result<String> {
+
    let (chase, missing) = {
+
        let mut chase = Vec::with_capacity(thread.len());
+
        let mut missing = None;
+
        while let Some(id) = reply_to {
+
            if let Some(comment) = thread.comment(&id) {
+
                chase.push(comment);
+
                reply_to = comment.reply_to();
+
            } else {
+
                missing = reply_to;
+
                break;
+
            }
+
        }
+

+
        (chase, missing)
+
    };
+

+
    let quotes = if chase.is_empty() {
+
        ""
+
    } else {
+
        "Quotes (lines starting with '>') will be preserved. Please remove those that you do not intend to keep.\n"
+
    };
+

+
    let mut buffer = term::format::html::commented(format!("HTML comments, such as this one, are deleted before posting.\n{quotes}Saving an empty file aborts the operation.").as_str());
+
    buffer.push('\n');
+

+
    for comment in chase.iter().rev() {
+
        buffer.reserve(2);
+
        buffer.push('\n');
+
        comment_quoted(comment, &mut buffer);
+
    }
+

+
    if let Some(id) = missing {
+
        buffer.push('\n');
+
        buffer.push_str(
+
            term::format::html::commented(
+
                format!("The comment with ID {id} that was replied to could not be found.")
+
                    .as_str(),
+
            )
+
            .as_str(),
+
        );
+
    }
+

+
    if let Some(edit) = edit {
+
        if !chase.is_empty() {
+
            buffer.push_str(
+
                "\n<!-- The contents of the comment you are editing follow below this line. -->\n",
+
            );
+
        }
+

+
        buffer.reserve(2 + edit.len());
+
        buffer.push('\n');
+
        buffer.push_str(edit);
+
    }
+

+
    let body = message.get(&buffer)?;
+
    if body.is_empty() {
+
        anyhow::bail!("aborting operation due to empty comment");
+
    }
+

+
    Ok(body)
+
}
+

+
fn comment_quoted(comment: &thread::Comment, buffer: &mut String) {
+
    let body = comment.body();
+
    let lines = body.lines();
+
    let hint = {
+
        let (lower, upper) = lines.size_hint();
+
        upper.unwrap_or(lower)
+
    };
+

+
    buffer.push_str(format!("{} wrote:\n", comment.author()).as_str());
+
    buffer.reserve(body.len() + hint * 2);
+

+
    for line in lines {
+
        buffer.push('>');
+
        if !line.is_empty() {
+
            buffer.push(' ');
+
        }
+

+
        buffer.push_str(line);
+
        buffer.push('\n');
+
    }
+
}