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}