radicle_cob/object/collaboration/
update.rs

1// Copyright © 2022 The Radicle Link Contributors
2use std::iter;
3
4use git_ext::Oid;
5use nonempty::NonEmpty;
6use radicle_crypto::PublicKey;
7
8use crate::{
9    change, change_graph::ChangeGraph, history::EntryId, CollaborativeObject, Embed, Evaluate,
10    ObjectId, Store, TypeName,
11};
12
13use super::error;
14
15/// Result of an `update` operation.
16#[derive(Debug)]
17pub struct Updated<T> {
18    /// The new head commit of the DAG.
19    pub head: Oid,
20    /// The newly updated collaborative object.
21    pub object: CollaborativeObject<T>,
22    /// Entry parents.
23    pub parents: Vec<EntryId>,
24}
25
26/// The data required to update an object
27pub struct Update {
28    /// The CRDT changes to add to the object.
29    pub changes: NonEmpty<Vec<u8>>,
30    /// The object ID of the object to be updated.
31    pub object_id: ObjectId,
32    /// The typename of the object to be updated.
33    pub type_name: TypeName,
34    /// The message to add when updating this object.
35    pub message: String,
36    /// Embedded files.
37    pub embeds: Vec<Embed<Oid>>,
38}
39
40/// Update an existing [`CollaborativeObject`].
41///
42/// The `storage` is the backing storage for storing
43/// [`crate::Entry`]s at content-addressable locations. Please see
44/// [`Store`] for further information.
45///
46/// The `signer` is expected to be a cryptographic signing key. This
47/// ensures that the objects origin is cryptographically verifiable.
48///
49/// The `resource` is the resource this change lives under, eg. a project.
50///
51/// The `parents` are other the parents of this object, for example a
52/// code commit.
53///
54/// The `identifier` is a unqiue id that is passed through to the
55/// [`crate::object::Storage`].
56///
57/// The `args` are the metadata for this [`CollaborativeObject`]
58/// udpate. See [`Update`] for further information.
59pub fn update<T, S, G>(
60    storage: &S,
61    signer: &G,
62    resource: Option<Oid>,
63    related: Vec<Oid>,
64    identifier: &PublicKey,
65    args: Update,
66) -> Result<Updated<T>, error::Update>
67where
68    T: Evaluate<S>,
69    S: Store,
70    G: crypto::Signer,
71{
72    let Update {
73        type_name: ref typename,
74        object_id,
75        embeds,
76        changes,
77        message,
78    } = args;
79
80    let existing_refs = storage
81        .objects(typename, &object_id)
82        .map_err(|err| error::Update::Refs { err: Box::new(err) })?;
83
84    let graph = ChangeGraph::load(storage, existing_refs.iter(), typename, &object_id)
85        .ok_or(error::Update::NoSuchObject)?;
86    let mut object: CollaborativeObject<T> =
87        graph.evaluate(storage).map_err(error::Update::evaluate)?;
88
89    // Create a commit for this change, but don't update any references yet.
90    let entry = storage.store(
91        resource,
92        related,
93        signer,
94        change::Template {
95            tips: object.history.tips().into_iter().collect(),
96            embeds,
97            contents: changes,
98            type_name: typename.clone(),
99            message,
100        },
101    )?;
102    let head = entry.id;
103    let parents = entry.parents.to_vec();
104
105    // Try to apply this change to our object. This prevents storing invalid updates.
106    // Note that if this returns with an error, we are left with an unreachable
107    // commit object created above. This is fine, as it will eventually get
108    // garbage-collected by Git.
109    object
110        .object
111        .apply(&entry, iter::empty(), storage)
112        .map_err(error::Update::evaluate)?;
113    object.history.extend(entry);
114
115    // Here we actually update the references to point to the new update.
116    storage
117        .update(identifier, typename, &object_id, &head)
118        .map_err(|err| error::Update::Refs { err: Box::new(err) })?;
119
120    Ok(Updated {
121        object,
122        head,
123        parents,
124    })
125}