From 78ba263d0a946c4b90235d562b56ceb7badfc965 Mon Sep 17 00:00:00 2001 From: Lorenz Leutgeb Date: Thu, 19 Jun 2025 13:12:46 +0200 Subject: [PATCH] radicle-cli/issue: Improve comment templating Writing a comment, only the most recent comment replied to is shown. Editing a comment, current contents are not prepared. Manual copy required. To improve: - Chase all replies to the root and quote them. Gracefully handle if this proces fails, i.e. a comment in the chain cannot be resolved. - If editing, copy the current contents. --- crates/radicle-cli/src/commands/issue.rs | 124 +++++++++++++++++++---- 1 file changed, 103 insertions(+), 21 deletions(-) diff --git a/crates/radicle-cli/src/commands/issue.rs b/crates/radicle-cli/src/commands/issue.rs index 4a853d19..75473520 100644 --- a/crates/radicle-cli/src/commands/issue.rs +++ b/crates/radicle-cli/src/commands/issue.rs @@ -11,6 +11,7 @@ use radicle::cob::common::{Label, Reaction}; use radicle::cob::issue::{CloseReason, State}; use radicle::cob::{issue, thread}; use radicle::crypto; +use radicle::git::Oid; use radicle::issue::cache::Issues as _; use radicle::node::device::Device; use radicle::node::NodeId; @@ -521,11 +522,18 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { message, reply_to, } => { + let reply_to = reply_to + .map(|rev| rev.resolve::(repo.raw())) + .transpose()?; + let signer = term::signer(&profile)?; let issue_id = id.resolve::(&repo.backend)?; let mut issue = issues.get_mut(&issue_id)?; - let (body, reply_to) = prompt_comment(message, reply_to, &issue, &repo)?; - let comment_id = issue.comment(body, reply_to, vec![], &signer)?; + + 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 options.quiet { term::print(comment_id); @@ -543,7 +551,18 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { let issue_id = id.resolve::(&repo.backend)?; let comment_id = comment_id.resolve(&repo.backend)?; let mut issue = issues.get_mut(&issue_id)?; - let (body, _) = prompt_comment(message, None, &issue, &repo)?; + + 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 options.quiet { @@ -892,29 +911,92 @@ where } /// Get a comment from the user, by prompting. -pub fn prompt_comment>( +pub fn prompt_comment( message: Message, - reply_to: Option, - issue: &issue::Issue, - repo: &R, -) -> anyhow::Result<(String, thread::CommentId)> { - let (root, r) = issue.root(); - let (reply_to, help) = if let Some(rev) = reply_to { - let id = rev.resolve::(repo.raw())?; - let parent = issue - .thread() - .comment(&id) - .ok_or(anyhow::anyhow!("comment '{rev}' not found"))?; + thread: &thread::Thread, + mut reply_to: Option, + edit: Option<&str>, +) -> anyhow::Result { + let (chase, missing) = { + let mut chase = Vec::with_capacity(thread.len()); + let mut missing = None; - (id, parent.body().trim()) - } else { - (*root, r.body().trim()) + 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 help = format!("\n{}\n", term::format::html::commented(help)); - let body = message.get(&help)?; + + 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\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, reply_to)) + 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'); + } }