diff --git a/node/src/git.rs b/node/src/git.rs index 9940370c..c957b857 100644 --- a/node/src/git.rs +++ b/node/src/git.rs @@ -62,3 +62,32 @@ pub fn parse_ref(s: &str) -> Result<(T, format::RefString), RefError Ok((id, refstr)) } + +/// Create an initial empty commit. +pub fn initial_commit<'a>( + repo: &'a git2::Repository, + sig: &git2::Signature, +) -> Result, git2::Error> { + let tree_id = repo.index()?.write_tree()?; + let tree = repo.find_tree(tree_id)?; + let oid = repo.commit(None, sig, sig, "Initial commit", &tree, &[])?; + let commit = repo.find_commit(oid).unwrap(); + + Ok(commit) +} + +/// Create a commit. +pub fn commit<'a>( + repo: &'a git2::Repository, + parent: &'a git2::Commit, + message: &str, + user: &str, +) -> Result, git2::Error> { + let sig = git2::Signature::now(user, "anonymous@radicle.xyz")?; + let tree_id = repo.index()?.write_tree()?; + let tree = repo.find_tree(tree_id)?; + let oid = repo.commit(None, &sig, &sig, message, &tree, &[parent])?; + let commit = repo.find_commit(oid).unwrap(); + + Ok(commit) +} diff --git a/node/src/identity.rs b/node/src/identity.rs index c554cab5..adadd44c 100644 --- a/node/src/identity.rs +++ b/node/src/identity.rs @@ -70,6 +70,12 @@ impl Did { } } +impl From for Did { + fn from(key: crypto::PublicKey) -> Self { + Self(key) + } +} + impl fmt::Display for Did { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.encode()) @@ -94,8 +100,9 @@ pub struct Delegate { pub struct Doc { pub name: String, pub description: String, + pub default_branch: String, pub version: u32, - pub parent: Oid, + pub parent: Option, pub delegate: NonEmpty, } diff --git a/node/src/lib.rs b/node/src/lib.rs index 7276de1b..de1279ac 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -12,6 +12,7 @@ mod hash; mod identity; mod logger; mod protocol; +mod rad; mod storage; #[cfg(test)] mod test; diff --git a/node/src/rad.rs b/node/src/rad.rs new file mode 100644 index 00000000..18e03ca6 --- /dev/null +++ b/node/src/rad.rs @@ -0,0 +1,110 @@ +use std::{fs, io}; + +use nonempty::NonEmpty; +use thiserror::Error; + +use crate::identity; +use crate::identity::{ProjId, UserId}; + +#[derive(Error, Debug)] +pub enum InitError { + #[error("doc: {0}")] + Doc(#[from] identity::DocError), + #[error("git: {0}")] + Git(#[from] git2::Error), + #[error("i/o: {0}")] + Io(#[from] io::Error), + #[error("cannot initialize project inside a bare repository")] + BareRepo, + #[error("cannot initialize project from detached head state")] + DetachedHead, + #[error("HEAD reference is not valid UTF-8")] + InvalidHead, +} + +/// Initialize a new radicle project from a git repository. +pub fn init( + repo: &git2::Repository, + name: &str, + description: &str, + delegate: UserId, +) -> Result { + let delegate = identity::Delegate { + // TODO: Use actual user name. + name: String::from("anonymous"), + id: identity::Did::from(delegate), + }; + + let head = repo.head()?; + let default_branch = if head.is_branch() { + head.shorthand().ok_or(InitError::InvalidHead)?.to_owned() + } else { + return Err(InitError::DetachedHead); + }; + + let doc = identity::Doc { + name: name.to_owned(), + description: description.to_owned(), + default_branch, + version: 1, + parent: None, + delegate: NonEmpty::new(delegate), + }; + let sig = repo + .signature() + .or_else(|_| git2::Signature::now("anonymous", "anonymous@anonymous.xyz"))?; + + let base = repo.workdir().ok_or(InitError::BareRepo)?; + let path = base.join("Project.toml"); + let file = fs::OpenOptions::new() + .create_new(true) + .write(true) + .open(&path)?; + let id = doc.write(file)?; + let relative = path.as_path().strip_prefix(base).unwrap(); + + let mut index = repo.index()?; + index.add_path(relative)?; + + let tree_id = index.write_tree()?; + let tree = repo.find_tree(tree_id)?; + let _oid = repo.commit( + Some("refs/heads/rad/id"), + &sig, + &sig, + "Initialize Radicle", + &tree, + &[], + )?; + + // Remove identity document from current branch. + // FIXME: We shouldn't have to do this, as the user may have an unrelated file + // called the same name. Ideally we are able to create the file in the id branch. + fs::remove_file(path)?; + + Ok(id) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::crypto::Signer; + use crate::git; + use crate::test::crypto; + + #[test] + fn test_init() { + let tempdir = tempfile::tempdir().unwrap(); + let repo = git2::Repository::init(tempdir.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(); + + repo.branch("master", &head, false).unwrap(); + + let signer = crypto::MockSigner::new(&mut fastrand::Rng::new()); + let delegate = *signer.public_key(); + + init(&repo, "acme", "Acme's repo", delegate).unwrap(); + } +} diff --git a/node/src/test/fixtures.rs b/node/src/test/fixtures.rs index 69092e35..072135d2 100644 --- a/node/src/test/fixtures.rs +++ b/node/src/test/fixtures.rs @@ -1,5 +1,6 @@ use std::path::Path; +use crate::git; use crate::identity::{ProjId, UserId}; use crate::storage::git::Storage; use crate::storage::{WriteRepository, WriteStorage}; @@ -19,53 +20,24 @@ pub fn storage>(path: P) -> Storage { for user in user_ids.iter() { let repo = repo.namespace(user).unwrap(); - let head = initial_commit(repo, &user.to_string()).unwrap(); + let sig = git2::Signature::now(&user.to_string(), "anonymous@radicle.xyz").unwrap(); + let head = git::initial_commit(repo, &sig).unwrap(); log::debug!("{}: creating {}...", proj, repo.namespace().unwrap()); repo.reference("refs/rad/root", head.id(), false, "test") .unwrap(); - let head = commit(repo, &head, "Second commit", &user.to_string()).unwrap(); + let head = git::commit(repo, &head, "Second commit", &user.to_string()).unwrap(); repo.branch("master", &head, false).unwrap(); - let head = commit(repo, &head, "Third commit", &user.to_string()).unwrap(); + let head = git::commit(repo, &head, "Third commit", &user.to_string()).unwrap(); repo.branch("patch/3", &head, false).unwrap(); } } storage } -/// Create a commit. -fn commit<'a>( - repo: &'a git2::Repository, - parent: &'a git2::Commit, - message: &str, - user: &str, -) -> Result, git2::Error> { - let sig = git2::Signature::now(user, "anonymous@radicle.xyz")?; - let tree_id = repo.index()?.write_tree()?; - let tree = repo.find_tree(tree_id)?; - let oid = repo.commit(None, &sig, &sig, message, &tree, &[parent])?; - let commit = repo.find_commit(oid).unwrap(); - - Ok(commit) -} - -/// Create an initial empty commit. -fn initial_commit<'a>( - repo: &'a git2::Repository, - user: &str, -) -> Result, git2::Error> { - let sig = git2::Signature::now(user, "anonymous@radicle.xyz")?; - let tree_id = repo.index()?.write_tree()?; - let tree = repo.find_tree(tree_id)?; - let oid = repo.commit(None, &sig, &sig, "Initial commit", &tree, &[])?; - let commit = repo.find_commit(oid).unwrap(); - - Ok(commit) -} - #[cfg(test)] mod tests { use super::*;