node: Implement project checkout

Signed-off-by: Alexis Sellier <self@cloudhead.io>
This commit is contained in:
Alexis Sellier 2022-09-07 13:53:17 +02:00
parent 09265eb3da
commit 4e9b60dee0
No known key found for this signature in database
6 changed files with 154 additions and 23 deletions

View File

@ -1,3 +1,4 @@
use std::path::Path;
use std::str::FromStr; use std::str::FromStr;
use git_ref_format as format; use git_ref_format as format;
@ -9,6 +10,7 @@ use crate::storage::RemoteId;
pub use ext::Oid; pub use ext::Oid;
pub use git_ref_format::{refname, RefStr, RefString}; pub use git_ref_format::{refname, RefStr, RefString};
pub use git_url as url;
pub use git_url::Url; pub use git_url::Url;
pub use radicle_git_ext as ext; pub use radicle_git_ext as ext;
@ -97,6 +99,30 @@ pub fn commit<'a>(
Ok(commit) 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::Remote<'r>, 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. /// Set the upstream of the given branch to the given remote.
/// ///
/// This writes to the `config` directly. The entry will look like the /// This writes to the `config` directly. The entry will look like the

View File

@ -1,4 +1,4 @@
use std::path::Path; use std::path::{Path, PathBuf};
use std::{ffi::OsString, fmt, io, str::FromStr}; use std::{ffi::OsString, fmt, io, str::FromStr};
use nonempty::NonEmpty; use nonempty::NonEmpty;
@ -148,6 +148,8 @@ pub struct Project {
pub doc: Doc, pub doc: Doc,
/// The project remotes. /// The project remotes.
pub remotes: Remotes<Verified>, pub remotes: Remotes<Verified>,
/// On-disk file path for this project's repository.
pub path: PathBuf,
} }
#[derive(Error, Debug)] #[derive(Error, Debug)]

View File

@ -1,6 +1,6 @@
use std::io; use std::io;
use std::path::Path;
use git_url::Url;
use nonempty::NonEmpty; use nonempty::NonEmpty;
use thiserror::Error; use thiserror::Error;
@ -59,12 +59,6 @@ pub fn init<'r, S: storage::WriteStorage<'r>>(
let mut doc_bytes = Vec::new(); let mut doc_bytes = Vec::new();
let id = doc.write(&mut doc_bytes)?; let id = doc.write(&mut doc_bytes)?;
let project = storage.repository(&id)?; 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, // 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 id_ref = format!("refs/remotes/{user_id}/{}", &*RADICLE_ID_REF);
let _oid = repo.commit(Some(&id_ref), &sig, &sig, "Initialize Radicle", &tree, &[])?; 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( git::set_upstream(
repo, repo,
REMOTE_NAME, 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 // TODO: Note that you'll likely want to use `RemoteCallbacks` and set
// `push_update_reference` to test whether all the references were pushed // `push_update_reference` to test whether all the references were pushed
// successfully. // successfully.
remote.push::<&str>( git::configure_remote(repo, REMOTE_NAME, user_id, project.path())?.push::<&str>(
&[&format!( &[&format!(
"refs/heads/{default_branch}:refs/remotes/{user_id}/heads/{default_branch}" "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)) 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<P: AsRef<Path>, S: storage::ReadStorage>(
proj: &ProjId,
path: P,
storage: S,
) -> Result<git2::Repository, CheckoutError> {
// 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::git;
use crate::identity::{Delegate, Did}; use crate::identity::{Delegate, Did};
use crate::storage::git::Storage; use crate::storage::git::Storage;
use crate::storage::ReadStorage; use crate::storage::ReadStorage;
use crate::test::crypto; use crate::test::{crypto, fixtures};
#[test] #[test]
fn test_init() { fn test_init() {
let tempdir = tempfile::tempdir().unwrap(); let tempdir = tempfile::tempdir().unwrap();
let signer = crypto::MockSigner::default(); let signer = crypto::MockSigner::default();
let mut storage = Storage::open(tempdir.path().join("storage"), signer).unwrap(); let mut storage = Storage::open(tempdir.path().join("storage"), signer).unwrap();
let repo = git2::Repository::init(tempdir.path().join("working")).unwrap(); let repo = fixtures::repository(tempdir.path().join("working"));
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 (id, refs) = init( let (id, refs) = init(
&repo, &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::<Vec<_>>(),
original
.find_remote(REMOTE_NAME)
.unwrap()
.refspecs()
.into_iter()
.map(|r| r.bytes().to_vec())
.collect::<Vec<_>>(),
);
}
} }

View File

@ -58,6 +58,7 @@ impl ReadStorage for Storage {
if let Some(doc) = repo.identity(local)? { if let Some(doc) = repo.identity(local)? {
let remotes = repo.remotes()?.collect::<Result<_, _>>()?; let remotes = repo.remotes()?.collect::<Result<_, _>>()?;
let path = repo.path().to_path_buf();
// TODO: We should check that there is at least one remote, which is // 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 // the one of the local user, otherwise it means the project is in
@ -67,6 +68,7 @@ impl ReadStorage for Storage {
id: id.clone(), id: id.clone(),
doc, doc,
remotes, remotes,
path,
})) }))
} else { } else {
Ok(None) Ok(None)

View File

@ -1,6 +1,7 @@
use std::collections::{BTreeMap, HashSet}; use std::collections::{BTreeMap, HashSet};
use std::hash::Hash; use std::hash::Hash;
use std::ops::RangeBounds; use std::ops::RangeBounds;
use std::path::PathBuf;
use nonempty::NonEmpty; use nonempty::NonEmpty;
use quickcheck::Arbitrary; use quickcheck::Arbitrary;
@ -56,8 +57,14 @@ impl Arbitrary for Project {
let doc = Doc::arbitrary(g); let doc = Doc::arbitrary(g);
let id = doc.write(&mut buf).unwrap(); let id = doc.write(&mut buf).unwrap();
let remotes = storage::Remotes::arbitrary(g); let remotes = storage::Remotes::arbitrary(g);
let path = PathBuf::arbitrary(g);
Self { id, doc, remotes } Self {
id,
doc,
remotes,
path,
}
} }
} }

View File

@ -63,6 +63,18 @@ pub fn storage<P: AsRef<Path>>(path: P) -> Storage {
storages.pop().unwrap() storages.pop().unwrap()
} }
/// Creates a regular repository at the given path with a couple of commits.
pub fn repository<P: AsRef<Path>>(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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;