diff --git a/radicle-cli/examples/rad-patch.md b/radicle-cli/examples/rad-patch.md index cd083f92..664b58c1 100644 --- a/radicle-cli/examples/rad-patch.md +++ b/radicle-cli/examples/rad-patch.md @@ -41,19 +41,22 @@ It will now be listed as one of the project's open patches. ``` $ rad patch -╭─────────────────────────────────────────────────────────────────────────────────────────╮ -│ Define power requirements 191a14e R0 3e674d1 (flux-capacitor-power) ahead 1, behind 0 │ -├─────────────────────────────────────────────────────────────────────────────────────────┤ -│ ● opened by did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (you) [ ... ]│ -╰─────────────────────────────────────────────────────────────────────────────────────────╯ +╭──────────────────────────────────────────────────────────────────────────────────────────────╮ +│ ● ID Title Author Head + - Opened │ +├──────────────────────────────────────────────────────────────────────────────────────────────┤ +│ ● 191a14e Define power requirements z6MknSL…StBU8Vi (you) 3e674d1 +0 -0 4 months ago │ +╰──────────────────────────────────────────────────────────────────────────────────────────────╯ ``` ``` $ rad patch show 191a14e520f2eeff7c0e3ee0a5523c5217eecb89 -p ╭─────────────────────────────────────────────────────────────────────────────────────────╮ -│ Title Define power requirements │ -│ Patch 191a14e520f2eeff7c0e3ee0a5523c5217eecb89 │ -│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │ -│ Status open │ +│ Title Define power requirements │ +│ Patch 191a14e520f2eeff7c0e3ee0a5523c5217eecb89 │ +│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │ +│ Head 3e674d1a1df90807e934f9ae5da2591dd6848a33 │ +│ Branches flux-capacitor-power │ +│ Commits ahead 1, behind 0 │ +│ Status open │ │ │ │ See details. │ ├─────────────────────────────────────────────────────────────────────────────────────────┤ @@ -120,12 +123,20 @@ $ rad review 191a14e520f2eeff7c0e3ee0a5523c5217eecb89 --accept --no-message --no Showing the patch list now will reveal the favorable verdict: ``` -$ rad patch -╭──────────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Define power requirements 191a14e R1 27857ec (flux-capacitor-power, patch/191a14e) ahead 2, behind 0 │ -├──────────────────────────────────────────────────────────────────────────────────────────────────────┤ -│ ● opened by did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (you) [ ... ]│ -│ ↑ updated to b8f7bfbbb3c6a207b349e9f45bf535c706805871 (27857ec) [ ... ]│ -│ ✓ accepted by z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (you) [ ... ]│ -╰──────────────────────────────────────────────────────────────────────────────────────────────────────╯ +$ rad patch show 191a14e +╭─────────────────────────────────────────────────────────────────────────────────────────╮ +│ Title Define power requirements │ +│ Patch 191a14e520f2eeff7c0e3ee0a5523c5217eecb89 │ +│ Author did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi │ +│ Head 27857ec9eb04c69cacab516e8bf4b5fd36090f66 │ +│ Branches flux-capacitor-power, patch/191a14e │ +│ Commits ahead 2, behind 0 │ +│ Status open │ +│ │ +│ See details. │ +├─────────────────────────────────────────────────────────────────────────────────────────┤ +│ ● opened by did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (you) [ ... ]│ +│ ↑ updated to b8f7bfbbb3c6a207b349e9f45bf535c706805871 (27857ec) [ ... ]│ +│ ✓ accepted by z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (you) [ ... ]│ +╰─────────────────────────────────────────────────────────────────────────────────────────╯ ``` diff --git a/radicle-cli/examples/workflow/4-patching-contributor.md b/radicle-cli/examples/workflow/4-patching-contributor.md index f20da2a7..1a1661c7 100644 --- a/radicle-cli/examples/workflow/4-patching-contributor.md +++ b/radicle-cli/examples/workflow/4-patching-contributor.md @@ -41,17 +41,20 @@ It will now be listed as one of the project's open patches. ``` $ rad patch -╭─────────────────────────────────────────────────────────────────────────────────────────╮ -│ Define power requirements a07ef77 R0 3e674d1 (flux-capacitor-power) ahead 1, behind 0 │ -├─────────────────────────────────────────────────────────────────────────────────────────┤ -│ ● opened by did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk (you) [ ... ]│ -╰─────────────────────────────────────────────────────────────────────────────────────────╯ +╭──────────────────────────────────────────────────────────────────────────────────────────────╮ +│ ● ID Title Author Head + - Opened │ +├──────────────────────────────────────────────────────────────────────────────────────────────┤ +│ ● a07ef77 Define power requirements z6Mkt67…v4N1tRk (you) 3e674d1 +0 -0 4 months ago │ +╰──────────────────────────────────────────────────────────────────────────────────────────────╯ $ rad patch show a07ef7743a32a2e902672ea3526d1db6ee08108a ╭─────────────────────────────────────────────────────────────────────────────────────────╮ -│ Title Define power requirements │ -│ Patch a07ef7743a32a2e902672ea3526d1db6ee08108a │ -│ Author did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk │ -│ Status open │ +│ Title Define power requirements │ +│ Patch a07ef7743a32a2e902672ea3526d1db6ee08108a │ +│ Author did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk │ +│ Head 3e674d1a1df90807e934f9ae5da2591dd6848a33 │ +│ Branches flux-capacitor-power │ +│ Commits ahead 1, behind 0 │ +│ Status open │ │ │ │ See details. │ ├─────────────────────────────────────────────────────────────────────────────────────────┤ diff --git a/radicle-cli/examples/workflow/5-patching-maintainer.md b/radicle-cli/examples/workflow/5-patching-maintainer.md index d47de3ba..d735585d 100644 --- a/radicle-cli/examples/workflow/5-patching-maintainer.md +++ b/radicle-cli/examples/workflow/5-patching-maintainer.md @@ -26,12 +26,19 @@ $ git branch -r bob/flux-capacitor-power bob/master rad/master -$ rad patch +$ rad patch show a07ef77 ╭───────────────────────────────────────────────────────────────────────────────────╮ -│ Define power requirements a07ef77 R1 27857ec ahead 2, behind 0 │ +│ Title Define power requirements │ +│ Patch a07ef7743a32a2e902672ea3526d1db6ee08108a │ +│ Author did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk │ +│ Head 27857ec9eb04c69cacab516e8bf4b5fd36090f66 │ +│ Commits ahead 2, behind 0 │ +│ Status open │ +│ │ +│ See details. │ ├───────────────────────────────────────────────────────────────────────────────────┤ -│ ● opened by did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk [ ... ]│ -│ ↑ updated to 11483929d8714a92992229f65433e06288f3b760 (27857ec) [ ... ]│ +│ ● opened by did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk [ ... ] │ +│ ↑ updated to 11483929d8714a92992229f65433e06288f3b760 (27857ec) [ ... ] │ ╰───────────────────────────────────────────────────────────────────────────────────╯ ``` @@ -76,15 +83,23 @@ Fast-forward The patch is now merged and closed :). ``` -$ rad patch --merged -╭───────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Define power requirements a07ef77 R2 f6484e0 (flux-capacitor-power, master) ahead 3, behind 0 │ -├───────────────────────────────────────────────────────────────────────────────────────────────┤ -│ ● opened by did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk [ ... ]│ -│ ↑ updated to 11483929d8714a92992229f65433e06288f3b760 (27857ec) [ ... ]│ -│ ↑ updated to 0795d619232479e910f95bb9c873ee1ec305c43c (f6484e0) [ ... ]│ -│ ✓ merged by did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (you) [ ... ]│ -╰───────────────────────────────────────────────────────────────────────────────────────────────╯ +$ rad patch show a07ef77 +╭─────────────────────────────────────────────────────────────────────────────────────────╮ +│ Title Define power requirements │ +│ Patch a07ef7743a32a2e902672ea3526d1db6ee08108a │ +│ Author did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk │ +│ Head f6484e0f43e48a8983b9b39bf9bd4cd889f1d520 │ +│ Branches flux-capacitor-power, master │ +│ Commits ahead 3, behind 0 │ +│ Status merged │ +│ │ +│ See details. │ +├─────────────────────────────────────────────────────────────────────────────────────────┤ +│ ● opened by did:key:z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk [ ... ] │ +│ ↑ updated to 11483929d8714a92992229f65433e06288f3b760 (27857ec) [ ... ] │ +│ ↑ updated to 0795d619232479e910f95bb9c873ee1ec305c43c (f6484e0) [ ... ] │ +│ ✓ merged by did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi (you) [ ... ] │ +╰─────────────────────────────────────────────────────────────────────────────────────────╯ ``` To publish our new state to the network, we simply push: diff --git a/radicle-cli/src/commands/patch.rs b/radicle-cli/src/commands/patch.rs index 65060ff9..b7bcc39a 100644 --- a/radicle-cli/src/commands/patch.rs +++ b/radicle-cli/src/commands/patch.rs @@ -270,7 +270,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { create::run(&repository, &profile, &workdir, message.clone(), options)?; } Operation::List { filter } => { - list::run(&repository, &profile, Some(workdir), filter)?; + list::run(&repository, &profile, filter)?; } Operation::Show { patch_id, diff } => { let patch_id = patch_id.resolve(&repository.backend)?; diff --git a/radicle-cli/src/commands/patch/common.rs b/radicle-cli/src/commands/patch/common.rs index 626ab501..2b394e73 100644 --- a/radicle-cli/src/commands/patch/common.rs +++ b/radicle-cli/src/commands/patch/common.rs @@ -68,8 +68,23 @@ pub fn patch_merge_target_oid(target: MergeTarget, repository: &Repository) -> a } } +/// Get the diff stats between two commits. +pub fn diff_stats( + repo: &git::raw::Repository, + old: &Oid, + new: &Oid, +) -> Result { + let old = repo.find_commit(*old)?; + let new = repo.find_commit(*new)?; + let old_tree = old.tree()?; + let new_tree = new.tree()?; + let diff = repo.diff_tree_to_tree(Some(&old_tree), Some(&new_tree), None)?; + + diff.stats() +} + /// Create a human friendly message about git's sync status. -pub fn pretty_sync_status( +pub fn ahead_behind( repo: &git::raw::Repository, revision_oid: Oid, head_oid: Oid, @@ -89,35 +104,21 @@ pub fn pretty_sync_status( .item(behind)) } -/// Make a human friendly string for commit version information. -/// -/// For example ' (branch1[, branch2])'. -pub fn pretty_commit_version( - revision_oid: &Oid, - repo: Option<&git::raw::Repository>, -) -> anyhow::Result { - let oid = term::format::secondary(term::format::oid(*revision_oid)); - let mut line = term::Line::new(oid); +/// Get the branches that point to a commit. +pub fn branches(target: &Oid, repo: &git::raw::Repository) -> anyhow::Result> { let mut branches: Vec = vec![]; - if let Some(repo) = repo { - for r in repo.references()?.flatten() { - if !r.is_branch() { - continue; - } - if let (Some(oid), Some(name)) = (&r.target(), &r.shorthand()) { - if oid == revision_oid { - branches.push(name.to_string()); - }; - }; + for r in repo.references()?.flatten() { + if !r.is_branch() { + continue; } - }; - if !branches.is_empty() { - line.push(term::Label::space()); - line.push(term::format::yellow(format!("({})", branches.join(", ")))); + if let (Some(oid), Some(name)) = (&r.target(), &r.shorthand()) { + if oid == target { + branches.push(name.to_string()); + }; + }; } - - Ok(line) + Ok(branches) } #[inline] diff --git a/radicle-cli/src/commands/patch/list.rs b/radicle-cli/src/commands/patch/list.rs index 87083261..894d5add 100644 --- a/radicle-cli/src/commands/patch/list.rs +++ b/radicle-cli/src/commands/patch/list.rs @@ -1,13 +1,13 @@ use anyhow::anyhow; use radicle::cob::patch; -use radicle::cob::patch::{Patch, PatchId, Patches, Revision, Verdict}; -use radicle::git; +use radicle::cob::patch::{Patch, PatchId, Patches, Verdict}; use radicle::prelude::*; use radicle::profile::Profile; use radicle::storage::git::Repository; use crate::terminal as term; +use term::table::{Table, TableOptions}; use term::Element as _; use super::common; @@ -16,7 +16,6 @@ use super::common; pub fn run( repository: &Repository, profile: &Profile, - workdir: Option, filter: Option, ) -> anyhow::Result<()> { let me = *profile.id(); @@ -48,62 +47,89 @@ pub fn run( return Ok(()); } + let mut table = Table::<9, term::Line>::new(TableOptions { + spacing: 2, + border: Some(term::colors::FAINT), + ..TableOptions::default() + }); + + table.push([ + term::format::dim(String::from("●")).into(), + term::format::bold(String::from("ID")).into(), + term::format::bold(String::from("Title")).into(), + term::format::bold(String::from("Author")).into(), + term::format::bold(String::new()).into(), + term::format::bold(String::from("Head")).into(), + term::format::bold(String::from("+")).into(), + term::format::bold(String::from("-")).into(), + term::format::bold(String::from("Opened")).into(), + ]); + table.divider(); + + let mut errors = Vec::new(); for (id, patch) in &mut own { - widget(&me, id, patch, workdir.as_ref(), repository)?.print(); + match row(&me, id, patch, repository) { + Ok(r) => table.push(r), + Err(e) => errors.push((patch.title(), id, e.to_string())), + } } for (id, patch) in &mut other { - widget(profile.id(), id, patch, workdir.as_ref(), repository)?.print(); + match row(&me, id, patch, repository) { + Ok(r) => table.push(r), + Err(e) => errors.push((patch.title(), id, e.to_string())), + } + } + table.print(); + + if !errors.is_empty() { + for (title, id, error) in errors { + term::error(format!( + "{} Patch {title:?} ({id}) failed to load: {error}", + term::format::negative("Error:") + )); + } } Ok(()) } -pub fn header( - patch_id: &PatchId, - patch: &Patch, - workdir: Option<&git::raw::Repository>, - repository: &Repository, - revision: &Revision, -) -> anyhow::Result { - let target_head = common::patch_merge_target_oid(patch.target(), repository)?; - let header = term::Line::spaced([ - term::format::bold(patch.title()).into(), - term::format::highlight(term::format::cob(patch_id)).into(), - term::format::dim(format!("R{}", patch.version())).into(), - ]) - .space() - .extend(common::pretty_commit_version(&revision.head(), workdir)?) - .space() - .extend(common::pretty_sync_status( - repository.raw(), - revision.head().into(), - target_head, - )?); - - Ok(header) -} - -/// Patch widget. -pub fn widget<'a>( +/// Patch row. +pub fn row( whoami: &PublicKey, - patch_id: &PatchId, + id: &PatchId, patch: &Patch, - workdir: Option<&git::raw::Repository>, repository: &Repository, -) -> anyhow::Result> { +) -> anyhow::Result<[term::Line; 9]> { + let state = patch.state(); let (_, revision) = patch .latest() .ok_or_else(|| anyhow!("patch is malformed: no revisions found"))?; - let header = header(patch_id, patch, workdir, repository, revision)?; - let mut widget = term::VStack::default() - .child(header) - .divider() - .border(Some(term::colors::FAINT)); + let stats = common::diff_stats(repository.raw(), revision.base(), &revision.head())?; + let author = patch.author().id; - for line in timeline(whoami, patch_id, patch, repository)? { - widget.push(line); - } - Ok(widget) + Ok([ + match state { + patch::State::Open => term::format::positive("●").into(), + patch::State::Archived { .. } => term::format::yellow("●").into(), + patch::State::Draft => term::format::dim("●").into(), + patch::State::Merged { .. } => term::format::primary("✔").into(), + }, + term::format::tertiary(term::format::cob(id)).into(), + term::format::default(patch.title().to_owned()).into(), + term::format::did(&author).dim().into(), + if author.as_key() == whoami { + term::format::primary("(you)".to_owned()).into() + } else { + term::format::default(String::new()).into() + }, + term::format::secondary(term::format::oid(revision.head())).into(), + term::format::positive(format!("+{}", stats.insertions())).into(), + term::format::negative(format!("-{}", stats.deletions())).into(), + term::format::timestamp(&patch.timestamp()) + .dim() + .italic() + .into(), + ]) } pub fn timeline( diff --git a/radicle-cli/src/commands/patch/show.rs b/radicle-cli/src/commands/patch/show.rs index 2eed7563..65049e20 100644 --- a/radicle-cli/src/commands/patch/show.rs +++ b/radicle-cli/src/commands/patch/show.rs @@ -5,7 +5,7 @@ use radicle::git; use radicle::storage::git::Repository; use radicle_term::{ table::{Table, TableOptions}, - textarea, Element, Paint, VStack, + textarea, Element, VStack, }; use crate::terminal as term; @@ -13,12 +13,7 @@ use crate::terminal as term; use super::common::*; use super::*; -fn show_patch_diff( - patch: &patch::Patch, - storage: &Repository, - // TODO: Tell user which working copy branches point to the patch. - _workdir: &git::raw::Repository, -) -> anyhow::Result<()> { +fn show_patch_diff(patch: &patch::Patch, storage: &Repository) -> anyhow::Result<()> { let target_head = patch_merge_target_oid(patch.target(), storage)?; let base_oid = storage.raw().merge_base(target_head, **patch.head())?; let diff = format!("{}..{}", base_oid, patch.head()); @@ -46,32 +41,53 @@ pub fn run( let Some(patch) = patches.get(patch_id)? else { anyhow::bail!("Patch `{patch_id}` not found"); }; + let (_, revision) = patch + .latest() + .ok_or_else(|| anyhow!("patch is malformed: no revisions found"))?; let state = patch.state(); + let branches = common::branches(&revision.head(), workdir)?; + let target_head = common::patch_merge_target_oid(patch.target(), stored)?; + let ahead_behind = common::ahead_behind(stored.raw(), revision.head().into(), target_head)?; - let mut attrs = Table::<2, Paint>::new(TableOptions { + let mut attrs = Table::<2, term::Line>::new(TableOptions { spacing: 2, ..TableOptions::default() }); attrs.push([ - term::format::tertiary("Title".to_owned()), - term::format::bold(patch.title().to_owned()), + term::format::tertiary("Title".to_owned()).into(), + term::format::bold(patch.title().to_owned()).into(), ]); attrs.push([ - term::format::tertiary("Patch".to_owned()), - term::format::default(patch_id.to_string()), + term::format::tertiary("Patch".to_owned()).into(), + term::format::default(patch_id.to_string()).into(), ]); attrs.push([ - term::format::tertiary("Author".to_owned()), - term::format::default(patch.author().id().to_string()), + term::format::tertiary("Author".to_owned()).into(), + term::format::default(patch.author().id().to_string()).into(), ]); attrs.push([ - term::format::tertiary("Status".to_owned()), + term::format::tertiary("Head".to_owned()).into(), + term::format::secondary(revision.head().to_string()).into(), + ]); + if !branches.is_empty() { + attrs.push([ + term::format::tertiary("Branches".to_owned()).into(), + term::format::yellow(branches.join(", ")).into(), + ]); + } + attrs.push([ + term::format::tertiary("Commits".to_owned()).into(), + ahead_behind, + ]); + attrs.push([ + term::format::tertiary("Status".to_owned()).into(), match state { patch::State::Open => term::format::positive(state.to_string()), patch::State::Draft => term::format::dim(state.to_string()), patch::State::Archived => term::format::yellow(state.to_string()), patch::State::Merged => term::format::primary(state.to_string()), - }, + } + .into(), ]); let description = patch.description().trim(); @@ -95,7 +111,7 @@ pub fn run( if diff { term::blank(); - show_patch_diff(&patch, stored, workdir)?; + show_patch_diff(&patch, stored)?; term::blank(); } Ok(()) diff --git a/radicle-term/src/cell.rs b/radicle-term/src/cell.rs index d5572f57..eb646e71 100644 --- a/radicle-term/src/cell.rs +++ b/radicle-term/src/cell.rs @@ -1,6 +1,6 @@ use std::fmt::Display; -use super::Paint; +use super::{Element, Line, Paint}; use unicode_width::UnicodeWidthStr; @@ -14,8 +14,10 @@ pub trait Cell: Display { /// Cell display width in number of terminal columns. fn width(&self) -> usize; /// Truncate cell if longer than given width. Shows the delimiter if truncated. + #[must_use] fn truncate(&self, width: usize, delim: &str) -> Self::Truncated; /// Pad the cell so that it is the given width, while keeping the content left-aligned. + #[must_use] fn pad(&self, width: usize) -> Self::Padded; } @@ -42,6 +44,27 @@ impl Cell for Paint { } } +impl Cell for Line { + type Truncated = Line; + type Padded = Line; + + fn width(&self) -> usize { + ::size(self).cols + } + + fn pad(&self, width: usize) -> Self::Padded { + let mut line = self.clone(); + Line::pad(&mut line, width); + line + } + + fn truncate(&self, width: usize, delim: &str) -> Self::Truncated { + let mut line = self.clone(); + Line::truncate(&mut line, width, delim); + line + } +} + impl Cell for Paint<&str> { type Truncated = Paint; type Padded = Paint; diff --git a/radicle-term/src/colors.rs b/radicle-term/src/colors.rs index 2b93920f..71f6f102 100644 --- a/radicle-term/src/colors.rs +++ b/radicle-term/src/colors.rs @@ -2,3 +2,6 @@ use crate::ansi::Color; /// The faintest color; useful for borders and such. pub const FAINT: Color = Color::Fixed(236); + +/// Negative color, useful for errors. +pub const NEGATIVE: Color = Color::Red; diff --git a/radicle-term/src/element.rs b/radicle-term/src/element.rs index 772b8ef8..a8bf812c 100644 --- a/radicle-term/src/element.rs +++ b/radicle-term/src/element.rs @@ -157,9 +157,9 @@ impl IntoIterator for Line { } } -impl From