cli: Display patches like issues, in `rad patch`

We also make sure not to bail if one of the patches fails to load.
This commit is contained in:
Alexis Sellier 2023-04-25 13:10:50 +02:00
parent b809bd3d73
commit 3b20d45d80
No known key found for this signature in database
11 changed files with 237 additions and 139 deletions

View File

@ -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) [ ... ]│
╰─────────────────────────────────────────────────────────────────────────────────────────╯
```

View File

@ -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. │
├─────────────────────────────────────────────────────────────────────────────────────────┤

View File

@ -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:

View File

@ -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)?;

View File

@ -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<git::raw::DiffStats, git::raw::Error> {
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 '<oid> (branch1[, branch2])'.
pub fn pretty_commit_version(
revision_oid: &Oid,
repo: Option<&git::raw::Repository>,
) -> anyhow::Result<term::Line> {
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<Vec<String>> {
let mut branches: Vec<String> = 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]

View File

@ -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<git::raw::Repository>,
filter: Option<patch::State>,
) -> 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<term::Line> {
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<term::VStack<'a>> {
) -> 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(

View File

@ -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<String>>::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(())

View File

@ -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<String> {
}
}
impl Cell for Line {
type Truncated = Line;
type Padded = Line;
fn width(&self) -> usize {
<Self as Element>::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<String>;
type Padded = Paint<String>;

View File

@ -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;

View File

@ -157,9 +157,9 @@ impl IntoIterator for Line {
}
}
impl From<Label> for Line {
fn from(label: Label) -> Self {
Self { items: vec![label] }
impl<T: Into<Label>> From<T> for Line {
fn from(value: T) -> Self {
Self::new(value)
}
}
@ -218,19 +218,19 @@ mod test {
let line = Line::default().item("banana").item("peach").item("apple");
let mut actual = line.clone();
actual.truncate(9, "");
actual = actual.truncate(9, "");
assert_eq!(actual.to_string(), "bananape…");
let mut actual = line.clone();
actual.truncate(7, "");
actual = actual.truncate(7, "");
assert_eq!(actual.to_string(), "banana…");
let mut actual = line.clone();
actual.truncate(1, "");
actual = actual.truncate(1, "");
assert_eq!(actual.to_string(), "");
let mut actual = line;
actual.truncate(0, "");
actual = actual.truncate(0, "");
assert_eq!(actual.to_string(), "");
}
}

View File

@ -20,7 +20,7 @@ use std::fmt;
use crate as term;
use crate::cell::Cell;
use crate::{Color, Label, Line, Max, Paint, Size};
use crate::{Color, Line, Max, Paint, Size};
pub use crate::Element;
@ -82,7 +82,7 @@ impl<const W: usize, T> Default for Table<W, T> {
impl<const W: usize, T: Cell + fmt::Debug> Element for Table<W, T>
where
T::Padded: Into<Label>,
T::Padded: Into<Line>,
{
fn size(&self) -> Size {
Table::size(self)
@ -123,11 +123,11 @@ where
} else {
self.widths[i] + self.opts.spacing
};
line.push(cell.pad(pad));
line = line.extend(cell.pad(pad).into());
}
if let Some(width) = width {
line.truncate(width, "");
line = line.truncate(width, "");
}
if let Some(color) = border {
line.push(Paint::new("").fg(color));