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#[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 if !path.file_type()?.is_dir() {
123 continue;
124 }
125 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 log::debug!(target: "storage", "Skipping temporary repository at '{}'", path.path().display());
134 continue;
135 } else if "lock" == ext {
136 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 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 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 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 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 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
292pub struct Repository {
294 pub id: RepoId,
296 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#[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#[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 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 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 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 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 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 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 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)?; 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 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 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 let mut signed = BTreeMap::from((*remote.refs).clone());
638 let mut failures = Validations::default();
639 let mut has_sigrefs = false;
640
641 for (refname, oid) in self.references_of(&remote.id)? {
643 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 if let Some((name, _)) = signed.into_iter().next() {
668 failures.push(Validation::MissingRef {
669 refname: name,
670 remote: remote.id,
671 });
672 }
673
674 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 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 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 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 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 let Ok(root) = self.identity_root_of(&remote) else {
900 continue;
901 };
902 let blob = Doc::blob_at(root, self)?;
903
904 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 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 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 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 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}