287 lines
7.8 KiB
Rust
287 lines
7.8 KiB
Rust
use std::marker::PhantomData;
|
|
use std::ops::Deref;
|
|
|
|
use nonempty::NonEmpty;
|
|
use radicle_crypto::ssh::ExtendedSignature;
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use crate::cob::op::Op;
|
|
use crate::cob::patch::Patch;
|
|
use crate::cob::store::encoding;
|
|
use crate::cob::{Entry, History, Manifest, Timestamp, Version};
|
|
use crate::cob::{Title, patch};
|
|
use crate::crypto::Signer;
|
|
use crate::git::{self, Oid};
|
|
use crate::node::device::Device;
|
|
use crate::prelude::Did;
|
|
use crate::profile::env;
|
|
use crate::storage::ReadRepository;
|
|
use crate::test::arbitrary;
|
|
|
|
use super::store::{Cob, CobWithType};
|
|
use super::thread;
|
|
|
|
/// Convenience type for building histories.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub struct HistoryBuilder<T> {
|
|
history: History,
|
|
resource: Option<Oid>,
|
|
time: Timestamp,
|
|
witness: PhantomData<T>,
|
|
}
|
|
|
|
impl<T> AsRef<History> for HistoryBuilder<T> {
|
|
fn as_ref(&self) -> &History {
|
|
&self.history
|
|
}
|
|
}
|
|
|
|
impl HistoryBuilder<thread::Thread> {
|
|
pub fn comment<G: Signer>(
|
|
&mut self,
|
|
body: impl ToString,
|
|
reply_to: Option<thread::CommentId>,
|
|
signer: &G,
|
|
) -> Oid {
|
|
let action = thread::Action::Comment {
|
|
body: body.to_string(),
|
|
reply_to,
|
|
};
|
|
self.commit(&action, signer)
|
|
}
|
|
}
|
|
|
|
impl<T: Cob> HistoryBuilder<T>
|
|
where
|
|
T: CobWithType,
|
|
T::Action: for<'de> Deserialize<'de> + Serialize + Eq + 'static,
|
|
{
|
|
pub fn new<G: Signer>(actions: &[T::Action], time: Timestamp, signer: &G) -> HistoryBuilder<T> {
|
|
let resource = Some(arbitrary::oid());
|
|
let revision = arbitrary::oid();
|
|
let (contents, oids): (Vec<Vec<u8>>, Vec<Oid>) = actions
|
|
.iter()
|
|
.map(|a| encoded::<T, _>(a, time, [], signer))
|
|
.unzip();
|
|
let contents = NonEmpty::from_vec(contents).unwrap();
|
|
let root = oids.first().unwrap();
|
|
let manifest = Manifest::new(T::type_name().clone(), Version::default());
|
|
let signature = signer.sign(&[0]);
|
|
let signature = ExtendedSignature::new(*signer.public_key(), signature);
|
|
let change = Entry {
|
|
id: *root,
|
|
signature,
|
|
resource,
|
|
contents,
|
|
timestamp: time.as_secs(),
|
|
revision,
|
|
parents: vec![],
|
|
related: vec![],
|
|
manifest,
|
|
};
|
|
|
|
Self {
|
|
history: History::new_from_root(change),
|
|
time,
|
|
resource,
|
|
witness: PhantomData,
|
|
}
|
|
}
|
|
|
|
pub fn root(&self) -> &Entry {
|
|
self.history.root()
|
|
}
|
|
|
|
pub fn merge(&mut self, other: Self) {
|
|
self.history.merge(other.history);
|
|
}
|
|
|
|
pub fn commit<G: Signer>(&mut self, action: &T::Action, signer: &G) -> crate::git::Oid {
|
|
let timestamp = self.time;
|
|
let tips = self.tips();
|
|
let revision = arbitrary::oid();
|
|
let (data, oid) = encoded::<T, _>(action, timestamp, tips, signer);
|
|
let manifest = Manifest::new(T::type_name().clone(), Version::default());
|
|
let signature = signer.sign(data.as_slice());
|
|
let signature = ExtendedSignature::new(*signer.public_key(), signature);
|
|
let change = Entry {
|
|
id: oid,
|
|
signature,
|
|
resource: self.resource,
|
|
contents: NonEmpty::new(data),
|
|
timestamp: timestamp.as_secs(),
|
|
revision,
|
|
parents: vec![],
|
|
related: vec![],
|
|
manifest,
|
|
};
|
|
self.history.extend(change);
|
|
|
|
oid
|
|
}
|
|
}
|
|
|
|
impl<A> Deref for HistoryBuilder<A> {
|
|
type Target = History;
|
|
|
|
fn deref(&self) -> &Self::Target {
|
|
&self.history
|
|
}
|
|
}
|
|
|
|
/// Create a new test history.
|
|
pub fn history<T, G: Signer>(
|
|
actions: &[T::Action],
|
|
time: Timestamp,
|
|
signer: &G,
|
|
) -> HistoryBuilder<T>
|
|
where
|
|
T: Cob + CobWithType,
|
|
T::Action: Serialize + Eq + 'static,
|
|
{
|
|
HistoryBuilder::new(actions, time, signer)
|
|
}
|
|
|
|
/// An object that can be used to create and sign operations.
|
|
pub struct Actor<G> {
|
|
pub signer: Device<G>,
|
|
}
|
|
|
|
impl<G: Default + Signer> Default for Actor<G> {
|
|
fn default() -> Self {
|
|
Self::new(Device::default())
|
|
}
|
|
}
|
|
|
|
impl<G> Actor<G> {
|
|
pub fn new(signer: Device<G>) -> Self {
|
|
Self { signer }
|
|
}
|
|
}
|
|
|
|
impl<G: Signer> Actor<G> {
|
|
/// Create a new operation.
|
|
pub fn op_with<T>(
|
|
&mut self,
|
|
actions: impl IntoIterator<Item = T::Action>,
|
|
identity: Option<Oid>,
|
|
timestamp: Timestamp,
|
|
) -> Op<T::Action>
|
|
where
|
|
T: Cob + CobWithType,
|
|
T::Action: Clone + Serialize,
|
|
{
|
|
let actions = actions.into_iter().collect::<Vec<_>>();
|
|
let data = encoding::encode(serde_json::json!({
|
|
"action": actions,
|
|
"nonce": fastrand::u64(..),
|
|
}))
|
|
.unwrap();
|
|
let oid =
|
|
crate::git::raw::Oid::hash_object(crate::git::raw::ObjectType::Blob, &data).unwrap();
|
|
let id = oid.into();
|
|
let author = *self.signer.public_key();
|
|
let actions = NonEmpty::from_vec(actions).unwrap();
|
|
let manifest = Manifest::new(T::type_name().clone(), Version::default());
|
|
let parents = vec![];
|
|
let related = vec![];
|
|
|
|
Op {
|
|
id,
|
|
actions,
|
|
author,
|
|
parents,
|
|
related,
|
|
timestamp,
|
|
identity,
|
|
manifest,
|
|
}
|
|
}
|
|
|
|
/// Create a new operation.
|
|
pub fn op<T>(&mut self, actions: impl IntoIterator<Item = T::Action>) -> Op<T::Action>
|
|
where
|
|
T: Cob + CobWithType,
|
|
T::Action: Clone + Serialize,
|
|
{
|
|
let identity = arbitrary::oid();
|
|
let timestamp = env::commit_time();
|
|
|
|
self.op_with::<T>(actions, Some(identity), timestamp.into())
|
|
}
|
|
|
|
/// Get the actor's DID.
|
|
pub fn did(&self) -> Did {
|
|
self.signer.public_key().into()
|
|
}
|
|
}
|
|
|
|
impl<G: Signer> Actor<G> {
|
|
/// Create a patch.
|
|
pub fn patch<R: ReadRepository>(
|
|
&mut self,
|
|
title: Title,
|
|
description: impl ToString,
|
|
base: crate::git::Oid,
|
|
oid: crate::git::Oid,
|
|
repo: &R,
|
|
) -> Result<Patch, patch::Error> {
|
|
Patch::from_root(
|
|
self.op::<Patch>([
|
|
patch::Action::Revision {
|
|
description: description.to_string(),
|
|
base,
|
|
oid,
|
|
resolves: Default::default(),
|
|
},
|
|
patch::Action::Edit {
|
|
title,
|
|
target: patch::MergeTarget::default(),
|
|
},
|
|
]),
|
|
repo,
|
|
)
|
|
}
|
|
}
|
|
|
|
/// Encode an action and return its hash.
|
|
///
|
|
/// Doesn't encode in the same way as we do in production, but attempts to include the same data
|
|
/// that feeds into the hash entropy, so that changing any input will change the resulting oid.
|
|
fn encoded<T: Cob, G: Signer>(
|
|
action: &T::Action,
|
|
timestamp: Timestamp,
|
|
parents: impl IntoIterator<Item = Oid>,
|
|
signer: &G,
|
|
) -> (Vec<u8>, crate::git::Oid) {
|
|
use radicle_git_metadata::{
|
|
author::{Author, Time},
|
|
commit::{CommitData, headers::Headers, trailers::OwnedTrailer},
|
|
};
|
|
|
|
let data = encoding::encode(action).unwrap();
|
|
let oid = crate::git::raw::Oid::hash_object(crate::git::raw::ObjectType::Blob, &data).unwrap();
|
|
let parents = parents.into_iter().map(|o| o.into());
|
|
let author = Author {
|
|
name: "radicle".to_owned(),
|
|
email: signer.public_key().to_human(),
|
|
time: Time::new(timestamp.as_secs() as i64, 0),
|
|
};
|
|
let commit = CommitData::<git::raw::Oid, git::raw::Oid>::new::<_, _, OwnedTrailer>(
|
|
oid,
|
|
parents,
|
|
author.clone(),
|
|
author,
|
|
Headers::new(),
|
|
String::default(),
|
|
[],
|
|
)
|
|
.to_string();
|
|
|
|
let hash =
|
|
crate::git::raw::Oid::hash_object(crate::git::raw::ObjectType::Commit, commit.as_bytes())
|
|
.unwrap();
|
|
|
|
(data, hash.into())
|
|
}
|