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