#![allow(clippy::or_fun_call)]
use std::collections::HashMap;
use std::ffi::OsString;
use std::path::Path;
use std::str::FromStr;
use anyhow::Context as _;
use chrono::prelude::*;
use radicle::identity::RepoId;
use radicle::identity::{DocAt, Identity};
use radicle::node::policy::SeedingPolicy;
use radicle::node::AliasStore as _;
use radicle::storage::git::{Repository, Storage};
use radicle::storage::refs::RefsAt;
use radicle::storage::{ReadRepository, ReadStorage};
use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help};
use crate::terminal::json;
use crate::terminal::Element;
pub const HELP: Help = Help {
name: "inspect",
description: "Inspect a Radicle repository",
version: env!("RADICLE_VERSION"),
usage: r#"
Usage
rad inspect <path> [<option>...]
rad inspect <rid> [<option>...]
rad inspect [<option>...]
Inspects the given path or RID. If neither is specified,
the current repository is inspected.
Options
--rid Return the repository identifier (RID)
--payload Inspect the repository's identity payload
--refs Inspect the repository's refs on the local device
--sigrefs Inspect the values of `rad/sigrefs` for all remotes of this repository
--identity Inspect the identity document
--visibility Inspect the repository's visibility
--delegates Inspect the repository's delegates
--policy Inspect the repository's seeding policy
--history Show the history of the repository identity document
--help Print help
"#,
};
#[derive(Default, Debug, Eq, PartialEq)]
pub enum Target {
Refs,
Payload,
Delegates,
Identity,
Visibility,
Sigrefs,
Policy,
History,
#[default]
RepoId,
}
#[derive(Default, Debug, Eq, PartialEq)]
pub struct Options {
pub rid: Option<RepoId>,
pub target: Target,
}
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 rid: Option<RepoId> = None;
let mut target = Target::default();
while let Some(arg) = parser.next()? {
match arg {
Long("help") | Short('h') => {
return Err(Error::Help.into());
}
Long("refs") => {
target = Target::Refs;
}
Long("payload") => {
target = Target::Payload;
}
Long("policy") => {
target = Target::Policy;
}
Long("delegates") => {
target = Target::Delegates;
}
Long("history") => {
target = Target::History;
}
Long("identity") => {
target = Target::Identity;
}
Long("sigrefs") => {
target = Target::Sigrefs;
}
Long("rid") => {
target = Target::RepoId;
}
Long("visibility") => {
target = Target::Visibility;
}
Value(val) if rid.is_none() => {
let val = val.to_string_lossy();
if let Ok(val) = RepoId::from_str(&val) {
rid = Some(val);
} else {
rid = radicle::rad::at(Path::new(val.as_ref()))
.map(|(_, id)| Some(id))
.context("Supplied argument is not a valid path")?;
}
}
_ => return Err(anyhow::anyhow!(arg.unexpected())),
}
}
Ok((Options { rid, target }, vec![]))
}
}
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let rid = match options.rid {
Some(rid) => rid,
None => radicle::rad::cwd()
.map(|(_, rid)| rid)
.context("Current directory is not a Radicle repository")?,
};
if options.target == Target::RepoId {
term::info!("{}", term::format::highlight(rid.urn()));
return Ok(());
}
let profile = ctx.profile()?;
let storage = &profile.storage;
match options.target {
Target::Refs => {
let (repo, _) = repo(rid, storage)?;
refs(&repo)?;
}
Target::Payload => {
let (_, doc) = repo(rid, storage)?;
json::to_pretty(&doc.payload(), Path::new("radicle.json"))?.print();
}
Target::Identity => {
let (_, doc) = repo(rid, storage)?;
json::to_pretty(&*doc, Path::new("radicle.json"))?.print();
}
Target::Sigrefs => {
let (repo, _) = repo(rid, storage)?;
for remote in repo.remote_ids()? {
let remote = remote?;
let refs = RefsAt::new(&repo, remote)?;
println!(
"{:<48} {}",
term::format::tertiary(remote.to_human()),
term::format::secondary(refs.at)
);
}
}
Target::Policy => {
let policies = profile.policies()?;
let seed = policies.seed_policy(&rid)?;
match seed.policy {
SeedingPolicy::Allow { scope } => {
println!(
"Repository {} is {} with scope {}",
term::format::tertiary(&rid),
term::format::positive("being seeded"),
term::format::dim(format!("`{scope}`"))
);
}
SeedingPolicy::Block => {
println!(
"Repository {} is {}",
term::format::tertiary(&rid),
term::format::negative("not being seeded"),
);
}
}
}
Target::Delegates => {
let (_, doc) = repo(rid, storage)?;
let aliases = profile.aliases();
for did in doc.delegates().iter() {
if let Some(alias) = aliases.alias(did) {
println!(
"{} {}",
term::format::tertiary(&did),
term::format::parens(term::format::dim(alias))
);
} else {
println!("{}", term::format::tertiary(&did));
}
}
}
Target::Visibility => {
let (_, doc) = repo(rid, storage)?;
println!("{}", term::format::visibility(doc.visibility()));
}
Target::History => {
let (repo, _) = repo(rid, storage)?;
let identity = Identity::load(&repo)?;
let head = repo.identity_head()?;
let history = repo.revwalk(head)?;
for oid in history {
let oid = oid?.into();
let tip = repo.commit(oid)?;
let Some(revision) = identity.revision(&tip.id().into()) else {
continue;
};
if !revision.is_accepted() {
continue;
}
let doc = &revision.doc;
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 {}", revision.blob);
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();
}
for line in json::to_pretty(&doc, Path::new("radicle.json"))? {
println!(" {line}");
}
println!();
}
}
Target::RepoId => {
}
}
Ok(())
}
fn repo(rid: RepoId, storage: &Storage) -> anyhow::Result<(Repository, DocAt)> {
let repo = storage
.repository(rid)
.context("No repository with the given RID exists")?;
let doc = repo.identity_doc()?;
Ok((repo, doc))
}
fn refs(repo: &radicle::storage::git::Repository) -> anyhow::Result<()> {
let mut refs = Vec::new();
for r in repo.references()? {
let r = r?;
if let Some(namespace) = r.namespace {
refs.push(format!("{}/{}", namespace, r.name));
}
}
print!("{}", tree(refs));
Ok(())
}
fn tree(mut refs: Vec<String>) -> String {
refs.sort();
let mut refs_expanded: Vec<Vec<String>> = Vec::new();
let mut ref_entries: HashMap<Vec<String>, usize> = HashMap::new();
let mut last: Vec<String> = Vec::new();
for r in refs {
let r: Vec<String> = r.split('/').map(|s| s.to_string()).collect();
for (i, v) in r.iter().enumerate() {
let last_v = last.get(i);
if Some(v) != last_v {
last = r.clone().iter().take(i + 1).map(String::from).collect();
refs_expanded.push(last.clone());
let mut dir = last.clone();
dir.pop();
if dir.is_empty() {
continue;
}
if let Some(num) = ref_entries.get_mut(&dir) {
*num += 1;
} else {
ref_entries.insert(dir, 1);
}
}
}
}
let mut tree = String::default();
for mut ref_components in refs_expanded {
let name = ref_components.pop().expect("non-empty vector");
if ref_components.is_empty() {
tree.push_str(&format!("{name}\n"));
continue;
}
for i in 1..ref_components.len() {
let parent: Vec<String> = ref_components.iter().take(i).cloned().collect();
let num = ref_entries.get(&parent).unwrap_or(&0);
if *num == 0 {
tree.push_str(" ");
} else {
tree.push_str("│ ");
}
}
if let Some(num) = ref_entries.get_mut(&ref_components) {
if *num == 1 {
tree.push_str(&format!("└── {name}\n"));
} else {
tree.push_str(&format!("├── {name}\n"));
}
*num -= 1;
}
}
tree
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_tree() {
let arg = vec![
String::from("z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/refs/heads/master"),
String::from("z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/refs/rad/id"),
String::from("z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi/refs/rad/sigrefs"),
];
let exp = r#"
z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi
└── refs
├── heads
│ └── master
└── rad
├── id
└── sigrefs
"#
.trim_start();
assert_eq!(tree(arg), exp);
assert_eq!(tree(vec![String::new()]), "\n");
}
}