Radish alpha
h
Radicle Heartwood Protocol & Stack
Radicle
Git (anonymous pull)
Log in to clone via SSH
node: Implement project checkout
Alexis Sellier committed 3 years ago
commit 4e9b60dee0ca0d817b13275aa778ccaaa55b4351
parent 09265eb3da221fe2dfb3e290f03b3e2d3f75db6c
6 files changed +154 -23
modified 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::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.
///
/// This writes to the `config` directly. The entry will look like the
modified 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<Verified>,
+
    /// On-disk file path for this project's repository.
+
    pub path: PathBuf,
}

#[derive(Error, Debug)]
modified 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<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)]
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::<Vec<_>>(),
+
            original
+
                .find_remote(REMOTE_NAME)
+
                .unwrap()
+
                .refspecs()
+
                .into_iter()
+
                .map(|r| r.bytes().to_vec())
+
                .collect::<Vec<_>>(),
+
        );
+
    }
}
modified 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::<Result<_, _>>()?;
+
            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)
modified 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,
+
        }
    }
}

modified node/src/test/fixtures.rs
@@ -63,6 +63,18 @@ pub fn storage<P: AsRef<Path>>(path: P) -> Storage {
    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)]
mod tests {
    use super::*;