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:
parent
74336f961a
commit
ee1c867800
|
|
@ -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)
|
||||||
|
```
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue