From ee1c867800eacea37d489d966622d0aff0af0b70 Mon Sep 17 00:00:00 2001 From: cloudhead Date: Mon, 29 Jul 2024 11:45:19 +0200 Subject: [PATCH] cli: Add way to init from existing repositories This adds an option to `rad init` to initialize a working copy with an existing radicle repository in storage: rad init --existing This sets up the remote(s) and configures the repo for you. --- radicle-cli/examples/rad-init-existing.md | 40 +++++++++++ radicle-cli/src/commands/init.rs | 86 +++++++++++++++++++---- radicle-cli/tests/commands.rs | 22 ++++++ radicle/src/git.rs | 8 +++ 4 files changed, 144 insertions(+), 12 deletions(-) create mode 100644 radicle-cli/examples/rad-init-existing.md diff --git a/radicle-cli/examples/rad-init-existing.md b/radicle-cli/examples/rad-init-existing.md new file mode 100644 index 00000000..a9bf5879 --- /dev/null +++ b/radicle-cli/examples/rad-init-existing.md @@ -0,0 +1,40 @@ +Let's clone a regular repository via plain Git: +``` +$ git clone $URL heartwood +$ cd heartwood +$ git rev-parse HEAD +f2de534b5e81d7c6e2dcaf58c3dd91573c0a0354 +``` + +We can see it's not a Radicle working copy: +``` (fail) +$ rad . +✗ Error: Current directory is not a Radicle repository +``` + +Let's pick an existing repository: +``` +$ rad inspect rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji +rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji +``` + +And initialize this working copy as that existing repository: +``` +$ rad init --existing rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji +✓ Initialized existing repository rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji in [..]/heartwood/.. +``` + +We can confirm that the working copy is initialized: +``` +$ rad . +rad:z42hL2jL4XNk6K8oHQaSWfMgCL7ji +$ git remote show rad +* remote rad + Fetch URL: rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji + Push URL: rad://z42hL2jL4XNk6K8oHQaSWfMgCL7ji/z6MknSLrJoTcukLrE435hVNQT4JUhbvWLX4kUzqkEStBU8Vi + HEAD branch: (unknown) + Remote branch: + master new (next fetch will store in remotes/rad) + Local ref configured for 'git push': + master pushes to master (up to date) +``` diff --git a/radicle-cli/src/commands/init.rs b/radicle-cli/src/commands/init.rs index 8aa2cce7..e68f6d03 100644 --- a/radicle-cli/src/commands/init.rs +++ b/radicle-cli/src/commands/init.rs @@ -18,6 +18,7 @@ use radicle::node::events::UploadPack; use radicle::node::policy::Scope; use radicle::node::{Event, Handle, NodeId, DEFAULT_SUBSCRIBE_TIMEOUT}; use radicle::prelude::Doc; +use radicle::storage::ReadStorage as _; use radicle::{profile, Node}; use crate::commands; @@ -43,6 +44,7 @@ Options --scope Repository follow scope: `followed` or `all` (default: all) --private Set repository visibility to *private* --public Set repository visibility to *public* + --existing Setup repository as an existing Radicle repository -u, --set-upstream 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 @@ -60,6 +62,7 @@ pub struct Options { pub branch: Option, pub interactive: Interactive, pub visibility: Option, + pub existing: Option, pub setup_signing: bool, pub scope: Scope, pub set_upstream: bool, @@ -81,6 +84,7 @@ impl Args for Options { let mut set_upstream = false; let mut setup_signing = false; let mut scope = Scope::All; + let mut existing = None; let mut seed = true; let mut verbose = false; let mut visibility = None; @@ -150,6 +154,12 @@ impl Args for Options { Long("public") => { visibility = Some(Visibility::Public); } + Long("existing") if existing.is_none() => { + let val = parser.value()?; + let rid = term::args::rid(&val)?; + + existing = Some(rid); + } Long("verbose") | Short('v') => { verbose = true; } @@ -170,6 +180,7 @@ impl Args for Options { description, branch, scope, + existing, interactive, set_upstream, setup_signing, @@ -184,29 +195,38 @@ impl Args for Options { pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { let profile = ctx.profile()?; - - 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; - let repo = match git::Repository::open(&path) { + let cwd = env::current_dir()?; + let path = options.path.as_deref().unwrap_or(cwd.as_path()); + let repo = match git::Repository::open(path) { Ok(r) => r, Err(e) if radicle::git::ext::is_not_found_err(&e) => { anyhow::bail!("a Git repository was not found at the given path") } Err(e) => return Err(e.into()), }; - if let Ok((remote, _)) = git::rad_remote(&repo) { if let Some(remote) = remote.url() { bail!("repository is already initialized with remote {remote}"); } } + if let Some(rid) = options.existing { + init_existing(repo, rid, options, &profile) + } else { + init(repo, options, &profile) + } +} + +pub fn init( + repo: git::Repository, + options: Options, + profile: &profile::Profile, +) -> anyhow::Result<()> { + let path = repo + .workdir() + .unwrap_or_else(|| repo.path()) + .canonicalize()?; + let interactive = options.interactive; let head: String = repo .head() .ok() @@ -319,7 +339,7 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()> term::format::dim("(RID)"), term::format::highlight(rid.urn()) ); - let directory = if path == cwd { + let directory = if path == env::current_dir()? { "this directory".to_owned() } else { term::format::tertiary(path.display()).to_string() @@ -350,6 +370,48 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()> Ok(()) } +pub fn init_existing( + working: git::Repository, + rid: RepoId, + options: Options, + profile: &profile::Profile, +) -> anyhow::Result<()> { + let stored = profile.storage.repository(rid)?; + let project = stored.project()?; + let url = radicle::git::Url::from(rid); + + radicle::git::configure_repository(&working)?; + radicle::git::configure_remote( + &working, + &radicle::rad::REMOTE_NAME, + &url, + &url.clone().with_namespace(profile.public_key), + )?; + + if options.set_upstream { + // Setup eg. `master` -> `rad/master` + radicle::git::set_upstream( + &working, + &*radicle::rad::REMOTE_NAME, + project.default_branch(), + radicle::git::refs::workdir::branch(project.default_branch()), + )?; + } + + term::success!( + "Initialized existing repository {} in {}..", + term::format::tertiary(rid), + term::format::dim( + working + .workdir() + .unwrap_or_else(|| working.path()) + .display() + ), + ); + + Ok(()) +} + #[derive(Debug)] enum SyncResult { NodeStopped, diff --git a/radicle-cli/tests/commands.rs b/radicle-cli/tests/commands.rs index 13067a3b..4e99253f 100644 --- a/radicle-cli/tests/commands.rs +++ b/radicle-cli/tests/commands.rs @@ -204,6 +204,28 @@ fn rad_init() { .unwrap(); } +#[test] +fn rad_init_existing() { + let mut environment = Environment::new(); + let mut profile = environment.node(config::node("alice")); + let working = tempfile::tempdir().unwrap(); + let rid = profile.project("heartwood", "Radicle Heartwood Protocol & Stack"); + + test( + "examples/rad-init-existing.md", + working.path(), + Some(&profile.home), + [( + "URL", + git::url::File::new(profile.storage.path()) + .rid(rid) + .to_string() + .as_str(), + )], + ) + .unwrap(); +} + #[test] fn rad_init_no_seed() { let mut environment = Environment::new(); diff --git a/radicle/src/git.rs b/radicle/src/git.rs index ee52d4ed..ebe0c2d5 100644 --- a/radicle/src/git.rs +++ b/radicle/src/git.rs @@ -697,6 +697,8 @@ pub mod process { pub mod url { use std::path::PathBuf; + use crate::prelude::RepoId; + /// A Git URL using the `file://` scheme. pub struct File { pub path: PathBuf, @@ -707,6 +709,12 @@ pub mod url { pub fn new(path: impl Into) -> Self { Self { path: path.into() } } + + /// Return a URL with the given RID set. + pub fn rid(mut self, rid: RepoId) -> Self { + self.path.push(rid.canonical()); + self + } } impl ToString for File {