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 <rid>

This sets up the remote(s) and configures the repo for you.
This commit is contained in:
cloudhead 2024-07-29 11:45:19 +02:00
parent 74336f961a
commit ee1c867800
No known key found for this signature in database
4 changed files with 144 additions and 12 deletions

View File

@ -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)
```

View File

@ -18,6 +18,7 @@ use radicle::node::events::UploadPack;
use radicle::node::policy::Scope; use radicle::node::policy::Scope;
use radicle::node::{Event, Handle, NodeId, DEFAULT_SUBSCRIBE_TIMEOUT}; use radicle::node::{Event, Handle, NodeId, DEFAULT_SUBSCRIBE_TIMEOUT};
use radicle::prelude::Doc; use radicle::prelude::Doc;
use radicle::storage::ReadStorage as _;
use radicle::{profile, Node}; use radicle::{profile, Node};
use crate::commands; use crate::commands;
@ -43,6 +44,7 @@ Options
--scope <scope> Repository follow scope: `followed` or `all` (default: all) --scope <scope> Repository follow scope: `followed` or `all` (default: all)
--private Set repository visibility to *private* --private Set repository visibility to *private*
--public Set repository visibility to *public* --public Set repository visibility to *public*
--existing <rid> Setup repository as an existing Radicle repository
-u, --set-upstream Setup the upstream of the default branch -u, --set-upstream Setup the upstream of the default branch
--setup-signing Setup the radicle key as a signing key for this repository --setup-signing Setup the radicle key as a signing key for this repository
--no-confirm Don't ask for confirmation during setup --no-confirm Don't ask for confirmation during setup
@ -60,6 +62,7 @@ pub struct Options {
pub branch: Option<String>, pub branch: Option<String>,
pub interactive: Interactive, pub interactive: Interactive,
pub visibility: Option<Visibility>, pub visibility: Option<Visibility>,
pub existing: Option<RepoId>,
pub setup_signing: bool, pub setup_signing: bool,
pub scope: Scope, pub scope: Scope,
pub set_upstream: bool, pub set_upstream: bool,
@ -81,6 +84,7 @@ impl Args for Options {
let mut set_upstream = false; let mut set_upstream = false;
let mut setup_signing = false; let mut setup_signing = false;
let mut scope = Scope::All; let mut scope = Scope::All;
let mut existing = None;
let mut seed = true; let mut seed = true;
let mut verbose = false; let mut verbose = false;
let mut visibility = None; let mut visibility = None;
@ -150,6 +154,12 @@ impl Args for Options {
Long("public") => { Long("public") => {
visibility = Some(Visibility::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') => { Long("verbose") | Short('v') => {
verbose = true; verbose = true;
} }
@ -170,6 +180,7 @@ impl Args for Options {
description, description,
branch, branch,
scope, scope,
existing,
interactive, interactive,
set_upstream, set_upstream,
setup_signing, setup_signing,
@ -184,29 +195,38 @@ impl Args for Options {
pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> { pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
let profile = ctx.profile()?; let profile = ctx.profile()?;
let cwd = env::current_dir()?;
init(options, &profile) let path = options.path.as_deref().unwrap_or(cwd.as_path());
} let repo = match git::Repository::open(path) {
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) {
Ok(r) => r, Ok(r) => r,
Err(e) if radicle::git::ext::is_not_found_err(&e) => { Err(e) if radicle::git::ext::is_not_found_err(&e) => {
anyhow::bail!("a Git repository was not found at the given path") anyhow::bail!("a Git repository was not found at the given path")
} }
Err(e) => return Err(e.into()), Err(e) => return Err(e.into()),
}; };
if let Ok((remote, _)) = git::rad_remote(&repo) { if let Ok((remote, _)) = git::rad_remote(&repo) {
if let Some(remote) = remote.url() { if let Some(remote) = remote.url() {
bail!("repository is already initialized with remote {remote}"); 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 let head: String = repo
.head() .head()
.ok() .ok()
@ -319,7 +339,7 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()>
term::format::dim("(RID)"), term::format::dim("(RID)"),
term::format::highlight(rid.urn()) term::format::highlight(rid.urn())
); );
let directory = if path == cwd { let directory = if path == env::current_dir()? {
"this directory".to_owned() "this directory".to_owned()
} else { } else {
term::format::tertiary(path.display()).to_string() term::format::tertiary(path.display()).to_string()
@ -350,6 +370,48 @@ pub fn init(options: Options, profile: &profile::Profile) -> anyhow::Result<()>
Ok(()) 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)] #[derive(Debug)]
enum SyncResult<T> { enum SyncResult<T> {
NodeStopped, NodeStopped,

View File

@ -204,6 +204,28 @@ fn rad_init() {
.unwrap(); .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] #[test]
fn rad_init_no_seed() { fn rad_init_no_seed() {
let mut environment = Environment::new(); let mut environment = Environment::new();

View File

@ -697,6 +697,8 @@ pub mod process {
pub mod url { pub mod url {
use std::path::PathBuf; use std::path::PathBuf;
use crate::prelude::RepoId;
/// A Git URL using the `file://` scheme. /// A Git URL using the `file://` scheme.
pub struct File { pub struct File {
pub path: PathBuf, pub path: PathBuf,
@ -707,6 +709,12 @@ pub mod url {
pub fn new(path: impl Into<PathBuf>) -> Self { pub fn new(path: impl Into<PathBuf>) -> Self {
Self { path: path.into() } 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 { impl ToString for File {