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:
parent
95a3146885
commit
a922ecf3ce
|
|
@ -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"]
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue