Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
Add `rad-inspect` command
xphoniex committed 3 years ago
commit 80401f89ae078d0414afc48d7f45da8eec246329
parent 507bc2237a80ba9839e33ec1b465d2a44fb4bbbb
8 files changed +236 -0
modified Cargo.lock
@@ -979,6 +979,17 @@ dependencies = [
]

[[package]]
+
name = "json-color"
+
version = "0.7.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e6dc8c55175cad7234a98cc3e31ba3009e276800271692ed3ad2c2f1c574b6e8"
+
dependencies = [
+
 "colored",
+
 "serde",
+
 "serde_json",
+
]
+

+
[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1498,9 +1509,11 @@ name = "radicle-cli"
version = "0.8.0"
dependencies = [
 "anyhow",
+
 "chrono",
 "console",
 "dialoguer",
 "indicatif",
+
 "json-color",
 "lexopt",
 "log",
 "radicle",
modified radicle-cli/Cargo.toml
@@ -7,9 +7,11 @@ edition = "2021"

[dependencies]
anyhow = { version = "1" }
+
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
console = { version = "0.15" }
dialoguer = { version = "0.10.0" }
indicatif = { version = "0.16.2" }
+
json-color = { version = "0.7" }
lexopt = { version = "0.2" }
log = { version = "0.4", features = ["std"] }
serde_json = { version = "1" }
modified radicle-cli/src/commands.rs
@@ -10,6 +10,8 @@ pub mod rad_edit;
pub mod rad_help;
#[path = "commands/init.rs"]
pub mod rad_init;
+
#[path = "commands/inspect.rs"]
+
pub mod rad_inspect;
#[path = "commands/ls.rs"]
pub mod rad_ls;
#[path = "commands/self.rs"]
modified radicle-cli/src/commands/help.rs
@@ -21,6 +21,7 @@ const COMMANDS: &[Help] = &[
    rad_untrack::HELP,
    rad_ls::HELP,
    rad_edit::HELP,
+
    rad_inspect::HELP,
    HELP,
];

added radicle-cli/src/commands/inspect.rs
@@ -0,0 +1,201 @@
+
#![allow(clippy::or_fun_call)]
+
use std::ffi::OsString;
+
use std::path::{Path, PathBuf};
+
use std::process::{Command, Stdio};
+
use std::str::FromStr;
+

+
use anyhow::{anyhow, Context as _};
+
use chrono::prelude::*;
+
use json_color::{Color, Colorizer};
+

+
use radicle::identity::project::{Doc, Untrusted};
+
use radicle::identity::Id;
+
use radicle::storage::{ReadRepository, ReadStorage, WriteStorage};
+

+
use crate::terminal as term;
+
use crate::terminal::args::{Args, Error, Help};
+

+
pub const HELP: Help = Help {
+
    name: "inspect",
+
    description: "Inspect a radicle identity or project directory",
+
    version: env!("CARGO_PKG_VERSION"),
+
    usage: r#"
+
Usage
+

+
    rad inspect <path> [<option>...]
+
    rad inspect <id>   [<option>...]
+
    rad inspect
+

+
    Inspects the given path or ID. If neither is specified,
+
    the current project 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
+
    --help      Print help
+
"#,
+
};
+

+
#[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,
+
}
+

+
impl Args for Options {
+
    fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
+
        use lexopt::prelude::*;
+

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

+
        while let Some(arg) = parser.next()? {
+
            match arg {
+
                Long("help") => {
+
                    return Err(Error::Help.into());
+
                }
+
                Long("refs") => {
+
                    refs = true;
+
                }
+
                Long("payload") => {
+
                    payload = true;
+
                }
+
                Long("history") => {
+
                    history = true;
+
                }
+
                Long("id") => {
+
                    id_only = true;
+
                }
+
                Value(val) if id.is_none() => {
+
                    let val = val.to_string_lossy();
+

+
                    if let Ok(val) = Id::from_str(&val) {
+
                        id = Some(val);
+
                    } else if let Ok(val) = PathBuf::from_str(&val) {
+
                        id = radicle::rad::repo(val)
+
                            .map(|(_, id)| Some(id))
+
                            .context("Supplied argument is not a valid `path`")?;
+
                    } else {
+
                        return Err(anyhow!("invalid `path` or `id` '{}'", val));
+
                    }
+
                }
+
                _ => return Err(anyhow::anyhow!(arg.unexpected())),
+
            }
+
        }
+

+
        Ok((
+
            Options {
+
                id,
+
                payload,
+
                history,
+
                refs,
+
                id_only,
+
            },
+
            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 => {
+
            let (_, id) = radicle::rad::repo(Path::new("."))
+
                .context("Current directory is not a Radicle project")?;
+

+
            id
+
        }
+
    };
+

+
    let project = storage
+
        .get(signer.public_key(), id)?
+
        .context("No project with such `id` exists")?;
+

+
    if options.refs {
+
        let path = profile
+
            .paths()
+
            .storage()
+
            .join(id.to_human())
+
            .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)?.collect::<Vec<_>>();
+
        let revision = history.len() as usize;
+

+
        for (counter, oid) in history.into_iter().rev().enumerate() {
+
            let oid = oid?.into();
+
            let tip = repo.commit(oid)?;
+
            let blob = Doc::blob_at(oid, &repo)?;
+
            let content: serde_json::Value = serde_json::from_slice(blob.content())?;
+
            let timezone = if tip.time().sign() == '+' {
+
                FixedOffset::east(tip.time().offset_minutes() * 60)
+
            } else {
+
                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();
+

+
            print!(
+
                "{}",
+
                term::TextBox::new(format!(
+
                    "commit {}\nblob   {}\ndate   {}\n\n{}",
+
                    term::format::yellow(oid),
+
                    term::format::dim(blob.id()),
+
                    term::format::dim(time),
+
                    colorizer().colorize_json_str(&serde_json::to_string_pretty(&content)?)?,
+
                ))
+
                .first(counter == 0)
+
                .last(counter + 1 == revision)
+
            );
+
        }
+
    } else if options.id_only {
+
        term::info!("{}", term::format::highlight(id.to_human()));
+
    } else {
+
        term::info!("{}", term::format::highlight(id));
+
    }
+

+
    Ok(())
+
}
+

+
// Used for JSON Colorizing
+
fn colorizer() -> Colorizer {
+
    Colorizer::new()
+
        .null(Color::Cyan)
+
        .boolean(Color::Yellow)
+
        .number(Color::Magenta)
+
        .string(Color::Green)
+
        .key(Color::Blue)
+
        .build()
+
}
modified radicle-cli/src/main.rs
@@ -174,6 +174,14 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
                args.to_vec(),
            );
        }
+
        "inspect" => {
+
            term::run_command_args::<rad_inspect::Options, _>(
+
                rad_inspect::HELP,
+
                "Inspect",
+
                rad_inspect::run,
+
                args.to_vec(),
+
            );
+
        }
        _ => {
            let exe = format!("{}-{}", NAME, exe);
            let status = process::Command::new(exe.clone()).args(args).status();
modified radicle/src/identity/project.rs
@@ -108,6 +108,7 @@ pub struct Doc<V> {
    pub delegates: NonEmpty<Delegate>,
    pub threshold: usize,

+
    #[serde(skip)]
    verified: PhantomData<V>,
}

modified radicle/src/rad.rs
@@ -332,6 +332,14 @@ pub fn cwd() -> Result<(git2::Repository, Id), RemoteError> {
    Ok((repo, id))
}

+
/// Get the repository of project in specified directory
+
pub fn repo(path: impl AsRef<Path>) -> Result<(git2::Repository, Id), RemoteError> {
+
    let repo = git2::Repository::open(path)?;
+
    let (_, id) = remote(&repo)?;
+

+
    Ok((repo, id))
+
}
+

#[cfg(test)]
mod tests {
    use std::collections::HashMap;