1#![expect(missing_docs)]
16
17use std::fmt::Debug;
18use std::fs;
19use std::fs::File;
20use std::io::Cursor;
21use std::io::Read as _;
22use std::io::Write as _;
23use std::path::Path;
24use std::path::PathBuf;
25use std::pin::Pin;
26use std::time::SystemTime;
27
28use async_trait::async_trait;
29use blake2::Blake2b512;
30use blake2::Digest as _;
31use futures::StreamExt as _;
32use futures::stream;
33use futures::stream::BoxStream;
34use pollster::FutureExt as _;
35use prost::Message as _;
36use tempfile::NamedTempFile;
37use tokio::io::AsyncRead;
38use tokio::io::AsyncReadExt as _;
39
40use crate::backend::Backend;
41use crate::backend::BackendError;
42use crate::backend::BackendResult;
43use crate::backend::ChangeId;
44use crate::backend::Commit;
45use crate::backend::CommitId;
46use crate::backend::CopyHistory;
47use crate::backend::CopyId;
48use crate::backend::CopyRecord;
49use crate::backend::FileId;
50use crate::backend::MillisSinceEpoch;
51use crate::backend::RelatedCopy;
52use crate::backend::SecureSig;
53use crate::backend::Signature;
54use crate::backend::SigningFn;
55use crate::backend::SymlinkId;
56use crate::backend::Timestamp;
57use crate::backend::Tree;
58use crate::backend::TreeId;
59use crate::backend::TreeValue;
60use crate::backend::make_root_commit;
61use crate::conflict_labels::ConflictLabels;
62use crate::content_hash::blake2b_hash;
63use crate::file_util::persist_content_addressed_temp_file;
64use crate::index::Index;
65use crate::merge::MergeBuilder;
66use crate::object_id::ObjectId;
67use crate::repo_path::RepoPath;
68use crate::repo_path::RepoPathBuf;
69use crate::repo_path::RepoPathComponentBuf;
70
71const COMMIT_ID_LENGTH: usize = 64;
72const CHANGE_ID_LENGTH: usize = 16;
73
74fn map_not_found_err(err: std::io::Error, id: &impl ObjectId) -> BackendError {
75 if err.kind() == std::io::ErrorKind::NotFound {
76 BackendError::ObjectNotFound {
77 object_type: id.object_type(),
78 hash: id.hex(),
79 source: Box::new(err),
80 }
81 } else {
82 BackendError::ReadObject {
83 object_type: id.object_type(),
84 hash: id.hex(),
85 source: Box::new(err),
86 }
87 }
88}
89
90fn to_other_err(err: impl Into<Box<dyn std::error::Error + Send + Sync>>) -> BackendError {
91 BackendError::Other(err.into())
92}
93
94#[derive(Debug)]
95pub struct SimpleBackend {
96 path: PathBuf,
97 root_commit_id: CommitId,
98 root_change_id: ChangeId,
99 empty_tree_id: TreeId,
100}
101
102impl SimpleBackend {
103 pub fn name() -> &'static str {
104 "Simple"
105 }
106
107 pub fn init(store_path: &Path) -> Self {
108 fs::create_dir(store_path.join("commits")).unwrap();
109 fs::create_dir(store_path.join("trees")).unwrap();
110 fs::create_dir(store_path.join("files")).unwrap();
111 fs::create_dir(store_path.join("symlinks")).unwrap();
112 fs::create_dir(store_path.join("conflicts")).unwrap();
113 let backend = Self::load(store_path);
114 let empty_tree_id = backend
115 .write_tree(RepoPath::root(), &Tree::default())
116 .block_on()
117 .unwrap();
118 assert_eq!(empty_tree_id, backend.empty_tree_id);
119 backend
120 }
121
122 pub fn load(store_path: &Path) -> Self {
123 let root_commit_id = CommitId::from_bytes(&[0; COMMIT_ID_LENGTH]);
124 let root_change_id = ChangeId::from_bytes(&[0; CHANGE_ID_LENGTH]);
125 let empty_tree_id = TreeId::from_hex(
126 "482ae5a29fbe856c7272f2071b8b0f0359ee2d89ff392b8a900643fbd0836eccd067b8bf41909e206c90d45d6e7d8b6686b93ecaee5fe1a9060d87b672101310",
127 );
128 Self {
129 path: store_path.to_path_buf(),
130 root_commit_id,
131 root_change_id,
132 empty_tree_id,
133 }
134 }
135
136 fn file_path(&self, id: &FileId) -> PathBuf {
137 self.path.join("files").join(id.hex())
138 }
139
140 fn symlink_path(&self, id: &SymlinkId) -> PathBuf {
141 self.path.join("symlinks").join(id.hex())
142 }
143
144 fn tree_path(&self, id: &TreeId) -> PathBuf {
145 self.path.join("trees").join(id.hex())
146 }
147
148 fn commit_path(&self, id: &CommitId) -> PathBuf {
149 self.path.join("commits").join(id.hex())
150 }
151}
152
153#[async_trait]
154impl Backend for SimpleBackend {
155 fn name(&self) -> &str {
156 Self::name()
157 }
158
159 fn commit_id_length(&self) -> usize {
160 COMMIT_ID_LENGTH
161 }
162
163 fn change_id_length(&self) -> usize {
164 CHANGE_ID_LENGTH
165 }
166
167 fn root_commit_id(&self) -> &CommitId {
168 &self.root_commit_id
169 }
170
171 fn root_change_id(&self) -> &ChangeId {
172 &self.root_change_id
173 }
174
175 fn empty_tree_id(&self) -> &TreeId {
176 &self.empty_tree_id
177 }
178
179 fn concurrency(&self) -> usize {
180 1
181 }
182
183 async fn read_file(
184 &self,
185 path: &RepoPath,
186 id: &FileId,
187 ) -> BackendResult<Pin<Box<dyn AsyncRead + Send>>> {
188 let disk_path = self.file_path(id);
189 let mut file = File::open(disk_path).map_err(|err| map_not_found_err(err, id))?;
190 let mut buf = vec![];
191 file.read_to_end(&mut buf)
192 .map_err(|err| BackendError::ReadFile {
193 path: path.to_owned(),
194 id: id.clone(),
195 source: err.into(),
196 })?;
197 Ok(Box::pin(Cursor::new(buf)))
198 }
199
200 async fn write_file(
201 &self,
202 _path: &RepoPath,
203 contents: &mut (dyn AsyncRead + Send + Unpin),
204 ) -> BackendResult<FileId> {
205 let temp_file = NamedTempFile::new_in(&self.path).map_err(to_other_err)?;
207 let mut file = temp_file.as_file();
208 let mut hasher = Blake2b512::new();
209 let mut buff: Vec<u8> = vec![0; 1 << 14];
210 loop {
211 let bytes_read = contents.read(&mut buff).await.map_err(to_other_err)?;
212 if bytes_read == 0 {
213 break;
214 }
215 let bytes = &buff[..bytes_read];
216 file.write_all(bytes).map_err(to_other_err)?;
217 hasher.update(bytes);
218 }
219 file.flush().map_err(to_other_err)?;
220 let id = FileId::new(hasher.finalize().to_vec());
221
222 persist_content_addressed_temp_file(temp_file, self.file_path(&id))
223 .map_err(to_other_err)?;
224 Ok(id)
225 }
226
227 async fn read_symlink(&self, _path: &RepoPath, id: &SymlinkId) -> BackendResult<String> {
228 let path = self.symlink_path(id);
229 let target = fs::read_to_string(path).map_err(|err| map_not_found_err(err, id))?;
230 Ok(target)
231 }
232
233 async fn write_symlink(&self, _path: &RepoPath, target: &str) -> BackendResult<SymlinkId> {
234 let mut temp_file = NamedTempFile::new_in(&self.path).map_err(to_other_err)?;
236 temp_file
237 .write_all(target.as_bytes())
238 .map_err(to_other_err)?;
239 let mut hasher = Blake2b512::new();
240 hasher.update(target.as_bytes());
241 let id = SymlinkId::new(hasher.finalize().to_vec());
242
243 persist_content_addressed_temp_file(temp_file, self.symlink_path(&id))
244 .map_err(to_other_err)?;
245 Ok(id)
246 }
247
248 async fn read_copy(&self, _id: &CopyId) -> BackendResult<CopyHistory> {
249 Err(BackendError::Unsupported(
250 "The simple backend doesn't support copies".to_string(),
251 ))
252 }
253
254 async fn write_copy(&self, _contents: &CopyHistory) -> BackendResult<CopyId> {
255 Err(BackendError::Unsupported(
256 "The simple backend doesn't support copies".to_string(),
257 ))
258 }
259
260 async fn get_related_copies(&self, _copy_id: &CopyId) -> BackendResult<Vec<RelatedCopy>> {
261 Err(BackendError::Unsupported(
262 "The simple backend doesn't support copies".to_string(),
263 ))
264 }
265
266 async fn read_tree(&self, _path: &RepoPath, id: &TreeId) -> BackendResult<Tree> {
267 let path = self.tree_path(id);
268 let buf = fs::read(path).map_err(|err| map_not_found_err(err, id))?;
269
270 let proto = crate::protos::simple_store::Tree::decode(&*buf).map_err(to_other_err)?;
271 Ok(tree_from_proto(proto))
272 }
273
274 async fn write_tree(&self, _path: &RepoPath, tree: &Tree) -> BackendResult<TreeId> {
275 let temp_file = NamedTempFile::new_in(&self.path).map_err(to_other_err)?;
277
278 let proto = tree_to_proto(tree);
279 temp_file
280 .as_file()
281 .write_all(&proto.encode_to_vec())
282 .map_err(to_other_err)?;
283
284 let id = TreeId::new(blake2b_hash(tree).to_vec());
285
286 persist_content_addressed_temp_file(temp_file, self.tree_path(&id))
287 .map_err(to_other_err)?;
288 Ok(id)
289 }
290
291 async fn read_commit(&self, id: &CommitId) -> BackendResult<Commit> {
292 if *id == self.root_commit_id {
293 return Ok(make_root_commit(
294 self.root_change_id().clone(),
295 self.empty_tree_id.clone(),
296 ));
297 }
298
299 let path = self.commit_path(id);
300 let buf = fs::read(path).map_err(|err| map_not_found_err(err, id))?;
301
302 let proto = crate::protos::simple_store::Commit::decode(&*buf).map_err(to_other_err)?;
303 Ok(commit_from_proto(proto))
304 }
305
306 async fn write_commit(
307 &self,
308 mut commit: Commit,
309 sign_with: Option<&mut SigningFn>,
310 ) -> BackendResult<(CommitId, Commit)> {
311 assert!(commit.secure_sig.is_none(), "commit.secure_sig was set");
312
313 if commit.parents.is_empty() {
314 return Err(BackendError::Other(
315 "Cannot write a commit with no parents".into(),
316 ));
317 }
318 let temp_file = NamedTempFile::new_in(&self.path).map_err(to_other_err)?;
320
321 let mut proto = commit_to_proto(&commit);
322 if let Some(sign) = sign_with {
323 let data = proto.encode_to_vec();
324 let sig = sign(&data).map_err(to_other_err)?;
325 proto.secure_sig = Some(sig.clone());
326 commit.secure_sig = Some(SecureSig { data, sig });
327 }
328
329 temp_file
330 .as_file()
331 .write_all(&proto.encode_to_vec())
332 .map_err(to_other_err)?;
333
334 let id = CommitId::new(blake2b_hash(&commit).to_vec());
335
336 persist_content_addressed_temp_file(temp_file, self.commit_path(&id))
337 .map_err(to_other_err)?;
338 Ok((id, commit))
339 }
340
341 fn get_copy_records(
342 &self,
343 _paths: Option<&[RepoPathBuf]>,
344 _root: &CommitId,
345 _head: &CommitId,
346 ) -> BackendResult<BoxStream<'_, BackendResult<CopyRecord>>> {
347 Ok(stream::empty().boxed())
348 }
349
350 fn gc(&self, _index: &dyn Index, _keep_newer: SystemTime) -> BackendResult<()> {
351 Ok(())
352 }
353}
354
355#[expect(clippy::assigning_clones)]
356pub fn commit_to_proto(commit: &Commit) -> crate::protos::simple_store::Commit {
357 let mut proto = crate::protos::simple_store::Commit::default();
358 for parent in &commit.parents {
359 proto.parents.push(parent.to_bytes());
360 }
361 for predecessor in &commit.predecessors {
362 proto.predecessors.push(predecessor.to_bytes());
363 }
364 proto.root_tree = commit.root_tree.iter().map(|id| id.to_bytes()).collect();
365 if !commit.conflict_labels.is_resolved() {
366 proto.conflict_labels = commit.conflict_labels.as_slice().to_owned();
367 }
368 proto.change_id = commit.change_id.to_bytes();
369 proto.description = commit.description.clone();
370 proto.author = Some(signature_to_proto(&commit.author));
371 proto.committer = Some(signature_to_proto(&commit.committer));
372 proto
373}
374
375fn commit_from_proto(mut proto: crate::protos::simple_store::Commit) -> Commit {
376 let secure_sig = proto.secure_sig.take().map(|sig| SecureSig {
379 data: proto.encode_to_vec(),
380 sig,
381 });
382
383 let parents = proto.parents.into_iter().map(CommitId::new).collect();
384 let predecessors = proto.predecessors.into_iter().map(CommitId::new).collect();
385 let merge_builder: MergeBuilder<_> = proto.root_tree.into_iter().map(TreeId::new).collect();
386 let root_tree = merge_builder.build();
387 let conflict_labels = ConflictLabels::from_vec(proto.conflict_labels);
388 let change_id = ChangeId::new(proto.change_id);
389 Commit {
390 parents,
391 predecessors,
392 root_tree,
393 conflict_labels: conflict_labels.into_merge(),
394 change_id,
395 description: proto.description,
396 author: signature_from_proto(proto.author.unwrap_or_default()),
397 committer: signature_from_proto(proto.committer.unwrap_or_default()),
398 secure_sig,
399 }
400}
401
402fn tree_to_proto(tree: &Tree) -> crate::protos::simple_store::Tree {
403 let mut proto = crate::protos::simple_store::Tree::default();
404 for entry in tree.entries() {
405 proto
406 .entries
407 .push(crate::protos::simple_store::tree::Entry {
408 name: entry.name().as_internal_str().to_owned(),
409 value: Some(tree_value_to_proto(entry.value())),
410 });
411 }
412 proto
413}
414
415fn tree_from_proto(proto: crate::protos::simple_store::Tree) -> Tree {
416 let entries = proto
418 .entries
419 .into_iter()
420 .map(|proto_entry| {
421 let value = tree_value_from_proto(proto_entry.value.unwrap());
422 (RepoPathComponentBuf::new(proto_entry.name).unwrap(), value)
423 })
424 .collect();
425 Tree::from_sorted_entries(entries)
426}
427
428fn tree_value_to_proto(value: &TreeValue) -> crate::protos::simple_store::TreeValue {
429 let mut proto = crate::protos::simple_store::TreeValue::default();
430 match value {
431 TreeValue::File {
432 id,
433 executable,
434 copy_id,
435 } => {
436 proto.value = Some(crate::protos::simple_store::tree_value::Value::File(
437 crate::protos::simple_store::tree_value::File {
438 id: id.to_bytes(),
439 executable: *executable,
440 copy_id: copy_id.to_bytes(),
441 },
442 ));
443 }
444 TreeValue::Symlink(id) => {
445 proto.value = Some(crate::protos::simple_store::tree_value::Value::SymlinkId(
446 id.to_bytes(),
447 ));
448 }
449 TreeValue::GitSubmodule(_id) => {
450 panic!("cannot store git submodules");
451 }
452 TreeValue::Tree(id) => {
453 proto.value = Some(crate::protos::simple_store::tree_value::Value::TreeId(
454 id.to_bytes(),
455 ));
456 }
457 }
458 proto
459}
460
461fn tree_value_from_proto(proto: crate::protos::simple_store::TreeValue) -> TreeValue {
462 match proto.value.unwrap() {
463 crate::protos::simple_store::tree_value::Value::TreeId(id) => {
464 TreeValue::Tree(TreeId::new(id))
465 }
466 crate::protos::simple_store::tree_value::Value::File(
467 crate::protos::simple_store::tree_value::File {
468 id,
469 executable,
470 copy_id,
471 },
472 ) => TreeValue::File {
473 id: FileId::new(id),
474 executable,
475 copy_id: CopyId::new(copy_id),
476 },
477 crate::protos::simple_store::tree_value::Value::SymlinkId(id) => {
478 TreeValue::Symlink(SymlinkId::new(id))
479 }
480 }
481}
482
483fn signature_to_proto(signature: &Signature) -> crate::protos::simple_store::commit::Signature {
484 crate::protos::simple_store::commit::Signature {
485 name: signature.name.clone(),
486 email: signature.email.clone(),
487 timestamp: Some(crate::protos::simple_store::commit::Timestamp {
488 millis_since_epoch: signature.timestamp.timestamp.0,
489 tz_offset: signature.timestamp.tz_offset,
490 }),
491 }
492}
493
494fn signature_from_proto(proto: crate::protos::simple_store::commit::Signature) -> Signature {
495 let timestamp = proto.timestamp.unwrap_or_default();
496 Signature {
497 name: proto.name,
498 email: proto.email,
499 timestamp: Timestamp {
500 timestamp: MillisSinceEpoch(timestamp.millis_since_epoch),
501 tz_offset: timestamp.tz_offset,
502 },
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use assert_matches::assert_matches;
509 use pollster::FutureExt as _;
510
511 use super::*;
512 use crate::merge::Merge;
513 use crate::tests::TestResult;
514 use crate::tests::new_temp_dir;
515
516 #[test]
518 fn write_commit_parents() -> TestResult {
519 let temp_dir = new_temp_dir();
520 let store_path = temp_dir.path();
521
522 let backend = SimpleBackend::init(store_path);
523 let mut commit = Commit {
524 parents: vec![],
525 predecessors: vec![],
526 root_tree: Merge::resolved(backend.empty_tree_id().clone()),
527 conflict_labels: Merge::resolved(String::new()),
528 change_id: ChangeId::from_hex("abc123"),
529 description: "".to_string(),
530 author: create_signature(),
531 committer: create_signature(),
532 secure_sig: None,
533 };
534
535 let write_commit = |commit: Commit| -> BackendResult<(CommitId, Commit)> {
536 backend.write_commit(commit, None).block_on()
537 };
538
539 commit.parents = vec![];
541 assert_matches!(
542 write_commit(commit.clone()),
543 Err(BackendError::Other(err)) if err.to_string().contains("no parents")
544 );
545
546 commit.parents = vec![backend.root_commit_id().clone()];
548 let first_id = write_commit(commit.clone())?.0;
549 let first_commit = backend.read_commit(&first_id).block_on()?;
550 assert_eq!(first_commit, commit);
551
552 commit.parents = vec![first_id.clone()];
554 let second_id = write_commit(commit.clone())?.0;
555 let second_commit = backend.read_commit(&second_id).block_on()?;
556 assert_eq!(second_commit, commit);
557
558 commit.parents = vec![first_id.clone(), second_id.clone()];
560 let merge_id = write_commit(commit.clone())?.0;
561 let merge_commit = backend.read_commit(&merge_id).block_on()?;
562 assert_eq!(merge_commit, commit);
563
564 commit.parents = vec![first_id, backend.root_commit_id().clone()];
566 let root_merge_id = write_commit(commit.clone())?.0;
567 let root_merge_commit = backend.read_commit(&root_merge_id).block_on()?;
568 assert_eq!(root_merge_commit, commit);
569 Ok(())
570 }
571
572 fn create_signature() -> Signature {
573 Signature {
574 name: "Someone".to_string(),
575 email: "someone@example.com".to_string(),
576 timestamp: Timestamp {
577 timestamp: MillisSinceEpoch(0),
578 tz_offset: 0,
579 },
580 }
581 }
582}