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