Implement `rad-checkout` command
Signed-off-by: Alexis Sellier <alexis@radicle.xyz>
This commit is contained in:
parent
32cd94dd23
commit
8fe949b2a0
|
|
@ -1 +1,2 @@
|
||||||
|
pub mod checkout;
|
||||||
pub mod init;
|
pub mod init;
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
#![allow(clippy::collapsible_if)]
|
#![allow(clippy::collapsible_if)]
|
||||||
pub mod commands;
|
pub mod commands;
|
||||||
pub mod git;
|
pub mod git;
|
||||||
|
pub mod project;
|
||||||
pub mod terminal;
|
pub mod terminal;
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,6 +11,7 @@ use crate::git;
|
||||||
use crate::identity::project::DocError;
|
use crate::identity::project::DocError;
|
||||||
use crate::identity::Id;
|
use crate::identity::Id;
|
||||||
use crate::node;
|
use crate::node;
|
||||||
|
use crate::node::NodeId;
|
||||||
use crate::storage::git::transport::{self, remote};
|
use crate::storage::git::transport::{self, remote};
|
||||||
use crate::storage::git::{ProjectError, Storage};
|
use crate::storage::git::{ProjectError, Storage};
|
||||||
use crate::storage::refs::SignedRefs;
|
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"));
|
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)]
|
#[derive(Error, Debug)]
|
||||||
pub enum InitError {
|
pub enum InitError {
|
||||||
#[error("doc: {0}")]
|
#[error("doc: {0}")]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue