Add `rad-init` command

Signed-off-by: Alexis Sellier <alexis@radicle.xyz>
This commit is contained in:
Alexis Sellier 2022-11-07 17:46:15 +01:00
parent 223d36086e
commit 32cd94dd23
No known key found for this signature in database
12 changed files with 813 additions and 21 deletions

1
Cargo.lock generated
View File

@ -1504,6 +1504,7 @@ dependencies = [
"radicle",
"radicle-cob",
"radicle-crypto",
"serde_json",
"thiserror",
"zeroize",
]

View File

@ -12,6 +12,7 @@ dialoguer = { version = "0.10.0" }
indicatif = { version = "0.16.2" }
lexopt = { version = "0.2" }
log = { version = "0.4", features = ["std"] }
serde_json = { version = "1" }
thiserror = { version = "1" }
zeroize = { version = "1.1" }

View File

@ -0,0 +1 @@
pub mod init;

View File

@ -0,0 +1,313 @@
#![allow(clippy::or_fun_call)]
use std::convert::TryFrom;
use std::env;
use std::ffi::OsString;
use std::path::PathBuf;
use anyhow::{anyhow, bail, Context as _};
use radicle::crypto::ssh;
use radicle::git::RefString;
use radicle::node::NodeId;
use crate::git;
use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help};
use crate::terminal::Interactive;
use radicle::profile;
use serde_json as json;
pub const HELP: Help = Help {
name: "init",
description: "Initialize radicle projects from git repositories",
version: env!("CARGO_PKG_VERSION"),
usage: r#"
Usage
rad init [<path>] [<option>...]
Options
--name Name of the project
--description Description of the project
--default-branch The default branch of the project
--set-upstream, -u Setup the upstream of the default branch
--setup-signing Setup the radicle key as a signing key for this repository
--no-confirm Don't ask for confirmation during setup
--help Print help
"#,
};
#[derive(Default)]
pub struct Options {
pub path: Option<PathBuf>,
pub name: Option<String>,
pub description: Option<String>,
pub branch: Option<String>,
pub interactive: Interactive,
pub setup_signing: bool,
pub set_upstream: bool,
}
impl Args for Options {
fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
use lexopt::prelude::*;
let mut parser = lexopt::Parser::from_args(args);
let mut path: Option<PathBuf> = None;
let mut name = None;
let mut description = None;
let mut branch = None;
let mut interactive = Interactive::Yes;
let mut set_upstream = false;
let mut setup_signing = false;
while let Some(arg) = parser.next()? {
match arg {
Long("name") if name.is_none() => {
let value = parser
.value()?
.to_str()
.ok_or(anyhow::anyhow!(
"invalid project name specified with `--name`"
))?
.to_owned();
name = Some(value);
}
Long("description") if description.is_none() => {
let value = parser
.value()?
.to_str()
.ok_or(anyhow::anyhow!(
"invalid project description specified with `--description`"
))?
.to_owned();
description = Some(value);
}
Long("default-branch") if branch.is_none() => {
let value = parser
.value()?
.to_str()
.ok_or(anyhow::anyhow!(
"invalid branch specified with `--default-branch`"
))?
.to_owned();
branch = Some(value);
}
Long("set-upstream") | Short('u') => {
set_upstream = true;
}
Long("setup-signing") => {
setup_signing = true;
}
Long("no-confirm") => {
interactive = Interactive::No;
}
Long("help") => {
return Err(Error::Help.into());
}
Value(val) if path.is_none() => {
path = Some(val.into());
}
_ => return Err(anyhow::anyhow!(arg.unexpected())),
}
}
Ok((
Options {
path,
name,
description,
branch,
interactive,
set_upstream,
setup_signing,
},
vec![],
))
}
}
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let profile = ctx.profile()?;
if git::check_version().is_err() {
term::warning(&format!(
"Your git version is unsupported, please upgrade to {} or later",
git::VERSION_REQUIRED,
));
term::blank();
}
init(options, &profile)
}
pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()> {
let cwd = std::env::current_dir()?;
let path = options.path.unwrap_or_else(|| cwd.clone());
let path = path.as_path().canonicalize()?;
let interactive = options.interactive;
term::headline(&format!(
"Initializing local 🌱 project in {}",
if path == cwd {
term::format::highlight(".")
} else {
term::format::highlight(&path.display())
}
));
let repo = git::Repository::open(&path)?;
if let Ok((remote, _)) = git::rad_remote(&repo) {
if let Some(remote) = remote.url() {
bail!("repository is already initialized with remote {remote}");
}
}
let signer = term::signer(profile)?;
let head: String = repo
.head()
.ok()
.and_then(|head| head.shorthand().map(|h| h.to_owned()))
.ok_or_else(|| anyhow!("error: repository head does not point to any commits"))?;
let name = options.name.unwrap_or_else(|| {
let default = path.file_name().map(|f| f.to_string_lossy().to_string());
term::text_input("Name", default).unwrap()
});
let description = options
.description
.unwrap_or_else(|| term::text_input("Description", None).unwrap());
let branch = options.branch.unwrap_or_else(|| {
if interactive.yes() {
term::text_input("Default branch", Some(head)).unwrap()
} else {
head
}
});
let branch = RefString::try_from(branch.clone())
.map_err(|e| anyhow!("invalid branch name {:?}: {}", branch, e))?;
let mut spinner = term::spinner("Initializing...");
match radicle::rad::init(
&repo,
&name,
&description,
branch,
&signer,
&profile.storage,
) {
Ok((id, doc, _)) => {
spinner.message(format!(
"Project {} created",
term::format::highlight(&doc.name)
));
spinner.finish();
if interactive.no() {
term::blob(json::to_string_pretty(&doc.payload)?);
term::blank();
}
if options.set_upstream || git::branch_remote(&repo, &doc.default_branch).is_err() {
// Setup eg. `master` -> `rad/master`
radicle::git::set_upstream(
&repo,
&radicle::rad::REMOTE_NAME,
&doc.default_branch,
&radicle::git::refs::workdir::branch(&doc.default_branch),
)?;
}
if options.setup_signing {
// Setup radicle signing key.
self::setup_signing(profile.id(), &repo, interactive)?;
}
term::blank();
term::info!(
"Your project id is {}. You can show it any time by running:",
term::format::highlight(id)
);
term::indented(&term::format::secondary("rad ."));
term::blank();
term::info!("To publish your project to the network, run:");
term::indented(&term::format::secondary("rad push"));
term::blank();
}
Err(err) => {
spinner.failed();
term::blank();
anyhow::bail!(err);
// TODO: Handle error: "this repository is already initialized with remote {}"
// TODO: Handle error: "the `{}` branch was either not found, or has no commits"
}
}
Ok(())
}
/// Setup radicle key as commit signing key in repository.
pub fn setup_signing(
node_id: &NodeId,
repo: &git::Repository,
interactive: Interactive,
) -> anyhow::Result<()> {
let repo = repo
.workdir()
.ok_or(anyhow!("cannot setup signing in bare repository"))?;
let key = ssh::fmt::fingerprint(node_id);
let yes = if !git::is_signing_configured(repo)? {
term::headline(&format!(
"Configuring 🌱 signing key {}...",
term::format::tertiary(key)
));
true
} else if interactive.yes() {
term::confirm(&format!(
"Configure 🌱 signing key {} in local checkout?",
term::format::tertiary(key),
))
} else {
true
};
if yes {
match git::write_gitsigners(repo, [node_id]) {
Ok(file) => {
git::ignore(repo, file.as_path())?;
term::success!("Created {} file", term::format::tertiary(file.display()));
}
Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => {
let ssh_key = ssh::fmt::key(node_id);
let gitsigners = term::format::tertiary(".gitsigners");
term::success!("Found existing {} file", gitsigners);
let ssh_keys =
git::read_gitsigners(repo).context("error reading .gitsigners file")?;
if ssh_keys.contains(&ssh_key) {
term::success!("Signing key is already in {} file", gitsigners);
} else if term::confirm(&format!("Add signing key to {}?", gitsigners)) {
git::add_gitsigners(repo, [node_id])?;
}
}
Err(err) => {
return Err(err.into());
}
}
git::configure_signing(repo, node_id)?;
term::success!(
"Signing configured in {}",
term::format::tertiary(".git/config")
);
}
Ok(())
}

462
radicle-cli/src/git.rs Normal file
View File

@ -0,0 +1,462 @@
//! Git-related functions and types.
use std::collections::HashSet;
use std::fs::{File, OpenOptions};
use std::io;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::str::FromStr;
use anyhow::anyhow;
use anyhow::Context as _;
use radicle::crypto::ssh;
use radicle::git::raw as git2;
use radicle::prelude::{Id, NodeId};
pub use radicle::git::raw::{
build::CheckoutBuilder, AnnotatedCommit, Commit, Direction, ErrorCode, MergeAnalysis,
MergeOptions, Oid, Reference, Repository, Signature,
};
pub const CONFIG_COMMIT_GPG_SIGN: &str = "commit.gpgsign";
pub const CONFIG_SIGNING_KEY: &str = "user.signingkey";
pub const CONFIG_GPG_FORMAT: &str = "gpg.format";
pub const CONFIG_GPG_SSH_PROGRAM: &str = "gpg.ssh.program";
pub const CONFIG_GPG_SSH_ALLOWED_SIGNERS: &str = "gpg.ssh.allowedSignersFile";
/// Minimum required git version.
pub const VERSION_REQUIRED: Version = Version {
major: 2,
minor: 34,
patch: 0,
};
/// A parsed git version.
#[derive(PartialEq, Eq, Debug, PartialOrd, Ord)]
pub struct Version {
pub major: u8,
pub minor: u8,
pub patch: u8,
}
impl std::fmt::Display for Version {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
impl std::str::FromStr for Version {
type Err = anyhow::Error;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let rest = input
.strip_prefix("git version ")
.ok_or_else(|| anyhow!("malformed git version string"))?;
let rest = rest
.split(' ')
.next()
.ok_or_else(|| anyhow!("malformed git version string"))?;
let rest = rest.trim_end();
let mut parts = rest.split('.');
let major = parts
.next()
.ok_or_else(|| anyhow!("malformed git version string"))?
.parse()?;
let minor = parts
.next()
.ok_or_else(|| anyhow!("malformed git version string"))?
.parse()?;
let patch = match parts.next() {
None => 0,
Some(patch) => patch.parse()?,
};
Ok(Self {
major,
minor,
patch,
})
}
}
/// Get the system's git version.
pub fn version() -> Result<Version, anyhow::Error> {
let output = Command::new("git").arg("version").output()?;
if output.status.success() {
let output = String::from_utf8(output.stdout)?;
let version = output
.parse()
.with_context(|| format!("unable to parse git version string {:?}", output))?;
return Ok(version);
}
Err(anyhow!("failed to run `git version`"))
}
/// Get the git repository in the current directory.
pub fn repository() -> Result<Repository, anyhow::Error> {
match Repository::open(".") {
Ok(repo) => Ok(repo),
Err(err) => Err(err).context("the current working directory is not a git repository"),
}
}
/// Execute a git command by spawning a child process.
pub fn git<S: AsRef<std::ffi::OsStr>>(
repo: &std::path::Path,
args: impl IntoIterator<Item = S>,
) -> Result<String, anyhow::Error> {
let output = Command::new("git").current_dir(repo).args(args).output()?;
if output.status.success() {
let out = if output.stdout.is_empty() {
&output.stderr
} else {
&output.stdout
};
return Ok(String::from_utf8_lossy(out).into());
}
Err(anyhow::Error::new(std::io::Error::new(
std::io::ErrorKind::Other,
String::from_utf8_lossy(&output.stderr),
)))
}
/// Configure SSH signing in the given git repo, for the given peer.
pub fn configure_signing(repo: &Path, node_id: &NodeId) -> Result<(), anyhow::Error> {
let key = ssh::fmt::key(node_id);
git(repo, ["config", "--local", CONFIG_SIGNING_KEY, &key])?;
git(repo, ["config", "--local", CONFIG_GPG_FORMAT, "ssh"])?;
git(repo, ["config", "--local", CONFIG_COMMIT_GPG_SIGN, "true"])?;
git(
repo,
["config", "--local", CONFIG_GPG_SSH_PROGRAM, "ssh-keygen"],
)?;
git(
repo,
[
"config",
"--local",
CONFIG_GPG_SSH_ALLOWED_SIGNERS,
".gitsigners",
],
)?;
Ok(())
}
/// Write a `.gitsigners` file in the given repository.
/// Fails if the file already exists.
pub fn write_gitsigners<'a>(
repo: &Path,
signers: impl IntoIterator<Item = &'a NodeId>,
) -> Result<PathBuf, io::Error> {
let path = Path::new(".gitsigners");
let mut file = OpenOptions::new()
.write(true)
.create_new(true)
.open(repo.join(path))?;
for node_id in signers.into_iter() {
write_gitsigner(&mut file, node_id)?;
}
Ok(path.to_path_buf())
}
/// Add signers to the repository's `.gitsigners` file.
pub fn add_gitsigners<'a>(
path: &Path,
signers: impl IntoIterator<Item = &'a NodeId>,
) -> Result<(), io::Error> {
let mut file = OpenOptions::new()
.append(true)
.open(path.join(".gitsigners"))?;
for node_id in signers.into_iter() {
write_gitsigner(&mut file, node_id)?;
}
Ok(())
}
/// Read a `.gitsigners` file. Returns SSH keys.
pub fn read_gitsigners(path: &Path) -> Result<HashSet<String>, io::Error> {
use std::io::BufRead;
let mut keys = HashSet::new();
let file = File::open(path.join(".gitsigners"))?;
for line in io::BufReader::new(file).lines() {
let line = line?;
if let Some((label, key)) = line.split_once(' ') {
if let Ok(peer) = NodeId::from_str(label) {
let expected = ssh::fmt::key(&peer);
if key != expected {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"key does not match peer id",
));
}
}
keys.insert(key.to_owned());
}
}
Ok(keys)
}
/// Add a path to the repository's git ignore file. Creates the
/// ignore file if it does not exist.
pub fn ignore(repo: &Path, item: &Path) -> Result<(), io::Error> {
let mut ignore = OpenOptions::new()
.append(true)
.create(true)
.open(repo.join(".gitignore"))?;
writeln!(ignore, "{}", item.display())
}
/// Check whether SSH or GPG signing is configured in the given repository.
pub fn is_signing_configured(repo: &Path) -> Result<bool, anyhow::Error> {
Ok(git(repo, ["config", CONFIG_SIGNING_KEY]).is_ok())
}
/// Return the list of radicle remotes for the given repository.
pub fn remotes(repo: &git2::Repository) -> anyhow::Result<Vec<(String, NodeId)>> {
let mut remotes = Vec::new();
for name in repo.remotes().iter().flatten().flatten() {
let remote = repo.find_remote(name)?;
for refspec in remote.refspecs() {
if refspec.direction() != git2::Direction::Fetch {
continue;
}
if let Some((peer, _)) = refspec.src().and_then(self::parse_remote) {
remotes.push((name.to_owned(), peer));
}
}
}
Ok(remotes)
}
/// Get the repository's "rad" remote.
pub fn rad_remote(repo: &Repository) -> anyhow::Result<(git2::Remote, Id)> {
match radicle::rad::remote(repo) {
Ok((remote, id)) => Ok((remote, id)),
Err(radicle::rad::RemoteError::NotFound(_)) => Err(anyhow!(
"could not find radicle remote in git config; did you forget to run `rad init`?"
)),
Err(err) => Err(err).context("could not read git remote configuration"),
}
}
/// Setup an upstream tracking branch for the given remote and branch.
/// Creates the tracking branch if it does not exist.
///
/// > scooby/master...rad/scooby/heads/master
///
pub fn set_tracking(repo: &Repository, remote: &NodeId, branch: &str) -> anyhow::Result<String> {
// The tracking branch name, eg. 'scooby/master'
let branch_name = format!("{remote}/{branch}");
// The remote branch being tracked, eg. 'rad/scooby/heads/master'
let remote_branch_name = format!("rad/{remote}/heads/{branch}");
// The target reference this branch should be set to.
let target = format!("refs/remotes/{remote_branch_name}");
let reference = repo.find_reference(&target)?;
let commit = reference.peel_to_commit()?;
repo.branch(&branch_name, &commit, true)?
.set_upstream(Some(&remote_branch_name))?;
Ok(branch_name)
}
/// Get the name of the remote of the given branch, if any.
pub fn branch_remote(repo: &Repository, branch: &str) -> anyhow::Result<String> {
let cfg = repo.config()?;
let remote = cfg.get_string(&format!("branch.{}.remote", branch))?;
Ok(remote)
}
/// Call `git pull`, optionally with `--force`.
pub fn pull(repo: &Path, force: bool) -> anyhow::Result<String> {
let mut args = vec!["-c", "color.diff=always", "pull", "-v"];
if force {
args.push("--force");
}
git(repo, args)
}
/// Clone the given repository via `git clone` into a directory.
pub fn clone(repo: &str, destination: &Path) -> Result<String, anyhow::Error> {
git(
Path::new("."),
["clone", repo, &destination.to_string_lossy()],
)
}
/// Check that the system's git version is supported. Returns an error otherwise.
pub fn check_version() -> Result<Version, anyhow::Error> {
let git_version = self::version()?;
if git_version < VERSION_REQUIRED {
anyhow::bail!("a minimum git version of {} is required", VERSION_REQUIRED);
}
Ok(git_version)
}
/// Parse a remote refspec into a peer id and ref.
pub fn parse_remote(refspec: &str) -> Option<(NodeId, &str)> {
refspec
.strip_prefix("refs/remotes/")
.and_then(|s| s.split_once('/'))
.and_then(|(peer, r)| NodeId::from_str(peer).ok().map(|p| (p, r)))
}
pub fn view_diff(
repo: &git2::Repository,
left: &git2::Oid,
right: &git2::Oid,
) -> anyhow::Result<()> {
// TODO(erikli): Replace with repo.diff()
let workdir = repo
.workdir()
.ok_or_else(|| anyhow!("Could not get workdir current repository."))?;
let left = format!("{:.7}", left.to_string());
let right = format!("{:.7}", right.to_string());
let mut git = Command::new("git")
.current_dir(workdir)
.args(["diff", &left, &right])
.spawn()?;
git.wait()?;
Ok(())
}
pub fn add_tag(
repo: &git2::Repository,
message: &str,
patch_tag_name: &str,
) -> anyhow::Result<git2::Oid> {
let head = repo.head()?;
let commit = head.peel(git2::ObjectType::Commit).unwrap();
let oid = repo.tag(patch_tag_name, &commit, &repo.signature()?, message, false)?;
Ok(oid)
}
pub fn push_tag(tag_name: &str) -> anyhow::Result<String> {
git(Path::new("."), vec!["push", "rad", "tag", tag_name])
}
pub fn push_branch(name: &str) -> anyhow::Result<String> {
git(Path::new("."), vec!["push", "rad", name])
}
fn write_gitsigner(mut w: impl io::Write, signer: &NodeId) -> io::Result<()> {
writeln!(w, "{} {}", signer, ssh::fmt::key(signer))
}
/// From a commit hash, return the signer's fingerprint, if any.
pub fn commit_ssh_fingerprint(path: &Path, sha1: &str) -> Result<Option<String>, io::Error> {
use std::io::BufRead;
use std::io::BufReader;
let output = Command::new("git")
.current_dir(path) // We need to place the command execution in the git dir
.args(["show", sha1, "--pretty=%GF", "--raw"])
.output()?;
if !output.status.success() {
return Err(io::Error::new(
io::ErrorKind::Other,
String::from_utf8_lossy(&output.stderr),
));
}
let string = BufReader::new(output.stdout.as_slice())
.lines()
.next()
.transpose()?;
// We only return a fingerprint if it's not an empty string
if let Some(s) = string {
if !s.is_empty() {
return Ok(Some(s));
}
}
Ok(None)
}
#[cfg(test)]
mod test {
use super::*;
use std::str::FromStr;
#[test]
fn test_version_ord() {
assert!(
Version {
major: 2,
minor: 34,
patch: 1
} > Version {
major: 2,
minor: 34,
patch: 0
}
);
assert!(
Version {
major: 2,
minor: 24,
patch: 12
} < Version {
major: 2,
minor: 34,
patch: 0
}
);
}
#[test]
fn test_version_from_str() {
assert_eq!(
Version::from_str("git version 2.34.1\n").ok(),
Some(Version {
major: 2,
minor: 34,
patch: 1
})
);
assert_eq!(
Version::from_str("git version 2.34.1 (macOS)").ok(),
Some(Version {
major: 2,
minor: 34,
patch: 1
})
);
assert_eq!(
Version::from_str("git version 2.34").ok(),
Some(Version {
major: 2,
minor: 34,
patch: 0
})
);
assert!(Version::from_str("2.34").is_err());
}
}

View File

@ -1,2 +1,4 @@
#![allow(clippy::collapsible_if)]
pub mod commands;
pub mod git;
pub mod terminal;

View File

@ -682,7 +682,7 @@ fn test_push_and_pull() {
sim.run_while([&mut alice, &mut bob, &mut eve], |s| !s.is_settled());
// Alice creates a new project.
let (proj_id, _) = rad::init(
let (proj_id, _, _) = rad::init(
&repo,
"alice",
"alice's repo",

View File

@ -8,7 +8,7 @@ fn main() -> anyhow::Result<()> {
let repo = radicle::git::raw::Repository::open(cwd)?;
let profile = Profile::load()?;
let signer = profile.signer()?;
let (id, _) = radicle::rad::init(
let (id, _, _) = radicle::rad::init(
&repo,
&name,
"",

View File

@ -20,3 +20,13 @@ pub mod test;
pub use profile::Profile;
pub use storage::git::Storage;
pub mod prelude {
use super::*;
pub use crypto::{Signer, Verified};
pub use identity::{Doc, Id};
pub use node::NodeId;
pub use profile::Profile;
pub use storage::BranchName;
}

View File

@ -49,7 +49,7 @@ pub fn init<G: Signer>(
default_branch: BranchName,
signer: &G,
storage: &Storage,
) -> Result<(Id, SignedRefs<Verified>), InitError> {
) -> Result<(Id, identity::Doc<Verified>, SignedRefs<Verified>), InitError> {
// TODO: Better error when project id already exists in storage, but remote doesn't.
let pk = signer.public_key();
let delegate = identity::Delegate {
@ -68,13 +68,6 @@ pub fn init<G: Signer>(
let (id, _, project) = doc.create(pk, "Initialize Radicle\n", storage)?;
let url = git::Url::from(id).with_namespace(*pk);
git::set_upstream(
repo,
&REMOTE_NAME,
&default_branch,
&git::refs::workdir::branch(&default_branch),
)?;
git::configure_remote(repo, &REMOTE_NAME, &url)?;
git::push(
repo,
@ -87,7 +80,7 @@ pub fn init<G: Signer>(
let signed = project.sign_refs(signer)?;
let _head = project.set_head()?;
Ok((id, signed))
Ok((id, doc, signed))
}
#[derive(Error, Debug)]
@ -121,10 +114,10 @@ pub fn fork_remote<G: Signer, S: storage::WriteStorage>(
// Creates or copies the following references:
//
// refs/remotes/<pk>/heads/master
// refs/remotes/<pk>/rad/id
// refs/remotes/<pk>/tags/*
// refs/remotes/<pk>/rad/signature
// refs/namespaces/<pk>/refs/heads/master
// refs/namespaces/<pk>/refs/rad/id
// refs/namespaces/<pk>/refs/rad/sigrefs
// refs/namespaces/<pk>/refs/tags/*
let me = signer.public_key();
let project = storage
@ -306,11 +299,19 @@ pub enum RemoteError {
Url(#[from] transport::local::UrlError),
#[error("invalid utf-8 string")]
InvalidUtf8,
#[error("remote `{0}` not found")]
NotFound(String),
}
/// Get the radicle ("rad") remote of a repository, and return the associated project id.
pub fn remote(repo: &git2::Repository) -> Result<(git2::Remote<'_>, Id), RemoteError> {
let remote = repo.find_remote(&REMOTE_NAME)?;
let remote = repo.find_remote(&REMOTE_NAME).map_err(|e| {
if e.code() == git2::ErrorCode::NotFound {
RemoteError::NotFound(REMOTE_NAME.to_string())
} else {
RemoteError::from(e)
}
})?;
let url = remote.url().ok_or(RemoteError::InvalidUtf8)?;
let url = git::Url::from_str(url)?;
@ -341,7 +342,7 @@ mod tests {
transport::local::register(storage.clone());
let (repo, _) = fixtures::repository(tempdir.path().join("working"));
let (proj, refs) = init(
let (proj, _, refs) = init(
&repo,
"acme",
"Acme's repo",
@ -399,7 +400,7 @@ mod tests {
// Alice creates a project.
let (original, _) = fixtures::repository(tempdir.path().join("original"));
let (id, alice_refs) = init(
let (id, _, alice_refs) = init(
&original,
"acme",
"Acme's repo",
@ -431,7 +432,7 @@ mod tests {
transport::local::register(storage.clone());
let (original, _) = fixtures::repository(tempdir.path().join("original"));
let (id, _) = init(
let (id, _, _) = init(
&original,
"acme",
"Acme's repo",
@ -440,6 +441,7 @@ mod tests {
&storage,
)
.unwrap();
git::set_upstream(&original, "rad", "master", "refs/heads/master").unwrap();
let copy = checkout(id, remote_id, tempdir.path().join("copy"), &storage).unwrap();

View File

@ -886,7 +886,7 @@ mod tests {
transport::local::register(storage.clone());
let (proj, _) = rad::init(
let (proj, _, _) = rad::init(
&source,
"radicle",
"radicle",

View File

@ -37,7 +37,7 @@ pub fn project<P: AsRef<Path>, G: Signer>(
transport::local::register(storage.clone());
let (repo, head) = repository(path);
let (id, refs) = rad::init(
let (id, _, refs) = rad::init(
&repo,
"acme",
"Acme's repository",