Skip to main content

jj_lib/
simple_backend.rs

1// Copyright 2020 The Jujutsu Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15#![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        // TODO: Write temporary file in the destination directory (#5712)
206        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        // TODO: Write temporary file in the destination directory (#5712)
235        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        // TODO: Write temporary file in the destination directory (#5712)
276        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        // TODO: Write temporary file in the destination directory (#5712)
319        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    // Note how .take() sets the secure_sig field to None before we encode the data.
377    // Needs to be done first since proto is partially moved a bunch below
378    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    // Serialized data should be sorted
417    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 that parents get written correctly
517    #[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        // No parents
540        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        // Only root commit as parent
547        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        // Only non-root commit as parent
553        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        // Merge commit
559        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        // Merge commit with root as one parent
565        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}