cli: New `cob` command for debugging

Implement a `rad cob show` command for debugging COBs.
Prints out the full history without decoding the ops.
This commit is contained in:
cloudhead 2023-08-16 14:16:30 +02:00
parent 95a3146885
commit a922ecf3ce
No known key found for this signature in database
10 changed files with 226 additions and 4 deletions

View File

@ -6,6 +6,8 @@ pub mod rad_auth;
pub mod rad_checkout;
#[path = "commands/clone.rs"]
pub mod rad_clone;
#[path = "commands/cob.rs"]
pub mod rad_cob;
#[path = "commands/comment.rs"]
pub mod rad_comment;
#[path = "commands/delegate.rs"]

View File

@ -0,0 +1,140 @@
use std::ffi::OsString;
use std::str::FromStr;
use anyhow::anyhow;
use chrono::prelude::*;
use radicle::cob;
use radicle::prelude::Id;
use radicle::storage::ReadStorage;
use crate::git::Rev;
use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help};
pub const HELP: Help = Help {
name: "cob",
description: "Manage collaborative objects",
version: env!("CARGO_PKG_VERSION"),
usage: r#"
Usage
rad cob <command> [<option>...]
rad cob show --repo <rid> --type <typename> --object <oid>
Commands
show Show a COB as raw operations
Options
--help Print help
"#,
};
enum Operation {
Show,
}
pub struct Options {
rid: Id,
op: Operation,
type_name: cob::TypeName,
oid: Rev,
}
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 op: Option<Operation> = None;
let mut type_name: Option<cob::TypeName> = None;
let mut oid: Option<Rev> = None;
let mut rid: Option<Id> = None;
while let Some(arg) = parser.next()? {
match arg {
Value(val) if op.is_none() => match val.to_string_lossy().as_ref() {
"s" | "show" => op = Some(Operation::Show),
unknown => anyhow::bail!("unknown operation '{unknown}'"),
},
Long("type") | Short('t') => {
let v = parser.value()?;
let v = term::args::string(&v);
let v = cob::TypeName::from_str(&v)?;
type_name = Some(v);
}
Long("object") => {
let v = parser.value()?;
let v = term::args::string(&v);
oid = Some(Rev::from(v));
}
Long("repo") => {
let v = parser.value()?;
let v = term::args::rid(&v)?;
rid = Some(v);
}
Long("help") | Short('h') => {
return Err(Error::Help.into());
}
_ => return Err(anyhow::anyhow!(arg.unexpected())),
}
}
Ok((
Options {
op: op.ok_or_else(|| anyhow!("a command must be specified"))?,
oid: oid
.ok_or_else(|| anyhow!("an object id must be specified with `--object`"))?,
rid: rid
.ok_or_else(|| anyhow!("a repository id must be specified with `--repo`"))?,
type_name: type_name
.ok_or_else(|| anyhow!("an object type must be specified with `--type`"))?,
},
vec![],
))
}
}
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let profile = ctx.profile()?;
let storage = &profile.storage;
let repo = storage.repository(options.rid)?;
match options.op {
Operation::Show => {
let oid = options.oid.resolve(&repo.backend)?;
let ops = cob::store::ops(&oid, &options.type_name, &repo)?;
for op in ops.into_iter().rev() {
let time = DateTime::<Utc>::from(
std::time::UNIX_EPOCH + std::time::Duration::from_secs(op.timestamp.as_secs()),
)
.to_rfc2822();
term::print(term::format::yellow(format!("commit {}", op.id)));
for parent in op.parents {
term::print(format!("parent {}", parent));
}
term::print(format!("author {}", op.author));
term::print(format!("date {}", time));
term::blank();
for action in op.actions {
let obj: serde_json::Value = serde_json::from_slice(&action)?;
let val = serde_json::to_string_pretty(&obj)?;
for line in val.lines() {
term::indented(term::format::dim(line));
}
term::blank();
}
}
}
}
Ok(())
}

View File

@ -129,6 +129,14 @@ fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>>
args.to_vec(),
);
}
"cob" => {
term::run_command_args::<rad_cob::Options, _>(
rad_cob::HELP,
"Cob",
rad_cob::run,
args.to_vec(),
);
}
"comment" => {
term::run_command_args::<rad_comment::Options, _>(
rad_comment::HELP,

View File

@ -101,6 +101,12 @@ impl ChangeGraph {
change.signature.key,
change.resource,
change.contents().clone(),
change
.parents
.iter()
.cloned()
.map(|oid| oid.into())
.collect(),
change.timestamp,
change.manifest.clone(),
);

View File

@ -1,5 +1,5 @@
// Copyright © 2021 The Radicle Link Contributors
#![allow(clippy::too_many_arguments)]
use std::{cmp::Ordering, collections::BTreeSet, ops::ControlFlow};
use git_ext::Oid;
@ -53,6 +53,7 @@ impl History {
actor,
resource,
contents,
parents: vec![],
timestamp,
manifest,
};
@ -114,6 +115,7 @@ impl History {
new_actor: PublicKey,
new_resource: Oid,
new_contents: Contents,
new_parents: Vec<EntryId>,
new_timestamp: Timestamp,
manifest: Manifest,
) where
@ -126,6 +128,7 @@ impl History {
new_actor,
new_resource,
new_contents,
new_parents,
new_timestamp,
manifest,
);

View File

@ -86,6 +86,8 @@ pub struct Entry {
/// The content-address for the resource this entry lives under.
/// If the resource was updated, this should point to its latest version.
pub(super) resource: Oid,
/// Parent entries.
pub(super) parents: Vec<EntryId>,
/// The contents of this entry.
pub(super) contents: Contents,
/// The entry timestamp, as seconds since epoch.
@ -100,6 +102,7 @@ impl Entry {
actor: PublicKey,
resource: Oid,
contents: Contents,
parents: Vec<EntryId>,
timestamp: Timestamp,
manifest: Manifest,
) -> Self
@ -112,6 +115,7 @@ impl Entry {
resource,
contents,
timestamp,
parents,
manifest,
}
}
@ -121,6 +125,11 @@ impl Entry {
self.resource
}
/// Parent entries.
pub fn parents(&self) -> &[EntryId] {
&self.parents
}
/// The public key of the actor.
pub fn actor(&self) -> &PublicKey {
&self.actor

View File

@ -3,7 +3,10 @@
use git_ext::Oid;
use nonempty::NonEmpty;
use crate::{change, change_graph::ChangeGraph, CollaborativeObject, ObjectId, Store, TypeName};
use crate::{
change, change_graph::ChangeGraph, history::EntryId, CollaborativeObject, ObjectId, Store,
TypeName,
};
use super::error;
@ -14,6 +17,8 @@ pub struct Updated {
pub head: Oid,
/// The newly updated collaborative object.
pub object: CollaborativeObject,
/// Entry parents.
pub parents: Vec<EntryId>,
}
/// The data required to update an object
@ -90,11 +95,14 @@ where
.update(identifier, typename, &object_id, &change)
.map_err(|err| error::Update::Refs { err: Box::new(err) })?;
let parents: Vec<EntryId> = change.parents.into_iter().map(|oid| oid.into()).collect();
object.history.extend(
change.id,
change.signature.key,
change.resource,
change.contents,
parents.clone(),
change.timestamp,
change.manifest,
);
@ -102,5 +110,6 @@ where
Ok(Updated {
object,
head: change.id,
parents,
})
}

View File

@ -34,6 +34,8 @@ pub struct Op<A> {
pub author: ActorId,
/// Timestamp of this operation.
pub timestamp: Timestamp,
/// Parent operations.
pub parents: Vec<EntryId>,
/// Head of identity document committed to by this operation.
pub identity: git::Oid,
/// Object manifest.
@ -66,6 +68,7 @@ impl<A> Op<A> {
actions: actions.into(),
author,
timestamp: timestamp.into(),
parents: vec![],
identity,
manifest,
}
@ -76,6 +79,20 @@ impl<A> Op<A> {
}
}
impl From<Entry> for Op<Vec<u8>> {
fn from(entry: Entry) -> Self {
Self {
id: *entry.id(),
actions: entry.contents().clone(),
author: *entry.actor(),
parents: entry.parents().to_owned(),
timestamp: Timestamp::from_secs(entry.timestamp()),
identity: entry.resource(),
manifest: entry.manifest().clone(),
}
}
}
impl<'a, A> TryFrom<&'a Entry> for Op<A>
where
for<'de> A: serde::Deserialize<'de>,
@ -100,6 +117,7 @@ where
actions,
author: *entry.actor(),
timestamp: Timestamp::from_secs(entry.timestamp()),
parents: entry.parents().to_owned(),
identity,
manifest,
};

View File

@ -335,7 +335,7 @@ impl<T: FromHistory> Transaction<T> {
/// Commit transaction.
///
/// Returns a list of operations that can be applied onto an in-memory CRDT.
/// Returns an operation that can be applied onto an in-memory state.
pub fn commit<R, G: Signer>(
self,
msg: &str,
@ -349,7 +349,11 @@ impl<T: FromHistory> Transaction<T> {
{
let actions = NonEmpty::from_vec(self.actions)
.expect("Transaction::commit: transaction must not be empty");
let Updated { head, object } = store.update(id, msg, actions.clone(), signer)?;
let Updated {
head,
object,
parents,
} = store.update(id, msg, actions.clone(), signer)?;
let id = EntryId::from(head);
let author = self.actor;
let timestamp = Timestamp::from_secs(object.history().timestamp());
@ -360,6 +364,7 @@ impl<T: FromHistory> Transaction<T> {
actions,
author,
timestamp,
parents,
identity,
manifest,
};
@ -368,6 +373,25 @@ impl<T: FromHistory> Transaction<T> {
}
}
/// Get an object's operations without decoding them.
pub fn ops<R: cob::Store>(
id: &ObjectId,
type_name: &TypeName,
repo: &R,
) -> Result<Vec<Op<Vec<u8>>>, Error> {
let cob = cob::get(repo, type_name, id)?;
if let Some(cob) = cob {
let ops = cob.history().traverse(Vec::new(), |mut ops, _, entry| {
ops.push(Op::from(entry.clone()));
ControlFlow::Continue(ops)
});
Ok(ops)
} else {
Err(Error::NotFound(type_name.clone(), *id))
}
}
pub mod encoding {
use serde::Serialize;

View File

@ -95,6 +95,7 @@ where
*signer.public_key(),
self.resource,
NonEmpty::new(data),
vec![],
timestamp.as_secs(),
manifest,
);
@ -160,11 +161,13 @@ impl<G: Signer> Actor<G> {
let author = *self.signer.public_key();
let actions = NonEmpty::new(action);
let manifest = Manifest::new(T::type_name().clone(), Version::default());
let parents = vec![];
Op {
id,
actions,
author,
parents,
timestamp,
identity,
manifest,