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