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