cli: Improve `rad inspect`

* Don't always require auth
* Fix `--refs` sub-command
This commit is contained in:
Alexis Sellier 2023-02-28 13:34:03 +01:00
parent 892f02bc43
commit 2a687502c5
No known key found for this signature in database
8 changed files with 234 additions and 106 deletions

View File

@ -0,0 +1,13 @@
The `rad inspect` command can be run without being authenticated with radicle:
```
$ rad self
✗ Self failed: Could not load radicle profile
✗ To setup your radicle profile, run `rad auth`.
```
```
$ rad inspect
rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
```

View File

@ -0,0 +1,70 @@
To display a repository's identifier, or *RID*, you may use the `rad inspect`
command from inside a working copy:
```
$ rad inspect
rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
```
As a shorthand, you can also simply use `rad .`:
```
$ rad .
rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji
```
It's also possible to display all of the repository's git references:
```
$ rad inspect --refs
.
`-- z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
`-- refs
|-- heads
| `-- master
`-- rad
|-- id
`-- sigrefs
```
Or display the repository identity's payload:
```
$ rad inspect --payload
{
"xyz.radicle.project": {
"defaultBranch": "master",
"description": "Radicle Heartwood Protocol & Stack",
"name": "heartwood"
}
}
```
Finally, the `--history` flag allows you to examine the identity document's
history:
```
$ rad inspect --history
commit 175267b8910895ba87760313af254c2900743912
blob d96f425412c9f8ad5d9a9a05c9831d0728e2338d
date Thu, 15 Dec 2022 17:28:04 +0000
Initialize Radicle
Rad-Signature: z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi z5nGqUvrmfiSyLjNCHWTWYvVMcPUZcvo9TxPKzEKXYBdSgUzbrqf1cYsmpGgbQvYunnsrLSsubEmxZaRdKM4quqQR
{
"delegates": [
"did:key:z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi"
],
"payload": {
"xyz.radicle.project": {
"defaultBranch": "master",
"description": "Radicle Heartwood Protocol & Stack",
"name": "heartwood"
}
},
"threshold": 1
}
```

View File

@ -18,35 +18,41 @@ use crate::terminal::args::{Args, Error, Help};
pub const HELP: Help = Help {
name: "inspect",
description: "Inspect an identity or project directory",
description: "Inspect a radicle repository",
version: env!("CARGO_PKG_VERSION"),
usage: r#"
Usage
rad inspect <path> [<option>...]
rad inspect <id> [<option>...]
rad inspect <rid> [<option>...]
rad inspect
Inspects the given path or ID. If neither is specified,
the current project is inspected.
Inspects the given path or RID. If neither is specified,
the current repository is inspected.
Options
--id Return the ID in simplified form
--payload Inspect the object's payload
--refs Inspect the object's refs on the local device (requires `tree`)
--history Show object's history
--id Return the repository identifier (RID)
--payload Inspect the repository's identity payload
--refs Inspect the repository's refs on the local device (requires `tree`)
--history Show the history of the repository identity document
--help Print help
"#,
};
#[derive(Default, Debug, Eq, PartialEq)]
pub enum Target {
Refs,
Payload,
History,
#[default]
Id,
}
#[derive(Default, Debug, Eq, PartialEq)]
pub struct Options {
pub id: Option<Id>,
pub refs: bool,
pub payload: bool,
pub history: bool,
pub id_only: bool,
pub target: Target,
}
impl Args for Options {
@ -55,10 +61,7 @@ impl Args for Options {
let mut parser = lexopt::Parser::from_args(args);
let mut id: Option<Id> = None;
let mut refs = false;
let mut payload = false;
let mut history = false;
let mut id_only = false;
let mut target = Target::default();
while let Some(arg) = parser.next()? {
match arg {
@ -66,16 +69,16 @@ impl Args for Options {
return Err(Error::Help.into());
}
Long("refs") => {
refs = true;
target = Target::Refs;
}
Long("payload") => {
payload = true;
target = Target::Payload;
}
Long("history") => {
history = true;
target = Target::History;
}
Long("id") => {
id_only = true;
target = Target::Id;
}
Value(val) if id.is_none() => {
let val = val.to_string_lossy();
@ -94,24 +97,11 @@ impl Args for Options {
}
}
Ok((
Options {
id,
payload,
history,
refs,
id_only,
},
vec![],
))
Ok((Options { id, target }, vec![]))
}
}
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let profile = ctx.profile()?;
let storage = &profile.storage;
let signer = term::signer(&profile)?;
let id = match options.id {
Some(id) => id,
None => {
@ -122,82 +112,94 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
}
};
if options.target == Target::Id {
term::info!("{}", term::format::highlight(id.urn()));
return Ok(());
}
let profile = ctx.profile()?;
let storage = &profile.storage;
let signer = term::signer(&profile)?;
let project = storage
.get(signer.public_key(), id)?
.context("No project with such `id` exists")?;
if options.refs {
let path = profile
.home
.storage()
.join(id.urn())
.join("refs")
.join("namespaces");
match options.target {
Target::Refs => {
let path = storage.path_of(&id).join("refs").join("namespaces");
Command::new("tree")
.current_dir(path)
.args(["--noreport", "--prune"])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()?
.wait()?;
} else if options.payload {
println!(
"{}",
colorizer().colorize_json_str(&serde_json::to_string_pretty(&project.payload)?)?
);
} else if options.history {
let repo = storage.repository(id)?;
let head = Doc::<Untrusted>::head(signer.public_key(), &repo)?;
let history = repo.revwalk(head)?;
for oid in history {
let oid = oid?.into();
let tip = repo.commit(oid)?;
let blob = Doc::<Unverified>::blob_at(oid, &repo)?;
let content: serde_json::Value = serde_json::from_slice(blob.content())?;
let timezone = if tip.time().sign() == '+' {
#[allow(deprecated)]
FixedOffset::east(tip.time().offset_minutes() * 60)
} else {
#[allow(deprecated)]
FixedOffset::west(tip.time().offset_minutes() * 60)
};
let time = DateTime::<Utc>::from(
std::time::UNIX_EPOCH + std::time::Duration::from_secs(tip.time().seconds() as u64),
)
.with_timezone(&timezone)
.to_rfc2822();
println!(
"{} {}",
term::format::yellow("commit"),
term::format::yellow(oid),
);
if let Ok(parent) = tip.parent_id(0) {
println!("parent {parent}");
}
println!("blob {}", blob.id());
println!("date {time}");
println!();
if let Some(msg) = tip.message() {
for line in msg.lines() {
term::indented(term::format::dim(line));
}
term::blank();
}
let json = colorizer().colorize_json_str(&serde_json::to_string_pretty(&content)?)?;
for line in json.lines() {
println!(" {line}");
}
println!();
Command::new("tree")
.current_dir(path)
.args(["--noreport", "--prune"])
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()?
.wait()?;
}
Target::Payload => {
println!(
"{}",
colorizer().colorize_json_str(&serde_json::to_string_pretty(&project.payload)?)?
);
}
Target::History => {
let repo = storage.repository(id)?;
let head = Doc::<Untrusted>::head(signer.public_key(), &repo)?;
let history = repo.revwalk(head)?;
for oid in history {
let oid = oid?.into();
let tip = repo.commit(oid)?;
let blob = Doc::<Unverified>::blob_at(oid, &repo)?;
let content: serde_json::Value = serde_json::from_slice(blob.content())?;
let timezone = if tip.time().sign() == '+' {
#[allow(deprecated)]
FixedOffset::east(tip.time().offset_minutes() * 60)
} else {
#[allow(deprecated)]
FixedOffset::west(tip.time().offset_minutes() * 60)
};
let time = DateTime::<Utc>::from(
std::time::UNIX_EPOCH
+ std::time::Duration::from_secs(tip.time().seconds() as u64),
)
.with_timezone(&timezone)
.to_rfc2822();
println!(
"{} {}",
term::format::yellow("commit"),
term::format::yellow(oid),
);
if let Ok(parent) = tip.parent_id(0) {
println!("parent {parent}");
}
println!("blob {}", blob.id());
println!("date {time}");
println!();
if let Some(msg) = tip.message() {
for line in msg.lines() {
if line.is_empty() {
println!();
} else {
term::indented(term::format::dim(line));
}
}
term::blank();
}
let json =
colorizer().colorize_json_str(&serde_json::to_string_pretty(&content)?)?;
for line in json.lines() {
println!(" {line}");
}
println!();
}
}
Target::Id => {
// Handled above.
}
} else if options.id_only {
term::info!("{}", term::format::highlight(id.urn()));
} else {
term::info!("{}", term::format::highlight(id));
}
Ok(())

View File

@ -19,6 +19,7 @@ use super::Error;
use super::{style, Paint};
pub const ERROR_PREFIX: Paint<&str> = Paint::red("");
pub const ERROR_HINT_PREFIX: Paint<&str> = Paint::yellow("");
pub const WARNING_PREFIX: Paint<&str> = Paint::yellow("!");
pub const TAB: &str = " ";
@ -151,7 +152,7 @@ pub fn fail(header: &str, error: &anyhow::Error) {
);
if let Some(Error::WithHint { hint, .. }) = error.downcast_ref::<Error>() {
println!("{} {}", Paint::yellow("×"), Paint::yellow(hint));
println!("{} {}", ERROR_HINT_PREFIX, Paint::yellow(hint));
blank();
}
}

View File

@ -96,6 +96,34 @@ fn rad_init() {
.unwrap();
}
#[test]
fn rad_inspect() {
let mut environment = Environment::new();
let profile = environment.profile("alice");
let working = tempfile::tempdir().unwrap();
// Setup a test repository.
fixtures::repository(working.path());
test(
"examples/rad-init.md",
working.path(),
Some(&profile.home),
[],
)
.unwrap();
test(
"examples/rad-inspect.md",
working.path(),
Some(&profile.home),
[],
)
.unwrap();
test("examples/rad-inspect-noauth.md", working.path(), None, []).unwrap();
}
#[test]
fn rad_checkout() {
let mut environment = Environment::new();

View File

@ -3,7 +3,7 @@ pub mod refs;
use std::collections::hash_map;
use std::ops::Deref;
use std::path::Path;
use std::path::{Path, PathBuf};
use std::{fmt, io};
use serde::{Deserialize, Serialize};
@ -262,6 +262,8 @@ pub trait ReadStorage {
/// Get the storage base path.
fn path(&self) -> &Path;
/// Get a repository's path.
fn path_of(&self, rid: &Id) -> PathBuf;
/// Get an identity document of a repository under a given remote.
fn get(
&self,
@ -380,6 +382,10 @@ where
self.deref().path()
}
fn path_of(&self, rid: &Id) -> PathBuf {
self.deref().path_of(rid)
}
fn contains(&self, rid: &Id) -> Result<bool, ProjectError> {
self.deref().contains(rid)
}

View File

@ -101,6 +101,10 @@ impl ReadStorage for Storage {
self.path.as_path()
}
fn path_of(&self, rid: &Id) -> PathBuf {
paths::repository(&self, rid)
}
fn contains(&self, rid: &Id) -> Result<bool, ProjectError> {
if paths::repository(&self, rid).exists() {
let _ = self.repository(*rid)?.head()?;

View File

@ -38,6 +38,10 @@ impl ReadStorage for MockStorage {
self.path.as_path()
}
fn path_of(&self, rid: &Id) -> PathBuf {
self.path().join(rid.canonical())
}
fn contains(&self, rid: &Id) -> Result<bool, ProjectError> {
Ok(self.inventory.contains_key(rid))
}