cli: Output explore URLs on push
This commit is contained in:
parent
fd38d88e14
commit
1e2776cb08
|
|
@ -4,7 +4,7 @@ In its simplest form, `rad config` prints the current configuration.
|
||||||
```
|
```
|
||||||
$ rad config
|
$ rad config
|
||||||
{
|
{
|
||||||
"publicExplorer": "https://app.radicle.xyz/nodes/$host/$rid",
|
"publicExplorer": "https://app.radicle.xyz/nodes/$host/$rid$path",
|
||||||
"preferredSeeds": [],
|
"preferredSeeds": [],
|
||||||
"cli": {
|
"cli": {
|
||||||
"hints": false
|
"hints": false
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
When preferred seeds are configured, opening a patch outputs the patch URL.
|
||||||
|
|
||||||
|
``` (stderr)
|
||||||
|
$ git checkout -b changes -q
|
||||||
|
$ git commit --allow-empty -q -m "Changes"
|
||||||
|
$ git push rad HEAD:refs/patches
|
||||||
|
✓ Patch e0b35c56eb265d49cddd72d91cf873f64037d96c opened
|
||||||
|
✓ Synced with 1 node(s)
|
||||||
|
|
||||||
|
https://app.radicle.xyz/nodes/[..]/rad:z3yXbb1sR6UG6ixxV2YF9jUP7ABra/patches/e0b35c56eb265d49cddd72d91cf873f64037d96c
|
||||||
|
|
||||||
|
To rad://z3yXbb1sR6UG6ixxV2YF9jUP7ABra/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||||
|
* [new reference] HEAD -> refs/patches
|
||||||
|
```
|
||||||
|
|
||||||
|
If we update the patch, the URL is also output.
|
||||||
|
|
||||||
|
``` (stderr)
|
||||||
|
$ git commit --amend --allow-empty -q -m "Other changes"
|
||||||
|
$ git push -f
|
||||||
|
✓ Patch e0b35c5 updated to revision 0ab4697ba5beee387f1211bdf0880a06564842ce
|
||||||
|
✓ Synced with 1 node(s)
|
||||||
|
|
||||||
|
https://app.radicle.xyz/nodes/[..]/rad:z3yXbb1sR6UG6ixxV2YF9jUP7ABra/patches/e0b35c56eb265d49cddd72d91cf873f64037d96c
|
||||||
|
|
||||||
|
To rad://z3yXbb1sR6UG6ixxV2YF9jUP7ABra/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||||
|
+ e12525d...b2b6432 changes -> patches/e0b35c56eb265d49cddd72d91cf873f64037d96c (forced update)
|
||||||
|
```
|
||||||
|
|
||||||
|
While simply pushing a commit outputs a URL to the new source tree.
|
||||||
|
|
||||||
|
``` (stderr)
|
||||||
|
$ git checkout master -q
|
||||||
|
$ git merge changes -q
|
||||||
|
$ git push rad master
|
||||||
|
✓ Patch e0b35c56eb265d49cddd72d91cf873f64037d96c merged
|
||||||
|
✓ Canonical head updated to b2b6432af93f8fe188e32d400263021b602cfec8
|
||||||
|
✓ Synced with 1 node(s)
|
||||||
|
|
||||||
|
https://app.radicle.xyz/nodes/[..]/rad:z3yXbb1sR6UG6ixxV2YF9jUP7ABra/tree/b2b6432af93f8fe188e32d400263021b602cfec8
|
||||||
|
|
||||||
|
To rad://z3yXbb1sR6UG6ixxV2YF9jUP7ABra/z6Mkt67GdsW7715MEfRuP4pSZxJRJh6kj6Y48WRqVv4N1tRk
|
||||||
|
f2de534..b2b6432 master -> master
|
||||||
|
```
|
||||||
|
|
@ -11,6 +11,7 @@ use anyhow::{anyhow, bail, Context as _};
|
||||||
use serde_json as json;
|
use serde_json as json;
|
||||||
|
|
||||||
use radicle::crypto::{ssh, Verified};
|
use radicle::crypto::{ssh, Verified};
|
||||||
|
use radicle::explorer::ExplorerUrl;
|
||||||
use radicle::git::RefString;
|
use radicle::git::RefString;
|
||||||
use radicle::identity::{Id, Visibility};
|
use radicle::identity::{Id, Visibility};
|
||||||
use radicle::node::policy::Scope;
|
use radicle::node::policy::Scope;
|
||||||
|
|
@ -356,7 +357,7 @@ fn sync(
|
||||||
rid: Id,
|
rid: Id,
|
||||||
node: &mut Node,
|
node: &mut Node,
|
||||||
config: &profile::Config,
|
config: &profile::Config,
|
||||||
) -> Result<SyncResult<Option<String>>, radicle::node::Error> {
|
) -> Result<SyncResult<Option<ExplorerUrl>>, radicle::node::Error> {
|
||||||
if !node.is_running() {
|
if !node.is_running() {
|
||||||
return Ok(SyncResult::NodeStopped);
|
return Ok(SyncResult::NodeStopped);
|
||||||
}
|
}
|
||||||
|
|
@ -423,11 +424,7 @@ fn sync(
|
||||||
for seed in &config.preferred_seeds {
|
for seed in &config.preferred_seeds {
|
||||||
if replicas.contains(&seed.id) {
|
if replicas.contains(&seed.id) {
|
||||||
return Ok(SyncResult::Synced {
|
return Ok(SyncResult::Synced {
|
||||||
result: Some(
|
result: Some(config.public_explorer.url(seed.addr.host.to_string(), rid)),
|
||||||
config
|
|
||||||
.public_explorer
|
|
||||||
.url(seed.addr.host.to_string().as_str(), &rid),
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1674,6 +1674,41 @@ fn rad_patch_pull_update() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rad_patch_open_explore() {
|
||||||
|
logger::init(log::Level::Debug);
|
||||||
|
|
||||||
|
let mut environment = Environment::new();
|
||||||
|
let mut alice = environment
|
||||||
|
.node(Config {
|
||||||
|
policy: Policy::Allow,
|
||||||
|
..Config::test(Alias::new("alice"))
|
||||||
|
})
|
||||||
|
.spawn();
|
||||||
|
|
||||||
|
let bob = environment.profile(profile::Config {
|
||||||
|
preferred_seeds: vec![alice.address()],
|
||||||
|
..config::profile("bob")
|
||||||
|
});
|
||||||
|
let mut bob = Node::new(bob).spawn();
|
||||||
|
let working = environment.tmp().join("working");
|
||||||
|
|
||||||
|
fixtures::repository(&working);
|
||||||
|
|
||||||
|
bob.init("heartwood", "", &working).unwrap();
|
||||||
|
bob.connect(&alice);
|
||||||
|
alice.handle.follow(bob.id, None).unwrap();
|
||||||
|
alice.converge([&bob]);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"examples/rad-patch-open-explore.md",
|
||||||
|
&working,
|
||||||
|
Some(&bob.home),
|
||||||
|
[],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rad_init_private() {
|
fn rad_init_private() {
|
||||||
let mut environment = Environment::new();
|
let mut environment = Environment::new();
|
||||||
|
|
|
||||||
|
|
@ -71,7 +71,7 @@ mod routes {
|
||||||
response.json().await,
|
response.json().await,
|
||||||
json!({
|
json!({
|
||||||
"config": {
|
"config": {
|
||||||
"publicExplorer": "https://app.radicle.xyz/nodes/$host/$rid",
|
"publicExplorer": "https://app.radicle.xyz/nodes/$host/$rid$path",
|
||||||
"preferredSeeds": [
|
"preferredSeeds": [
|
||||||
"z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7@seed.radicle.garden:8776"
|
"z6MkrLMMsiPWUcNPHcRajuMi9mDfYckSoJyPwwnknocNYPm7@seed.radicle.garden:8776"
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ use std::{
|
||||||
|
|
||||||
use crossbeam_channel as chan;
|
use crossbeam_channel as chan;
|
||||||
|
|
||||||
use radicle::cob;
|
|
||||||
use radicle::cob::issue;
|
use radicle::cob::issue;
|
||||||
use radicle::crypto::ssh::{keystore::MemorySigner, Keystore};
|
use radicle::crypto::ssh::{keystore::MemorySigner, Keystore};
|
||||||
use radicle::crypto::test::signer::MockSigner;
|
use radicle::crypto::test::signer::MockSigner;
|
||||||
|
|
@ -31,6 +30,7 @@ use radicle::storage::{ReadStorage as _, RemoteRepository as _, SignRepository a
|
||||||
use radicle::test::fixtures;
|
use radicle::test::fixtures;
|
||||||
use radicle::Storage;
|
use radicle::Storage;
|
||||||
use radicle::{cli, node};
|
use radicle::{cli, node};
|
||||||
|
use radicle::{cob, explorer};
|
||||||
|
|
||||||
use crate::node::NodeId;
|
use crate::node::NodeId;
|
||||||
use crate::service::Event;
|
use crate::service::Event;
|
||||||
|
|
@ -91,7 +91,7 @@ impl Environment {
|
||||||
profile::Config {
|
profile::Config {
|
||||||
node: node::Config::test(alias),
|
node: node::Config::test(alias),
|
||||||
cli: cli::Config { hints: false },
|
cli: cli::Config { hints: false },
|
||||||
public_explorer: profile::Explorer::default(),
|
public_explorer: explorer::Explorer::default(),
|
||||||
preferred_seeds: vec![],
|
preferred_seeds: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -315,6 +315,23 @@ impl<G: Signer + cyphernet::Ecdh> NodeHandle<G> {
|
||||||
self.rad("sync", &[rid.to_string().as_str(), "--announce"], cwd)
|
self.rad("sync", &[rid.to_string().as_str(), "--announce"], cwd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Init a repo.
|
||||||
|
pub fn init<P: AsRef<Path>>(&self, name: &str, desc: &str, cwd: P) -> io::Result<()> {
|
||||||
|
self.rad(
|
||||||
|
"init",
|
||||||
|
&[
|
||||||
|
"--name",
|
||||||
|
name,
|
||||||
|
"--description",
|
||||||
|
desc,
|
||||||
|
"--default-branch",
|
||||||
|
"master",
|
||||||
|
"--public",
|
||||||
|
],
|
||||||
|
cwd,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/// Run a `rad` CLI command.
|
/// Run a `rad` CLI command.
|
||||||
pub fn rad<P: AsRef<Path>>(&self, cmd: &str, args: &[&str], cwd: P) -> io::Result<()> {
|
pub fn rad<P: AsRef<Path>>(&self, cmd: &str, args: &[&str], cwd: P) -> io::Result<()> {
|
||||||
let cwd = cwd.as_ref();
|
let cwd = cwd.as_ref();
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use std::collections::HashSet;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::io::IsTerminal;
|
use std::io::IsTerminal;
|
||||||
use std::ops::ControlFlow;
|
use std::ops::ControlFlow;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
@ -11,6 +11,7 @@ use thiserror::Error;
|
||||||
use radicle::cob::object::ParseObjectId;
|
use radicle::cob::object::ParseObjectId;
|
||||||
use radicle::cob::patch;
|
use radicle::cob::patch;
|
||||||
use radicle::crypto::Signer;
|
use radicle::crypto::Signer;
|
||||||
|
use radicle::explorer::ExplorerResource;
|
||||||
use radicle::identity::Did;
|
use radicle::identity::Did;
|
||||||
use radicle::node;
|
use radicle::node;
|
||||||
use radicle::node::{Handle, NodeId};
|
use radicle::node::{Handle, NodeId};
|
||||||
|
|
@ -97,8 +98,11 @@ pub enum Error {
|
||||||
Repository(#[from] radicle::storage::RepositoryError),
|
Repository(#[from] radicle::storage::RepositoryError),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Push command.
|
||||||
enum Command {
|
enum Command {
|
||||||
|
/// Update ref.
|
||||||
Push(git::Refspec<git::RefString, git::RefString>),
|
Push(git::Refspec<git::RefString, git::RefString>),
|
||||||
|
/// Delete ref.
|
||||||
Delete(git::RefString),
|
Delete(git::RefString),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -159,7 +163,7 @@ pub fn run(
|
||||||
})?;
|
})?;
|
||||||
let signer = profile.signer()?;
|
let signer = profile.signer()?;
|
||||||
let mut line = String::new();
|
let mut line = String::new();
|
||||||
let mut ok = HashSet::new();
|
let mut ok = HashMap::new();
|
||||||
let hints = profile.hints();
|
let hints = profile.hints();
|
||||||
|
|
||||||
assert_eq!(signer.public_key(), &nid);
|
assert_eq!(signer.public_key(), &nid);
|
||||||
|
|
@ -198,6 +202,7 @@ pub fn run(
|
||||||
.raw()
|
.raw()
|
||||||
.find_reference(&refname)
|
.find_reference(&refname)
|
||||||
.and_then(|mut r| r.delete())
|
.and_then(|mut r| r.delete())
|
||||||
|
.map(|_| None)
|
||||||
.map_err(Error::from)
|
.map_err(Error::from)
|
||||||
}
|
}
|
||||||
Command::Push(git::Refspec { src, dst, force }) => {
|
Command::Push(git::Refspec { src, dst, force }) => {
|
||||||
|
|
@ -269,9 +274,9 @@ pub fn run(
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
// Let Git tooling know that this ref has been pushed.
|
// Let Git tooling know that this ref has been pushed.
|
||||||
Ok(()) => {
|
Ok(resource) => {
|
||||||
println!("ok {}", cmd.dst());
|
println!("ok {}", cmd.dst());
|
||||||
ok.insert(spec);
|
ok.insert(spec, resource);
|
||||||
}
|
}
|
||||||
// Let Git tooling know that there was an error pushing the ref.
|
// Let Git tooling know that there was an error pushing the ref.
|
||||||
Err(e) => println!("error {} {e}", cmd.dst()),
|
Err(e) => println!("error {} {e}", cmd.dst()),
|
||||||
|
|
@ -298,7 +303,7 @@ pub fn run(
|
||||||
let node = radicle::Node::new(profile.socket());
|
let node = radicle::Node::new(profile.socket());
|
||||||
if node.is_running() {
|
if node.is_running() {
|
||||||
// Nb. allow this to fail. The push to local storage was still successful.
|
// Nb. allow this to fail. The push to local storage was still successful.
|
||||||
sync(stored.id, node).ok();
|
sync(stored.id, ok.into_values().flatten(), node, profile).ok();
|
||||||
} else if hints {
|
} else if hints {
|
||||||
hint("offline push, your node is not running");
|
hint("offline push, your node is not running");
|
||||||
hint("to sync with the network, run `rad node start`");
|
hint("to sync with the network, run `rad node start`");
|
||||||
|
|
@ -321,7 +326,7 @@ fn patch_open<G: Signer>(
|
||||||
signer: &G,
|
signer: &G,
|
||||||
profile: &Profile,
|
profile: &Profile,
|
||||||
opts: Options,
|
opts: Options,
|
||||||
) -> Result<(), Error> {
|
) -> Result<Option<ExplorerResource>, Error> {
|
||||||
let reference = working.find_reference(src.as_str())?;
|
let reference = working.find_reference(src.as_str())?;
|
||||||
let commit = reference.peel_to_commit()?;
|
let commit = reference.peel_to_commit()?;
|
||||||
let dst = git::refs::storage::staging::patch(nid, commit.id());
|
let dst = git::refs::storage::staging::patch(nid, commit.id());
|
||||||
|
|
@ -412,8 +417,7 @@ fn patch_open<G: Signer>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Ok(Some(ExplorerResource::Patch { id: patch }))
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
Err(e) => Err(e),
|
Err(e) => Err(e),
|
||||||
};
|
};
|
||||||
|
|
@ -440,7 +444,7 @@ fn patch_update<G: Signer>(
|
||||||
stored: &storage::git::Repository,
|
stored: &storage::git::Repository,
|
||||||
signer: &G,
|
signer: &G,
|
||||||
opts: Options,
|
opts: Options,
|
||||||
) -> Result<(), Error> {
|
) -> Result<Option<ExplorerResource>, Error> {
|
||||||
let reference = working.find_reference(src.as_str())?;
|
let reference = working.find_reference(src.as_str())?;
|
||||||
let commit = reference.peel_to_commit()?;
|
let commit = reference.peel_to_commit()?;
|
||||||
let patch_id = radicle::cob::ObjectId::from(oid);
|
let patch_id = radicle::cob::ObjectId::from(oid);
|
||||||
|
|
@ -455,7 +459,7 @@ fn patch_update<G: Signer>(
|
||||||
|
|
||||||
// Don't update patch if it already has a revision matching this commit.
|
// Don't update patch if it already has a revision matching this commit.
|
||||||
if patch.revisions().any(|(_, r)| *r.head() == commit.id()) {
|
if patch.revisions().any(|(_, r)| *r.head() == commit.id()) {
|
||||||
return Ok(());
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let message = cli::patch::get_update_message(
|
let message = cli::patch::get_update_message(
|
||||||
opts.message,
|
opts.message,
|
||||||
|
|
@ -490,7 +494,7 @@ fn patch_update<G: Signer>(
|
||||||
patch_merge(patch, revision, head, working, signer)?;
|
patch_merge(patch, revision, head, working, signer)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(Some(ExplorerResource::Patch { id: patch_id }))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn push<G: Signer>(
|
fn push<G: Signer>(
|
||||||
|
|
@ -501,7 +505,7 @@ fn push<G: Signer>(
|
||||||
working: &git::raw::Repository,
|
working: &git::raw::Repository,
|
||||||
stored: &storage::git::Repository,
|
stored: &storage::git::Repository,
|
||||||
signer: &G,
|
signer: &G,
|
||||||
) -> Result<(), Error> {
|
) -> Result<Option<ExplorerResource>, Error> {
|
||||||
let head = working.find_reference(src.as_str())?;
|
let head = working.find_reference(src.as_str())?;
|
||||||
let head = head.peel_to_commit()?.id();
|
let head = head.peel_to_commit()?.id();
|
||||||
let dst = dst.with_namespace(nid.into());
|
let dst = dst.with_namespace(nid.into());
|
||||||
|
|
@ -524,7 +528,7 @@ fn push<G: Signer>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(Some(ExplorerResource::Tree { oid: head.into() }))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Merge all patches that have been included in the base branch.
|
/// Merge all patches that have been included in the base branch.
|
||||||
|
|
@ -626,9 +630,15 @@ fn push_ref(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sync with the network.
|
/// Sync with the network.
|
||||||
fn sync(rid: Id, mut node: radicle::Node) -> Result<(), radicle::node::Error> {
|
fn sync(
|
||||||
|
rid: Id,
|
||||||
|
updated: impl Iterator<Item = ExplorerResource>,
|
||||||
|
mut node: radicle::Node,
|
||||||
|
profile: &Profile,
|
||||||
|
) -> Result<(), radicle::node::Error> {
|
||||||
let seeds = node.seeds(rid)?;
|
let seeds = node.seeds(rid)?;
|
||||||
let connected = seeds.connected().map(|s| s.nid).collect::<Vec<_>>();
|
let connected = seeds.connected().map(|s| s.nid).collect::<Vec<_>>();
|
||||||
|
let mut replicated = HashSet::new();
|
||||||
|
|
||||||
if connected.is_empty() {
|
if connected.is_empty() {
|
||||||
eprintln!("Not connected to any seeds.");
|
eprintln!("Not connected to any seeds.");
|
||||||
|
|
@ -647,6 +657,7 @@ fn sync(rid: Id, mut node: radicle::Node) -> Result<(), radicle::node::Error> {
|
||||||
|event, _| match event {
|
|event, _| match event {
|
||||||
node::AnnounceEvent::Announced => ControlFlow::Continue(()),
|
node::AnnounceEvent::Announced => ControlFlow::Continue(()),
|
||||||
node::AnnounceEvent::RefsSynced { remote } => {
|
node::AnnounceEvent::RefsSynced { remote } => {
|
||||||
|
replicated.insert(remote);
|
||||||
spinner.message(format!("Synced with {remote}.."));
|
spinner.message(format!("Synced with {remote}.."));
|
||||||
ControlFlow::Continue(())
|
ControlFlow::Continue(())
|
||||||
}
|
}
|
||||||
|
|
@ -659,5 +670,31 @@ fn sync(rid: Id, mut node: radicle::Node) -> Result<(), radicle::node::Error> {
|
||||||
spinner.message(format!("Synced with {} node(s)", result.synced.len()));
|
spinner.message(format!("Synced with {} node(s)", result.synced.len()));
|
||||||
spinner.finish();
|
spinner.finish();
|
||||||
}
|
}
|
||||||
|
let mut urls = Vec::new();
|
||||||
|
|
||||||
|
for seed in &profile.config.preferred_seeds {
|
||||||
|
if replicated.contains(&seed.id) {
|
||||||
|
for resource in updated {
|
||||||
|
let url = profile
|
||||||
|
.config
|
||||||
|
.public_explorer
|
||||||
|
.url(seed.addr.host.clone(), rid)
|
||||||
|
.resource(resource);
|
||||||
|
|
||||||
|
urls.push(url);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print URLs to the updated resources.
|
||||||
|
if !urls.is_empty() {
|
||||||
|
eprintln!();
|
||||||
|
for url in urls {
|
||||||
|
eprintln!(" {}", cli::format::dim(url));
|
||||||
|
}
|
||||||
|
eprintln!();
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,128 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
use crate::prelude::Id;
|
||||||
|
use crate::{cob, git};
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ExplorerError {
|
||||||
|
#[error("invalid explorer URL {0:?}: unknown protocol")]
|
||||||
|
UnknownProtocol(String),
|
||||||
|
#[error("invalid explorer URL {0:?}: missing `$host` component")]
|
||||||
|
MissingHost(String),
|
||||||
|
#[error("invalid explorer URL {0:?}: missing `$rid` component")]
|
||||||
|
MissingRid(String),
|
||||||
|
#[error("invalid explorer URL {0:?}: missing `$path` component")]
|
||||||
|
MissingPath(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A resource such as a branch, patch or commit.
|
||||||
|
#[derive(Debug, Hash, PartialEq, Eq)]
|
||||||
|
pub enum ExplorerResource {
|
||||||
|
/// Git tree object. Used for the repository root.
|
||||||
|
Tree { oid: git::Oid },
|
||||||
|
/// A Patch COB.
|
||||||
|
Patch { id: cob::ObjectId },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ExplorerResource {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Tree { oid } => {
|
||||||
|
write!(f, "/tree/{oid}")
|
||||||
|
}
|
||||||
|
Self::Patch { id } => {
|
||||||
|
write!(f, "/patches/{id}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A URL to a specific repository or resource within a repository.
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub struct ExplorerUrl {
|
||||||
|
/// URL template.
|
||||||
|
pub template: Explorer,
|
||||||
|
/// Host serving the repository.
|
||||||
|
pub host: String,
|
||||||
|
/// Repository.
|
||||||
|
pub rid: Id,
|
||||||
|
/// Resource under the repository.
|
||||||
|
pub resource: Option<ExplorerResource>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExplorerUrl {
|
||||||
|
/// Set a resource on for this URL.
|
||||||
|
pub fn resource(mut self, resource: ExplorerResource) -> Self {
|
||||||
|
self.resource = Some(resource);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ExplorerUrl {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.write_str(
|
||||||
|
self.template
|
||||||
|
.0
|
||||||
|
.replace("$host", &self.host)
|
||||||
|
.replace("$rid", self.rid.urn().as_str())
|
||||||
|
.replace(
|
||||||
|
"$path",
|
||||||
|
self.resource
|
||||||
|
.as_ref()
|
||||||
|
.map(|r| r.to_string())
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or(""),
|
||||||
|
)
|
||||||
|
.as_str(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A public explorer, eg. `https://app.radicle.xyz`.
|
||||||
|
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
||||||
|
#[serde(transparent)]
|
||||||
|
pub struct Explorer(String);
|
||||||
|
|
||||||
|
impl Default for Explorer {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(String::from(
|
||||||
|
"https://app.radicle.xyz/nodes/$host/$rid$path",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Explorer {
|
||||||
|
/// Get the explorer URL, filling in the host and RID.
|
||||||
|
pub fn url(&self, host: impl ToString, rid: Id) -> ExplorerUrl {
|
||||||
|
ExplorerUrl {
|
||||||
|
template: self.clone(),
|
||||||
|
host: host.to_string(),
|
||||||
|
rid,
|
||||||
|
resource: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromStr for Explorer {
|
||||||
|
type Err = ExplorerError;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let url = s.to_owned();
|
||||||
|
|
||||||
|
if !url.starts_with("http://") && !url.starts_with("https://") {
|
||||||
|
return Err(ExplorerError::UnknownProtocol(url));
|
||||||
|
}
|
||||||
|
if !url.contains("$host") {
|
||||||
|
return Err(ExplorerError::MissingHost(url));
|
||||||
|
}
|
||||||
|
if !url.contains("$rid") {
|
||||||
|
return Err(ExplorerError::MissingRid(url));
|
||||||
|
}
|
||||||
|
if !url.contains("$path") {
|
||||||
|
return Err(ExplorerError::MissingPath(url));
|
||||||
|
}
|
||||||
|
Ok(Explorer(url))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -13,6 +13,7 @@ mod canonical;
|
||||||
pub mod cli;
|
pub mod cli;
|
||||||
pub mod cob;
|
pub mod cob;
|
||||||
pub mod collections;
|
pub mod collections;
|
||||||
|
pub mod explorer;
|
||||||
pub mod git;
|
pub mod git;
|
||||||
pub mod identity;
|
pub mod identity;
|
||||||
#[cfg(feature = "logger")]
|
#[cfg(feature = "logger")]
|
||||||
|
|
|
||||||
|
|
@ -20,10 +20,11 @@ use thiserror::Error;
|
||||||
use crate::crypto::ssh::agent::Agent;
|
use crate::crypto::ssh::agent::Agent;
|
||||||
use crate::crypto::ssh::{keystore, Keystore, Passphrase};
|
use crate::crypto::ssh::{keystore, Keystore, Passphrase};
|
||||||
use crate::crypto::{PublicKey, Signer};
|
use crate::crypto::{PublicKey, Signer};
|
||||||
|
use crate::explorer::Explorer;
|
||||||
use crate::node::policy::config::store::Read;
|
use crate::node::policy::config::store::Read;
|
||||||
use crate::node::{policy, Alias, AliasStore};
|
use crate::node::{policy, Alias, AliasStore};
|
||||||
use crate::prelude::Did;
|
use crate::prelude::Did;
|
||||||
use crate::prelude::{Id, NodeId};
|
use crate::prelude::NodeId;
|
||||||
use crate::storage::git::transport;
|
use crate::storage::git::transport;
|
||||||
use crate::storage::git::Storage;
|
use crate::storage::git::Storage;
|
||||||
use crate::{cli, git, node};
|
use crate::{cli, git, node};
|
||||||
|
|
@ -81,55 +82,6 @@ pub mod env {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
|
||||||
pub enum ExplorerUrlError {
|
|
||||||
#[error("invalid explorer URL {0:?}: unknown protocol")]
|
|
||||||
UnknownProtocol(String),
|
|
||||||
#[error("invalid explorer URL {0:?}: missing `$host` component")]
|
|
||||||
MissingHost(String),
|
|
||||||
#[error("invalid explorer URL {0:?}: missing `$rid` component")]
|
|
||||||
MissingRid(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A public explorer, eg. `https://app.radicle.xyz`.
|
|
||||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
|
|
||||||
#[serde(transparent)]
|
|
||||||
pub struct Explorer(String);
|
|
||||||
|
|
||||||
impl Default for Explorer {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self(String::from("https://app.radicle.xyz/nodes/$host/$rid"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Explorer {
|
|
||||||
/// Get the explorer URL, filling in the host and RID.
|
|
||||||
pub fn url(&self, host: &str, rid: &Id) -> String {
|
|
||||||
self.0
|
|
||||||
.replace("$host", host)
|
|
||||||
.replace("$rid", rid.urn().as_str())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FromStr for Explorer {
|
|
||||||
type Err = ExplorerUrlError;
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
let url = s.to_owned();
|
|
||||||
|
|
||||||
if !url.starts_with("http://") && !url.starts_with("https://") {
|
|
||||||
return Err(ExplorerUrlError::UnknownProtocol(url));
|
|
||||||
}
|
|
||||||
if !url.contains("$host") {
|
|
||||||
return Err(ExplorerUrlError::MissingHost(url));
|
|
||||||
}
|
|
||||||
if !url.contains("$rid") {
|
|
||||||
return Err(ExplorerUrlError::MissingRid(url));
|
|
||||||
}
|
|
||||||
Ok(Explorer(url))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue