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#[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 if !path.file_type()?.is_dir() {
120 continue;
121 }
122 if path.file_name().to_string_lossy().starts_with('.') {
124 continue;
125 }
126 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 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 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 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 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 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
277pub struct Repository {
279 pub id: RepoId,
281 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#[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#[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 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 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 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 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 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 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)?; 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 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 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 let mut signed = BTreeMap::from((*remote.refs).clone());
607 let mut failures = Validations::default();
608 let mut has_sigrefs = false;
609
610 for (refname, oid) in self.references_of(&remote.id)? {
612 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 if let Some((name, _)) = signed.into_iter().next() {
637 failures.push(Validation::MissingRef {
638 refname: name,
639 remote: remote.id,
640 });
641 }
642
643 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 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 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 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 let Ok(root) = self.identity_root_of(&remote) else {
861 continue;
862 };
863 let blob = Doc::blob_at(root, self)?;
864
865 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 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 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 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 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}