1#![expect(missing_docs)]
16
17use std::sync::Arc;
18
19use thiserror::Error;
20
21use crate::backend::Timestamp;
22use crate::index::IndexError;
23use crate::index::IndexStoreError;
24use crate::index::ReadonlyIndex;
25use crate::op_heads_store::OpHeadsStore;
26use crate::op_heads_store::OpHeadsStoreError;
27use crate::op_store;
28use crate::op_store::OpStoreError;
29use crate::op_store::OperationMetadata;
30use crate::op_store::TimestampRange;
31use crate::operation::Operation;
32use crate::ref_name::WorkspaceName;
33use crate::repo::MutableRepo;
34use crate::repo::ReadonlyRepo;
35use crate::repo::Repo as _;
36use crate::repo::RepoLoader;
37use crate::repo::RepoLoaderError;
38use crate::settings::UserSettings;
39use crate::view::View;
40
41#[derive(Debug, Error)]
43#[error("Failed to commit new operation")]
44pub enum TransactionCommitError {
45 Index(#[from] IndexError),
46 IndexStore(#[from] IndexStoreError),
47 OpHeadsStore(#[from] OpHeadsStoreError),
48 OpStore(#[from] OpStoreError),
49}
50
51pub struct Transaction {
63 mut_repo: MutableRepo,
64 parent_ops: Vec<Operation>,
65 op_metadata: OperationMetadata,
66 end_time: Option<Timestamp>,
67}
68
69impl Transaction {
70 pub fn new(mut_repo: MutableRepo, user_settings: &UserSettings) -> Self {
71 let parent_ops = vec![mut_repo.base_repo().operation().clone()];
72 let op_metadata = create_op_metadata(user_settings, "".to_string(), false);
73 let end_time = user_settings.operation_timestamp();
74 Self {
75 mut_repo,
76 parent_ops,
77 op_metadata,
78 end_time,
79 }
80 }
81
82 pub fn base_repo(&self) -> &Arc<ReadonlyRepo> {
83 self.mut_repo.base_repo()
84 }
85
86 pub fn parent_ops(&self) -> &[Operation] {
87 &self.parent_ops
88 }
89
90 pub fn set_attribute(&mut self, key: String, value: String) {
91 self.op_metadata.attributes.insert(key, value);
92 }
93
94 pub fn repo(&self) -> &MutableRepo {
95 &self.mut_repo
96 }
97
98 pub fn repo_mut(&mut self) -> &mut MutableRepo {
99 &mut self.mut_repo
100 }
101
102 pub async fn merge_operation(
104 &mut self,
105 base_op: &Operation,
106 other_op: &Operation,
107 ) -> Result<(), RepoLoaderError> {
108 let repo_loader = self.base_repo().loader();
109 let base_op_repo = repo_loader.load_at(base_op).await?;
110 let other_repo = repo_loader.load_at(other_op).await?;
111 self.parent_ops.push(other_op.clone());
112 self.repo_mut().merge(&base_op_repo, &other_repo).await?;
113 Ok(())
114 }
115
116 pub fn set_is_snapshot(&mut self, is_snapshot: bool) {
117 self.op_metadata.is_snapshot = is_snapshot;
118 }
119
120 pub fn set_workspace_name(&mut self, workspace_name: &WorkspaceName) {
121 self.op_metadata.workspace_name = Some(workspace_name.to_owned());
122 }
123
124 pub async fn commit(
126 self,
127 description: impl Into<String>,
128 ) -> Result<Arc<ReadonlyRepo>, TransactionCommitError> {
129 self.write(description).await?.publish().await
130 }
131
132 pub async fn write(
136 mut self,
137 description: impl Into<String>,
138 ) -> Result<UnpublishedOperation, TransactionCommitError> {
139 let mut_repo = self.mut_repo;
140 assert!(
142 !mut_repo.has_rewrites(),
143 "BUG: Descendants have not been rebased after the last rewrites."
144 );
145 let base_repo = mut_repo.base_repo().clone();
146 let (mut_index, view, predecessors) = mut_repo.consume().await?;
147 assert!(
148 view.is_heads_normalized(),
149 "BUG: View heads must be normalized before persisting in the database"
150 );
151
152 let operation = {
153 let view_id = base_repo.op_store().write_view(view.store_view()).await?;
154 self.op_metadata.description = description.into();
155 self.op_metadata.time.end = self.end_time.unwrap_or_else(Timestamp::now);
156 let parents = self.parent_ops.iter().map(|op| op.id().clone()).collect();
157 let store_operation = op_store::Operation {
158 view_id,
159 parents,
160 metadata: self.op_metadata,
161 commit_predecessors: Some(predecessors),
162 };
163 let new_op_id = base_repo
164 .op_store()
165 .write_operation(&store_operation)
166 .await?;
167 Operation::new(base_repo.op_store().clone(), new_op_id, store_operation)
168 };
169
170 let index = base_repo.index_store().write_index(mut_index, &operation)?;
171 let unpublished = UnpublishedOperation::new(base_repo.loader(), operation, view, index);
172 Ok(unpublished)
173 }
174}
175
176pub fn create_op_metadata(
177 user_settings: &UserSettings,
178 description: String,
179 is_snapshot: bool,
180) -> OperationMetadata {
181 let timestamp = user_settings
182 .operation_timestamp()
183 .unwrap_or_else(Timestamp::now);
184 let hostname = user_settings.operation_hostname().to_owned();
185 let username = user_settings.operation_username().to_owned();
186 OperationMetadata {
187 time: TimestampRange {
188 start: timestamp,
189 end: timestamp,
190 },
191 description,
192 hostname,
193 username,
194 is_snapshot,
195 workspace_name: None,
196 attributes: Default::default(),
197 }
198}
199
200#[must_use = "Either publish() or leave_unpublished() must be called to finish the operation."]
209pub struct UnpublishedOperation {
210 op_heads_store: Arc<dyn OpHeadsStore>,
211 repo: Arc<ReadonlyRepo>,
212}
213
214impl UnpublishedOperation {
215 fn new(
216 repo_loader: &RepoLoader,
217 operation: Operation,
218 view: View,
219 index: Box<dyn ReadonlyIndex>,
220 ) -> Self {
221 Self {
222 op_heads_store: repo_loader.op_heads_store().clone(),
223 repo: repo_loader.create_from(operation, view, index),
224 }
225 }
226
227 pub fn operation(&self) -> &Operation {
228 self.repo.operation()
229 }
230
231 pub async fn publish(self) -> Result<Arc<ReadonlyRepo>, TransactionCommitError> {
232 let _lock = self.op_heads_store.lock().await?;
233 self.op_heads_store
234 .update_op_heads(self.operation().parent_ids(), self.operation().id())
235 .await?;
236 Ok(self.repo)
237 }
238
239 pub fn leave_unpublished(self) -> Arc<ReadonlyRepo> {
240 self.repo
241 }
242}