Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
node: Propagate ref updates
Alexis Sellier committed 3 years ago
commit db6932d58ca22165fe8a438da519f1eee897b530
parent cb4939fd752544fb962dcc7dab5843ee97d1f51b
6 files changed +109 -28
modified node/src/control.rs
@@ -128,8 +128,12 @@ fn fetch<W: Write, H: Handle>(id: Id, mut writer: W, handle: &H) -> Result<(), D

            for result in results.iter() {
                match result {
-
                    FetchResult::Fetched { from } => {
+
                    FetchResult::Fetched { from, updated } => {
                        writeln!(writer, "ok: {} fetched from {}", &id, from)?;
+

+
                        for update in updated {
+
                            writeln!(writer, "{}", update)?;
+
                        }
                    }
                    FetchResult::Error { from, error } => {
                        writeln!(
modified node/src/protocol.rs
@@ -27,8 +27,8 @@ use crate::protocol::config::ProjectTracking;
use crate::protocol::message::Message;
use crate::protocol::peer::{Peer, PeerError, PeerState};
use crate::protocol::wire::Encode;
-
use crate::storage::{self, ReadRepository, WriteRepository};
-
use crate::storage::{Inventory, WriteStorage};
+
use crate::storage;
+
use crate::storage::{Inventory, ReadRepository, RefUpdate, WriteRepository, WriteStorage};

pub use crate::protocol::config::{Config, Network};

@@ -82,7 +82,10 @@ pub enum FetchLookup {
#[derive(Debug)]
pub enum FetchResult {
    /// Successful fetch from a seed.
-
    Fetched { from: net::SocketAddr },
+
    Fetched {
+
        from: net::SocketAddr,
+
        updated: Vec<RefUpdate>,
+
    },
    /// Error fetching the resource from a seed.
    Error {
        from: net::SocketAddr,
@@ -412,8 +415,13 @@ where
                        path: format!("/{}", id).into(),
                        ..Url::default()
                    }) {
-
                        Ok(()) => {
-
                            results_.send(FetchResult::Fetched { from: peer.addr }).ok();
+
                        Ok(updated) => {
+
                            results_
+
                                .send(FetchResult::Fetched {
+
                                    from: peer.addr,
+
                                    updated,
+
                                })
+
                                .ok();
                        }
                        Err(err) => {
                            results_
modified node/src/storage.rs
@@ -2,10 +2,10 @@ pub mod git;
pub mod refs;

use std::collections::hash_map;
-
use std::io;
use std::marker::PhantomData;
use std::ops::{Deref, DerefMut};
use std::path::Path;
+
use std::{fmt, io};

use radicle_git_ext as git_ext;
use thiserror::Error;
@@ -15,7 +15,7 @@ pub use radicle_git_ext::Oid;
use crate::collections::HashMap;
use crate::crypto::{self, PublicKey, Unverified, Verified};
use crate::git::Url;
-
use crate::git::{RefError, RefStr};
+
use crate::git::{RefError, RefStr, RefString};
use crate::identity;
use crate::identity::{Id, IdError, Project};
use crate::storage::refs::Refs;
@@ -48,6 +48,51 @@ pub enum Error {

pub type RemoteId = PublicKey;

+
/// An update to a reference.
+
#[derive(Debug, Clone, PartialEq, Eq)]
+
pub enum RefUpdate {
+
    Updated { name: RefString, old: Oid, new: Oid },
+
    Created { name: RefString, oid: Oid },
+
    Deleted { name: RefString, oid: Oid },
+
    Skipped { name: RefString, oid: Oid },
+
}
+

+
impl RefUpdate {
+
    pub fn from(name: RefString, old: impl Into<Oid>, new: impl Into<Oid>) -> Self {
+
        let old = old.into();
+
        let new = new.into();
+

+
        if old.is_zero() {
+
            Self::Created { name, oid: new }
+
        } else if new.is_zero() {
+
            Self::Deleted { name, oid: old }
+
        } else if old != new {
+
            Self::Updated { name, old, new }
+
        } else {
+
            Self::Skipped { name, oid: old }
+
        }
+
    }
+
}
+

+
impl fmt::Display for RefUpdate {
+
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+
        match self {
+
            Self::Updated { name, old, new } => {
+
                write!(f, "~ {:.7}..{:.7} {}", old, new, name)
+
            }
+
            Self::Created { name, oid } => {
+
                write!(f, "* 0000000..{:.7} {}", oid, name)
+
            }
+
            Self::Deleted { name, oid } => {
+
                write!(f, "- {:.7}..0000000 {}", oid, name)
+
            }
+
            Self::Skipped { name, oid } => {
+
                write!(f, "= {:.7}..{:.7} {}", oid, oid, name)
+
            }
+
        }
+
    }
+
}
+

/// Project remotes. Tracks the git state of a project.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Remotes<V>(HashMap<RemoteId, Remote<V>>);
@@ -192,7 +237,7 @@ pub trait ReadRepository<'r> {
}

pub trait WriteRepository<'r>: ReadRepository<'r> {
-
    fn fetch(&mut self, url: &Url) -> Result<(), git2::Error>;
+
    fn fetch(&mut self, url: &Url) -> Result<Vec<RefUpdate>, git2::Error>;
    fn raw(&self) -> &git2::Repository;
}

modified node/src/storage/git.rs
@@ -18,7 +18,7 @@ use crate::storage::{
    Error, Inventory, ReadRepository, ReadStorage, Remote, WriteRepository, WriteStorage,
};

-
use super::RemoteId;
+
use super::{RefUpdate, RemoteId};

pub static RADICLE_ID_REF: Lazy<git::RefString> = Lazy::new(|| git::refname!("heads/radicle/id"));
pub static REMOTES_GLOB: Lazy<refspec::PatternString> =
@@ -308,9 +308,8 @@ impl<'r> ReadRepository<'r> for Repository {

impl<'r> WriteRepository<'r> for Repository {
    /// Fetch all remotes of a project from the given URL.
-
    fn fetch(&mut self, url: &git::Url) -> Result<(), git2::Error> {
+
    fn fetch(&mut self, url: &git::Url) -> Result<Vec<RefUpdate>, git2::Error> {
        // TODO: Have function to fetch specific remotes.
-
        // TODO: Return meaningful info on success.
        //
        // Repository layout should look like this:
        //
@@ -322,15 +321,32 @@ impl<'r> WriteRepository<'r> for Repository {
        //
        let url = url.to_string();
        let refs: &[&str] = &["refs/remotes/*:refs/remotes/*"];
-
        let mut remote = self.backend.remote_anonymous(&url)?;
-
        let mut opts = git2::FetchOptions::default();
+
        let mut updates = Vec::new();
+
        let mut callbacks = git2::RemoteCallbacks::new();

-
        // TODO: Make sure we verify before pruning, as pruning may get us into
-
        // a state we can't roll back.
-
        opts.prune(git2::FetchPrune::On);
-
        remote.fetch(refs, Some(&mut opts), None)?;
+
        callbacks.update_tips(|name, old, new| {
+
            if let Ok(name) = git::RefString::try_from(name) {
+
                updates.push(RefUpdate::from(name, old, new));
+
            } else {
+
                log::warn!("Invalid ref `{}` detected; aborting fetch", name);
+
                return false;
+
            }
+
            // Returning `true` ensures the process is not aborted.
+
            true
+
        });
+

+
        {
+
            let mut remote = self.backend.remote_anonymous(&url)?;
+
            let mut opts = git2::FetchOptions::default();
+
            opts.remote_callbacks(callbacks);
+

+
            // TODO: Make sure we verify before pruning, as pruning may get us into
+
            // a state we can't roll back.
+
            opts.prune(git2::FetchPrune::On);
+
            remote.fetch(refs, Some(&mut opts), None)?;
+
        }

-
        Ok(())
+
        Ok(updates)
    }

    fn raw(&self) -> &git2::Repository {
@@ -392,20 +408,28 @@ mod tests {
        let inventory = alice.inventory().unwrap();
        let proj = inventory.first().unwrap();
        let repo = alice.repository(proj).unwrap();
-
        let remotes = repo.remotes().unwrap();
+
        let remotes = repo.remotes().unwrap().collect::<Vec<_>>();
        let refname = git::refname!("heads/master");

        // Have Bob fetch Alice's refs.
-
        bob.repository(proj)
+
        let updates = bob
+
            .repository(proj)
            .unwrap()
            .fetch(&git::Url {
-
                host: Some(alice.path().to_string_lossy().to_string()),
                scheme: git_url::Scheme::File,
-
                path: format!("/{}", proj).into(),
+
                path: alice
+
                    .path()
+
                    .join(proj.to_string())
+
                    .to_string_lossy()
+
                    .into_owned()
+
                    .into(),
                ..git::Url::default()
            })
            .unwrap();

+
        // Four refs are created for each remote.
+
        assert_eq!(updates.len(), remotes.len() * 4);
+

        for remote in remotes {
            let (id, _) = remote.unwrap();
            let alice_repo = alice.repository(proj).unwrap();
modified node/src/test/fixtures.rs
@@ -9,8 +9,8 @@ use crate::test::crypto::MockSigner;

pub fn storage<P: AsRef<Path>>(path: P) -> Storage {
    let path = path.as_ref();
-
    let proj_ids = arbitrary::set::<Id>(3..5);
-
    let signers = arbitrary::set::<MockSigner>(1..3);
+
    let proj_ids = arbitrary::set::<Id>(3..=3);
+
    let signers = arbitrary::set::<MockSigner>(3..=3);
    let mut storages = signers
        .into_iter()
        .map(|s| Storage::open(path, s).unwrap())
modified node/src/test/storage.rs
@@ -3,7 +3,7 @@ use git_url::Url;
use crate::crypto::{PublicKey, Verified};
use crate::git;
use crate::identity::{Id, Project};
-
use crate::storage::refs;
+
use crate::storage::{refs, RefUpdate};
use crate::storage::{
    Error, Inventory, ReadRepository, ReadStorage, Remote, RemoteId, WriteRepository, WriteStorage,
};
@@ -122,8 +122,8 @@ impl ReadRepository<'_> for MockRepository {
}

impl WriteRepository<'_> for MockRepository {
-
    fn fetch(&mut self, _url: &Url) -> Result<(), git2::Error> {
-
        Ok(())
+
    fn fetch(&mut self, _url: &Url) -> Result<Vec<RefUpdate>, git2::Error> {
+
        Ok(vec![])
    }

    fn raw(&self) -> &git2::Repository {