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}