1#![expect(missing_docs)]
16
17use std::any::Any;
18use std::fmt::Debug;
19use std::pin::Pin;
20use std::slice;
21use std::time::SystemTime;
22
23use async_trait::async_trait;
24use chrono::TimeZone as _;
25use futures::stream::BoxStream;
26use thiserror::Error;
27use tokio::io::AsyncRead;
28
29use crate::content_hash::ContentHash;
30use crate::hex_util;
31use crate::index::Index;
32use crate::merge::Merge;
33use crate::object_id::ObjectId as _;
34use crate::object_id::id_type;
35use crate::repo_path::RepoPath;
36use crate::repo_path::RepoPathBuf;
37use crate::repo_path::RepoPathComponent;
38use crate::repo_path::RepoPathComponentBuf;
39use crate::signing::SignResult;
40
41id_type!(
42 pub CommitId { hex() }
45);
46id_type!(
47 pub ChangeId { reverse_hex() }
50);
51id_type!(pub TreeId { hex() });
52id_type!(pub FileId { hex() });
53id_type!(pub SymlinkId { hex() });
54id_type!(pub CopyId { hex() });
55
56impl ChangeId {
57 pub fn try_from_reverse_hex(hex: impl AsRef<[u8]>) -> Option<Self> {
59 hex_util::decode_reverse_hex(hex).map(Self)
60 }
61
62 pub fn reverse_hex(&self) -> String {
65 hex_util::encode_reverse_hex(&self.0)
66 }
67}
68
69impl CopyId {
70 pub fn placeholder() -> Self {
74 Self::new(vec![])
75 }
76}
77
78#[derive(Debug, Error)]
79#[error("Out-of-range date")]
80pub struct TimestampOutOfRange;
81
82#[derive(ContentHash, Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)]
83pub struct MillisSinceEpoch(pub i64);
84
85#[derive(ContentHash, Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)]
86pub struct Timestamp {
87 pub timestamp: MillisSinceEpoch,
88 pub tz_offset: i32,
90}
91
92impl Timestamp {
93 pub fn now() -> Self {
94 Self::from_datetime(chrono::offset::Local::now())
95 }
96
97 pub fn from_datetime<Tz: chrono::TimeZone<Offset = chrono::offset::FixedOffset>>(
98 datetime: chrono::DateTime<Tz>,
99 ) -> Self {
100 Self {
101 timestamp: MillisSinceEpoch(datetime.timestamp_millis()),
102 tz_offset: datetime.offset().local_minus_utc() / 60,
103 }
104 }
105
106 pub fn to_datetime(
107 &self,
108 ) -> Result<chrono::DateTime<chrono::FixedOffset>, TimestampOutOfRange> {
109 let utc = match chrono::Utc.timestamp_opt(
110 self.timestamp.0.div_euclid(1000),
111 (self.timestamp.0.rem_euclid(1000)) as u32 * 1000000,
112 ) {
113 chrono::LocalResult::None => {
114 return Err(TimestampOutOfRange);
115 }
116 chrono::LocalResult::Single(x) => x,
117 chrono::LocalResult::Ambiguous(y, _z) => y,
118 };
119
120 Ok(utc.with_timezone(
121 &chrono::FixedOffset::east_opt(self.tz_offset * 60)
122 .unwrap_or_else(|| chrono::FixedOffset::east_opt(0).unwrap()),
123 ))
124 }
125}
126
127impl serde::Serialize for Timestamp {
128 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
129 where
130 S: serde::Serializer,
131 {
132 let t = self.to_datetime().map_err(serde::ser::Error::custom)?;
134 t.serialize(serializer)
135 }
136}
137
138#[derive(ContentHash, Debug, PartialEq, Eq, Clone, serde::Serialize)]
140pub struct Signature {
141 pub name: String,
142 pub email: String,
143 pub timestamp: Timestamp,
144}
145
146#[derive(ContentHash, Debug, PartialEq, Eq, Clone)]
148pub struct SecureSig {
149 pub data: Vec<u8>,
150 pub sig: Vec<u8>,
151}
152
153pub type SigningFn<'a> = dyn FnMut(&[u8]) -> SignResult<Vec<u8>> + Send + 'a;
154
155#[derive(ContentHash, Debug, Clone)]
161pub enum MergedTreeId {
162 Legacy(TreeId),
164 Merge(Merge<TreeId>),
166}
167
168impl PartialEq for MergedTreeId {
169 fn eq(&self, other: &Self) -> bool {
172 self.to_merge() == other.to_merge()
173 }
174}
175
176impl Eq for MergedTreeId {}
177
178impl MergedTreeId {
179 pub fn resolved(tree_id: TreeId) -> Self {
181 Self::Merge(Merge::resolved(tree_id))
182 }
183
184 pub fn to_merge(&self) -> Merge<TreeId> {
186 match self {
187 Self::Legacy(tree_id) => Merge::resolved(tree_id.clone()),
188 Self::Merge(tree_ids) => tree_ids.clone(),
189 }
190 }
191}
192
193#[derive(ContentHash, Debug, PartialEq, Eq, Clone, serde::Serialize)]
194pub struct Commit {
195 pub parents: Vec<CommitId>,
196 #[serde(skip)] pub predecessors: Vec<CommitId>,
200 #[serde(skip)] pub root_tree: MergedTreeId,
202 pub change_id: ChangeId,
203 pub description: String,
204 pub author: Signature,
205 pub committer: Signature,
206 #[serde(skip)] pub secure_sig: Option<SecureSig>,
208}
209
210#[derive(Debug, PartialEq, Eq, Clone)]
212pub struct CopyRecord {
213 pub target: RepoPathBuf,
215 pub target_commit: CommitId,
217 pub source: RepoPathBuf,
223 pub source_file: FileId,
224 pub source_commit: CommitId,
235}
236
237#[derive(ContentHash, Debug, PartialEq, Eq, Clone, PartialOrd, Ord)]
240pub struct CopyHistory {
241 pub current_path: RepoPathBuf,
243 pub parents: Vec<CopyId>,
248 pub salt: Vec<u8>,
253}
254
255#[derive(Debug, Error)]
257#[error(transparent)]
258pub struct BackendInitError(pub Box<dyn std::error::Error + Send + Sync>);
259
260#[derive(Debug, Error)]
262#[error(transparent)]
263pub struct BackendLoadError(pub Box<dyn std::error::Error + Send + Sync>);
264
265#[derive(Debug, Error)]
267pub enum BackendError {
268 #[error(
269 "Invalid hash length for object of type {object_type} (expected {expected} bytes, got \
270 {actual} bytes): {hash}"
271 )]
272 InvalidHashLength {
273 expected: usize,
274 actual: usize,
275 object_type: String,
276 hash: String,
277 },
278 #[error("Invalid UTF-8 for object {hash} of type {object_type}")]
279 InvalidUtf8 {
280 object_type: String,
281 hash: String,
282 source: std::str::Utf8Error,
283 },
284 #[error("Object {hash} of type {object_type} not found")]
285 ObjectNotFound {
286 object_type: String,
287 hash: String,
288 source: Box<dyn std::error::Error + Send + Sync>,
289 },
290 #[error("Error when reading object {hash} of type {object_type}")]
291 ReadObject {
292 object_type: String,
293 hash: String,
294 source: Box<dyn std::error::Error + Send + Sync>,
295 },
296 #[error("Access denied to read object {hash} of type {object_type}")]
297 ReadAccessDenied {
298 object_type: String,
299 hash: String,
300 source: Box<dyn std::error::Error + Send + Sync>,
301 },
302 #[error(
303 "Error when reading file content for file {path} with id {id}",
304 path = path.as_internal_file_string()
305 )]
306 ReadFile {
307 path: RepoPathBuf,
308 id: FileId,
309 source: Box<dyn std::error::Error + Send + Sync>,
310 },
311 #[error("Could not write object of type {object_type}")]
312 WriteObject {
313 object_type: &'static str,
314 source: Box<dyn std::error::Error + Send + Sync>,
315 },
316 #[error(transparent)]
317 Other(Box<dyn std::error::Error + Send + Sync>),
318 #[error("{0}")]
321 Unsupported(String),
322}
323
324pub type BackendResult<T> = Result<T, BackendError>;
325
326#[derive(ContentHash, Debug, PartialEq, Eq, Clone, Hash)]
327pub enum TreeValue {
328 File {
331 id: FileId,
332 executable: bool,
333 copy_id: CopyId,
334 },
335 Symlink(SymlinkId),
336 Tree(TreeId),
337 GitSubmodule(CommitId),
338}
339
340impl TreeValue {
341 pub fn hex(&self) -> String {
342 match self {
343 Self::File { id, .. } => id.hex(),
344 Self::Symlink(id) => id.hex(),
345 Self::Tree(id) => id.hex(),
346 Self::GitSubmodule(id) => id.hex(),
347 }
348 }
349}
350
351#[derive(Debug, PartialEq, Eq, Clone)]
352pub struct TreeEntry<'a> {
353 name: &'a RepoPathComponent,
354 value: &'a TreeValue,
355}
356
357impl<'a> TreeEntry<'a> {
358 pub fn new(name: &'a RepoPathComponent, value: &'a TreeValue) -> Self {
359 Self { name, value }
360 }
361
362 pub fn name(&self) -> &'a RepoPathComponent {
363 self.name
364 }
365
366 pub fn value(&self) -> &'a TreeValue {
367 self.value
368 }
369}
370
371pub struct TreeEntriesNonRecursiveIterator<'a> {
372 iter: slice::Iter<'a, (RepoPathComponentBuf, TreeValue)>,
373}
374
375impl<'a> Iterator for TreeEntriesNonRecursiveIterator<'a> {
376 type Item = TreeEntry<'a>;
377
378 fn next(&mut self) -> Option<Self::Item> {
379 self.iter
380 .next()
381 .map(|(name, value)| TreeEntry { name, value })
382 }
383}
384
385#[derive(ContentHash, Default, PartialEq, Eq, Debug, Clone)]
386pub struct Tree {
387 entries: Vec<(RepoPathComponentBuf, TreeValue)>,
388}
389
390impl Tree {
391 pub fn from_sorted_entries(entries: Vec<(RepoPathComponentBuf, TreeValue)>) -> Self {
392 debug_assert!(entries.is_sorted_by(|(a, _), (b, _)| a < b));
393 Self { entries }
394 }
395
396 pub fn is_empty(&self) -> bool {
397 self.entries.is_empty()
398 }
399
400 pub fn names(&self) -> impl Iterator<Item = &RepoPathComponent> {
401 self.entries.iter().map(|(name, _)| name.as_ref())
402 }
403
404 pub fn entries(&self) -> TreeEntriesNonRecursiveIterator<'_> {
405 TreeEntriesNonRecursiveIterator {
406 iter: self.entries.iter(),
407 }
408 }
409
410 pub fn entry(&self, name: &RepoPathComponent) -> Option<TreeEntry<'_>> {
411 let index = self
412 .entries
413 .binary_search_by_key(&name, |(name, _)| name)
414 .ok()?;
415 let (name, value) = &self.entries[index];
416 Some(TreeEntry { name, value })
417 }
418
419 pub fn value(&self, name: &RepoPathComponent) -> Option<&TreeValue> {
420 self.entry(name).map(|entry| entry.value)
421 }
422}
423
424pub fn make_root_commit(root_change_id: ChangeId, empty_tree_id: TreeId) -> Commit {
425 let timestamp = Timestamp {
426 timestamp: MillisSinceEpoch(0),
427 tz_offset: 0,
428 };
429 let signature = Signature {
430 name: String::new(),
431 email: String::new(),
432 timestamp,
433 };
434 Commit {
435 parents: vec![],
436 predecessors: vec![],
437 root_tree: MergedTreeId::resolved(empty_tree_id),
438 change_id: root_change_id,
439 description: String::new(),
440 author: signature.clone(),
441 committer: signature,
442 secure_sig: None,
443 }
444}
445
446#[async_trait]
448pub trait Backend: Any + Send + Sync + Debug {
449 fn name(&self) -> &str;
452
453 fn commit_id_length(&self) -> usize;
455
456 fn change_id_length(&self) -> usize;
458
459 fn root_commit_id(&self) -> &CommitId;
460
461 fn root_change_id(&self) -> &ChangeId;
462
463 fn empty_tree_id(&self) -> &TreeId;
464
465 fn concurrency(&self) -> usize;
473
474 async fn read_file(
475 &self,
476 path: &RepoPath,
477 id: &FileId,
478 ) -> BackendResult<Pin<Box<dyn AsyncRead + Send>>>;
479
480 async fn write_file(
481 &self,
482 path: &RepoPath,
483 contents: &mut (dyn AsyncRead + Send + Unpin),
484 ) -> BackendResult<FileId>;
485
486 async fn read_symlink(&self, path: &RepoPath, id: &SymlinkId) -> BackendResult<String>;
487
488 async fn write_symlink(&self, path: &RepoPath, target: &str) -> BackendResult<SymlinkId>;
489
490 async fn read_copy(&self, id: &CopyId) -> BackendResult<CopyHistory>;
495
496 async fn write_copy(&self, copy: &CopyHistory) -> BackendResult<CopyId>;
501
502 async fn get_related_copies(&self, copy_id: &CopyId) -> BackendResult<Vec<CopyHistory>>;
512
513 async fn read_tree(&self, path: &RepoPath, id: &TreeId) -> BackendResult<Tree>;
514
515 async fn write_tree(&self, path: &RepoPath, contents: &Tree) -> BackendResult<TreeId>;
516
517 async fn read_commit(&self, id: &CommitId) -> BackendResult<Commit>;
518
519 async fn write_commit(
533 &self,
534 contents: Commit,
535 sign_with: Option<&mut SigningFn>,
536 ) -> BackendResult<(CommitId, Commit)>;
537
538 fn get_copy_records(
551 &self,
552 paths: Option<&[RepoPathBuf]>,
553 root: &CommitId,
554 head: &CommitId,
555 ) -> BackendResult<BoxStream<'_, BackendResult<CopyRecord>>>;
556
557 fn gc(&self, index: &dyn Index, keep_newer: SystemTime) -> BackendResult<()>;
563}
564
565impl dyn Backend {
566 pub fn downcast_ref<T: Backend>(&self) -> Option<&T> {
568 (self as &dyn Any).downcast_ref()
569 }
570}