Skip to main content

radicle/storage/
git.rs

1#![warn(clippy::unwrap_used)]
2pub mod cob;
3pub mod transport;
4
5pub mod temp;
6pub use temp::TempRepository;
7
8use std::collections::{BTreeMap, BTreeSet, HashMap};
9use std::ops::{Deref, DerefMut};
10use std::path::{Path, PathBuf};
11use std::sync::LazyLock;
12use std::{fs, io};
13
14use crate::git::canonical::Quorum;
15use crate::git::raw::ErrorExt as _;
16use crate::identity::crefs::GetCanonicalRefs as _;
17use crate::identity::doc::DocError;
18use crate::identity::{CanonicalRefs, Doc, DocAt, RepoId};
19use crate::identity::{Identity, Project};
20use crate::node::device::Device;
21use crate::storage::refs::{FeatureLevel, Refs, SignedRefs};
22use crate::storage::{refs, SignedRefsInfo};
23use crate::storage::{
24    ReadRepository, ReadStorage, Remote, Remotes, RepositoryInfo, SetHead, SignRepository,
25    WriteRepository, WriteStorage,
26};
27use crate::{git, git::Oid, node};
28
29use crate::git::fmt::{
30    refname, refspec, refspec::PatternStr, refspec::PatternString, Qualified, RefString,
31};
32use crate::git::RefError;
33use crate::git::UserInfo;
34pub use crate::storage::{Error, RepositoryError};
35
36use super::refs::{sigrefs, RefsAt};
37use super::{RemoteId, RemoteRepository, ValidateRepository};
38
39pub static NAMESPACES_GLOB: LazyLock<PatternString> =
40    LazyLock::new(|| git::fmt::pattern!("refs/namespaces/*"));
41pub static SIGREFS_GLOB: LazyLock<refspec::PatternString> =
42    LazyLock::new(|| git::fmt::pattern!("refs/namespaces/*/rad/sigrefs"));
43pub static CANONICAL_IDENTITY: LazyLock<git::fmt::Qualified> = LazyLock::new(|| {
44    git::fmt::Qualified::from_components(
45        git::fmt::component!("rad"),
46        git::fmt::component!("id"),
47        None,
48    )
49});
50
51/// A parsed Git reference.
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct Ref {
54    pub oid: crate::git::Oid,
55    pub name: RefString,
56    pub namespace: Option<RemoteId>,
57}
58
59impl TryFrom<git::raw::Reference<'_>> for Ref {
60    type Error = RefError;
61
62    fn try_from(r: git::raw::Reference) -> Result<Self, Self::Error> {
63        let name = r.name().ok_or(RefError::InvalidName)?;
64        let (namespace, name) = match git::parse_ref_namespaced::<RemoteId>(name) {
65            Ok((namespace, refname)) => (Some(namespace), refname.to_ref_string()),
66            Err(RefError::MissingNamespace(refname)) => (None, refname),
67            Err(err) => return Err(err),
68        };
69        let oid = r.resolve()?.target().ok_or(RefError::NoTarget)?;
70
71        Ok(Self {
72            namespace,
73            name,
74            oid: oid.into(),
75        })
76    }
77}
78
79#[derive(Debug, Clone)]
80pub struct Storage {
81    path: PathBuf,
82    info: UserInfo,
83}
84
85impl ReadStorage for Storage {
86    type Repository = Repository;
87
88    fn info(&self) -> &UserInfo {
89        &self.info
90    }
91
92    fn path(&self) -> &Path {
93        self.path.as_path()
94    }
95
96    fn path_of(&self, rid: &RepoId) -> PathBuf {
97        paths::repository(&self, rid)
98    }
99
100    fn contains(&self, rid: &RepoId) -> Result<bool, RepositoryError> {
101        if paths::repository(&self, rid).exists() {
102            let _ = self.repository(*rid)?.head()?;
103            return Ok(true);
104        }
105        Ok(false)
106    }
107
108    fn repository(&self, rid: RepoId) -> Result<Self::Repository, RepositoryError> {
109        Repository::open(paths::repository(self, &rid), rid)
110    }
111
112    fn repositories(&self) -> Result<Vec<RepositoryInfo>, Error> {
113        let mut repos = Vec::new();
114
115        for result in fs::read_dir(&self.path)? {
116            let path = result?;
117
118            // Skip non-directories.
119            if !path.file_type()?.is_dir() {
120                continue;
121            }
122            // Skip hidden files.
123            if path.file_name().to_string_lossy().starts_with('.') {
124                continue;
125            }
126
127            if let Some(ext) = path.path().extension().and_then(|s| s.to_str()) {
128                if ext == TempRepository::EXT {
129                    // Skip temporary repositories
130                    log::debug!(target: "storage", "Skipping temporary repository at '{}'", path.path().display());
131                    continue;
132                } else if "lock" == ext {
133                    // In previous versions, the extension ".lock" was used for temporary repositories.
134                    // This is to handle those names in a backward-compatible way.
135                    log::debug!(target: "storage", "Skipping locked repository at '{}'", path.path().display());
136                    continue;
137                } else {
138                    log::warn!(target: "storage", "Found path '{}' with unexpected extension '{ext}'", path.path().display());
139                }
140            }
141
142            let rid = RepoId::try_from(path.file_name())
143                .map_err(|_| Error::InvalidId(path.file_name()))?;
144
145            let repo = match self.repository(rid) {
146                Ok(repo) => repo,
147                Err(e) => {
148                    log::warn!(target: "storage", "Repository {rid} is invalid: {e}");
149                    continue;
150                }
151            };
152            let doc = match repo.identity_doc() {
153                Ok(doc) => doc.into(),
154                Err(e) => {
155                    log::warn!(target: "storage", "Repository {rid} is invalid: looking up doc: {e}");
156                    continue;
157                }
158            };
159
160            // For performance reasons, we don't do a full repository check here.
161            let head = match repo.head() {
162                Ok((_, head)) => head,
163                Err(e) => {
164                    log::warn!(target: "storage", "Repository {rid} is invalid: looking up head: {e}");
165                    continue;
166                }
167            };
168            // Nb. This will be `None` if they were not found.
169            let refs = SignedRefsInfo::new(refs::SignedRefs::load(self.info.key, &repo))
170                .map_err(|err| Error::Refs(refs::Error::Read(err)))?;
171
172            let synced_at = match &refs {
173                SignedRefsInfo::Some(refs) => Some(node::SyncedAt::new(refs.at, &repo)?),
174                _ => None,
175            };
176
177            repos.push(RepositoryInfo {
178                rid,
179                head,
180                doc,
181                refs,
182                synced_at,
183            });
184        }
185        Ok(repos)
186    }
187}
188
189impl WriteStorage for Storage {
190    type RepositoryMut = Repository;
191
192    fn repository_mut(&self, rid: RepoId) -> Result<Self::RepositoryMut, RepositoryError> {
193        Repository::open(paths::repository(self, &rid), rid)
194    }
195
196    fn create(&self, rid: RepoId) -> Result<Self::RepositoryMut, Error> {
197        Repository::create(paths::repository(self, &rid), rid, &self.info)
198    }
199
200    fn clean(&self, rid: RepoId) -> Result<Vec<RemoteId>, RepositoryError> {
201        let repo = self.repository(rid)?;
202        // N.b. we remove the repository if the `local` peer has no
203        // `rad/sigrefs`. There's no risk of them corrupting data.
204        let has_sigrefs = SignedRefs::load(self.info.key, &repo)
205            .map_err(|err| RepositoryError::from(refs::Error::Read(err)))?
206            .is_some();
207        if has_sigrefs {
208            repo.clean(&self.info.key)
209        } else {
210            let remotes = repo.remote_ids()?.collect::<Result<_, _>>()?;
211            repo.remove()?;
212            Ok(remotes)
213        }
214    }
215}
216
217impl Storage {
218    /// Open a new storage instance and load its inventory.
219    pub fn open<P: AsRef<Path>>(path: P, info: UserInfo) -> Result<Self, Error> {
220        let path = path.as_ref().to_path_buf();
221
222        match fs::create_dir_all(&path) {
223            Err(err) if err.kind() == io::ErrorKind::AlreadyExists => {}
224            Err(err) => return Err(Error::Io(err)),
225            Ok(()) => {}
226        }
227        Ok(Self { path, info })
228    }
229
230    /// Create a [`Repository`] in a temporary directory.
231    ///
232    /// This is used to prevent other processes accessing it during
233    /// initialization. Usually, callers will want to move the repository
234    /// to its destination after initialization in the temporary location.
235    pub fn temporary_repository(&self, rid: RepoId) -> Result<TempRepository, RepositoryError> {
236        if self.contains(&rid)? {
237            return Err(Error::Io(io::Error::new(
238                io::ErrorKind::AlreadyExists,
239                format!("refusing to create temporary repository for {rid}"),
240            ))
241            .into());
242        }
243        TempRepository::new(self.path(), rid, &self.info)
244    }
245
246    pub fn path(&self) -> &Path {
247        self.path.as_path()
248    }
249
250    pub fn repositories_by_id<'a, I>(
251        &self,
252        rids: I,
253    ) -> impl Iterator<Item = Result<RepositoryInfo, RepositoryError>> + use<'_, 'a, I>
254    where
255        I: Iterator<Item = &'a RepoId>,
256    {
257        rids.map(|rid| {
258            let repo = self.repository(*rid)?;
259            let (_, head) = repo.head()?;
260
261            let refs = SignedRefsInfo::new(refs::SignedRefs::load(self.info.key, &repo))
262                .map_err(|err| Error::Refs(refs::Error::Read(err)))?;
263
264            let synced_at = match &refs {
265                SignedRefsInfo::Some(refs) => Some(node::SyncedAt::new(refs.at, &repo)?),
266                _ => None,
267            };
268
269            Ok(RepositoryInfo {
270                rid: *rid,
271                head,
272                doc: repo.identity_doc()?.into(),
273                refs,
274                synced_at,
275            })
276        })
277    }
278
279    pub fn inspect(&self) -> Result<(), RepositoryError> {
280        for r in self.repositories()? {
281            let rid = r.rid;
282            let repo = self.repository(rid)?;
283
284            for r in repo.raw().references()? {
285                let r = r?;
286                let name = r.name().ok_or(Error::InvalidRef)?;
287                let oid = r.resolve()?.target().ok_or(Error::InvalidRef)?;
288
289                println!("{} {oid} {name}", rid.urn());
290            }
291        }
292        Ok(())
293    }
294}
295
296/// Git implementation of [`WriteRepository`] using the `git2` crate.
297pub struct Repository {
298    /// The repository identifier (RID).
299    pub id: RepoId,
300    /// The backing Git repository.
301    pub backend: git::raw::Repository,
302}
303
304impl AsRef<Repository> for Repository {
305    fn as_ref(&self) -> &Repository {
306        self
307    }
308}
309
310impl git::canonical::effects::Ancestry for Repository {
311    fn graph_ahead_behind(
312        &self,
313        commit: Oid,
314        upstream: Oid,
315    ) -> Result<git::canonical::GraphAheadBehind, git::canonical::effects::GraphDescendant> {
316        git::canonical::effects::Ancestry::graph_ahead_behind(&self.backend, commit, upstream)
317    }
318}
319
320impl git::canonical::effects::FindMergeBase for Repository {
321    fn merge_base(
322        &self,
323        a: Oid,
324        b: Oid,
325    ) -> Result<git::canonical::MergeBase, git::canonical::effects::MergeBaseError> {
326        git::canonical::effects::FindMergeBase::merge_base(&self.backend, a, b)
327    }
328}
329
330impl git::canonical::effects::FindObjects for Repository {
331    fn find_objects<'a, 'b, I>(
332        &self,
333        refname: &Qualified<'a>,
334        dids: I,
335    ) -> Result<git::canonical::FoundObjects, git::canonical::effects::FindObjectsError>
336    where
337        I: Iterator<Item = &'b crate::prelude::Did>,
338    {
339        git::canonical::effects::FindObjects::find_objects(&self.backend, refname, dids)
340    }
341}
342
343/// A set of [`Validation`] errors that a caller **must use**.
344#[must_use]
345#[derive(Debug, Default)]
346pub struct Validations(pub Vec<Validation>);
347
348impl Validations {
349    pub fn append(&mut self, vs: &mut Self) {
350        self.0.append(&mut vs.0)
351    }
352}
353
354impl IntoIterator for Validations {
355    type Item = Validation;
356    type IntoIter = std::vec::IntoIter<Self::Item>;
357
358    fn into_iter(self) -> Self::IntoIter {
359        self.0.into_iter()
360    }
361}
362
363impl Deref for Validations {
364    type Target = Vec<Validation>;
365
366    fn deref(&self) -> &Self::Target {
367        &self.0
368    }
369}
370
371impl DerefMut for Validations {
372    fn deref_mut(&mut self) -> &mut Self::Target {
373        &mut self.0
374    }
375}
376
377/// Validation errors that can occur when verifying the layout of the
378/// storage. These errors include checking the validity of the
379/// `rad/sigrefs` contents and the identity of the repository.
380#[derive(Debug, Error)]
381pub enum Validation {
382    #[error("found unsigned ref `{0}`")]
383    UnsignedRef(RefString),
384    #[error("expected `refs/namespaces/{remote}/{refname}` at {expected} but found {actual}")]
385    MismatchedRef {
386        remote: RemoteId,
387        refname: RefString,
388        expected: Oid,
389        actual: Oid,
390    },
391    #[error("missing `refs/namespaces/{remote}/{refname}`")]
392    MissingRef {
393        remote: RemoteId,
394        refname: RefString,
395    },
396    #[error("missing `refs/namespaces/{0}/refs/rad/sigrefs`")]
397    MissingRadSigRefs(RemoteId),
398    #[error("failed to read `refs/namespaces/{remote}/refs/rad/sigrefs`: {source}")]
399    Read {
400        remote: RemoteId,
401        #[source]
402        source: crate::storage::refs::sigrefs::read::error::Read,
403    },
404    #[error(
405        "rejecting `refs/namespaces/{remote}/refs/rad/sigrefs` on feature level '{actual}', below required minimum '{minimum}'"
406    )]
407    InsufficientFeatureLevel {
408        remote: RemoteId,
409        actual: FeatureLevel,
410        minimum: FeatureLevel,
411    },
412}
413
414impl Repository {
415    /// Open an existing repository.
416    pub fn open<P: AsRef<Path>>(path: P, id: RepoId) -> Result<Self, RepositoryError> {
417        let backend = git::raw::Repository::open_ext(
418            path.as_ref(),
419            git::raw::RepositoryOpenFlags::empty()
420                | git::raw::RepositoryOpenFlags::BARE
421                | git::raw::RepositoryOpenFlags::NO_DOTGIT
422                | git::raw::RepositoryOpenFlags::NO_SEARCH,
423            &[] as &[&std::ffi::OsStr],
424        )?;
425
426        Ok(Self { id, backend })
427    }
428
429    /// Create a new repository.
430    pub fn create<P: AsRef<Path>>(path: P, id: RepoId, info: &UserInfo) -> Result<Self, Error> {
431        let backend = git::raw::Repository::init_opts(
432            &path,
433            git::raw::RepositoryInitOptions::new()
434                .bare(true)
435                .no_reinit(true)
436                .external_template(false),
437        )?;
438
439        {
440            // Even though `external_template(false)` is called above,
441            // libgit2 places stub files in the repository:
442            // https://github.com/libgit2/libgit2/blob/ca225744b992bf2bf24e9a2eb357ddef78179667/src/libgit2/repo_template.h#L50-L54
443            // This is helpful for a "normal" repository, directly interacted
444            // with by a human, but not necessary for our use case.
445            // Attempt to remove these files, but ignore any errors.
446            // An alternative solution would be to define our own template,
447            // but distributing that template is way more complex than
448            // deleting a handful of files.
449            let _ = fs::remove_dir_all(path.as_ref().join("hooks"));
450            let _ = fs::remove_dir_all(path.as_ref().join("info"));
451            let _ = fs::remove_file(path.as_ref().join("description"));
452        }
453
454        let mut config = backend.config()?;
455
456        config.set_str("user.name", &info.name())?;
457        config.set_str("user.email", &info.email())?;
458
459        Ok(Self { id, backend })
460    }
461
462    /// Remove an existing repository
463    pub fn remove(&self) -> Result<(), Error> {
464        let path = self.backend.path();
465        if path.exists() {
466            fs::remove_dir_all(path)?;
467        }
468        Ok(())
469    }
470
471    /// Remove all the remotes of a repository that are not the
472    /// delegates of the repository or the local peer.
473    ///
474    /// N.b. failure to delete remotes or references will not result
475    /// in an early exit. Instead, this method continues to delete the
476    /// next available remote or reference.
477    pub fn clean(&self, local: &RemoteId) -> Result<Vec<RemoteId>, RepositoryError> {
478        let delegates = self
479            .delegates()?
480            .into_iter()
481            .map(|did| *did)
482            .collect::<BTreeSet<_>>();
483        let mut deleted = Vec::new();
484        for id in self.remote_ids()? {
485            let id = match id {
486                Ok(id) => id,
487                Err(e) => {
488                    log::error!(target: "storage", "Failed to clean up remote: {e}");
489                    continue;
490                }
491            };
492
493            // N.b. it is fatal to delete local or delegates
494            if *local == id || delegates.contains(&id) {
495                continue;
496            }
497
498            let glob = git::fmt::refname!("refs/namespaces")
499                .join(git::fmt::Component::from(&id))
500                .with_pattern(git::fmt::refspec::STAR);
501            let refs = match self.references_glob(&glob) {
502                Ok(refs) => refs,
503                Err(e) => {
504                    log::error!(target: "storage", "Failed to clean up remote '{id}': {e}");
505                    continue;
506                }
507            };
508            for (refname, _) in refs {
509                if let Ok(mut r) = self.backend.find_reference(refname.as_str()) {
510                    if let Err(e) = r.delete() {
511                        log::error!(target: "storage", "Failed to clean up reference '{refname}': {e}");
512                    }
513                } else {
514                    log::error!(target: "storage", "Failed to clean up reference '{refname}'");
515                }
516            }
517            deleted.push(id);
518        }
519
520        Ok(deleted)
521    }
522
523    /// Create the repository's identity branch.
524    pub fn init<G, S>(
525        doc: &Doc,
526        storage: &S,
527        signer: &Device<G>,
528    ) -> Result<(Self, crate::git::Oid), RepositoryError>
529    where
530        G: crypto::signature::Signer<crypto::Signature>,
531        S: WriteStorage,
532    {
533        let (doc_oid, doc_bytes) = doc.encode()?;
534        let id = RepoId::from(doc_oid);
535        let repo = Self::create(paths::repository(storage, &id), id, storage.info())?;
536        let oid = repo.backend.blob(&doc_bytes)?; // Store document blob in repository.
537
538        debug_assert_eq!(doc_oid, oid);
539
540        let commit = doc.init(&repo, signer)?;
541
542        Ok((repo, commit))
543    }
544
545    pub fn inspect(&self) -> Result<(), Error> {
546        for r in self.backend.references()? {
547            let r = r?;
548            let name = r.name().ok_or(Error::InvalidRef)?;
549            let oid = r.resolve()?.target().ok_or(Error::InvalidRef)?;
550
551            println!("{oid} {name}");
552        }
553        Ok(())
554    }
555
556    /// Iterate over all references.
557    pub fn references(
558        &self,
559    ) -> Result<impl Iterator<Item = Result<Ref, refs::Error>> + '_, git::raw::Error> {
560        let refs = self
561            .backend
562            .references()?
563            .map(|reference| {
564                let r = reference?;
565
566                match Ref::try_from(r) {
567                    Err(err) => Err(err.into()),
568                    Ok(r) => Ok(Some(r)),
569                }
570            })
571            .filter_map(Result::transpose);
572
573        Ok(refs)
574    }
575
576    /// Get the canonical project information.
577    pub fn project(&self) -> Result<Project, RepositoryError> {
578        let head = self.identity_head()?;
579        let doc = self.identity_doc_at(head)?;
580        let proj = doc.project()?;
581
582        Ok(proj)
583    }
584
585    pub fn identity_doc_of(&self, remote: &RemoteId) -> Result<Doc, DocError> {
586        let oid = self.identity_head_of(remote)?;
587        Doc::load_at(oid, self).map(|d| d.into())
588    }
589
590    pub fn remote_ids(
591        &self,
592    ) -> Result<impl Iterator<Item = Result<RemoteId, refs::Error>> + '_, git::raw::Error> {
593        let iter = self.backend.references_glob(SIGREFS_GLOB.as_str())?.map(
594            |reference| -> Result<RemoteId, refs::Error> {
595                let r = reference?;
596                let name = r.name().ok_or(refs::Error::InvalidRef)?;
597                let (id, _) = git::parse_ref_namespaced::<RemoteId>(name)?;
598
599                Ok(id)
600            },
601        );
602        Ok(iter)
603    }
604
605    pub fn remotes(
606        &self,
607    ) -> Result<impl Iterator<Item = Result<(RemoteId, Remote), refs::Error>> + '_, git::raw::Error>
608    {
609        let remotes =
610            self.backend
611                .references_glob(SIGREFS_GLOB.as_str())?
612                .map(|reference| -> Result<_, _> {
613                    let r = reference?;
614                    let name = r.name().ok_or(refs::Error::InvalidRef)?;
615                    let (id, _) = git::parse_ref_namespaced::<RemoteId>(name)?;
616                    let remote = self.remote(&id)?;
617
618                    Ok((id, remote))
619                });
620        Ok(remotes)
621    }
622}
623
624impl RemoteRepository for Repository {
625    fn remotes(&self) -> Result<Remotes, refs::Error> {
626        let mut remotes = Vec::new();
627        for remote in Repository::remotes(self)? {
628            remotes.push(remote?);
629        }
630        Ok(Remotes::from_iter(remotes))
631    }
632
633    fn remote(&self, remote: &RemoteId) -> Result<Remote, refs::Error> {
634        let refs = SignedRefs::load(*remote, self)?;
635        let refs = refs.ok_or_else(|| {
636            refs::Error::Read(refs::sigrefs::read::error::Read::MissingSigrefs {
637                namespace: *remote,
638            })
639        })?;
640        Ok(Remote::new(refs))
641    }
642
643    fn remote_refs_at(&self) -> Result<Vec<RefsAt>, refs::Error> {
644        let mut all = Vec::new();
645
646        for remote in self.remote_ids()? {
647            let remote = remote?;
648            let refs_at = RefsAt::new(self, remote)?;
649
650            all.push(refs_at);
651        }
652        Ok(all)
653    }
654}
655
656impl ValidateRepository for Repository {
657    fn validate_remote(&self, remote: &Remote) -> Result<Validations, Error> {
658        // Contains a copy of the signed refs of this remote.
659        let mut signed = BTreeMap::from((*remote.refs).clone());
660        let mut failures = Validations::default();
661        let mut has_sigrefs = false;
662
663        // Check all repository references, making sure they are present in the signed refs map.
664        for (refname, oid) in self.references_of(&remote.id())? {
665            // Skip validation of the signed refs branch, as it is not part of `Remote`.
666            if refname == refs::SIGREFS_BRANCH.to_ref_string() {
667                has_sigrefs = true;
668                continue;
669            }
670            if let Some(signed_oid) = signed.remove(&refname) {
671                if oid != signed_oid {
672                    failures.push(Validation::MismatchedRef {
673                        remote: remote.id(),
674                        refname,
675                        expected: signed_oid,
676                        actual: oid,
677                    });
678                }
679            } else {
680                failures.push(Validation::UnsignedRef(refname));
681            }
682        }
683
684        if !has_sigrefs {
685            failures.push(Validation::MissingRadSigRefs(remote.id()));
686        }
687
688        // The refs that are left in the map, are ones that were signed, but are not
689        // in the repository. If any are left, bail.
690        if let Some((name, _)) = signed.into_iter().next() {
691            failures.push(Validation::MissingRef {
692                refname: name,
693                remote: remote.id(),
694            });
695        }
696
697        // Nb. As it stands, it doesn't make sense to verify a single remote's identity branch,
698        // since it is a COB.
699
700        Ok(failures)
701    }
702}
703
704impl ReadRepository for Repository {
705    fn id(&self) -> RepoId {
706        self.id
707    }
708
709    fn is_empty(&self) -> Result<bool, git::raw::Error> {
710        Ok(self.remotes()?.next().is_none())
711    }
712
713    fn path(&self) -> &Path {
714        self.backend.path()
715    }
716
717    fn blob_at<P: AsRef<Path>>(
718        &self,
719        commit_id: Oid,
720        path: P,
721    ) -> Result<git::raw::Blob<'_>, git::raw::Error> {
722        let commit = self.backend.find_commit(git::raw::Oid::from(commit_id))?;
723        let tree = commit.tree()?;
724        let entry = tree.get_path(path.as_ref())?;
725        let obj = entry.to_object(&self.backend)?;
726        let blob = obj.into_blob().map_err(|_|
727            crate::git::raw::Error::new(
728                crate::git::raw::ErrorCode::NotFound,
729                crate::git::raw::ErrorClass::None,
730                format!("Path '{}' in tree of commit {commit_id} was expected to be a blob, but is not.", path.as_ref().display()),
731            )
732        )?;
733
734        Ok(blob)
735    }
736
737    fn blob(&self, oid: Oid) -> Result<git::raw::Blob<'_>, git::raw::Error> {
738        self.backend.find_blob(oid.into())
739    }
740
741    fn reference(
742        &self,
743        remote: &RemoteId,
744        name: &git::fmt::Qualified,
745    ) -> Result<git::raw::Reference<'_>, git::raw::Error> {
746        let name = name.with_namespace(remote.into());
747        self.backend.find_reference(&name)
748    }
749
750    fn reference_oid(
751        &self,
752        remote: &RemoteId,
753        reference: &git::fmt::Qualified,
754    ) -> Result<Oid, crate::git::raw::Error> {
755        let name = reference.with_namespace(remote.into());
756        let oid = self.backend.refname_to_id(&name)?;
757
758        Ok(oid.into())
759    }
760
761    fn commit(&self, oid: Oid) -> Result<git::raw::Commit<'_>, git::raw::Error> {
762        self.backend.find_commit(oid.into())
763    }
764
765    fn revwalk(&self, head: Oid) -> Result<git::raw::Revwalk<'_>, git::raw::Error> {
766        let mut revwalk = self.backend.revwalk()?;
767        revwalk.push(head.into())?;
768
769        Ok(revwalk)
770    }
771
772    fn contains(&self, oid: Oid) -> Result<bool, crate::git::raw::Error> {
773        self.backend.odb().map(|odb| odb.exists(oid.into()))
774    }
775
776    fn is_ancestor_of(&self, ancestor: Oid, head: Oid) -> Result<bool, crate::git::raw::Error> {
777        self.backend
778            .graph_descendant_of(head.into(), ancestor.into())
779    }
780
781    /// The published references of the given `remote`.
782    ///
783    /// Note that this includes all references, including `refs/rad/sigrefs`.
784    /// This reference must be removed before signing the payload.
785    ///
786    /// # Skipped References
787    ///
788    /// References created by [`staging::patch`], i.e. references that begin
789    /// with `refs/tmp/heads`, are skipped.
790    ///
791    /// [`staging::patch`]: crate::git::refs::storage::staging::patch
792    fn references_of(&self, remote: &RemoteId) -> Result<Refs, Error> {
793        let entries = self
794            .backend
795            .references_glob(format!("refs/namespaces/{remote}/*").as_str())?;
796
797        let mut refs = Refs::new();
798
799        for e in entries {
800            let e = e?;
801            let name = e.name().ok_or(Error::InvalidRef)?;
802            let (_, refname) = git::parse_ref::<RemoteId>(name)?;
803            let oid = e.resolve()?.target().ok_or(Error::InvalidRef)?;
804            let (_, category, subcategory, _) = refname.non_empty_components();
805
806            match (category.as_str(), subcategory.as_str()) {
807                ("tmp", "heads") => continue,
808                _ => {
809                    refs.insert(refname.into(), oid.into());
810                }
811            }
812        }
813        Ok(refs)
814    }
815
816    fn references_glob(
817        &self,
818        pattern: &PatternStr,
819    ) -> Result<Vec<(Qualified<'_>, Oid)>, crate::git::raw::Error> {
820        let mut refs = Vec::new();
821
822        for r in self.backend.references_glob(pattern)? {
823            let r = r?;
824
825            let Some(oid) = r.resolve()?.target() else {
826                continue;
827            };
828
829            if let Some(name) = r
830                .name()
831                .and_then(|n| git::fmt::RefStr::try_from_str(n).ok())
832                .and_then(git::fmt::Qualified::from_refstr)
833            {
834                refs.push((name.to_owned(), oid.into()));
835            }
836        }
837        Ok(refs)
838    }
839
840    fn identity_doc_at(&self, head: Oid) -> Result<DocAt, DocError> {
841        Doc::load_at(head, self)
842    }
843
844    fn head(&self) -> Result<(Qualified<'_>, Oid), RepositoryError> {
845        // If `HEAD` is already set locally, just return that.
846        if let Ok(head) = self.backend.head() {
847            if let Ok((name, oid)) = git::refs::qualified_from(&head) {
848                return Ok((name.to_owned(), oid));
849            }
850        }
851        self.canonical_head()
852    }
853
854    fn canonical_head(&self) -> Result<(Qualified<'_>, Oid), RepositoryError> {
855        let doc = self.identity_doc()?;
856        let refname = git::refs::branch(doc.project()?.default_branch());
857        let crefs = match doc.canonical_refs()? {
858            Some(crefs) => crefs,
859            // Fallback to constructing the default branch via the project
860            // payload
861            None => CanonicalRefs::from_iter([doc.default_branch_rule()?]),
862        };
863        Ok(crefs
864            .rules()
865            .canonical(refname, self)
866            .ok_or(RepositoryError::MissingBranchRule)?
867            .find_objects()?
868            .quorum()?)
869        .map(
870            |Quorum {
871                 refname, object, ..
872             }| (refname, object.id()),
873        )
874    }
875
876    fn identity_head(&self) -> Result<Oid, RepositoryError> {
877        let result = self
878            .backend
879            .refname_to_id(CANONICAL_IDENTITY.as_str())
880            .map(Oid::from);
881
882        match result {
883            Ok(oid) => Ok(oid),
884            Err(err) if err.is_not_found() => self.canonical_identity_head(),
885            Err(err) => Err(err.into()),
886        }
887    }
888
889    fn identity_head_of(&self, remote: &RemoteId) -> Result<Oid, crate::git::raw::Error> {
890        self.reference_oid(remote, &git::refs::storage::IDENTITY_BRANCH)
891    }
892
893    fn identity_root(&self) -> Result<Oid, RepositoryError> {
894        let oid = self.backend.refname_to_id(CANONICAL_IDENTITY.as_str())?;
895        let root = self
896            .revwalk(oid.into())?
897            .last()
898            .ok_or(RepositoryError::Doc(DocError::Missing))??;
899
900        Ok(root.into())
901    }
902
903    fn identity_root_of(&self, remote: &RemoteId) -> Result<Oid, RepositoryError> {
904        // Remotes that run newer clients will have this reference set. For older clients,
905        // compute the root OID based on the identity head.
906        if let Ok(root) = self.reference_oid(remote, &git::refs::storage::IDENTITY_ROOT) {
907            return Ok(root);
908        }
909        let oid = self.identity_head_of(remote)?;
910        let root = self
911            .revwalk(oid)?
912            .last()
913            .ok_or(RepositoryError::Doc(DocError::Missing))??;
914
915        Ok(root.into())
916    }
917
918    fn canonical_identity_head(&self) -> Result<Oid, RepositoryError> {
919        for remote in self.remote_ids()? {
920            let remote = remote?;
921            // Nb. A remote may not have an identity document if the user has not contributed
922            // any changes to the identity COB.
923            let Ok(root) = self.identity_root_of(&remote) else {
924                continue;
925            };
926            let blob = Doc::blob_at(root, self)?;
927
928            // We've got an identity that goes back to the correct root.
929            if *self.id == blob.id() {
930                let identity = Identity::get(&root.into(), self)?;
931
932                return Ok(identity.head());
933            }
934        }
935        Err(DocError::Missing.into())
936    }
937
938    fn merge_base(&self, left: &Oid, right: &Oid) -> Result<Oid, crate::git::raw::Error> {
939        self.backend
940            .merge_base(left.into(), right.into())
941            .map(Oid::from)
942    }
943}
944
945impl WriteRepository for Repository {
946    fn set_head(&self) -> Result<SetHead, RepositoryError> {
947        let head_ref = refname!("HEAD");
948        let old = self
949            .raw()
950            .refname_to_id(&head_ref)
951            .ok()
952            .map(|oid| oid.into());
953
954        let (branch_ref, new) = self.canonical_head()?;
955
956        if old == Some(new) {
957            return Ok(SetHead { old, new });
958        }
959        log::debug!(target: "storage", "Setting ref: {} -> {}", &branch_ref, new);
960        self.raw()
961            .reference(&branch_ref, new.into(), true, "set-local-branch (radicle)")?;
962
963        log::debug!(target: "storage", "Setting ref: {head_ref} -> {branch_ref}");
964        self.raw()
965            .reference_symbolic(&head_ref, &branch_ref, true, "set-head (radicle)")?;
966
967        Ok(SetHead { old, new })
968    }
969
970    fn set_identity_head_to(&self, commit: Oid) -> Result<(), RepositoryError> {
971        log::debug!(target: "storage", "Setting ref: {} -> {}", *CANONICAL_IDENTITY, commit);
972        self.raw().reference(
973            CANONICAL_IDENTITY.as_str(),
974            commit.into(),
975            true,
976            "set-local-branch (radicle)",
977        )?;
978        Ok(())
979    }
980
981    fn set_remote_identity_root_to(
982        &self,
983        remote: &RemoteId,
984        root: Oid,
985    ) -> Result<(), RepositoryError> {
986        let refname = git::refs::storage::id_root(remote);
987
988        self.raw()
989            .reference(refname.as_str(), root.into(), true, "set-id-root (radicle)")?;
990
991        Ok(())
992    }
993
994    fn set_user(&self, info: &UserInfo) -> Result<(), Error> {
995        let mut config = self.backend.config()?;
996        config.set_str("user.name", &info.name())?;
997        config.set_str("user.email", &info.email())?;
998        Ok(())
999    }
1000
1001    fn raw(&self) -> &git::raw::Repository {
1002        &self.backend
1003    }
1004}
1005
1006impl SignRepository for Repository {
1007    fn sign_refs<G: crypto::signature::Signer<crypto::Signature>>(
1008        &self,
1009        signer: &Device<G>,
1010    ) -> Result<SignedRefs, RepositoryError> {
1011        self.sign_refs_with(signer, false)
1012    }
1013
1014    fn force_sign_refs<G: crypto::signature::Signer<crypto::Signature>>(
1015        &self,
1016        signer: &Device<G>,
1017    ) -> Result<SignedRefs, RepositoryError> {
1018        self.sign_refs_with(signer, true)
1019    }
1020}
1021
1022impl Repository {
1023    fn sign_refs_with<G: crypto::signature::Signer<crypto::Signature>>(
1024        &self,
1025        signer: &Device<G>,
1026        force: bool,
1027    ) -> Result<SignedRefs, RepositoryError> {
1028        let remote = signer.public_key();
1029        // Ensure the root reference is set, which is checked during sigref verification.
1030        if self
1031            .reference_oid(remote, &git::refs::storage::IDENTITY_ROOT)
1032            .is_err()
1033        {
1034            self.set_remote_identity_root(remote)?;
1035        }
1036
1037        let committer = refs::sigrefs::git::Committer::from_env_or_now(remote);
1038
1039        let refs = self.references_of(remote)?;
1040        let signed = if force {
1041            refs.force_save(*remote, committer, self, signer)?
1042        } else {
1043            refs.save(*remote, committer, self, signer)?
1044        };
1045
1046        Ok(signed)
1047    }
1048}
1049
1050impl sigrefs::git::object::Reader for Repository {
1051    fn read_commit(
1052        &self,
1053        oid: &Oid,
1054    ) -> Result<Option<Vec<u8>>, sigrefs::git::object::error::ReadCommit> {
1055        self.backend.read_commit(oid)
1056    }
1057
1058    fn read_blob(
1059        &self,
1060        commit: &Oid,
1061        path: &Path,
1062    ) -> Result<Option<sigrefs::git::object::Blob>, sigrefs::git::object::error::ReadBlob> {
1063        self.backend.read_blob(commit, path)
1064    }
1065}
1066
1067impl sigrefs::git::object::Writer for Repository {
1068    fn write_tree(
1069        &self,
1070        refs: sigrefs::git::object::RefsEntry,
1071        signature: sigrefs::git::object::SignatureEntry,
1072    ) -> Result<Oid, sigrefs::git::object::error::WriteTree> {
1073        self.backend.write_tree(refs, signature)
1074    }
1075
1076    fn write_commit(&self, bytes: &[u8]) -> Result<Oid, sigrefs::git::object::error::WriteCommit> {
1077        self.backend.write_commit(bytes)
1078    }
1079}
1080
1081impl sigrefs::git::reference::Reader for Repository {
1082    fn find_reference(
1083        &self,
1084        reference: &git::fmt::Namespaced,
1085    ) -> Result<Option<Oid>, sigrefs::git::reference::error::FindReference> {
1086        sigrefs::git::reference::Reader::find_reference(&self.backend, reference)
1087    }
1088}
1089
1090impl sigrefs::git::reference::Writer for Repository {
1091    fn write_reference(
1092        &self,
1093        reference: &git::fmt::Namespaced,
1094        commit: Oid,
1095        parent: Option<Oid>,
1096        reflog: String,
1097    ) -> Result<(), sigrefs::git::reference::error::WriteReference> {
1098        self.backend
1099            .write_reference(reference, commit, parent, reflog)
1100    }
1101}
1102
1103pub mod trailers {
1104    use std::str::FromStr;
1105
1106    use thiserror::Error;
1107
1108    use super::*;
1109    use crypto::{PublicKey, PublicKeyError};
1110    use crypto::{Signature, SignatureError};
1111
1112    pub const SIGNATURE_TRAILER: &str = "Rad-Signature";
1113
1114    #[derive(Error, Debug)]
1115    pub enum Error {
1116        #[error("invalid format for signature trailer")]
1117        SignatureTrailerFormat,
1118        #[error("invalid public key in signature trailer")]
1119        PublicKey(#[from] PublicKeyError),
1120        #[error("invalid signature in trailer")]
1121        Signature(#[from] SignatureError),
1122    }
1123
1124    pub fn parse_signatures(msg: &str) -> Result<HashMap<PublicKey, Signature>, Error> {
1125        let trailers =
1126            git::raw::message_trailers_strs(msg).map_err(|_| Error::SignatureTrailerFormat)?;
1127        let mut signatures = HashMap::with_capacity(trailers.len());
1128
1129        for (key, val) in trailers.iter() {
1130            if key == SIGNATURE_TRAILER {
1131                if let Some((pk, sig)) = val.split_once(' ') {
1132                    let pk = PublicKey::from_str(pk)?;
1133                    let sig = Signature::from_str(sig)?;
1134
1135                    signatures.insert(pk, sig);
1136                } else {
1137                    return Err(Error::SignatureTrailerFormat);
1138                }
1139            }
1140        }
1141        Ok(signatures)
1142    }
1143}
1144
1145pub mod paths {
1146    use std::path::PathBuf;
1147
1148    use super::ReadStorage;
1149    use super::RepoId;
1150
1151    pub fn repository<S: ReadStorage>(storage: &S, proj: &RepoId) -> PathBuf {
1152        storage.path().join(proj.canonical())
1153    }
1154}
1155
1156#[cfg(test)]
1157#[allow(clippy::unwrap_used)]
1158mod tests {
1159
1160    use super::*;
1161    use crate::git;
1162
1163    use crate::storage::{ReadRepository, ReadStorage};
1164    use crate::test::fixtures;
1165
1166    #[test]
1167    fn test_references_of() {
1168        let tmp = tempfile::tempdir().unwrap();
1169        let signer = Device::mock();
1170        let storage = Storage::open(tmp.path().join("storage"), fixtures::user()).unwrap();
1171
1172        transport::local::register(storage.clone());
1173
1174        let (rid, _, _, _) =
1175            fixtures::project(tmp.path().join("project"), &storage, &signer).unwrap();
1176        let repo = storage.repository(rid).unwrap();
1177        let id = repo.identity().unwrap().head();
1178        let cob = format!("refs/cobs/xyz.radicle.id/{id}");
1179
1180        let mut refs = repo
1181            .references_of(signer.public_key())
1182            .unwrap()
1183            .keys()
1184            .map(|r| r.to_string())
1185            .collect::<Vec<_>>();
1186        refs.sort();
1187
1188        assert_eq!(
1189            refs,
1190            vec![
1191                &cob,
1192                "refs/heads/master",
1193                "refs/rad/id",
1194                "refs/rad/root",
1195                "refs/rad/sigrefs"
1196            ]
1197        );
1198    }
1199
1200    #[test]
1201    fn test_sign_refs() {
1202        let tmp = tempfile::tempdir().unwrap();
1203        let mut rng = fastrand::Rng::new();
1204        let signer = Device::mock_rng(&mut rng);
1205        let storage = Storage::open(tmp.path(), fixtures::user()).unwrap();
1206        let alice = *signer.public_key();
1207        let (rid, _, working, _) =
1208            fixtures::project(tmp.path().join("project"), &storage, &signer).unwrap();
1209        let stored = storage.repository(rid).unwrap();
1210        let sig =
1211            git::raw::Signature::now(&alice.to_string(), "anonymous@radicle.example.com").unwrap();
1212        let head = working.head().unwrap().peel_to_commit().unwrap();
1213
1214        git::commit(
1215            &working,
1216            &head,
1217            &git::fmt::RefString::try_from(format!("refs/remotes/{alice}/heads/master")).unwrap(),
1218            "Second commit",
1219            &sig,
1220            &head.tree().unwrap(),
1221        )
1222        .unwrap();
1223
1224        let signed = stored.sign_refs(&signer).unwrap();
1225        let remote = stored.remote(&alice).unwrap();
1226        let mut unsigned = stored.references_of(&alice).unwrap();
1227
1228        // The signed refs doesn't contain the signature ref itself.
1229        unsigned.remove_sigrefs().unwrap();
1230
1231        assert_eq!(remote.refs.refs(), signed.refs());
1232        assert_eq!(*remote.refs.refs(), unsigned);
1233    }
1234}