radicle_cob/object/collaboration/
update.rs

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