Implement `rad-checkout` command

Signed-off-by: Alexis Sellier <alexis@radicle.xyz>
This commit is contained in:
Alexis Sellier 2022-11-07 22:04:47 +01:00
parent 32cd94dd23
commit 8fe949b2a0
No known key found for this signature in database
5 changed files with 184 additions and 0 deletions

View File

@ -1 +1,2 @@
pub mod checkout;
pub mod init;

View File

@ -0,0 +1,136 @@
use std::ffi::OsString;
use std::path::PathBuf;
use anyhow::anyhow;
use anyhow::Context as _;
use radicle::prelude::*;
use radicle::storage::WriteStorage;
use crate::project;
use crate::terminal as term;
use crate::terminal::args::{Args, Error, Help};
pub const HELP: Help = Help {
name: "checkout",
description: "Checkout a radicle project working copy",
version: env!("CARGO_PKG_VERSION"),
usage: r#"
Usage
rad checkout <id> [<option>...]
Options
--no-confirm Don't ask for confirmation during checkout
--help Print help
"#,
};
pub struct Options {
pub id: Id,
}
impl Args for Options {
fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
use lexopt::prelude::*;
use std::str::FromStr;
let mut parser = lexopt::Parser::from_args(args);
let mut id = None;
while let Some(arg) = parser.next()? {
match arg {
Long("no-confirm") => {
// Ignored for now.
}
Long("help") => return Err(Error::Help.into()),
Value(val) if id.is_none() => {
let val = val.to_string_lossy();
let val = Id::from_str(&val).context(format!("invalid id '{}'", val))?;
id = Some(val);
}
_ => return Err(anyhow::anyhow!(arg.unexpected())),
}
}
Ok((
Options {
id: id.ok_or_else(|| anyhow!("a project id to checkout must be provided"))?,
},
vec![],
))
}
}
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let profile = ctx.profile()?;
let path = execute(options, &profile)?;
term::headline(&format!(
"🌱 Project checkout successful under ./{}",
term::format::highlight(path.file_name().unwrap_or_default().to_string_lossy())
));
Ok(())
}
pub fn execute(options: Options, profile: &Profile) -> anyhow::Result<PathBuf> {
let storage = &profile.storage;
let repo = storage.repository(options.id)?;
let project = repo
.project_of(profile.id())
.context("project could not be found in local storage")?;
let path = PathBuf::from(project.name.clone());
if path.exists() {
anyhow::bail!("the local path {:?} already exists", path.as_path());
}
term::headline(&format!(
"Initializing local checkout for 🌱 {} ({})",
term::format::highlight(options.id),
project.name,
));
let spinner = term::spinner("Performing checkout...");
let working = match radicle::rad::checkout(options.id, profile.id(), path.clone(), &storage) {
Ok(working) => working,
Err(err) => {
spinner.failed();
term::blank();
return Err(err.into());
}
};
spinner.finish();
// Setup a remote and tracking branch for all project delegates except yourself.
let setup = project::SetupRemote {
project: options.id,
default_branch: project.default_branch.clone(),
repo: &working,
fetch: true,
tracking: true,
};
for remote_id in repo.remote_ids()? {
let remote_id = remote_id?;
if &remote_id == profile.id() {
continue;
}
if let Some((remote, branch)) = setup.run(remote_id)? {
let remote = remote.name().unwrap(); // Only valid UTF-8 is used.
//
term::success!("Remote {} set", term::format::highlight(remote));
term::success!(
"Remote-tracking branch {} created for {}",
term::format::highlight(&branch),
term::format::tertiary(term::format::node(&remote_id))
);
}
}
Ok(path)
}

View File

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

View File

@ -0,0 +1,40 @@
use radicle::git::raw::Remote;
use radicle::prelude::*;
use radicle::rad;
use crate::git;
/// Setup a project remote and tracking branch.
pub struct SetupRemote<'a> {
/// The project id.
pub project: Id,
/// The project default branch.
pub default_branch: BranchName,
/// The repository in which to setup the remote.
pub repo: &'a git::Repository,
/// Whether or not to fetch the remote immediately.
pub fetch: bool,
/// Whether or not to setup a remote tracking branch.
pub tracking: bool,
}
impl<'a> SetupRemote<'a> {
/// Run the setup for the given peer.
pub fn run(&self, node: NodeId) -> anyhow::Result<Option<(Remote, String)>> {
let url = radicle::git::Url::from(self.project).with_namespace(node);
let mut remote =
radicle::git::configure_remote(self.repo, rad::peer_remote(&node).as_str(), &url)?;
// Fetch the refs into the working copy.
if self.fetch {
remote.fetch::<&str>(&[], None, None)?;
}
// Setup remote-tracking branch.
if self.tracking {
let branch = git::set_tracking(self.repo, &node, &self.default_branch)?;
return Ok(Some((remote, branch)));
}
Ok(None)
}
}

View File

@ -11,6 +11,7 @@ use crate::git;
use crate::identity::project::DocError;
use crate::identity::Id;
use crate::node;
use crate::node::NodeId;
use crate::storage::git::transport::{self, remote};
use crate::storage::git::{ProjectError, Storage};
use crate::storage::refs::SignedRefs;
@ -19,6 +20,11 @@ use crate::{identity, storage};
pub static REMOTE_NAME: Lazy<git::RefString> = Lazy::new(|| git::refname!("rad"));
/// Radicle remote name for peer, eg. `rad/<node-id>`
pub fn peer_remote(peer: &NodeId) -> String {
format!("rad/{peer}")
}
#[derive(Error, Debug)]
pub enum InitError {
#[error("doc: {0}")]