Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
fetch: Improve `refs/rad/id` resolution
Adrian Duke committed 2 months ago
commit 6318aa2191e843256829d064ed30b607fbf0f4d3
parent c06b00e330d82c8b8221cc8f8776c883208d159f
2 files changed +169 -4
modified crates/radicle-fetch/src/state.rs
@@ -87,6 +87,8 @@ pub mod error {
        Resolve(#[from] git::repository::error::Resolve),
        #[error(transparent)]
        Verified(#[from] radicle::identity::DocError),
+
        #[error("failed to verify `refs/rad/id`: {0}")]
+
        Graph(#[source] radicle::git::raw::Error),
    }
}

@@ -637,13 +639,62 @@ where
        self.handle.verified(head)
    }

+
    /// Resolve the verified [`Doc`], by choosing a `refs/rad/id` head to
+
    /// resolve with.
+
    ///
+
    /// There are two `refs/rad/id` to possibly choose from:
+
    ///
+
    ///   1. The `refs/rad/id` of the fetching node, if present
+
    ///   2. The `refs/rad/id` of the node being fetched from, if set.
+
    ///
+
    /// If *neither* of these are present, then `None` is returned.
+
    ///
+
    /// If *one or the other* of these are present, then try to load the verified
+
    /// the [`Doc`].
+
    ///
+
    /// If *both* of these are present, then check which one is the latest
+
    /// update, falling back to the one present in the local repository.
    pub fn canonical(&self) -> Result<Option<Doc>, error::Canonical> {
        let tip = self.refname_to_id(refs::REFS_RAD_ID.clone())?;
        let cached_tip = self.canonical_rad_id();
+
        let tip = CanonicalTip::new(tip, cached_tip);

-
        tip.or(cached_tip)
-
            .map(|tip| self.verified(tip).map_err(error::Canonical::from))
-
            .transpose()
+
        match tip {
+
            CanonicalTip::Neither => Ok(None),
+
            CanonicalTip::Repository(oid) => {
+
                self.verified(oid).map(Some).map_err(error::Canonical::from)
+
            }
+
            CanonicalTip::Cached(oid) => {
+
                self.verified(oid).map(Some).map_err(error::Canonical::from)
+
            }
+
            CanonicalTip::Both { repository, cached } => {
+
                let repo = self.handle.repository();
+
                match repo
+
                    .backend
+
                    .graph_ahead_behind(repository.into(), cached.into())
+
                {
+
                    Ok((ahead, behind)) => match (ahead, behind) {
+
                        (_, 0) => self
+
                            .verified(repository)
+
                            .map(Some)
+
                            .map_err(error::Canonical::from),
+
                        (0, m) if m > 0 => self
+
                            .verified(cached)
+
                            .map(Some)
+
                            .map_err(error::Canonical::from),
+
                        _ => self
+
                            .verified(repository)
+
                            .map(Some)
+
                            .map_err(error::Canonical::from),
+
                    },
+
                    Err(err) if err.code() == radicle::git::raw::ErrorCode::NotFound => self
+
                        .verified(repository)
+
                        .map(Some)
+
                        .map_err(error::Canonical::from),
+
                    Err(err) => Err(error::Canonical::Graph(err)),
+
                }
+
            }
+
        }
    }

    pub fn load(&self, remote: &PublicKey) -> Result<Option<SignedRefsAt>, sigrefs::error::Load> {
@@ -731,3 +782,21 @@ where
        Ok(validations)
    }
}
+

+
enum CanonicalTip {
+
    Neither,
+
    Repository(Oid),
+
    Cached(Oid),
+
    Both { repository: Oid, cached: Oid },
+
}
+

+
impl CanonicalTip {
+
    fn new(repository: Option<Oid>, cached: Option<Oid>) -> Self {
+
        match (repository, cached) {
+
            (None, None) => CanonicalTip::Neither,
+
            (None, Some(cached)) => CanonicalTip::Cached(cached),
+
            (Some(repository), None) => CanonicalTip::Repository(repository),
+
            (Some(repository), Some(cached)) => CanonicalTip::Both { repository, cached },
+
        }
+
    }
+
}
modified crates/radicle-node/src/tests/e2e.rs
@@ -1,6 +1,8 @@
use std::{collections::HashSet, thread, time};

+
use radicle::cob;
use radicle::cob::Title;
+
use radicle_crypto::test::signer::MockSigner;
use test_log::test;

use radicle::git::raw::ErrorExt as _;
@@ -20,7 +22,7 @@ use crate::node::config::Limits;
use crate::node::{Config, ConnectOptions};
use crate::service;
use crate::storage::git::transport;
-
use crate::test::node::{converge, Node};
+
use crate::test::node::{converge, Node, NodeHandle};

mod config {
    use super::*;
@@ -1559,3 +1561,97 @@ fn test_fetch_emits_canonical_ref_update() {
        )
        .unwrap();
}
+

+
#[test]
+
fn test_non_fastword_identity_doc() {
+
    use radicle::identity::Identity;
+

+
    let tmp = tempfile::tempdir().unwrap();
+

+
    let mut alice = Node::init(tmp.path(), Config::test(Alias::new("alice")));
+
    let bob = Node::init(tmp.path(), Config::test(Alias::new("bob")));
+
    let eve = Node::init(tmp.path(), Config::test(Alias::new("eve")));
+
    let alice_laptop = Node::init(tmp.path(), Config::test(Alias::new("alice-laptop")));
+

+
    let rid = alice.project("acme", "");
+

+
    let mut alice = alice.spawn();
+
    let mut alice_laptop = alice_laptop.spawn();
+
    let mut bob = bob.spawn();
+
    let mut eve = eve.spawn();
+

+
    let has_issue = |node: &NodeHandle<MockSigner>, issue: &cob::ObjectId| -> bool {
+
        let repo = node.storage.repository(rid).unwrap();
+
        repo.contains(**issue).unwrap()
+
    };
+

+
    alice.connect(&alice_laptop);
+
    alice.connect(&bob);
+
    alice.connect(&eve);
+
    eve.connect(&bob);
+
    eve.connect(&alice_laptop);
+

+
    // Bob and Eve have the same state for the repository
+
    bob.handle.seed(rid, Scope::Followed).unwrap();
+
    bob.handle.fetch(rid, alice.id, DEFAULT_TIMEOUT).unwrap();
+

+
    alice_laptop.handle.seed(rid, Scope::All).unwrap();
+
    alice_laptop
+
        .handle
+
        .fetch(rid, alice.id, DEFAULT_TIMEOUT)
+
        .unwrap();
+
    // Alice pushes new references to her laptop
+
    let issue = alice_laptop.issue(
+
        rid,
+
        "Feature #1".parse().unwrap(),
+
        "Implementing new feature",
+
    );
+

+
    // Eve will fetch these references since her scope is "all"
+
    eve.handle.seed(rid, Scope::All).unwrap();
+
    eve.handle
+
        .fetch(rid, alice_laptop.id, DEFAULT_TIMEOUT)
+
        .unwrap();
+
    assert!(has_issue(&eve, &issue));
+

+
    // Alice updates the identity of the document to include her laptop
+
    let (prev, next) = {
+
        let repo = alice.storage.repository(rid).unwrap();
+
        let mut identity = Identity::load_mut(&repo).unwrap();
+
        let prev = identity.current;
+
        let doc = repo
+
            .identity_doc()
+
            .unwrap()
+
            .doc
+
            .with_edits(|raw| raw.delegate(alice_laptop.id.into()))
+
            .unwrap();
+
        let rev = identity
+
            .update(Title::new("Add Laptop").unwrap(), "", &doc, &alice.signer)
+
            .unwrap();
+
        repo.set_identity_head_to(rev).unwrap();
+
        (prev, rev)
+
    };
+

+
    // Bob fetches from Alice and we see the identity document was updated.
+
    //
+
    // Bob does not have the issue because Alice does not have the updates from
+
    // Alice's Laptop.
+
    let result = bob.handle.fetch(rid, alice.id, DEFAULT_TIMEOUT).unwrap();
+
    assert!(matches!(result, FetchResult::Success { .. }));
+
    assert!(!has_issue(&bob, &issue));
+
    let repo = bob.storage.repository(rid).unwrap();
+
    let identity = Identity::load_mut(&repo).unwrap();
+
    assert_eq!(identity.current, next);
+
    assert_eq!(identity.parent, Some(prev));
+

+
    // Bob fetches from Eve, the identity document should remain the same, but
+
    // since Bob now knows that Alice's Laptop is a delegate, the issue should
+
    // be fetched.
+
    bob.handle.fetch(rid, eve.id, DEFAULT_TIMEOUT).unwrap();
+
    assert!(matches!(result, FetchResult::Success { .. }));
+
    assert!(has_issue(&bob, &issue));
+
    let repo = bob.storage.repository(rid).unwrap();
+
    let identity = Identity::load_mut(&repo).unwrap();
+
    assert_eq!(identity.current, next);
+
    assert_eq!(identity.parent, Some(prev));
+
}