Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cli: Improve `rad inspect`
Alexis Sellier committed 3 years ago
commit 2a687502c55803b6a20a77538fc5ef6b39d047f5
parent 892f02bc43e4e06764f08e842cff037c3abe6c58
8 files changed +230 -102
added radicle-cli/examples/rad-inspect-noauth.md
@@ -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
+
```
added radicle-cli/examples/rad-inspect.md
@@ -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
+
 }
+

+
```
modified radicle-cli/src/commands/inspect.rs
@@ -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");
-

-
        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();
-

+
    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()?;
+
        }
+
        Target::Payload => {
            println!(
-
                "{} {}",
-
                term::format::yellow("commit"),
-
                term::format::yellow(oid),
+
                "{}",
+
                colorizer().colorize_json_str(&serde_json::to_string_pretty(&project.payload)?)?
            );
-
            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));
+
        }
+
        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();
                }
-
                term::blank();
-
            }

-
            let json = colorizer().colorize_json_str(&serde_json::to_string_pretty(&content)?)?;
-
            for line in json.lines() {
-
                println!(" {line}");
+
                let json =
+
                    colorizer().colorize_json_str(&serde_json::to_string_pretty(&content)?)?;
+
                for line in json.lines() {
+
                    println!(" {line}");
+
                }
+
                println!();
            }
-
            println!();
        }
-
    } else if options.id_only {
-
        term::info!("{}", term::format::highlight(id.urn()));
-
    } else {
-
        term::info!("{}", term::format::highlight(id));
+
        Target::Id => {
+
            // Handled above.
+
        }
    }

    Ok(())
modified radicle-cli/src/terminal/io.rs
@@ -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();
    }
}
modified radicle-cli/tests/commands.rs
@@ -97,6 +97,34 @@ fn rad_init() {
}

#[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();
    let profile = environment.profile("alice");
modified radicle/src/storage.rs
@@ -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)
    }
modified radicle/src/storage/git.rs
@@ -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()?;
modified radicle/src/test/storage.rs
@@ -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))
    }