Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
cli: New `cob` command for debugging
cloudhead committed 2 years ago
commit a922ecf3ceb4eb4fc22236b0895b18548b50ceb4
parent 95a314688589b8b974015fef57f1e0b0751692ad
10 files changed +226 -4
modified radicle-cli/src/commands.rs
@@ -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"]
added radicle-cli/src/commands/cob.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(())
+
}
modified radicle-cli/src/main.rs
@@ -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,
modified radicle-cob/src/change_graph.rs
@@ -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(),
                );
modified radicle-cob/src/history.rs
@@ -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,
        );
modified radicle-cob/src/history/entry.rs
@@ -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
modified radicle-cob/src/object/collaboration/update.rs
@@ -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,
    })
}
modified radicle/src/cob/op.rs
@@ -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,
        };
modified radicle/src/cob/store.rs
@@ -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;

modified radicle/src/cob/test.rs
@@ -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,