node: Propagate ref updates

Signed-off-by: Alexis Sellier <self@cloudhead.io>
This commit is contained in:
Alexis Sellier 2022-09-09 14:33:04 +02:00
parent cb4939fd75
commit db6932d58c
No known key found for this signature in database
6 changed files with 109 additions and 28 deletions

View File

@ -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!(

View File

@ -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_

View File

@ -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;
}

View File

@ -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
});
Ok(())
{
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(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();

View File

@ -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())

View File

@ -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 {