From 4e9b60dee0ca0d817b13275aa778ccaaa55b4351 Mon Sep 17 00:00:00 2001 From: Alexis Sellier Date: Wed, 7 Sep 2022 13:53:17 +0200 Subject: [PATCH] node: Implement project checkout Signed-off-by: Alexis Sellier --- node/src/git.rs | 26 ++++++++ node/src/identity.rs | 4 +- node/src/rad.rs | 124 ++++++++++++++++++++++++++++++------- node/src/storage/git.rs | 2 + node/src/test/arbitrary.rs | 9 ++- node/src/test/fixtures.rs | 12 ++++ 6 files changed, 154 insertions(+), 23 deletions(-) diff --git a/node/src/git.rs b/node/src/git.rs index 4d43c441..2dbf425f 100644 --- a/node/src/git.rs +++ b/node/src/git.rs @@ -1,3 +1,4 @@ +use std::path::Path; use std::str::FromStr; use git_ref_format as format; @@ -9,6 +10,7 @@ use crate::storage::RemoteId; pub use ext::Oid; pub use git_ref_format::{refname, RefStr, RefString}; +pub use git_url as url; pub use git_url::Url; pub use radicle_git_ext as ext; @@ -97,6 +99,30 @@ pub fn commit<'a>( Ok(commit) } +/// Configure a repository's radicle remote. +/// +/// Takes the repository in which to configure the remote, the name of the remote, the public +/// key of the remote, and the path to the remote repository on the filesystem. +pub fn configure_remote<'r>( + repo: &'r git2::Repository, + remote_name: &str, + remote_id: &RemoteId, + remote_path: &Path, +) -> Result, git2::Error> { + let url = Url { + scheme: git_url::Scheme::File, + path: remote_path.to_string_lossy().to_string().into(), + + ..Url::default() + }; + let fetch = format!("+refs/remotes/{remote_id}/heads/*:refs/remotes/rad/*"); + let push = format!("refs/heads/*:refs/remotes/{remote_id}/heads/*"); + let remote = repo.remote_with_fetch(remote_name, url.to_string().as_str(), &fetch)?; + repo.remote_add_push(remote_name, &push)?; + + Ok(remote) +} + /// Set the upstream of the given branch to the given remote. /// /// This writes to the `config` directly. The entry will look like the diff --git a/node/src/identity.rs b/node/src/identity.rs index 6e6d3b37..f9214c09 100644 --- a/node/src/identity.rs +++ b/node/src/identity.rs @@ -1,4 +1,4 @@ -use std::path::Path; +use std::path::{Path, PathBuf}; use std::{ffi::OsString, fmt, io, str::FromStr}; use nonempty::NonEmpty; @@ -148,6 +148,8 @@ pub struct Project { pub doc: Doc, /// The project remotes. pub remotes: Remotes, + /// On-disk file path for this project's repository. + pub path: PathBuf, } #[derive(Error, Debug)] diff --git a/node/src/rad.rs b/node/src/rad.rs index fca6f416..28132e5f 100644 --- a/node/src/rad.rs +++ b/node/src/rad.rs @@ -1,6 +1,6 @@ use std::io; +use std::path::Path; -use git_url::Url; use nonempty::NonEmpty; use thiserror::Error; @@ -59,12 +59,6 @@ pub fn init<'r, S: storage::WriteStorage<'r>>( let mut doc_bytes = Vec::new(); let id = doc.write(&mut doc_bytes)?; let project = storage.repository(&id)?; - let url = Url { - scheme: git_url::Scheme::File, - path: project.path().to_string_lossy().to_string().into(), - - ..Url::default() - }; { // Within this scope, redefine `repo` to refer to the project storage, @@ -92,12 +86,6 @@ pub fn init<'r, S: storage::WriteStorage<'r>>( let id_ref = format!("refs/remotes/{user_id}/{}", &*RADICLE_ID_REF); let _oid = repo.commit(Some(&id_ref), &sig, &sig, "Initialize Radicle", &tree, &[])?; } - - let fetch = format!("+refs/remotes/{user_id}/heads/*:refs/remotes/rad/*"); - let push = format!("refs/heads/*:refs/remotes/{user_id}/heads/*"); - let mut remote = repo.remote_with_fetch(REMOTE_NAME, url.to_string().as_str(), &fetch)?; - repo.remote_add_push(REMOTE_NAME, &push)?; - git::set_upstream( repo, REMOTE_NAME, @@ -108,7 +96,7 @@ pub fn init<'r, S: storage::WriteStorage<'r>>( // TODO: Note that you'll likely want to use `RemoteCallbacks` and set // `push_update_reference` to test whether all the references were pushed // successfully. - remote.push::<&str>( + git::configure_remote(repo, REMOTE_NAME, user_id, project.path())?.push::<&str>( &[&format!( "refs/heads/{default_branch}:refs/remotes/{user_id}/heads/{default_branch}" )], @@ -119,25 +107,71 @@ pub fn init<'r, S: storage::WriteStorage<'r>>( Ok((id, signed)) } +#[derive(Error, Debug)] +pub enum CheckoutError { + #[error("git: {0}")] + Git(#[from] git2::Error), + #[error("storage: {0}")] + Storage(#[from] storage::Error), + #[error("project `{0}` was not found in storage")] + NotFound(ProjId), +} + +pub fn checkout, S: storage::ReadStorage>( + proj: &ProjId, + path: P, + storage: S, +) -> Result { + // TODO: Decide on whether we can use `clone_local` + // TODO: Look into sharing object databases. + let project = storage + .get(proj)? + .ok_or_else(|| CheckoutError::NotFound(proj.clone()))?; + + let mut opts = git2::RepositoryInitOptions::new(); + opts.no_reinit(true).description(&project.doc.description); + + let repo = git2::Repository::init_opts(path, &opts)?; + let remote_id = storage.user_id(); + let default_branch = project.doc.default_branch.as_str(); + + // Configure and fetch all refs from remote. + git::configure_remote(&repo, REMOTE_NAME, remote_id, &project.path)?.fetch::<&str>( + &[], + None, + None, + )?; + + { + let remote_head_ref = format!("refs/remotes/{REMOTE_NAME}/{default_branch}"); + let remote_head_commit = repo.find_reference(&remote_head_ref)?.peel_to_commit()?; + let _ = repo.branch(default_branch, &remote_head_commit, true)?; + } + + git::set_upstream( + &repo, + REMOTE_NAME, + default_branch, + &format!("refs/remotes/{remote_id}/heads/{default_branch}"), + )?; + + Ok(repo) +} + #[cfg(test)] mod tests { use super::*; - use crate::git; use crate::identity::{Delegate, Did}; use crate::storage::git::Storage; use crate::storage::ReadStorage; - use crate::test::crypto; + use crate::test::{crypto, fixtures}; #[test] fn test_init() { let tempdir = tempfile::tempdir().unwrap(); let signer = crypto::MockSigner::default(); let mut storage = Storage::open(tempdir.path().join("storage"), signer).unwrap(); - let repo = git2::Repository::init(tempdir.path().join("working")).unwrap(); - let sig = git2::Signature::now("anonymous", "anonymous@radicle.xyz").unwrap(); - let head = git::initial_commit(&repo, &sig).unwrap(); - let head = git::commit(&repo, &head, "Second commit", "anonymous").unwrap(); - let _branch = repo.branch("master", &head, false).unwrap(); + let repo = fixtures::repository(tempdir.path().join("working")); let (id, refs) = init( &repo, @@ -163,4 +197,52 @@ mod tests { } ); } + + #[test] + fn test_checkout() { + let tempdir = tempfile::tempdir().unwrap(); + let signer = crypto::MockSigner::default(); + let mut storage = Storage::open(tempdir.path().join("storage"), signer).unwrap(); + let original = fixtures::repository(tempdir.path().join("original")); + + let (id, _) = init( + &original, + "acme", + "Acme's repo", + BranchName::from("master"), + &mut storage, + ) + .unwrap(); + + let copy = checkout(&id, tempdir.path().join("copy"), &mut storage).unwrap(); + + assert_eq!( + copy.head().unwrap().target(), + original.head().unwrap().target() + ); + assert_eq!( + copy.branch_upstream_name("refs/heads/master") + .unwrap() + .to_vec(), + original + .branch_upstream_name("refs/heads/master") + .unwrap() + .to_vec() + ); + assert_eq!( + copy.find_remote(REMOTE_NAME) + .unwrap() + .refspecs() + .into_iter() + .map(|r| r.bytes().to_vec()) + .collect::>(), + original + .find_remote(REMOTE_NAME) + .unwrap() + .refspecs() + .into_iter() + .map(|r| r.bytes().to_vec()) + .collect::>(), + ); + } } diff --git a/node/src/storage/git.rs b/node/src/storage/git.rs index 2f3a1cf5..42b7dc85 100644 --- a/node/src/storage/git.rs +++ b/node/src/storage/git.rs @@ -58,6 +58,7 @@ impl ReadStorage for Storage { if let Some(doc) = repo.identity(local)? { let remotes = repo.remotes()?.collect::>()?; + let path = repo.path().to_path_buf(); // TODO: We should check that there is at least one remote, which is // the one of the local user, otherwise it means the project is in @@ -67,6 +68,7 @@ impl ReadStorage for Storage { id: id.clone(), doc, remotes, + path, })) } else { Ok(None) diff --git a/node/src/test/arbitrary.rs b/node/src/test/arbitrary.rs index de73ee90..ffae5618 100644 --- a/node/src/test/arbitrary.rs +++ b/node/src/test/arbitrary.rs @@ -1,6 +1,7 @@ use std::collections::{BTreeMap, HashSet}; use std::hash::Hash; use std::ops::RangeBounds; +use std::path::PathBuf; use nonempty::NonEmpty; use quickcheck::Arbitrary; @@ -56,8 +57,14 @@ impl Arbitrary for Project { let doc = Doc::arbitrary(g); let id = doc.write(&mut buf).unwrap(); let remotes = storage::Remotes::arbitrary(g); + let path = PathBuf::arbitrary(g); - Self { id, doc, remotes } + Self { + id, + doc, + remotes, + path, + } } } diff --git a/node/src/test/fixtures.rs b/node/src/test/fixtures.rs index 94aa4092..33243060 100644 --- a/node/src/test/fixtures.rs +++ b/node/src/test/fixtures.rs @@ -63,6 +63,18 @@ pub fn storage>(path: P) -> Storage { storages.pop().unwrap() } +/// Creates a regular repository at the given path with a couple of commits. +pub fn repository>(path: P) -> git2::Repository { + let repo = git2::Repository::init(path).unwrap(); + { + let sig = git2::Signature::now("anonymous", "anonymous@radicle.xyz").unwrap(); + let head = git::initial_commit(&repo, &sig).unwrap(); + let head = git::commit(&repo, &head, "Second commit", "anonymous").unwrap(); + let _branch = repo.branch("master", &head, false).unwrap(); + } + repo +} + #[cfg(test)] mod tests { use super::*;